diff --git a/explorer/explorer.go b/explorer/explorer.go index 3eee13a..fbabeb8 100644 --- a/explorer/explorer.go +++ b/explorer/explorer.go @@ -36,8 +36,9 @@ type Store interface { BestTip(height uint64) (types.ChainIndex, error) MerkleProof(leafIndex uint64) ([]types.Hash256, error) Transactions(ids []types.TransactionID) ([]Transaction, error) - UnspentSiacoinOutputs(address types.Address, offset, limit uint64) ([]SiacoinOutput, error) - UnspentSiafundOutputs(address types.Address, offset, limit uint64) ([]SiafundOutput, error) + AddressTransactions(addr types.Address, limit, offset uint64) (results []types.TransactionID, err 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) Contracts(ids []types.FileContractID) (result []FileContract, err error) AddressEvents(address types.Address, offset, limit int) (events []Event, err error) @@ -136,6 +137,11 @@ func (e *Explorer) Transactions(ids []types.TransactionID) ([]Transaction, error return e.s.Transactions(ids) } +// AddressTransactions returns the transactions involving the address. +func (e *Explorer) AddressTransactions(addr types.Address, limit, offset uint64) (results []types.TransactionID, err error) { + return e.s.AddressTransactions(addr, limit, offset) +} + // 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.go b/persist/sqlite/consensus.go index 012af61..fa56615 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -43,9 +43,55 @@ func (ut *updateTx) addMinerPayouts(bid types.BlockID, height uint64, scos []typ return nil } -func (ut *updateTx) addArbitraryData(id int64, txn types.Transaction) error { - stmt, err := ut.tx.Prepare(`INSERT INTO transaction_arbitrary_data(transaction_id, transaction_order, data) VALUES (?, ?, ?)`) +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 { return fmt.Errorf("addArbitraryData: failed to prepare statement: %w", err) } @@ -243,7 +289,9 @@ func (ut *updateTx) addTransactions(bid types.BlockID, txns []types.Transaction, if _, err := blockTransactionsStmt.Exec(encode(bid), txnID, i); err != nil { return fmt.Errorf("failed to insert into block_transactions: %w", err) - } else if err := ut.addArbitraryData(txnID, txn); err != nil { + } 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 := ut.addSiacoinInputs(txnID, txn); err != nil { return fmt.Errorf("failed to add siacoin inputs: %w", err) diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index ad66a09..ca7e5aa 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -2,6 +2,7 @@ package sqlite_test import ( "errors" + "math" "math/bits" "path/filepath" "reflect" @@ -572,6 +573,29 @@ func TestSendTransactions(t *testing.T) { check(t, "value", e.sfValue, sfo.SiafundOutput.Value) } } + + { + addr1Txns, err := db.AddressTransactions(addr1, math.MaxInt64, 0) + if err != nil { + panic(err) + } + // miner payment plus any of the transactions from this loop + check(t, "addr1 txns", 1+1+i, len(addr1Txns)) + + addr2Txns, err := db.AddressTransactions(addr2, math.MaxInt64, 0) + if err != nil { + panic(err) + } + // should only have transactions generated in this loop + check(t, "addr2 txns", 1+i, len(addr2Txns)) + + addr3Txns, err := db.AddressTransactions(addr3, math.MaxInt64, 0) + if err != nil { + panic(err) + } + // should only have transactions generated in this loop + check(t, "addr3 txns", 1+i, len(addr3Txns)) + } } } diff --git a/persist/sqlite/init.sql b/persist/sqlite/init.sql index 11e2617..0454713 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 b3040db..17f1338 100644 --- a/persist/sqlite/transactions.go +++ b/persist/sqlite/transactions.go @@ -417,3 +417,32 @@ func (s *Store) Transactions(ids []types.TransactionID) (results []explorer.Tran }) return } + +// AddressTransactions implements explorer.Store. +func (s *Store) AddressTransactions(addr types.Address, limit, offset uint64) (results []types.TransactionID, err error) { + err = s.transaction(func(tx txn) error { + query := `SELECT transactions.transaction_id +FROM transaction_addresses +INNER JOIN transactions ON (transactions.id = transaction_addresses.transaction_id) +WHERE transaction_addresses.address = ? +ORDER BY transactions.transaction_id ASC +LIMIT ? OFFSET ? +` + + rows, err := tx.Query(query, dbEncode(addr), limit, offset) + if err != nil { + return fmt.Errorf("failed to get txn IDs: %w", err) + } + defer rows.Close() + + for rows.Next() { + var id types.TransactionID + if err := rows.Scan(dbDecode(&id)); err != nil { + return fmt.Errorf("failed to scan txn ID: %w", err) + } + results = append(results, id) + } + return nil + }) + return +}