diff --git a/api/client.go b/api/client.go index 09de6a25..0f959e6e 100644 --- a/api/client.go +++ b/api/client.go @@ -124,6 +124,11 @@ func (c *Client) Transactions(ids []types.TransactionID) (resp []explorer.Transa return } +func (c *Client) TransactionIndices(id types.TransactionID, offset, limit uint64) (resp []types.ChainIndex, err error) { + err = c.c.GET(fmt.Sprintf("/transactions/%s/indices?offset=%d&limit=%d", id, offset, limit), &resp) + return +} + // AddressSiacoinUTXOs returns the specified address' unspent outputs. func (c *Client) AddressSiacoinUTXOs(address types.Address, offset, limit uint64) (resp []explorer.SiacoinOutput, err error) { err = c.c.GET(fmt.Sprintf("/addresses/%s/utxos/siacoin?offset=%d&limit=%d", address, offset, limit), &resp) diff --git a/api/server.go b/api/server.go index 305aee76..081e2cff 100644 --- a/api/server.go +++ b/api/server.go @@ -52,6 +52,7 @@ type ( BestTip(height uint64) (types.ChainIndex, error) Metrics(id types.BlockID) (explorer.Metrics, error) Transactions(ids []types.TransactionID) ([]explorer.Transaction, error) + TransactionIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error) SiacoinElements(ids []types.SiacoinOutputID) (result []explorer.SiacoinOutput, err error) SiafundElements(ids []types.SiafundOutputID) (result []explorer.SiafundOutput, err error) @@ -251,6 +252,19 @@ func (s *server) transactionsIDHandler(jc jape.Context) { jc.Encode(txns[0]) } +func (s *server) transactionsIDIndicesHandler(jc jape.Context) { + var id types.TransactionID + if jc.DecodeParam("id", &id) != nil { + return + } + + indices, err := s.e.TransactionIndices(id, 0, 100) + if jc.Check("failed to get transaction indices", err) != nil { + return + } + jc.Encode(indices) +} + func (s *server) transactionsBatchHandler(jc jape.Context) { var ids []types.TransactionID if jc.Decode(&ids) != nil { @@ -479,8 +493,9 @@ func NewServer(e Explorer, cm ChainManager, s Syncer) http.Handler { "GET /blocks/:id": srv.blocksIDHandler, - "GET /transactions/:id": srv.transactionsIDHandler, - "POST /transactions": srv.transactionsBatchHandler, + "GET /transactions/:id": srv.transactionsIDHandler, + "POST /transactions": srv.transactionsBatchHandler, + "GET /transactions/:id/indices": srv.transactionsIDIndicesHandler, "GET /addresses/:address/utxos/siacoin": srv.addressessAddressUtxosSiacoinHandler, "GET /addresses/:address/utxos/siafund": srv.addressessAddressUtxosSiafundHandler, diff --git a/explorer/explorer.go b/explorer/explorer.go index 68dbb780..12b81b3b 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -38,6 +38,7 @@ type Store interface { MerkleProof(leafIndex uint64) ([]types.Hash256, error) Metrics(id types.BlockID) (Metrics, error) Transactions(ids []types.TransactionID) ([]Transaction, error) + TransactionIndices(txid types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error) UnspentSiafundOutputs(address types.Address, offset, limit uint64) ([]SiafundOutput, error) AddressEvents(address types.Address, offset, limit uint64) (events []Event, err error) @@ -146,6 +147,10 @@ func (e *Explorer) Transactions(ids []types.TransactionID) ([]Transaction, error return e.s.Transactions(ids) } +func (e *Explorer) TransactionIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) { + return e.s.TransactionIndices(id, offset, limit) +} + // UnspentSiacoinOutputs returns the unspent siacoin outputs owned by the // specified address. func (e *Explorer) UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error) { diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index ee30532c..a35c0bc6 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -411,6 +411,19 @@ func TestSendTransactions(t *testing.T) { check(t, "siafunds", expectSF, sf) } + checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) { + indices, err := db.TransactionIndices(txnID, 0, 100) + switch { + case err != nil: + t.Fatal(err) + case len(indices) != len(expected): + t.Fatalf("expected %d indices, got %d", len(expected), len(indices)) + } + for i := range indices { + check(t, "index", expected[i], indices[i]) + } + } + checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) { check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs)) check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs)) @@ -577,7 +590,7 @@ func TestSendTransactions(t *testing.T) { // with the actual transactions for i := range b.Transactions { checkTransaction(b.Transactions[i], block.Transactions[i]) - check(t, "chain indices", []types.ChainIndex{cm.Tip()}, block.Transactions[i].ChainIndices) + checkChainIndices(t, b.Transactions[i].ID(), []types.ChainIndex{cm.Tip()}) txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()}) if err != nil { @@ -1624,6 +1637,19 @@ func TestRevertSendTransactions(t *testing.T) { check(t, "siafunds", expectSF, sf) } + checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) { + indices, err := db.TransactionIndices(txnID, 0, 100) + switch { + case err != nil: + t.Fatal(err) + case len(indices) != len(expected): + t.Fatalf("expected %d indices, got %d", len(expected), len(indices)) + } + for i := range indices { + check(t, "index", expected[i], indices[i]) + } + } + checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) { check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs)) check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs)) @@ -1781,7 +1807,7 @@ func TestRevertSendTransactions(t *testing.T) { // with the actual transactions for i := range b.Transactions { checkTransaction(b.Transactions[i], block.Transactions[i]) - check(t, "chain indices", []types.ChainIndex{cm.Tip()}, block.Transactions[i].ChainIndices) + checkChainIndices(t, b.Transactions[i].ID(), []types.ChainIndex{cm.Tip()}) txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()}) if err != nil { diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index fb59f3e5..28d7d3a2 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -8,29 +8,31 @@ import ( ) // transactionChainIndices returns the chain indices of the blocks the transaction -// was in. -func transactionChainIndices(tx *txn, txnIDs []int64) (map[int64][]types.ChainIndex, error) { - query := `SELECT bt.transaction_id, bt.block_id, b.height -FROM block_transactions bt -JOIN blocks b ON bt.block_id = b.id -WHERE bt.transaction_id IN (` + queryPlaceHolders(len(txnIDs)) + `) -ORDER BY bt.block_order ASC` - rows, err := tx.Query(query, queryArgs(txnIDs)...) - if err != nil { - return nil, err - } - defer rows.Close() - - result := make(map[int64][]types.ChainIndex) - for rows.Next() { - var txnID int64 - var index types.ChainIndex - if err := rows.Scan(&txnID, decode(&index.ID), decode(&index.Height)); err != nil { - return nil, fmt.Errorf("failed to scan chain index: %w", err) +// was included in. If the transaction has not been included in any blocks, the +// result will be nil,nil. +func (s *Store) TransactionIndices(txnID types.TransactionID, offset, limit uint64) (indices []types.ChainIndex, err error) { + err = s.transaction(func(tx *txn) error { + rows, err := tx.Query(`SELECT DISTINCT b.id, b.height FROM blocks b +INNER JOIN block_transactions bt ON (bt.block_id = b.id) +INNER JOIN transactions t ON (t.id = bt.transaction_id) +WHERE t.transaction_id = ? +ORDER BY b.height DESC +LIMIT ? OFFSET ?`, encode(txnID), limit, offset) + if err != nil { + return err } - result[txnID] = append(result[txnID], index) - } - return result, nil + defer rows.Close() + + for rows.Next() { + var index types.ChainIndex + if err := rows.Scan(decode(&index.ID), decode(&index.Height)); err != nil { + return fmt.Errorf("failed to scan chain index: %w", err) + } + indices = append(indices, index) + } + return rows.Err() + }) + return } // transactionMinerFee returns the miner fees for each transaction. @@ -484,11 +486,6 @@ func getTransactions(tx *txn, idMap map[int64]transactionID) ([]explorer.Transac dbIDs[id.order] = dbID } - txnChainIndices, err := transactionChainIndices(tx, dbIDs) - if err != nil { - return nil, fmt.Errorf("getTransactions: failed to get chain indices: %w", err) - } - txnArbitraryData, err := transactionArbitraryData(tx, dbIDs) if err != nil { return nil, fmt.Errorf("getTransactions: failed to get arbitrary data: %w", err) @@ -543,7 +540,6 @@ func getTransactions(tx *txn, idMap map[int64]transactionID) ([]explorer.Transac for _, dbID := range dbIDs { txn := explorer.Transaction{ ID: idMap[dbID].id, - ChainIndices: txnChainIndices[dbID], SiacoinInputs: txnSiacoinInputs[dbID], SiacoinOutputs: txnSiacoinOutputs[dbID], SiafundInputs: txnSiafundInputs[dbID],