Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions backend/coins/btc/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -318,9 +333,10 @@ 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()
account.transactions = transactions.NewTransactions(
account.coin.Net(), account.db, theHeaders, account.Synchronizer,
Expand All @@ -344,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.
Expand Down Expand Up @@ -672,7 +692,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()
}
Expand Down
110 changes: 63 additions & 47 deletions backend/coins/btc/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 24 additions & 1 deletion backend/coins/btc/coin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/hex"
"math/big"
"os"
"path"
"testing"

"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/errors"
Expand Down Expand Up @@ -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() {
Expand All @@ -67,6 +68,28 @@ func TestSuite(t *testing.T) {
suite.Run(t, &testSuite{code: coin.CodeLTC, unit: "LTC", net: &ltc.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())
Expand Down
34 changes: 17 additions & 17 deletions backend/coins/btc/db/transactionsdb/transactionsdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions backend/coins/btc/db/transactionsdb/transactionsdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading