diff --git a/explorer/explorer.go b/explorer/explorer.go index 22da69ba..bd703458 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -14,6 +14,7 @@ type Store interface { Block(id types.BlockID) (Block, error) BestTip(height uint64) (types.ChainIndex, error) Transactions(ids []types.TransactionID) ([]Transaction, error) + TransactionAddresses(id types.TransactionID, limit, offset uint64) ([]types.Address, error) UnspentSiacoinOutputs(address types.Address, limit, offset uint64) ([]SiacoinOutput, error) UnspentSiafundOutputs(address types.Address, limit, offset uint64) ([]SiafundOutput, error) Balance(address types.Address) (sc types.Currency, immatureSC types.Currency, sf uint64, err error) @@ -57,6 +58,11 @@ func (e *Explorer) Transactions(ids []types.TransactionID) ([]Transaction, error return e.s.Transactions(ids) } +// TransactionAddresses returns the addresses in the transaction. +func (e *Explorer) TransactionAddresses(id types.TransactionID, limit, offset uint64) (results []types.Address, err error) { + return e.s.TransactionAddresses(id, limit, offset) +} + // UnspentSiacoinOutputs returns the unspent siacoin outputs owned by the // specified address. func (e *Explorer) UnspentSiacoinOutputs(address types.Address, limit, offset uint64) ([]SiacoinOutput, error) { diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index a4d358be..89d0d05b 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -36,6 +36,53 @@ func (s *Store) addMinerPayouts(dbTxn txn, bid types.BlockID, height uint64, sco return nil } +func (s *Store) addTransactionAddresses(dbTxn txn, id int64, txn types.Transaction) error { + m := make(map[types.Address]struct{}) + for _, sci := range txn.SiacoinInputs { + m[sci.UnlockConditions.UnlockHash()] = struct{}{} + } + for _, sco := range txn.SiacoinOutputs { + m[sco.Address] = struct{}{} + } + for _, sfi := range txn.SiafundInputs { + m[sfi.UnlockConditions.UnlockHash()] = struct{}{} + } + for _, sfo := range txn.SiafundOutputs { + m[sfo.Address] = struct{}{} + } + for _, fc := range txn.FileContracts { + for _, vpo := range fc.ValidProofOutputs { + m[vpo.Address] = struct{}{} + } + for _, mpo := range fc.MissedProofOutputs { + m[mpo.Address] = struct{}{} + } + m[types.Address(fc.UnlockHash)] = struct{}{} + } + for _, fcr := range txn.FileContractRevisions { + for _, vpo := range fcr.FileContract.ValidProofOutputs { + m[vpo.Address] = struct{}{} + } + for _, mpo := range fcr.FileContract.MissedProofOutputs { + m[mpo.Address] = struct{}{} + } + m[fcr.UnlockConditions.UnlockHash()] = struct{}{} + } + + stmt, err := dbTxn.Prepare(`INSERT INTO transaction_addresses(transaction_id, address) VALUES (?, ?);`) + if err != nil { + return fmt.Errorf("addTransactionAddresses: failed to prepare statement: %w", err) + } + defer stmt.Close() + + for addr := range m { + if _, err := stmt.Exec(id, dbEncode(addr)); err != nil { + return fmt.Errorf("addTransactionAddresses: failed to execute statement: %w", err) + } + } + return nil +} + func (s *Store) addArbitraryData(dbTxn txn, id int64, txn types.Transaction) error { stmt, err := dbTxn.Prepare(`INSERT INTO transaction_arbitrary_data(transaction_id, transaction_order, data) VALUES (?, ?, ?)`) if err != nil { @@ -235,6 +282,8 @@ func (s *Store) addTransactions(dbTxn txn, bid types.BlockID, txns []types.Trans if _, err := blockTransactionsStmt.Exec(dbEncode(bid), txnID, i); err != nil { return fmt.Errorf("failed to insert into block_transactions: %w", err) + } else if err := s.addTransactionAddresses(dbTxn, txnID, txn); err != nil { + return fmt.Errorf("failed to add transaction addresses: %w", err) } else if err := s.addArbitraryData(dbTxn, txnID, txn); err != nil { return fmt.Errorf("failed to add arbitrary data: %w", err) } else if err := s.addSiacoinInputs(dbTxn, txnID, txn); err != nil { diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 5311f479..44cc17a8 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -3,6 +3,7 @@ package sqlite_test import ( "path/filepath" "reflect" + "sort" "testing" "go.sia.tech/core/consensus" @@ -516,6 +517,30 @@ func TestSendTransactions(t *testing.T) { } } } + + { + expected := []types.Address{addr1, addr2, addr3} + addresses, err := db.TransactionAddresses(parentTxn.ID(), 3, 0) + if err != nil { + t.Fatal(err) + } + if len(expected) != len(addresses) { + t.Fatalf("expected %d addresses, got %d", len(expected), len(addresses)) + } + + sort.Slice(expected, func(i, j int) bool { + return expected[i][0] > expected[j][0] + }) + sort.Slice(addresses, func(i, j int) bool { + return addresses[i][0] > addresses[j][0] + }) + + for i := range expected { + if expected[i] != addresses[i] { + t.Fatalf("expect address %v, got %v", expected[i], addresses[i]) + } + } + } } } diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index d253cd3c..0488066d 100644 --- a/persist/sqlite/init.sql +++ b/persist/sqlite/init.sql @@ -128,6 +128,14 @@ CREATE TABLE block_transactions ( CREATE INDEX block_transactions_block_id_index ON block_transactions(block_id); +CREATE TABLE transaction_addresses ( + transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, + address BLOB NOT NULL, + UNIQUE(transaction_id, address) +); + +CREATE INDEX transaction_addresses_transaction_id_index ON transaction_addresses(transaction_id); + CREATE TABLE transaction_arbitrary_data ( transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL, transaction_order INTEGER NOT NULL, diff --git a/persist/sqlite/transactions.go b/persist/sqlite/transactions.go index 9b630857..e1cc2a10 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -425,3 +425,36 @@ func (s *Store) Transactions(ids []types.TransactionID) (results []explorer.Tran }) return } + +// TransactionAddresses implements explorer.Store. +func (s *Store) TransactionAddresses(id types.TransactionID, limit, offset uint64) (results []types.Address, err error) { + err = s.transaction(func(tx txn) error { + dbIDs, err := transactionDatabaseIDs(tx, []types.TransactionID{id}) + if err != nil { + return fmt.Errorf("failed to get transaction IDs: %w", err) + } else if len(dbIDs) == 0 { + return errors.New("no such transaction") + } + + query := `SELECT address +FROM transaction_addresses +WHERE transaction_id = ? +LIMIT ? OFFSET ? +` + rows, err := tx.Query(query, dbIDs[0], limit, offset) + if err != nil { + return fmt.Errorf("failed to get addresses: %w", err) + } + + for rows.Next() { + var addr types.Address + if err := rows.Scan(dbDecode(&addr)); err != nil { + return fmt.Errorf("failed to scan address: %w", err) + } + results = append(results, addr) + } + + return nil + }) + return +}