From b6ba2cd2671dddf4b4fd343a4805fbd4afdba2a7 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 16 May 2026 19:22:24 +0000 Subject: [PATCH 1/5] btc: return transaction sync errors --- backend/coins/btc/account.go | 5 +- .../btc/transactions/mocks/transactions.go | 8 +- .../coins/btc/transactions/transactions.go | 123 ++++++++++++------ .../btc/transactions/transactions_test.go | 2 +- .../coins/btc/transactions/verification.go | 4 +- 5 files changed, 91 insertions(+), 51 deletions(-) diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index 8efc6cc346..ebefc7d0fe 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -672,7 +672,10 @@ func (account *Account) onAddressStatus(address *addresses.AccountAddress, statu return } - account.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), history) + if err := account.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), history); err != nil { + account.reportFatalSyncError(err, "UpdateAddressHistory failed") + return + } account.incAndEmitSyncCounter() account.ensureAddresses() } diff --git a/backend/coins/btc/transactions/mocks/transactions.go b/backend/coins/btc/transactions/mocks/transactions.go index ddd81f91a0..00f936c149 100644 --- a/backend/coins/btc/transactions/mocks/transactions.go +++ b/backend/coins/btc/transactions/mocks/transactions.go @@ -33,7 +33,7 @@ var _ transactions.Interface = &InterfaceMock{} // TransactionsFunc: func(isChange func(blockchain.ScriptHashHex) bool) (accounts.OrderedTransactions, error) { // panic("mock out the Transactions method") // }, -// UpdateAddressHistoryFunc: func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +// UpdateAddressHistoryFunc: func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { // panic("mock out the UpdateAddressHistory method") // }, // } @@ -56,7 +56,7 @@ type InterfaceMock struct { TransactionsFunc func(isChange func(blockchain.ScriptHashHex) bool) (accounts.OrderedTransactions, error) // UpdateAddressHistoryFunc mocks the UpdateAddressHistory method. - UpdateAddressHistoryFunc func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) + UpdateAddressHistoryFunc func(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error // calls tracks calls to the methods. calls struct { @@ -203,7 +203,7 @@ func (mock *InterfaceMock) TransactionsCalls() []struct { } // UpdateAddressHistory calls UpdateAddressHistoryFunc. -func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { if mock.UpdateAddressHistoryFunc == nil { panic("InterfaceMock.UpdateAddressHistoryFunc: method is nil but Interface.UpdateAddressHistory was just called") } @@ -217,7 +217,7 @@ func (mock *InterfaceMock) UpdateAddressHistory(scriptHashHex blockchain.ScriptH mock.lockUpdateAddressHistory.Lock() mock.calls.UpdateAddressHistory = append(mock.calls.UpdateAddressHistory, callInfo) mock.lockUpdateAddressHistory.Unlock() - mock.UpdateAddressHistoryFunc(scriptHashHex, txs) + return mock.UpdateAddressHistoryFunc(scriptHashHex, txs) } // UpdateAddressHistoryCalls gets all the calls that were made to UpdateAddressHistory. diff --git a/backend/coins/btc/transactions/transactions.go b/backend/coins/btc/transactions/transactions.go index e9247d6842..3b0392ddbf 100644 --- a/backend/coins/btc/transactions/transactions.go +++ b/backend/coins/btc/transactions/transactions.go @@ -58,7 +58,7 @@ type Interface interface { // UpdateAddressHistory should be called when initializing a wallet address, or when the history of // an address changes (a new transaction that touches it appears or disappears). The transactions // are downloaded and indexed. - UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) + UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error } // Transactions handles wallet transactions: keeping an index of the transactions, inputs, (unspent) @@ -195,14 +195,14 @@ func (transactions *Transactions) processTxForAddress( tx *wire.MsgTx, height int, headerTimestamp *time.Time, -) { +) error { txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve tx info") + return errp.WithMessage(err, "failed to retrieve tx info") } if err := dbTx.PutTx(txHash, tx, height, headerTimestamp); err != nil { - transactions.log.WithError(err).Panic("Failed to put tx") + return errp.WithMessage(err, "failed to put tx") } if err := transactions.notifier.Put(txHash[:]); err != nil { @@ -216,9 +216,9 @@ func (transactions *Transactions) processTxForAddress( } if err := dbTx.AddAddressToTx(txHash, scriptHashHex); err != nil { - transactions.log.WithError(err).Panic("Failed to add address to tx") + return errp.WithMessage(err, "failed to add address to tx") } - transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx) + return transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx) } // Go through the tx and extract all inputs and outputs which touch the address. @@ -226,7 +226,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash, - tx *wire.MsgTx) { + tx *wire.MsgTx) error { // Gather transaction inputs that spend outputs of the given address. for _, txIn := range tx.TxIn { // Since transactions can be processed in any order, and we might process the same tx @@ -235,7 +235,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( // since the output that it spends might be indexed later. txInTxHash, err := dbTx.Input(txIn.PreviousOutPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve input from previous outpoint") + return errp.WithMessage(err, "failed to retrieve input from previous outpoint") } if txInTxHash != nil && *txInTxHash != txHash { transactions.log.WithFields(logrus.Fields{"txIn.PreviousOutPoint": txIn.PreviousOutPoint, @@ -243,7 +243,7 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( Warning("Double spend detected") } if err := dbTx.PutInput(txIn.PreviousOutPoint, txHash); err != nil { - transactions.log.WithError(err).Panic("Failed to store the transaction input") + return errp.WithMessage(err, "failed to store the transaction input") } } // Gather transaction outputs that belong to us. @@ -255,23 +255,24 @@ func (transactions *Transactions) processInputsAndOutputsForAddress( txOut, ) if err != nil { - transactions.log.WithError(err).Panic("Failed to store the transaction output") + return errp.WithMessage(err, "failed to store the transaction output") } } } + return nil } -func (transactions *Transactions) allInputsOurs(dbTx DBTxInterface, transaction *wire.MsgTx) bool { +func (transactions *Transactions) allInputsOurs(dbTx DBTxInterface, transaction *wire.MsgTx) (bool, error) { for _, txIn := range transaction.TxIn { txOut, err := dbTx.Output(txIn.PreviousOutPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve output") + return false, errp.WithMessage(err, "failed to retrieve output") } if txOut == nil { - return false + return false, nil } } - return true + return true, nil } // SpendableOutputs returns all unspent outputs of the wallet which are eligible to be spent. Those @@ -285,7 +286,10 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab } result := map[wire.OutPoint]*SpendableOutput{} for outPoint, txOut := range outputs { - spent := transactions.isInputSpent(dbTx, outPoint) + spent, err := transactions.isInputSpent(dbTx, outPoint) + if err != nil { + return nil, err + } if !spent { txInfo, err := dbTx.TxInfo(outPoint.Hash) if err != nil { @@ -293,7 +297,15 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab } confirmed := txInfo.Height > 0 - if confirmed || transactions.allInputsOurs(dbTx, txInfo.Tx) { + spendable := confirmed + if !spendable { + allInputsOurs, err := transactions.allInputsOurs(dbTx, txInfo.Tx) + if err != nil { + return nil, err + } + spendable = allInputsOurs + } + if spendable { result[outPoint] = &SpendableOutput{ TxOut: txOut, HeaderTimestamp: txInfo.HeaderTimestamp, @@ -305,31 +317,31 @@ func (transactions *Transactions) SpendableOutputs() (map[wire.OutPoint]*Spendab }) } -func (transactions *Transactions) isInputSpent(dbTx DBTxInterface, outPoint wire.OutPoint) bool { +func (transactions *Transactions) isInputSpent(dbTx DBTxInterface, outPoint wire.OutPoint) (bool, error) { input, err := dbTx.Input(outPoint) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve input for outPoint") + return false, errp.WithMessage(err, "failed to retrieve input for outPoint") } - return input != nil + return input != nil, nil } func (transactions *Transactions) removeTxForAddress( - dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) { + dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) error { transactions.log.Debug("Remove transaction for address") txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve tx info") + return errp.WithMessage(err, "failed to retrieve tx info") } if txInfo == nil { // Not yet indexed. transactions.log.Debug("Transaction hash not listed") - return + return nil } transactions.log.Debug("Deleting transaction address") empty, err := dbTx.RemoveAddressFromTx(txHash, scriptHashHex) if err != nil { - transactions.log.WithError(err).Panic("Failed to remove address from tx") + return errp.WithMessage(err, "failed to remove address from tx") } if empty { // Tx is not touching any of our outputs anymore. Remove. @@ -352,15 +364,16 @@ func (transactions *Transactions) removeTxForAddress( transactions.log.WithError(err).Error("Failed notifier.Delete") } } + return nil } // UpdateAddressHistory should be called when initializing a wallet address, or when the history of // an address changes (a new transaction that touches it appears or disappears). The transactions // are downloaded and indexed. -func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) { +func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain.ScriptHashHex, txs []*blockchain.TxInfo) error { if transactions.isClosed() { transactions.log.Debug("UpdateAddressHistory after the instance was closed") - return + return nil } err := DBUpdate(transactions.db, func(dbTx DBTxInterface) error { txsSet := map[chainhash.Hash]struct{}{} @@ -381,7 +394,9 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. // A tx was previously in the address history but is not anymore. If the tx was already // downloaded and indexed, it will be removed. If it is currently downloading (enqueued for // indexing), it will not be processed. - transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()) + if err := transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()); err != nil { + return err + } } if err := dbTx.PutAddressHistory(scriptHashHex, txs); err != nil { @@ -390,14 +405,22 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. for _, txInfo := range txs { txHash := txInfo.TXHash.Hash() height := txInfo.Height - tx, headerTimestamp := transactions.getTransactionCached(dbTx, txHash, height) - transactions.processTxForAddress(dbTx, scriptHashHex, txHash, tx, height, headerTimestamp) + tx, headerTimestamp, err := transactions.getTransactionCached(dbTx, txHash, height) + if err != nil { + return err + } + if err := transactions.processTxForAddress( + dbTx, scriptHashHex, txHash, tx, height, headerTimestamp, + ); err != nil { + return err + } } return nil }) if err != nil { - transactions.log.WithError(err).Panic("Failed to update address history") + return errp.WithMessage(err, "failed to update address history") } + return nil } // getTransactionCached requires transactions lock. @@ -405,22 +428,22 @@ func (transactions *Transactions) getTransactionCached( dbTx DBTxInterface, txHash chainhash.Hash, height int, -) (*wire.MsgTx, *time.Time) { +) (*wire.MsgTx, *time.Time, error) { txInfo, err := dbTx.TxInfo(txHash) if err != nil { - transactions.log.WithError(err).Panic("Failed to retrieve transaction info") + return nil, nil, errp.WithMessage(err, "failed to retrieve transaction info") } headerTimestamp := transactions.getCachedTimestampAtHeight(height, txInfo.HeaderTimestamp) if txInfo.Tx != nil { - return txInfo.Tx, headerTimestamp + return txInfo.Tx, headerTimestamp, nil } tx, err := transactions.blockchain.TransactionGet(txHash) if err != nil { - transactions.log.WithError(err).Panic("TransactionGet failed") + return nil, nil, errp.WithMessage(err, "TransactionGet failed") } - return tx, headerTimestamp + return tx, headerTimestamp, nil } // Balance computes the confirmed and unconfirmed balance of the account. @@ -433,7 +456,11 @@ func (transactions *Transactions) Balance() (*accounts.Balance, error) { var available, incoming int64 for outPoint, txOut := range outputs { // What is spent can not be available nor incoming. - if spent := transactions.isInputSpent(dbTx, outPoint); spent { + spent, err := transactions.isInputSpent(dbTx, outPoint) + if err != nil { + return nil, err + } + if spent { continue } txInfo, err := dbTx.TxInfo(outPoint.Hash) @@ -441,7 +468,15 @@ func (transactions *Transactions) Balance() (*accounts.Balance, error) { return nil, err } confirmed := txInfo.Height > 0 - if confirmed || transactions.allInputsOurs(dbTx, txInfo.Tx) { + availableOutput := confirmed + if !availableOutput { + allInputsOurs, err := transactions.allInputsOurs(dbTx, txInfo.Tx) + if err != nil { + return nil, err + } + availableOutput = allInputsOurs + } + if availableOutput { available += txOut.Value } else { incoming += txOut.Value @@ -464,15 +499,14 @@ func (transactions *Transactions) outputToAddress(pkScript []byte) string { func (transactions *Transactions) txInfo( dbTx DBTxInterface, txInfo *DBTxInfo, - isChange func(blockchain.ScriptHashHex) bool) *accounts.TransactionData { + isChange func(blockchain.ScriptHashHex) bool) (*accounts.TransactionData, error) { var sumOurInputs btcutil.Amount var result btcutil.Amount allInputsOurs := true for _, txIn := range txInfo.Tx.TxIn { spentOut, err := dbTx.Output(txIn.PreviousOutPoint) if err != nil { - // TODO - transactions.log.WithError(err).Panic("Output() failed") + return nil, errp.WithMessage(err, "Output() failed") } if spentOut != nil { sumOurInputs += btcutil.Amount(spentOut.Value) @@ -491,8 +525,7 @@ func (transactions *Transactions) txInfo( Index: uint32(index), }) if err != nil { - // TODO - transactions.log.WithError(err).Panic("Output() failed") + return nil, errp.WithMessage(err, "Output() failed") } addressAndAmount := accounts.AddressAndAmount{ Address: transactions.outputToAddress(txOut.PkScript), @@ -580,7 +613,7 @@ func (transactions *Transactions) txInfo( Weight: btcdBlockchain.GetTransactionWeight(btcutilTx), CreatedTimestamp: txInfo.CreatedTimestamp, IsErc20: false, - } + }, nil } // Transactions returns an ordered list of transactions. @@ -597,7 +630,11 @@ func (transactions *Transactions) Transactions( if err != nil { return nil, err } - txs = append(txs, transactions.txInfo(dbTx, txInfo, isChange)) + txData, err := transactions.txInfo(dbTx, txInfo, isChange) + if err != nil { + return nil, err + } + txs = append(txs, txData) } return accounts.NewOrderedTransactions(txs), nil }) diff --git a/backend/coins/btc/transactions/transactions_test.go b/backend/coins/btc/transactions/transactions_test.go index a4ecff863d..8238f009ab 100644 --- a/backend/coins/btc/transactions/transactions_test.go +++ b/backend/coins/btc/transactions/transactions_test.go @@ -134,7 +134,7 @@ func (s *transactionsSuite) updateAddressHistory( s.notifierMock.On("Put", tx.TXHash[:]).Return(nil).Once() } - s.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), txs) + s.Require().NoError(s.transactions.UpdateAddressHistory(address.PubkeyScriptHashHex(), txs)) } func newTx( diff --git a/backend/coins/btc/transactions/verification.go b/backend/coins/btc/transactions/verification.go index 0cb8c38330..b6b454a4ba 100644 --- a/backend/coins/btc/transactions/verification.go +++ b/backend/coins/btc/transactions/verification.go @@ -51,8 +51,8 @@ func hashMerkleRoot(merkle []blockchain.TXHash, start chainhash.Hash, pos int) c func (transactions *Transactions) verifyTransactions() { unverifiedTransactions, err := transactions.unverifiedTransactions() if err != nil { - // TODO - panic(err) + transactions.log.WithError(err).Error("unverifiedTransactions") + return } transactions.log.Debugf("verifying %d transactions", len(unverifiedTransactions)) for txHash, height := range unverifiedTransactions { From 12a3f1b9d49e97038f2f9c930b9f321608b78c4d Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 16 May 2026 19:23:33 +0000 Subject: [PATCH 2/5] btc: return transaction db errors --- .../btc/db/transactionsdb/transactionsdb.go | 34 +++++++++---------- .../db/transactionsdb/transactionsdb_test.go | 10 +++--- backend/coins/btc/transactions/db.go | 6 ++-- .../coins/btc/transactions/transactions.go | 14 +++++--- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/backend/coins/btc/db/transactionsdb/transactionsdb.go b/backend/coins/btc/db/transactionsdb/transactionsdb.go index 5d67a86f01..b2837127d9 100644 --- a/backend/coins/btc/db/transactionsdb/transactionsdb.go +++ b/backend/coins/btc/db/transactionsdb/transactionsdb.go @@ -155,16 +155,16 @@ func (tx *Tx) PutTx(txHash chainhash.Hash, msgTx *wire.MsgTx, height int, header return nil } -// DeleteTx implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteTx(txHash chainhash.Hash) { +// DeleteTx implements transactions.DBTxInterface. +func (tx *Tx) DeleteTx(txHash chainhash.Hash) error { bucketTransactions, err := tx.tx.CreateBucketIfNotExists([]byte(bucketTransactionsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketTransactions.Delete(txHash[:]); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // AddAddressToTx implements transactions.DBTxInterface. @@ -215,10 +215,10 @@ func (tx *Tx) UnverifiedTransactions() ([]chainhash.Hash, error) { func (tx *Tx) MarkTxVerified(txHash chainhash.Hash, headerTimestamp time.Time) error { bucketUnverifiedTransactions, err := tx.tx.CreateBucketIfNotExists([]byte(bucketUnverifiedTransactionsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketUnverifiedTransactions.Delete(txHash[:]); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } return tx.modifyTx(txHash[:], func(walletTx *transactions.DBTxInfo) { truth := true @@ -248,16 +248,16 @@ func (tx *Tx) Input(outPoint wire.OutPoint) (*chainhash.Hash, error) { return nil, nil } -// DeleteInput implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteInput(outPoint wire.OutPoint) { +// DeleteInput implements transactions.DBTxInterface. +func (tx *Tx) DeleteInput(outPoint wire.OutPoint) error { bucketInputs, err := tx.tx.CreateBucketIfNotExists([]byte(bucketInputsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketInputs.Delete([]byte(outPoint.String())); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // PutOutput implements transactions.DBTxInterface. @@ -308,16 +308,16 @@ func (tx *Tx) Outputs() (map[wire.OutPoint]*wire.TxOut, error) { return outputs, nil } -// DeleteOutput implements transactions.DBTxInterface. It panics if called from a read-only db -// transaction. -func (tx *Tx) DeleteOutput(outPoint wire.OutPoint) { +// DeleteOutput implements transactions.DBTxInterface. +func (tx *Tx) DeleteOutput(outPoint wire.OutPoint) error { bucketOutputs, err := tx.tx.CreateBucketIfNotExists([]byte(bucketOutputsKey)) if err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } if err := bucketOutputs.Delete([]byte(outPoint.String())); err != nil { - panic(errp.WithStack(err)) + return errp.WithStack(err) } + return nil } // PutAddressHistory implements transactions.DBTxInterface. diff --git a/backend/coins/btc/db/transactionsdb/transactionsdb_test.go b/backend/coins/btc/db/transactionsdb/transactionsdb_test.go index 24acda39c2..04343c710a 100644 --- a/backend/coins/btc/db/transactionsdb/transactionsdb_test.go +++ b/backend/coins/btc/db/transactionsdb/transactionsdb_test.go @@ -263,7 +263,7 @@ func TestTxQuick(t *testing.T) { require.True(t, !txInfo.CreatedTimestamp.After(now) || *txInfo.CreatedTimestamp == now) - tx.DeleteTx(txHash) + require.NoError(t, tx.DeleteTx(txHash)) delete(allTxHashes, txHash) require.True(t, checkTxHashes()) }) @@ -328,7 +328,7 @@ func TestInput(t *testing.T) { require.Nil(t, input) // no-op, does not exist yet - tx.DeleteInput(outpoint1) + require.NoError(t, tx.DeleteInput(outpoint1)) require.NoError(t, tx.PutInput(outpoint1, txhash1)) require.NoError(t, tx.PutInput(outpoint2, txhash2)) @@ -351,7 +351,7 @@ func TestInput(t *testing.T) { require.NoError(t, err) require.Equal(t, &txhash2, input) - tx.DeleteInput(outpoint1) + require.NoError(t, tx.DeleteInput(outpoint1)) input, err = tx.Input(outpoint1) require.NoError(t, err) require.Nil(t, input) @@ -376,7 +376,7 @@ func TestInputQuick(t *testing.T) { for _, outPoint := range allOutpoints { t.Run("", func(t *testing.T) { - tx.DeleteInput(outPoint) + require.NoError(t, tx.DeleteInput(outPoint)) txHash, err := tx.Input(outPoint) require.NoError(t, err) require.Nil(t, txHash) @@ -475,7 +475,7 @@ func TestOutputsQuick(t *testing.T) { for outPoint := range allOutputs { t.Run("", func(t *testing.T) { delete(allOutputs, outPoint) - tx.DeleteOutput(outPoint) + require.NoError(t, tx.DeleteOutput(outPoint)) require.True(t, checkOutputs()) output, err := tx.Output(outPoint) require.NoError(t, err) diff --git a/backend/coins/btc/transactions/db.go b/backend/coins/btc/transactions/db.go index 7abf281078..9f5aed96d8 100644 --- a/backend/coins/btc/transactions/db.go +++ b/backend/coins/btc/transactions/db.go @@ -41,7 +41,7 @@ type DBTxInterface interface { PutTx(txHash chainhash.Hash, tx *wire.MsgTx, height int, headerTimestamp *time.Time) error // DeleteTx deletes a transaction (nothing happens if not found). - DeleteTx(txHash chainhash.Hash) + DeleteTx(txHash chainhash.Hash) error // AddAddressToTx adds an address associated with a transaction. Retrieve them with `TxInfo()`. AddAddressToTx(chainhash.Hash, blockchain.ScriptHashHex) error @@ -69,7 +69,7 @@ type DBTxInterface interface { Input(wire.OutPoint) (*chainhash.Hash, error) // DeleteInput deletes an input (nothing happens if not found). - DeleteInput(wire.OutPoint) + DeleteInput(wire.OutPoint) error // PutOutput stores an Output. PutOutput(wire.OutPoint, *wire.TxOut) error @@ -79,7 +79,7 @@ type DBTxInterface interface { Outputs() (map[wire.OutPoint]*wire.TxOut, error) // DeleteOutput deletes an output (nothing happens if not found). - DeleteOutput(wire.OutPoint) + DeleteOutput(wire.OutPoint) error // PutAddressHistory stores an address history. PutAddressHistory(blockchain.ScriptHashHex, blockchain.TxHistory) error diff --git a/backend/coins/btc/transactions/transactions.go b/backend/coins/btc/transactions/transactions.go index 3b0392ddbf..2e618251f8 100644 --- a/backend/coins/btc/transactions/transactions.go +++ b/backend/coins/btc/transactions/transactions.go @@ -348,18 +348,24 @@ func (transactions *Transactions) removeTxForAddress( for _, txIn := range txInfo.Tx.TxIn { transactions.log.Debug("Deleting transaction iput") - dbTx.DeleteInput(txIn.PreviousOutPoint) + if err := dbTx.DeleteInput(txIn.PreviousOutPoint); err != nil { + return errp.WithMessage(err, "failed to delete input") + } } // Remove the outputs added by this tx. for index := range txInfo.Tx.TxOut { - dbTx.DeleteOutput(wire.OutPoint{ + if err := dbTx.DeleteOutput(wire.OutPoint{ Hash: txHash, Index: uint32(index), - }) + }); err != nil { + return errp.WithMessage(err, "failed to delete output") + } } - dbTx.DeleteTx(txHash) + if err := dbTx.DeleteTx(txHash); err != nil { + return errp.WithMessage(err, "failed to delete tx") + } if err := transactions.notifier.Delete(txHash[:]); err != nil { transactions.log.WithError(err).Error("Failed notifier.Delete") } From 4b0e507c52e7205313bf9d7c9ba9d99918282977 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 16 May 2026 19:26:49 +0000 Subject: [PATCH 3/5] btc: return header sync errors --- backend/coins/btc/account.go | 4 +- backend/coins/btc/coin.go | 110 +++++++++++-------- backend/coins/btc/coin_test.go | 25 ++++- backend/coins/btc/headers/headers.go | 31 +++--- backend/coins/btc/headers/headers_test.go | 2 +- backend/coins/btc/headers/mocks/Interface.go | 20 +++- backend/coins/coin/coin.go | 2 +- backend/coins/coin/mocks/coin.go | 8 +- backend/coins/eth/account.go | 4 +- backend/coins/eth/coin.go | 4 +- 10 files changed, 133 insertions(+), 77 deletions(-) diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index ebefc7d0fe..008a09eda1 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -318,7 +318,9 @@ func (account *Account) Initialize() error { account.log.Debug("Connection to blockchain backend established") } } - account.coin.Initialize() + if err := account.coin.Initialize(); err != nil { + return err + } account.SetOffline(account.coin.Blockchain().ConnectionError()) account.coin.Blockchain().RegisterOnConnectionErrorChangedEvent(onConnectionStatusChanged) theHeaders := account.coin.Headers() diff --git a/backend/coins/btc/coin.go b/backend/coins/btc/coin.go index 62f5e2c0c5..8063228e0a 100644 --- a/backend/coins/btc/coin.go +++ b/backend/coins/btc/coin.go @@ -30,9 +30,10 @@ import ( // Coin models a Bitcoin-related coin. type Coin struct { - initOnce sync.Once - code coinpkg.Code - name string + initLock sync.Mutex + initialized bool + code coinpkg.Code + name string // unit is the main unit of the coin, e.g. 'BTC' unit string // formatUnit keeps track of the unit used, e.g. 'BTC' or 'sat' depening on if sat mode is enabled @@ -93,63 +94,78 @@ func (coin *Coin) TstSetMakeBlockchain(f func() blockchain.Interface) { } // Initialize implements coinpkg.Coin. -func (coin *Coin) Initialize() { - coin.initOnce.Do(func() { - // Init blockchain - coin.blockchain = coin.makeBlockchain() +func (coin *Coin) Initialize() error { + coin.initLock.Lock() + defer coin.initLock.Unlock() + if coin.initialized { + return nil + } - // Init Headers + // Init blockchain + blockchain := coin.makeBlockchain() - // delete old db version (up to v4.10.0, bbolt was used): - oldDBFilename := path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.db", coin.code)) - if _, err := os.Stat(oldDBFilename); err == nil { - _ = os.Remove(oldDBFilename) - } + // Init Headers - db, err := headersdb.NewDB( - path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.bin", coin.code)), - coin.log) - if err != nil { - coin.log.WithError(err).Panic("Could not open headers DB") + // delete old db version (up to v4.10.0, bbolt was used): + oldDBFilename := path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.db", coin.code)) + if _, err := os.Stat(oldDBFilename); err == nil { + _ = os.Remove(oldDBFilename) + } + + db, err := headersdb.NewDB( + path.Join(coin.dbFolder, fmt.Sprintf("headers-%s.bin", coin.code)), + coin.log) + if err != nil { + blockchain.Close() + return errp.WithMessage(err, "could not open headers DB") + } + theHeaders := headers.NewHeaders( + coin.net, + db, + blockchain, + coin.log) + if err := theHeaders.Initialize(); err != nil { + if closeErr := db.Close(); closeErr != nil { + coin.log.WithError(closeErr).Error("could not close headers DB") } - coin.headers = headers.NewHeaders( - coin.net, - db, - coin.blockchain, - coin.log) - coin.headers.Initialize() - coin.headers.SubscribeEvent(func(event headers.Event) { - if event == headers.EventSyncing || event == headers.EventSynced { - status, err := coin.headers.Status() - if err != nil { - coin.log.WithError(err).Error("Could not get headers status") - coin.Notify(observable.Event{ - Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), - Action: action.Replace, - Object: struct { - Success bool `json:"success"` - ErrorMessage string `json:"errorMessage,omitempty"` - }{ - Success: false, - ErrorMessage: err.Error(), - }, - }) - return - } + blockchain.Close() + return errp.WithMessage(err, "could not initialize headers") + } + coin.blockchain = blockchain + coin.headers = theHeaders + coin.headers.SubscribeEvent(func(event headers.Event) { + if event == headers.EventSyncing || event == headers.EventSynced { + status, err := coin.headers.Status() + if err != nil { + coin.log.WithError(err).Error("Could not get headers status") coin.Notify(observable.Event{ Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), Action: action.Replace, Object: struct { - Success bool `json:"success"` - Status *headers.Status `json:"status"` + Success bool `json:"success"` + ErrorMessage string `json:"errorMessage,omitempty"` }{ - Success: true, - Status: status, + Success: false, + ErrorMessage: err.Error(), }, }) + return } - }) + coin.Notify(observable.Event{ + Subject: fmt.Sprintf("coins/%s/headers/status", coin.code), + Action: action.Replace, + Object: struct { + Success bool `json:"success"` + Status *headers.Status `json:"status"` + }{ + Success: true, + Status: status, + }, + }) + } }) + coin.initialized = true + return nil } // Name implements coinpkg.Coin. diff --git a/backend/coins/btc/coin_test.go b/backend/coins/btc/coin_test.go index d394252a11..905d18978b 100644 --- a/backend/coins/btc/coin_test.go +++ b/backend/coins/btc/coin_test.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "math/big" "os" + "path" "testing" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/errors" @@ -53,7 +54,7 @@ func (s *testSuite) SetupTest() { } s.coin.TstSetMakeBlockchain(func() blockchain.Interface { return blockchainMock }) - s.coin.Initialize() + s.Require().NoError(s.coin.Initialize()) } func (s *testSuite) TearDownTest() { @@ -67,6 +68,28 @@ func TestSuite(t *testing.T) { suite.Run(t, &testSuite{code: coin.CodeLTC, unit: "LTC", net: <c.MainNetParams}) } +func (s *testSuite) TestInitializeRetriesAfterError() { + dbFolder := path.Join(s.dbFolder, "missing") + btcCoin := NewCoin(s.code, "Some coin", s.unit, coin.BtcUnitDefault, s.net, dbFolder, nil, + explorer, addressExplorer, socksproxy.NewSocksProxy(false, "")) + closeCount := 0 + btcCoin.TstSetMakeBlockchain(func() blockchain.Interface { + return &blockchainMock.BlockchainMock{ + MockClose: func() { + closeCount++ + }, + MockHeadersSubscribe: func(func(*types.Header)) {}, + } + }) + + s.Require().Error(btcCoin.Initialize()) + s.Require().Equal(1, closeCount) + + s.Require().NoError(os.MkdirAll(dbFolder, 0700)) + s.Require().NoError(btcCoin.Initialize()) + s.Require().Equal(1, closeCount) +} + func (s *testSuite) TestCoin() { s.Require().Equal(s.net, s.coin.Net()) s.Require().Equal(s.code, s.coin.Code()) diff --git a/backend/coins/btc/headers/headers.go b/backend/coins/btc/headers/headers.go index dde32ee176..1b5846ccd0 100644 --- a/backend/coins/btc/headers/headers.go +++ b/backend/coins/btc/headers/headers.go @@ -82,9 +82,9 @@ const ( // Interface represents the public API of this package. // -//go:generate mockery -name Interface +//go:generate mockery --name Interface type Interface interface { - Initialize() + Initialize() error SubscribeEvent(f func(Event)) func() VerifiedHeaderByHeight(int) (*wire.BlockHeader, error) HeaderByHeight(int) (*wire.BlockHeader, error) @@ -180,8 +180,12 @@ func (headers *Headers) TipHeight() int { } // Initialize starts the syncing process. -func (headers *Headers) Initialize() { - headers.tipAtInitTime = headers.tip() +func (headers *Headers) Initialize() error { + tip, err := headers.db.Tip() + if err != nil { + return err + } + headers.tipAtInitTime = tip headers.log.Infof("last tip loaded: %d", headers.tipAtInitTime) go headers.download() go headers.blockchain.HeadersSubscribe( @@ -190,6 +194,7 @@ func (headers *Headers) Initialize() { }, ) headers.kickChan <- struct{}{} + return nil } func (headers *Headers) download() { @@ -370,15 +375,16 @@ func (headers *Headers) canConnect(db DBInterface, tip int, header *wire.BlockHe return nil } -func (headers *Headers) reorg(db DBInterface, tip int) { +func (headers *Headers) reorg(db DBInterface, tip int) error { // Simple reorg method: re-fetch headers up to the maximum reorg limit. The server can shorten // our chain by sending a fake header and set us back by `reorgLimit` blocks, but it needs to // contain the correct PoW to do so. newTip := max(tip-reorgLimit, -1) if err := db.RevertTo(newTip); err != nil { - panic(err) + return err } headers.kick() + return nil } func (headers *Headers) notifyEvent(event Event) { @@ -395,7 +401,9 @@ func (headers *Headers) processBatch( err := headers.canConnect(db, tip+1, header) if errp.Cause(err) == errPrevHash { headers.log.WithError(err).Infof("Reorg detected at height %d", tip+1) - headers.reorg(db, tip) + if err := headers.reorg(db, tip); err != nil { + return err + } return nil } if err != nil { @@ -463,15 +471,6 @@ func (headers *Headers) update(blockHeight int) { headers.notifyEvent(EventNewTip) } -func (headers *Headers) tip() int { - defer headers.lock.RLock()() - tip, err := headers.db.Tip() - if err != nil { - panic(err) - } - return tip -} - // Status returns the current sync status. func (headers *Headers) Status() (*Status, error) { defer headers.lock.RLock()() diff --git a/backend/coins/btc/headers/headers_test.go b/backend/coins/btc/headers/headers_test.go index ee44430787..aef5e0da58 100644 --- a/backend/coins/btc/headers/headers_test.go +++ b/backend/coins/btc/headers/headers_test.go @@ -23,7 +23,7 @@ func TestClose(t *testing.T) { headers.testDownloadFinished = func() { close(didFinish) } - headers.Initialize() + require.NoError(t, headers.Initialize()) require.NoError(t, headers.Close()) select { diff --git a/backend/coins/btc/headers/mocks/Interface.go b/backend/coins/btc/headers/mocks/Interface.go index 2a3a6d4244..8fca9e93a9 100644 --- a/backend/coins/btc/headers/mocks/Interface.go +++ b/backend/coins/btc/headers/mocks/Interface.go @@ -4,9 +4,8 @@ package mocks import ( headers "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/headers" - mock "github.com/stretchr/testify/mock" - wire "github.com/btcsuite/btcd/wire" + mock "github.com/stretchr/testify/mock" ) // Interface is an autogenerated mock type for the Interface type @@ -45,8 +44,21 @@ func (_m *Interface) HeaderByHeight(_a0 int) (*wire.BlockHeader, error) { } // Initialize provides a mock function with no fields -func (_m *Interface) Initialize() { - _m.Called() +func (_m *Interface) Initialize() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Initialize") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 } // Status provides a mock function with no fields diff --git a/backend/coins/coin/coin.go b/backend/coins/coin/coin.go index 6fa7b87650..498e22553f 100644 --- a/backend/coins/coin/coin.go +++ b/backend/coins/coin/coin.go @@ -61,7 +61,7 @@ type Coin interface { BlockExplorerTransactionURLPrefix() string // Initialize initializes the coin by connecting to a full node, downloading the headers, etc. - Initialize() + Initialize() error // SmallestUnit returns the name of the smallest unit of a given coin SmallestUnit() string diff --git a/backend/coins/coin/mocks/coin.go b/backend/coins/coin/mocks/coin.go index e357d2862d..a81fa2d4a3 100644 --- a/backend/coins/coin/mocks/coin.go +++ b/backend/coins/coin/mocks/coin.go @@ -38,7 +38,7 @@ var _ coin.Coin = &CoinMock{} // GetFormatUnitFunc: func(isFee bool) string { // panic("mock out the GetFormatUnit method") // }, -// InitializeFunc: func() { +// InitializeFunc: func() error { // panic("mock out the Initialize method") // }, // NameFunc: func() string { @@ -88,7 +88,7 @@ type CoinMock struct { GetFormatUnitFunc func(isFee bool) string // InitializeFunc mocks the Initialize method. - InitializeFunc func() + InitializeFunc func() error // NameFunc mocks the Name method. NameFunc func() string @@ -376,7 +376,7 @@ func (mock *CoinMock) GetFormatUnitCalls() []struct { } // Initialize calls InitializeFunc. -func (mock *CoinMock) Initialize() { +func (mock *CoinMock) Initialize() error { if mock.InitializeFunc == nil { panic("CoinMock.InitializeFunc: method is nil but Coin.Initialize was just called") } @@ -385,7 +385,7 @@ func (mock *CoinMock) Initialize() { mock.lockInitialize.Lock() mock.calls.Initialize = append(mock.calls.Initialize, callInfo) mock.lockInitialize.Unlock() - mock.InitializeFunc() + return mock.InitializeFunc() } // InitializeCalls gets all the calls that were made to Initialize. diff --git a/backend/coins/eth/account.go b/backend/coins/eth/account.go index c32dfb6669..ffc8df4480 100644 --- a/backend/coins/eth/account.go +++ b/backend/coins/eth/account.go @@ -191,7 +191,9 @@ func (account *Account) Initialize() error { account.signingConfiguration.ExtendedPublicKey(), ) - account.coin.Initialize() + if err := account.coin.Initialize(); err != nil { + return err + } account.initDone = account.Synchronizer.IncRequestsCounter() if !account.Config().SkipInitialSync { go account.EnqueueUpdate() diff --git a/backend/coins/eth/coin.go b/backend/coins/eth/coin.go index 3b406c48e1..6b7b1e6d31 100644 --- a/backend/coins/eth/coin.go +++ b/backend/coins/eth/coin.go @@ -100,7 +100,9 @@ func (coin *Coin) ChainIDstr() string { } // Initialize implements coin.Coin. -func (coin *Coin) Initialize() {} +func (coin *Coin) Initialize() error { + return nil +} // Name implements coin.Coin. func (coin *Coin) Name() string { From 3ec8cdf32af2e29992c02dd61176010e5ebb0c30 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 16 May 2026 20:51:25 +0000 Subject: [PATCH 4/5] btc: clean up failed account init --- backend/coins/btc/account.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/coins/btc/account.go b/backend/coins/btc/account.go index 008a09eda1..d80de29791 100644 --- a/backend/coins/btc/account.go +++ b/backend/coins/btc/account.go @@ -282,7 +282,22 @@ func (account *Account) Initialize() error { if account.initialized { return nil } - account.initialized = true + defer func() { + if account.initialized { + return + } + if account.transactions != nil { + account.transactions.Close() + account.transactions = nil + } + if account.db != nil { + if closeErr := account.db.Close(); closeErr != nil { + account.log.WithError(closeErr).Error("couldn't close db") + } + account.db = nil + } + account.subaccounts = nil + }() signingConfigurations := account.Config().Config.SigningConfigurations if len(signingConfigurations) == 0 { @@ -322,7 +337,6 @@ func (account *Account) Initialize() error { return err } account.SetOffline(account.coin.Blockchain().ConnectionError()) - account.coin.Blockchain().RegisterOnConnectionErrorChangedEvent(onConnectionStatusChanged) theHeaders := account.coin.Headers() account.transactions = transactions.NewTransactions( account.coin.Net(), account.db, theHeaders, account.Synchronizer, @@ -346,9 +360,13 @@ func (account *Account) Initialize() error { account.subaccounts = append(account.subaccounts, subacc) } + if err := account.BaseAccount.Initialize(accountIdentifier); err != nil { + return err + } + account.coin.Blockchain().RegisterOnConnectionErrorChangedEvent(onConnectionStatusChanged) + account.initialized = true go account.ensureAddresses() - - return account.BaseAccount.Initialize(accountIdentifier) + return nil } // XPubVersionForScriptType returns the xpub version bytes for the given coin and script type. From dcd56e5e909fe7c1a689ea05a021ac782ee41f07 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Sat, 16 May 2026 20:52:44 +0000 Subject: [PATCH 5/5] btc: defer transaction side effects Transaction history updates used to publish notifier events and start transaction verification while the database update transaction was still open. If a later DB operation failed and rolled back, the app could notify listeners or verify transactions that were never committed. Collect the notifier and verification work during the DB update and run it only after DBUpdate returns successfully. --- .../coins/btc/transactions/transactions.go | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/backend/coins/btc/transactions/transactions.go b/backend/coins/btc/transactions/transactions.go index 2e618251f8..a6fb5130e5 100644 --- a/backend/coins/btc/transactions/transactions.go +++ b/backend/coins/btc/transactions/transactions.go @@ -188,6 +188,9 @@ func (transactions *Transactions) getCachedTimestampAtHeight(height int, txInfoH return headerTimestamp } +// processTxForAddress stores and indexes a transaction for the given address. +// It returns true if the transaction became newly confirmed while processing this address, so +// verification can be started after the DB update succeeds. func (transactions *Transactions) processTxForAddress( dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, @@ -195,30 +198,25 @@ func (transactions *Transactions) processTxForAddress( tx *wire.MsgTx, height int, headerTimestamp *time.Time, -) error { +) (bool, error) { txInfo, err := dbTx.TxInfo(txHash) if err != nil { - return errp.WithMessage(err, "failed to retrieve tx info") + return false, errp.WithMessage(err, "failed to retrieve tx info") } if err := dbTx.PutTx(txHash, tx, height, headerTimestamp); err != nil { - return errp.WithMessage(err, "failed to put tx") + return false, errp.WithMessage(err, "failed to put tx") } - if err := transactions.notifier.Put(txHash[:]); err != nil { - transactions.log.WithError(err).Error("Failed notifier.Put") - } - - // Newly confirmed tx. Try to verify it. - if txInfo.Height <= 0 && height > 0 { - transactions.log.Debug("Try to verify newly confirmed tx") - go transactions.verifyTransaction(txHash, height) - } + newlyConfirmed := txInfo.Height <= 0 && height > 0 if err := dbTx.AddAddressToTx(txHash, scriptHashHex); err != nil { - return errp.WithMessage(err, "failed to add address to tx") + return false, errp.WithMessage(err, "failed to add address to tx") } - return transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx) + if err := transactions.processInputsAndOutputsForAddress(dbTx, scriptHashHex, txHash, tx); err != nil { + return false, err + } + return newlyConfirmed, nil } // Go through the tx and extract all inputs and outputs which touch the address. @@ -325,23 +323,26 @@ func (transactions *Transactions) isInputSpent(dbTx DBTxInterface, outPoint wire return input != nil, nil } +// removeTxForAddress removes the address association for a transaction. +// It returns true if the transaction was removed from the wallet index entirely, so notifier +// cleanup can happen after the DB update succeeds. func (transactions *Transactions) removeTxForAddress( - dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) error { + dbTx DBTxInterface, scriptHashHex blockchain.ScriptHashHex, txHash chainhash.Hash) (bool, error) { transactions.log.Debug("Remove transaction for address") txInfo, err := dbTx.TxInfo(txHash) if err != nil { - return errp.WithMessage(err, "failed to retrieve tx info") + return false, errp.WithMessage(err, "failed to retrieve tx info") } if txInfo == nil { // Not yet indexed. transactions.log.Debug("Transaction hash not listed") - return nil + return false, nil } transactions.log.Debug("Deleting transaction address") empty, err := dbTx.RemoveAddressFromTx(txHash, scriptHashHex) if err != nil { - return errp.WithMessage(err, "failed to remove address from tx") + return false, errp.WithMessage(err, "failed to remove address from tx") } if empty { // Tx is not touching any of our outputs anymore. Remove. @@ -349,7 +350,7 @@ func (transactions *Transactions) removeTxForAddress( for _, txIn := range txInfo.Tx.TxIn { transactions.log.Debug("Deleting transaction iput") if err := dbTx.DeleteInput(txIn.PreviousOutPoint); err != nil { - return errp.WithMessage(err, "failed to delete input") + return false, errp.WithMessage(err, "failed to delete input") } } @@ -359,18 +360,15 @@ func (transactions *Transactions) removeTxForAddress( Hash: txHash, Index: uint32(index), }); err != nil { - return errp.WithMessage(err, "failed to delete output") + return false, errp.WithMessage(err, "failed to delete output") } } if err := dbTx.DeleteTx(txHash); err != nil { - return errp.WithMessage(err, "failed to delete tx") - } - if err := transactions.notifier.Delete(txHash[:]); err != nil { - transactions.log.WithError(err).Error("Failed notifier.Delete") + return false, errp.WithMessage(err, "failed to delete tx") } } - return nil + return empty, nil } // UpdateAddressHistory should be called when initializing a wallet address, or when the history of @@ -381,6 +379,13 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. transactions.log.Debug("UpdateAddressHistory after the instance was closed") return nil } + type txToVerify struct { + txHash chainhash.Hash + height int + } + var txsToNotify []chainhash.Hash + var txsToVerify []txToVerify + var txsToDeleteFromNotifier []chainhash.Hash err := DBUpdate(transactions.db, func(dbTx DBTxInterface) error { txsSet := map[chainhash.Hash]struct{}{} for _, txInfo := range txs { @@ -400,9 +405,13 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. // A tx was previously in the address history but is not anymore. If the tx was already // downloaded and indexed, it will be removed. If it is currently downloading (enqueued for // indexing), it will not be processed. - if err := transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()); err != nil { + removed, err := transactions.removeTxForAddress(dbTx, scriptHashHex, entry.TXHash.Hash()) + if err != nil { return err } + if removed { + txsToDeleteFromNotifier = append(txsToDeleteFromNotifier, entry.TXHash.Hash()) + } } if err := dbTx.PutAddressHistory(scriptHashHex, txs); err != nil { @@ -415,17 +424,40 @@ func (transactions *Transactions) UpdateAddressHistory(scriptHashHex blockchain. if err != nil { return err } - if err := transactions.processTxForAddress( + newlyConfirmed, err := transactions.processTxForAddress( dbTx, scriptHashHex, txHash, tx, height, headerTimestamp, - ); err != nil { + ) + if err != nil { return err } + txsToNotify = append(txsToNotify, txHash) + if newlyConfirmed { + txsToVerify = append(txsToVerify, txToVerify{txHash: txHash, height: height}) + } } return nil }) if err != nil { return errp.WithMessage(err, "failed to update address history") } + for _, txHash := range txsToDeleteFromNotifier { + if err := transactions.notifier.Delete(txHash[:]); err != nil { + transactions.log.WithError(err).Error("Failed notifier.Delete") + } + } + for _, txHash := range txsToNotify { + if err := transactions.notifier.Put(txHash[:]); err != nil { + transactions.log.WithError(err).Error("Failed notifier.Put") + } + } + if len(txsToVerify) != 0 { + go func() { + for _, tx := range txsToVerify { + transactions.log.Debug("Try to verify newly confirmed tx") + transactions.verifyTransaction(tx.txHash, tx.height) + } + }() + } return nil }