From 74536bdf7cecc96646f4591794f4879cfe2d397b Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Fri, 6 Sep 2024 12:00:48 -0700 Subject: [PATCH 1/4] explorer: remove chain indices from transaction type --- explorer/types.go | 1 - 1 file changed, 1 deletion(-) diff --git a/explorer/types.go b/explorer/types.go index fade0967..8e41d4e9 100644 --- a/explorer/types.go +++ b/explorer/types.go @@ -113,7 +113,6 @@ type FileContractRevision struct { // A Transaction is a transaction that uses the wrapped types above. type Transaction struct { ID types.TransactionID `json:"id"` - ChainIndices []types.ChainIndex `json:"chainIndices,omitempty"` SiacoinInputs []SiacoinInput `json:"siacoinInputs,omitempty"` SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` SiafundInputs []SiafundInput `json:"siafundInputs,omitempty"` From c9d33fb9422b00d68355f3ee69df7e70e095620b Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Fri, 6 Sep 2024 12:01:00 -0700 Subject: [PATCH 2/4] api,explorer,sqlite: add additional endpoint for getting chain indices --- api/client.go | 5 +++ api/server.go | 19 ++++++++++-- explorer/explorer.go | 5 +++ persist/sqlite/consensus_test.go | 30 ++++++++++++++++-- persist/sqlite/transactions.go | 52 +++++++++++++++----------------- 5 files changed, 79 insertions(+), 32 deletions(-) diff --git a/api/client.go b/api/client.go index 28181427..5cc43985 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 839e2c0e..9901c144 100644 --- a/api/server.go +++ b/api/server.go @@ -53,6 +53,7 @@ type ( Metrics(id types.BlockID) (explorer.Metrics, error) HostMetrics() (explorer.HostMetrics, 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) @@ -262,6 +263,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 { @@ -507,8 +521,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 e4a353f6..d48bfb4a 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -45,6 +45,7 @@ type Store interface { Metrics(id types.BlockID) (Metrics, error) HostMetrics() (HostMetrics, 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) @@ -196,6 +197,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 e1dc8e9b..a3772dd0 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -412,6 +412,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)) @@ -578,7 +591,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 { @@ -1625,6 +1638,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)) @@ -1782,7 +1808,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], From ddd5055a1ea8b6da95dabaf8f5a77ab5e876ab58 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Fri, 6 Sep 2024 12:08:10 -0700 Subject: [PATCH 3/4] api: honor limit and offset --- api/server.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/server.go b/api/server.go index 9901c144..6a058b73 100644 --- a/api/server.go +++ b/api/server.go @@ -269,7 +269,17 @@ func (s *server) transactionsIDIndicesHandler(jc jape.Context) { return } - indices, err := s.e.TransactionIndices(id, 0, 100) + limit := uint64(100) + offset := uint64(0) + if jc.DecodeForm("limit", &limit) != nil || jc.DecodeForm("offset", &offset) != nil { + return + } + + if limit > 500 { + limit = 500 + } + + indices, err := s.e.TransactionIndices(id, offset, limit) if jc.Check("failed to get transaction indices", err) != nil { return } From 8b47464b198cf43ea2d354c5924cbd5a3fd3dcb7 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Fri, 6 Sep 2024 12:10:47 -0700 Subject: [PATCH 4/4] api,explorer,sqlite: fix lint --- api/client.go | 4 +++- api/server.go | 4 ++-- explorer/explorer.go | 9 ++++++--- persist/sqlite/consensus_test.go | 4 ++-- persist/sqlite/transactions.go | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/api/client.go b/api/client.go index 5cc43985..bd3e30a9 100644 --- a/api/client.go +++ b/api/client.go @@ -124,7 +124,9 @@ 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) { +// TransactionChainIndices returns chain indices a transaction was +// included in. +func (c *Client) TransactionChainIndices(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 } diff --git a/api/server.go b/api/server.go index 6a058b73..1092513c 100644 --- a/api/server.go +++ b/api/server.go @@ -53,7 +53,7 @@ type ( Metrics(id types.BlockID) (explorer.Metrics, error) HostMetrics() (explorer.HostMetrics, error) Transactions(ids []types.TransactionID) ([]explorer.Transaction, error) - TransactionIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) + TransactionChainIndices(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) @@ -279,7 +279,7 @@ func (s *server) transactionsIDIndicesHandler(jc jape.Context) { limit = 500 } - indices, err := s.e.TransactionIndices(id, offset, limit) + indices, err := s.e.TransactionChainIndices(id, offset, limit) if jc.Check("failed to get transaction indices", err) != nil { return } diff --git a/explorer/explorer.go b/explorer/explorer.go index d48bfb4a..c26299e2 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -45,7 +45,7 @@ type Store interface { Metrics(id types.BlockID) (Metrics, error) HostMetrics() (HostMetrics, error) Transactions(ids []types.TransactionID) ([]Transaction, error) - TransactionIndices(txid types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) + TransactionChainIndices(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) @@ -197,8 +197,11 @@ 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) +// TransactionChainIndices returns the chain indices of the blocks the transaction +// was included in. If the transaction has not been included in any blocks, the +// result will be nil,nil. +func (e *Explorer) TransactionChainIndices(id types.TransactionID, offset, limit uint64) ([]types.ChainIndex, error) { + return e.s.TransactionChainIndices(id, offset, limit) } // UnspentSiacoinOutputs returns the unspent siacoin outputs owned by the diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index a3772dd0..34292fb0 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -413,7 +413,7 @@ func TestSendTransactions(t *testing.T) { } checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) { - indices, err := db.TransactionIndices(txnID, 0, 100) + indices, err := db.TransactionChainIndices(txnID, 0, 100) switch { case err != nil: t.Fatal(err) @@ -1639,7 +1639,7 @@ func TestRevertSendTransactions(t *testing.T) { } checkChainIndices := func(t *testing.T, txnID types.TransactionID, expected []types.ChainIndex) { - indices, err := db.TransactionIndices(txnID, 0, 100) + indices, err := db.TransactionChainIndices(txnID, 0, 100) switch { case err != nil: t.Fatal(err) diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index 28d7d3a2..801d7d16 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -7,10 +7,10 @@ import ( "go.sia.tech/explored/explorer" ) -// transactionChainIndices returns the chain indices of the blocks the transaction +// TransactionChainIndices returns the chain indices of the blocks the transaction // 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) { +func (s *Store) TransactionChainIndices(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)