diff --git a/waddrmgr/scoped_manager.go b/waddrmgr/scoped_manager.go index dfa7a8c..0f6ee3a 100644 --- a/waddrmgr/scoped_manager.go +++ b/waddrmgr/scoped_manager.go @@ -818,33 +818,46 @@ func (s *ScopedKeyManager) nextAddresses(ns walletdb.ReadWriteBucket, } } - // Finally update the next address tracking and add the addresses to - // the cache after the newly generated addresses have been successfully - // added to the db. managedAddresses := make([]ManagedAddress, 0, len(addressInfo)) for _, info := range addressInfo { ma := info.managedAddr - s.addrs[addrKey(ma.Address().ScriptAddress())] = ma - - // Add the new managed address to the list of addresses that - // need their private keys derived when the address manager is - // next unlocked. - if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { - s.deriveOnUnlock = append(s.deriveOnUnlock, info) - } - managedAddresses = append(managedAddresses, ma) } - // Set the last address and next address for tracking. - ma := addressInfo[len(addressInfo)-1].managedAddr - if internal { - acctInfo.nextInternalIndex = nextIndex - acctInfo.lastInternalAddr = ma - } else { - acctInfo.nextExternalIndex = nextIndex - acctInfo.lastExternalAddr = ma + // Finally, create a closure that will update the next address tracking + // and add the addresses to the cache after the newly generated + // addresses have been successfully committed to the db. + onCommit := func() { + // Since this closure will be called when the DB transaction + // gets committed, we won't longer be holding the manager's + // mutex at that point. We must therefore re-acquire it before + // continuing. + s.mtx.Lock() + defer s.mtx.Unlock() + + for _, info := range addressInfo { + ma := info.managedAddr + s.addrs[addrKey(ma.Address().ScriptAddress())] = ma + + // Add the new managed address to the list of addresses + // that need their private keys derived when the + // address manager is next unlocked. + if s.rootManager.IsLocked() && !s.rootManager.WatchOnly() { + s.deriveOnUnlock = append(s.deriveOnUnlock, info) + } + } + + // Set the last address and next address for tracking. + ma := addressInfo[len(addressInfo)-1].managedAddr + if internal { + acctInfo.nextInternalIndex = nextIndex + acctInfo.lastInternalAddr = ma + } else { + acctInfo.nextExternalIndex = nextIndex + acctInfo.lastExternalAddr = ma + } } + ns.Tx().OnCommit(onCommit) return managedAddresses, nil } diff --git a/wallet/createtx.go b/wallet/createtx.go index a9580cd..9fa0973 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -101,60 +101,77 @@ func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) { // UTXO set and minconf policy. An additional output may be added to return // change to the wallet. An appropriate fee is included based on the wallet's // current relay fee. The wallet must be unlocked to create the transaction. +// +// NOTE: The dryRun argument can be set true to create a tx that doesn't alter +// the database. A tx created with this set to true will intentionally have no +// input scripts added and SHOULD NOT be broadcasted. func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, - minconf int32, feeSatPerKb btcutil.Amount) (tx *txauthor.AuthoredTx, err error) { + minconf int32, feeSatPerKb btcutil.Amount, dryRun bool) ( + tx *txauthor.AuthoredTx, err error) { chainClient, err := w.requireChainClient() if err != nil { return nil, err } - err = walletdb.Update(w.db, func(dbtx walletdb.ReadWriteTx) error { - addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + dbtx, err := w.db.BeginReadWriteTx() + if err != nil { + return nil, err + } + defer dbtx.Rollback() - // Get current block's height and hash. - bs, err := chainClient.BlockStamp() + addrmgrNs := dbtx.ReadWriteBucket(waddrmgrNamespaceKey) + + // Get current block's height and hash. + bs, err := chainClient.BlockStamp() + if err != nil { + return nil, err + } + + eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) + if err != nil { + return nil, err + } + + inputSource := makeInputSource(eligible) + changeSource := func() ([]byte, error) { + // Derive the change output script. As a hack to allow + // spending from the imported account, change addresses are + // created from account 0. + var changeAddr btcutil.Address + var err error + if account == waddrmgr.ImportedAddrAccount { + changeAddr, err = w.newChangeAddress(addrmgrNs, 0) + } else { + changeAddr, err = w.newChangeAddress(addrmgrNs, account) + } if err != nil { - return err + return nil, err } + return txscript.PayToAddrScript(changeAddr) + } + tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, + inputSource, changeSource) + if err != nil { + return nil, err + } - eligible, err := w.findEligibleOutputs(dbtx, account, minconf, bs) - if err != nil { - return err - } + // Randomize change position, if change exists, before signing. This + // doesn't affect the serialize size, so the change amount will still + // be valid. + if tx.ChangeIndex >= 0 { + tx.RandomizeChangePosition() + } - inputSource := makeInputSource(eligible) - changeSource := func() ([]byte, error) { - // Derive the change output script. As a hack to allow - // spending from the imported account, change addresses - // are created from account 0. - var changeAddr btcutil.Address - var err error - if account == waddrmgr.ImportedAddrAccount { - changeAddr, err = w.newChangeAddress(addrmgrNs, 0) - } else { - changeAddr, err = w.newChangeAddress(addrmgrNs, account) - } - if err != nil { - return nil, err - } - return txscript.PayToAddrScript(changeAddr) - } - tx, err = txauthor.NewUnsignedTransaction(outputs, feeSatPerKb, - inputSource, changeSource) - if err != nil { - return err - } + // If a dry run was requested, we return now before adding the input + // scripts, and don't commit the database transaction. The DB will be + // rolled back when this method returns to ensure the dry run didn't + // alter the DB in any way. + if dryRun { + return tx, nil + } - // Randomize change position, if change exists, before signing. - // This doesn't affect the serialize size, so the change amount - // will still be valid. - if tx.ChangeIndex >= 0 { - tx.RandomizeChangePosition() - } - - return tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) - }) + err = tx.AddAllInputScripts(secretSource{w.Manager, addrmgrNs}) if err != nil { return nil, err } @@ -164,6 +181,10 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, return nil, err } + if err := dbtx.Commit(); err != nil { + return nil, err + } + if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount { changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) log.Warnf("Spend from imported account produced change: moving"+ diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go new file mode 100644 index 0000000..1ffccf3 --- /dev/null +++ b/wallet/createtx_test.go @@ -0,0 +1,204 @@ +// Copyright (c) 2018 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package wallet + +import ( + "bytes" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +// TestTxToOutput checks that no new address is added to he database if we +// request a dry run of the txToOutputs call. It also makes sure a subsequent +// non-dry run call produces a similar transaction to the dry-run. +func TestTxToOutputsDryRun(t *testing.T) { + // Set up a wallet. + dir, err := ioutil.TempDir("", "createtx_test") + if err != nil { + t.Fatalf("Failed to create db dir: %v", err) + } + defer os.RemoveAll(dir) + + seed, err := hdkeychain.GenerateSeed(hdkeychain.MinSeedBytes) + if err != nil { + t.Fatalf("unable to create seed: %v", err) + } + + pubPass := []byte("hello") + privPass := []byte("world") + + loader := NewLoader(&chaincfg.TestNet3Params, dir, 250) + w, err := loader.CreateNewWallet(pubPass, privPass, seed, time.Now()) + if err != nil { + t.Fatalf("unable to create wallet: %v", err) + } + chainClient := &mockChainClient{} + w.chainClient = chainClient + if err := w.Unlock(privPass, time.After(10*time.Minute)); err != nil { + t.Fatalf("unable to unlock wallet: %v", err) + } + + // Create an address we can use to send some coins to. + addr, err := w.CurrentAddress(0, waddrmgr.KeyScopeBIP0044) + if err != nil { + t.Fatalf("unable to get current address: %v", addr) + } + p2shAddr, err := txscript.PayToAddrScript(addr) + if err != nil { + t.Fatalf("unable to convert wallet address to p2sh: %v", err) + } + + // Add an output paying to the wallet's address to the database. + txOut := wire.NewTxOut(100000, p2shAddr) + incomingTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + {}, + }, + TxOut: []*wire.TxOut{ + txOut, + }, + } + + var b bytes.Buffer + if err := incomingTx.Serialize(&b); err != nil { + t.Fatalf("unable to serialize tx: %v", err) + } + txBytes := b.Bytes() + + rec, err := wtxmgr.NewTxRecord(txBytes, time.Now()) + if err != nil { + t.Fatalf("unable to create tx record: %v", err) + } + + // The block meta will be inserted to tell the wallet this is a + // confirmed transaction. + blockHash, _ := chainhash.NewHashFromStr( + "00000000000000017188b968a371bab95aa43522665353b646e41865abae02a4") + block := &wtxmgr.BlockMeta{ + Block: wtxmgr.Block{Hash: *blockHash, Height: 276425}, + Time: time.Unix(1387737310, 0), + } + + if err := walletdb.Update(w.db, func(tx walletdb.ReadWriteTx) error { + ns := tx.ReadWriteBucket(wtxmgrNamespaceKey) + err = w.TxStore.InsertTx(ns, rec, block) + if err != nil { + return err + } + err = w.TxStore.AddCredit(ns, rec, block, 0, false) + if err != nil { + return err + } + return nil + }); err != nil { + t.Fatalf("failed inserting tx: %v", err) + } + + // Now tell the wallet to create a transaction paying to the specified + // outputs. + txOuts := []*wire.TxOut{ + { + PkScript: p2shAddr, + Value: 10000, + }, + { + PkScript: p2shAddr, + Value: 20000, + }, + } + + // First do a few dry-runs, making sure the number of addresses in the + // database us not inflated. + dryRunTx, err := w.txToOutputs(txOuts, 0, 1, 1000, true) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change := dryRunTx.Tx.TxOut[dryRunTx.ChangeIndex] + + addresses, err := w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 1 { + t.Fatalf("expected 1 address, found %v", len(addresses)) + } + + dryRunTx2, err := w.txToOutputs(txOuts, 0, 1, 1000, true) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change2 := dryRunTx2.Tx.TxOut[dryRunTx2.ChangeIndex] + + addresses, err = w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 1 { + t.Fatalf("expected 1 address, found %v", len(addresses)) + } + + // The two dry-run TXs should be invalid, since they don't have + // signatures. + err = validateMsgTx( + dryRunTx.Tx, dryRunTx.PrevScripts, dryRunTx.PrevInputValues, + ) + if err == nil { + t.Fatalf("Expected tx to be invalid") + } + + err = validateMsgTx( + dryRunTx2.Tx, dryRunTx2.PrevScripts, dryRunTx2.PrevInputValues, + ) + if err == nil { + t.Fatalf("Expected tx to be invalid") + } + + // Now we do a proper, non-dry run. This should add a change address + // to the database. + tx, err := w.txToOutputs(txOuts, 0, 1, 1000, false) + if err != nil { + t.Fatalf("unable to author tx: %v", err) + } + change3 := tx.Tx.TxOut[tx.ChangeIndex] + + addresses, err = w.AccountAddresses(0) + if err != nil { + t.Fatalf("unable to get addresses: %v", err) + } + + if len(addresses) != 2 { + t.Fatalf("expected 2 addresses, found %v", len(addresses)) + } + + err = validateMsgTx(tx.Tx, tx.PrevScripts, tx.PrevInputValues) + if err != nil { + t.Fatalf("Expected tx to be valid: %v", err) + } + + // Finally, we check that all the transaction were using the same + // change address. + if !bytes.Equal(change.PkScript, change2.PkScript) { + t.Fatalf("first dry-run using different change address " + + "than second") + } + if !bytes.Equal(change2.PkScript, change3.PkScript) { + t.Fatalf("dry-run using different change address " + + "than wet run") + } +} diff --git a/wallet/mock.go b/wallet/mock.go new file mode 100644 index 0000000..a626515 --- /dev/null +++ b/wallet/mock.go @@ -0,0 +1,81 @@ +package wallet + +import ( + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +type mockChainClient struct { +} + +var _ chain.Interface = (*mockChainClient)(nil) + +func (m *mockChainClient) Start() error { + return nil +} + +func (m *mockChainClient) Stop() { +} + +func (m *mockChainClient) WaitForShutdown() {} + +func (m *mockChainClient) GetBestBlock() (*chainhash.Hash, int32, error) { + return nil, 0, nil +} + +func (m *mockChainClient) GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) { + return nil, nil +} + +func (m *mockChainClient) GetBlockHash(int64) (*chainhash.Hash, error) { + return nil, nil +} + +func (m *mockChainClient) GetBlockHeader(*chainhash.Hash) (*wire.BlockHeader, + error) { + return nil, nil +} + +func (m *mockChainClient) FilterBlocks(*chain.FilterBlocksRequest) ( + *chain.FilterBlocksResponse, error) { + return nil, nil +} + +func (m *mockChainClient) BlockStamp() (*waddrmgr.BlockStamp, error) { + return &waddrmgr.BlockStamp{ + Height: 500000, + Hash: chainhash.Hash{}, + Timestamp: time.Unix(1234, 0), + }, nil +} + +func (m *mockChainClient) SendRawTransaction(*wire.MsgTx, bool) ( + *chainhash.Hash, error) { + return nil, nil +} + +func (m *mockChainClient) Rescan(*chainhash.Hash, []btcutil.Address, + map[wire.OutPoint]btcutil.Address) error { + return nil +} + +func (m *mockChainClient) NotifyReceived([]btcutil.Address) error { + return nil +} + +func (m *mockChainClient) NotifyBlocks() error { + return nil +} + +func (m *mockChainClient) Notifications() <-chan interface{} { + return nil +} + +func (m *mockChainClient) BackEnd() string { + return "mock" +} diff --git a/wallet/wallet.go b/wallet/wallet.go index b823d90..f6283d9 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -1148,6 +1148,7 @@ type ( outputs []*wire.TxOut minconf int32 feeSatPerKB btcutil.Amount + dryRun bool resp chan createTxResponse } createTxResponse struct { @@ -1178,7 +1179,7 @@ out: continue } tx, err := w.txToOutputs(txr.outputs, txr.account, - txr.minconf, txr.feeSatPerKB) + txr.minconf, txr.feeSatPerKB, txr.dryRun) heldUnlock.release() txr.resp <- createTxResponse{tx, err} case <-quit: @@ -1189,19 +1190,24 @@ out: } // CreateSimpleTx creates a new signed transaction spending unspent P2PKH -// outputs with at laest minconf confirmations spending to any number of +// outputs with at least minconf confirmations spending to any number of // address/amount pairs. Change and an appropriate transaction fee are // automatically included, if necessary. All transaction creation through this // function is serialized to prevent the creation of many transactions which // spend the same outputs. +// +// NOTE: The dryRun argument can be set true to create a tx that doesn't alter +// the database. A tx created with this set to true SHOULD NOT be broadcasted. func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut, - minconf int32, satPerKb btcutil.Amount) (*txauthor.AuthoredTx, error) { + minconf int32, satPerKb btcutil.Amount, dryRun bool) ( + *txauthor.AuthoredTx, error) { req := createTxRequest{ account: account, outputs: outputs, minconf: minconf, feeSatPerKB: satPerKb, + dryRun: dryRun, resp: make(chan createTxResponse), } w.createTxRequests <- req @@ -3209,7 +3215,9 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, // transaction will be added to the database in order to ensure that we // continue to re-broadcast the transaction upon restarts until it has // been confirmed. - createdTx, err := w.CreateSimpleTx(account, outputs, minconf, satPerKb) + createdTx, err := w.CreateSimpleTx( + account, outputs, minconf, satPerKb, false, + ) if err != nil { return nil, err } diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go index 86ba173..bf34e7a 100644 --- a/walletdb/bdb/db.go +++ b/walletdb/bdb/db.go @@ -86,7 +86,7 @@ func (tx *transaction) DeleteTopLevelBucket(key []byte) error { // Commit commits all changes that have been made through the root bucket and // all of its sub-buckets to persistent storage. // -// This function is part of the walletdb.Tx interface implementation. +// This function is part of the walletdb.ReadWriteTx interface implementation. func (tx *transaction) Commit() error { return convertErr(tx.boltTx.Commit()) } @@ -94,11 +94,19 @@ func (tx *transaction) Commit() error { // Rollback undoes all changes that have been made to the root bucket and all of // its sub-buckets. // -// This function is part of the walletdb.Tx interface implementation. +// This function is part of the walletdb.ReadTx interface implementation. func (tx *transaction) Rollback() error { return convertErr(tx.boltTx.Rollback()) } +// OnCommit takes a function closure that will be executed when the transaction +// successfully gets committed. +// +// This function is part of the walletdb.ReadWriteTx interface implementation. +func (tx *transaction) OnCommit(f func()) { + tx.boltTx.OnCommit(f) +} + // bucket is an internal type used to represent a collection of key/value pairs // and implements the walletdb Bucket interfaces. type bucket bbolt.Bucket @@ -128,7 +136,7 @@ func (b *bucket) NestedReadBucket(key []byte) walletdb.ReadBucket { // if the key is empty, or ErrIncompatibleValue if the key value is otherwise // invalid. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) CreateBucket(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bbolt.Bucket)(b).CreateBucket(key) if err != nil { @@ -141,7 +149,7 @@ func (b *bucket) CreateBucket(key []byte) (walletdb.ReadWriteBucket, error) { // given key if it does not already exist. Returns ErrBucketNameRequired if the // key is empty or ErrIncompatibleValue if the key value is otherwise invalid. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.ReadWriteBucket, error) { boltBucket, err := (*bbolt.Bucket)(b).CreateBucketIfNotExists(key) if err != nil { @@ -154,7 +162,7 @@ func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.ReadWriteBucket, // ErrTxNotWritable if attempted against a read-only transaction and // ErrBucketNotFound if the specified bucket does not exist. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) DeleteNestedBucket(key []byte) error { return convertErr((*bbolt.Bucket)(b).DeleteBucket(key)) } @@ -167,7 +175,7 @@ func (b *bucket) DeleteNestedBucket(key []byte) error { // transaction. Attempting to access them after a transaction has ended will // likely result in an access violation. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadBucket interface implementation. func (b *bucket) ForEach(fn func(k, v []byte) error) error { return convertErr((*bbolt.Bucket)(b).ForEach(fn)) } @@ -176,7 +184,7 @@ func (b *bucket) ForEach(fn func(k, v []byte) error) error { // already exist are added and keys that already exist are overwritten. Returns // ErrTxNotWritable if attempted against a read-only transaction. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) Put(key, value []byte) error { return convertErr((*bbolt.Bucket)(b).Put(key, value)) } @@ -188,7 +196,7 @@ func (b *bucket) Put(key, value []byte) error { // transaction. Attempting to access it after a transaction has ended // will likely result in an access violation. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadBucket interface implementation. func (b *bucket) Get(key []byte) []byte { return (*bbolt.Bucket)(b).Get(key) } @@ -197,7 +205,7 @@ func (b *bucket) Get(key []byte) []byte { // not exist does not return an error. Returns ErrTxNotWritable if attempted // against a read-only transaction. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) Delete(key []byte) error { return convertErr((*bbolt.Bucket)(b).Delete(key)) } @@ -209,11 +217,20 @@ func (b *bucket) ReadCursor() walletdb.ReadCursor { // ReadWriteCursor returns a new cursor, allowing for iteration over the bucket's // key/value pairs and nested buckets in forward or backward order. // -// This function is part of the walletdb.Bucket interface implementation. +// This function is part of the walletdb.ReadWriteBucket interface implementation. func (b *bucket) ReadWriteCursor() walletdb.ReadWriteCursor { return (*cursor)((*bbolt.Bucket)(b).Cursor()) } +// Tx returns the bucket's transaction. +// +// This function is part of the walletdb.ReadWriteBucket interface implementation. +func (b *bucket) Tx() walletdb.ReadWriteTx { + return &transaction{ + (*bbolt.Bucket)(b).Tx(), + } +} + // cursor represents a cursor over key/value pairs and nested buckets of a // bucket. // @@ -228,35 +245,35 @@ type cursor bbolt.Cursor // transaction, or ErrIncompatibleValue if attempted when the cursor points to a // nested bucket. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadWriteCursor interface implementation. func (c *cursor) Delete() error { return convertErr((*bbolt.Cursor)(c).Delete()) } // First positions the cursor at the first key/value pair and returns the pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) First() (key, value []byte) { return (*bbolt.Cursor)(c).First() } // Last positions the cursor at the last key/value pair and returns the pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Last() (key, value []byte) { return (*bbolt.Cursor)(c).Last() } // Next moves the cursor one key/value pair forward and returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Next() (key, value []byte) { return (*bbolt.Cursor)(c).Next() } // Prev moves the cursor one key/value pair backward and returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Prev() (key, value []byte) { return (*bbolt.Cursor)(c).Prev() } @@ -264,7 +281,7 @@ func (c *cursor) Prev() (key, value []byte) { // Seek positions the cursor at the passed seek key. If the key does not exist, // the cursor is moved to the next key after seek. Returns the new pair. // -// This function is part of the walletdb.Cursor interface implementation. +// This function is part of the walletdb.ReadCursor interface implementation. func (c *cursor) Seek(seek []byte) (key, value []byte) { return (*bbolt.Cursor)(c).Seek(seek) } diff --git a/walletdb/interface.go b/walletdb/interface.go index 754e08b..f89fa91 100644 --- a/walletdb/interface.go +++ b/walletdb/interface.go @@ -42,6 +42,10 @@ type ReadWriteTx interface { // Commit commits all changes that have been on the transaction's root // buckets and all of their sub-buckets to persistent storage. Commit() error + + // OnCommit takes a function closure that will be executed when the + // transaction successfully gets committed. + OnCommit(func()) } // ReadBucket represents a bucket (a hierarchical structure within the database) @@ -119,6 +123,9 @@ type ReadWriteBucket interface { // Cursor returns a new cursor, allowing for iteration over the bucket's // key/value pairs and nested buckets in forward or backward order. ReadWriteCursor() ReadWriteCursor + + // Tx returns the bucket's transaction. + Tx() ReadWriteTx } // ReadCursor represents a bucket cursor that can be positioned at the start or