Skip to content

Commit 390d8f6

Browse files
committed
Merge branch 'sendToSelfApp'
2 parents 797af2f + eb321f4 commit 390d8f6

File tree

23 files changed

+942
-532
lines changed

23 files changed

+942
-532
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Introduce full screen selector for mobile in place of dropdown
99
- Fix wrong estimated confirmation time for ERC20 tokens.
1010
- Enable unlock test wallet in testnet
11+
- Added support to show on the BitBox when a transaction's recipient is an address of a different account on the device.
1112

1213
# v4.47.2
1314
- Linux: fix compatiblity with some versions of Mesa that are incompatible with the bundled wayland libraries

backend/accounts.go

+31
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import (
2727
accountsTypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/types"
2828
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/bitsurance"
2929
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc"
30+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
31+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain"
3032
coinpkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
3133
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth"
3234
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/config"
@@ -872,12 +874,41 @@ func (backend *Backend) createAndAddAccount(coin coinpkg.Coin, persistedConfig *
872874
BtcCurrencyUnit: backend.config.AppConfig().Backend.BtcUnit,
873875
}
874876

877+
// This function is passed as a callback to the BTC account constructor. It is called when the
878+
// keystore needs to determine whether an address belongs to an account on its same keystore.
879+
getAddressCallback := func(askingAccount *btc.Account, scriptHashHex blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error) {
880+
accountsByKeystore, err := backend.AccountsByKeystore()
881+
if err != nil {
882+
return nil, false, err
883+
}
884+
rootFingerprint, err := backend.keystore.RootFingerprint()
885+
if err != nil {
886+
return nil, false, err
887+
}
888+
for _, account := range accountsByKeystore[hex.EncodeToString(rootFingerprint)] {
889+
// This only makes sense for BTC accounts.
890+
btcAccount, ok := account.(*btc.Account)
891+
if !ok {
892+
continue
893+
}
894+
// Only return an address if the coin codes match.
895+
if btcAccount.Coin().Code() != askingAccount.Coin().Code() {
896+
continue
897+
}
898+
if address := btcAccount.GetAddress(scriptHashHex); address != nil {
899+
return address, askingAccount == btcAccount, nil
900+
}
901+
}
902+
return nil, false, nil
903+
}
904+
875905
switch specificCoin := coin.(type) {
876906
case *btc.Coin:
877907
account = backend.makeBtcAccount(
878908
accountConfig,
879909
specificCoin,
880910
backend.arguments.GapLimits(),
911+
getAddressCallback,
881912
backend.log,
882913
)
883914
backend.addAccount(account)

backend/accounts_test.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
2525
accountsTypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts/types"
2626
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc"
27+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
28+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain"
2729
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
2830
coinpkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
2931
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth"
@@ -1449,7 +1451,7 @@ func TestAccountsTotalBalanceByKeystore(t *testing.T) {
14491451
b := newBackend(t, testnetDisabled, regtestDisabled)
14501452
defer b.Close()
14511453

1452-
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, log *logrus.Entry) accounts.Interface {
1454+
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), log *logrus.Entry) accounts.Interface {
14531455
accountMock := MockBtcAccount(t, config, coin, gapLimits, log)
14541456
accountMock.BalanceFunc = func() (*accounts.Balance, error) {
14551457
return accounts.NewBalance(coinpkg.NewAmountFromInt64(100000), coinpkg.NewAmountFromInt64(0)), nil

backend/backend.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/arguments"
3131
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/banners"
3232
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc"
33+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/addresses"
34+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/blockchain"
3335
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/electrum"
3436
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/btc/types"
3537
coinpkg "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
@@ -203,7 +205,7 @@ type Backend struct {
203205

204206
// makeBtcAccount creates a BTC account. In production this is `btc.NewAccount`, but can be
205207
// overridden in unit tests for mocking.
206-
makeBtcAccount func(*accounts.AccountConfig, *btc.Coin, *types.GapLimits, *logrus.Entry) accounts.Interface
208+
makeBtcAccount func(*accounts.AccountConfig, *btc.Coin, *types.GapLimits, func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), *logrus.Entry) accounts.Interface
207209
// makeEthAccount creates an ETH account. In production this is `eth.NewAccount`, but can be
208210
// overridden in unit tests for mocking.
209211
makeEthAccount func(*accounts.AccountConfig, *eth.Coin, *http.Client, *logrus.Entry) accounts.Interface
@@ -262,8 +264,8 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
262264
coins: map[coinpkg.Code]coinpkg.Coin{},
263265
accounts: []accounts.Interface{},
264266
aopp: AOPP{State: aoppStateInactive},
265-
makeBtcAccount: func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, log *logrus.Entry) accounts.Interface {
266-
return btc.NewAccount(config, coin, gapLimits, log, hclient)
267+
makeBtcAccount: func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), log *logrus.Entry) accounts.Interface {
268+
return btc.NewAccount(config, coin, gapLimits, getAddress, log, hclient)
267269
},
268270
makeEthAccount: func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {
269271
return eth.NewAccount(config, coin, httpClient, log)

backend/backend_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ func newBackend(t *testing.T, testing, regtest bool) *Backend {
271271
}
272272
b.ratesUpdater.SetCoingeckoURL("unused") // avoid hitting real API
273273

274-
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, log *logrus.Entry) accounts.Interface {
274+
b.makeBtcAccount = func(config *accounts.AccountConfig, coin *btc.Coin, gapLimits *types.GapLimits, getAddress func(*btc.Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error), log *logrus.Entry) accounts.Interface {
275275
return MockBtcAccount(t, config, coin, gapLimits, log)
276276
}
277277
b.makeEthAccount = func(config *accounts.AccountConfig, coin *eth.Coin, httpClient *http.Client, log *logrus.Entry) accounts.Interface {

backend/coins/btc/account.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,20 @@ type Account struct {
120120
log *logrus.Entry
121121

122122
httpClient *http.Client
123+
124+
// getAddressFromSameKeystore is a function that retrieves an address from any account on the same keystore as this one.
125+
getAddressFromSameKeystore func(*Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error)
123126
}
124127

125128
// NewAccount creates a new account.
126129
//
127130
// forceGaplimits: if not nil, these limits will be used and persisted for future use.
131+
// getAddressFromSameKeystore: function to retrieve an address from any account on the same keystore.
128132
func NewAccount(
129133
config *accounts.AccountConfig,
130134
coin *Coin,
131135
forceGapLimits *types.GapLimits,
136+
getAddressFromSameKeystore func(*Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error),
132137
log *logrus.Entry,
133138
httpClient *http.Client,
134139
) *Account {
@@ -137,13 +142,13 @@ func NewAccount(
137142
log.Debug("Creating new account")
138143

139144
account := &Account{
140-
BaseAccount: accounts.NewBaseAccount(config, coin, log),
141-
coin: coin,
142-
dbSubfolder: "", // set in Initialize()
143-
forceGapLimits: forceGapLimits,
144-
145-
log: log,
146-
httpClient: httpClient,
145+
BaseAccount: accounts.NewBaseAccount(config, coin, log),
146+
coin: coin,
147+
dbSubfolder: "", // set in Initialize()
148+
forceGapLimits: forceGapLimits,
149+
getAddressFromSameKeystore: getAddressFromSameKeystore,
150+
log: log,
151+
httpClient: httpClient,
147152
}
148153
return account
149154
}
@@ -865,7 +870,7 @@ func (account *Account) SpendableOutputs() ([]*SpendableOutput, error) {
865870
&SpendableOutput{
866871
OutPoint: outPoint,
867872
SpendableOutput: txOut,
868-
Address: account.getAddress(scriptHashHex),
873+
Address: account.GetAddress(scriptHashHex),
869874
IsChange: account.IsChange(scriptHashHex),
870875
})
871876
}

backend/coins/btc/account_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func mockAccount(t *testing.T, accountConfig *config.Account) *Account {
9999
return mockKeystore(), nil
100100
},
101101
},
102-
coin, nil,
102+
coin, nil, nil,
103103
logging.Get().WithGroup("account_test"),
104104
nil,
105105
)

backend/coins/btc/sign.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,16 @@ type ProposedTransaction struct {
3232
TXProposal *maketx.TxProposal
3333
// List of signing configurations that might be used in the tx inputs.
3434
AccountSigningConfigurations []*signing.Configuration
35-
GetAccountAddress func(blockchain.ScriptHashHex) *addresses.AccountAddress
3635
GetPrevTx func(chainhash.Hash) (*wire.MsgTx, error)
3736
// Signatures collects the signatures, one per transaction input.
3837
Signatures []*types.Signature
3938
FormatUnit coin.BtcUnit
39+
// GetKeystoreAddress returns the address from the same keystore given the script hash,
40+
// or nil if not found.
41+
// It also returns a boolean indicating whether the address belongs to the account
42+
// that is asking for the address or not.
43+
GetKeystoreAddress func(*Account, blockchain.ScriptHashHex) (*addresses.AccountAddress, bool, error)
44+
SendingAccount *Account
4045
}
4146

4247
// Finalize adds the signatureScript/witness for each input based on the available signatures and
@@ -68,7 +73,8 @@ func (account *Account) signTransaction(
6873
proposedTransaction := &ProposedTransaction{
6974
TXProposal: txProposal,
7075
AccountSigningConfigurations: signingConfigs,
71-
GetAccountAddress: account.getAddress,
76+
GetKeystoreAddress: account.getAddressFromSameKeystore,
77+
SendingAccount: account,
7278
GetPrevTx: getPrevTx,
7379
Signatures: make([]*types.Signature, len(txProposal.Transaction.TxIn)),
7480
FormatUnit: account.coin.formatUnit,

backend/coins/btc/transaction.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
168168
}
169169
wireUTXO[outPoint] = maketx.UTXO{
170170
TxOut: txOut.TxOut,
171-
Address: account.getAddress(
171+
Address: account.GetAddress(
172172
blockchain.NewScriptHashHex(txOut.TxOut.PkScript)),
173173
}
174174
}
@@ -235,9 +235,9 @@ func (account *Account) newTx(args *accounts.TxProposalArgs) (
235235
return utxo, txProposal, nil
236236
}
237237

238-
// getAddress returns the address in the account with the given `scriptHashHex`. Returns nil if the
238+
// GetAddress returns the address in the account with the given `scriptHashHex`. Returns nil if the
239239
// address does not exist in the account.
240-
func (account *Account) getAddress(scriptHashHex blockchain.ScriptHashHex) *addresses.AccountAddress {
240+
func (account *Account) GetAddress(scriptHashHex blockchain.ScriptHashHex) *addresses.AccountAddress {
241241
for _, subacc := range account.subaccounts {
242242
if address := subacc.receiveAddresses.LookupByScriptHashHex(scriptHashHex); address != nil {
243243
return address

backend/devices/bitbox02/keystore.go

+50-16
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,21 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
359359

360360
txChangeAddress := btcProposedTx.TXProposal.ChangeAddress
361361

362+
// outputScriptConfigs represent the script configurations of a specific account and include the
363+
// script type (e.g. p2wpkh, p2tr..) and the account keypath
364+
outputScriptConfigs := []*messages.BTCScriptConfigWithKeypath{}
365+
// addOutputScriptConfig returns the index of the scriptConfig in outputScriptConfigs, adding it if it isn't
366+
// present. Must be a Simple configuration.
367+
addOutputScriptConfig := func(scriptConfig *messages.BTCScriptConfigWithKeypath) uint32 {
368+
for i, sc := range outputScriptConfigs {
369+
if sc.ScriptConfig.Config.(*messages.BTCScriptConfig_SimpleType_).SimpleType == scriptConfig.ScriptConfig.Config.(*messages.BTCScriptConfig_SimpleType_).SimpleType {
370+
return uint32(i)
371+
}
372+
}
373+
outputScriptConfigs = append(outputScriptConfigs, scriptConfig)
374+
return uint32(len(outputScriptConfigs) - 1)
375+
}
376+
362377
// iterate over tx outputs to add the related scriptconfigs, flag internal addresses and build
363378
// the output signing requests.
364379
outputs := make([]*messages.BTCSignOutputRequest, len(tx.TxOut))
@@ -399,38 +414,56 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
399414
txOut.PkScript,
400415
)
401416

402-
// outputAccountAddress represents the same address as outputAddress, but embeds the account configuration.
417+
// outputAddress represents the same address as outputAddress, but embeds the account configuration.
403418
// It is nil if the address is external.
404-
outputAccountAddress := btcProposedTx.GetAccountAddress(blockchain.NewScriptHashHex(txOut.PkScript))
419+
outputAddress, sameAccount, err := btcProposedTx.GetKeystoreAddress(btcProposedTx.SendingAccount, blockchain.NewScriptHashHex(txOut.PkScript))
420+
if err != nil {
421+
return errp.Newf("failed to get address: %v", err)
422+
}
405423

406-
isOurs := outputAccountAddress != nil
424+
isOurs := outputAddress != nil
407425
if !isChange && !keystore.device.Version().AtLeast(semver.NewSemVer(9, 15, 0)) {
408426
// For firmware older than 9.15.0, non-change outputs cannot be marked internal.
409427
isOurs = false
410428
}
411429

412430
var keypath []uint32
413431
var scriptConfigIndex int
432+
var outputScriptConfigIndex *uint32
414433
if isOurs {
415-
keypath = outputAccountAddress.Configuration.AbsoluteKeypath().ToUInt32()
416-
accountConfiguration := outputAccountAddress.AccountConfiguration
434+
accountConfiguration := outputAddress.AccountConfiguration
417435
msgScriptType, ok := btcMsgScriptTypeMap[accountConfiguration.ScriptType()]
418436
if !ok {
419437
return errp.Newf("Unsupported script type %s", accountConfiguration.ScriptType())
420438
}
421-
scriptConfigIndex = addScriptConfig(&messages.BTCScriptConfigWithKeypath{
422-
ScriptConfig: firmware.NewBTCScriptConfigSimple(msgScriptType),
423-
Keypath: accountConfiguration.AbsoluteKeypath().ToUInt32(),
424-
})
439+
switch {
440+
case sameAccount:
441+
keypath = outputAddress.Configuration.AbsoluteKeypath().ToUInt32()
442+
scriptConfigIndex = addScriptConfig(&messages.BTCScriptConfigWithKeypath{
443+
ScriptConfig: firmware.NewBTCScriptConfigSimple(msgScriptType),
444+
Keypath: accountConfiguration.AbsoluteKeypath().ToUInt32(),
445+
})
446+
case keystore.device.Version().AtLeast(semver.NewSemVer(9, 22, 0)):
447+
keypath = outputAddress.Configuration.AbsoluteKeypath().ToUInt32()
448+
outputScriptConfigIdx := addOutputScriptConfig(&messages.BTCScriptConfigWithKeypath{
449+
ScriptConfig: firmware.NewBTCScriptConfigSimple(msgScriptType),
450+
Keypath: accountConfiguration.AbsoluteKeypath().ToUInt32(),
451+
})
452+
outputScriptConfigIndex = &outputScriptConfigIdx
453+
default:
454+
isOurs = false
455+
}
425456
}
457+
426458
outputs[index] = &messages.BTCSignOutputRequest{
427-
Ours: isOurs,
428-
Type: msgOutputType,
429-
Value: uint64(txOut.Value),
430-
Payload: payload,
431-
Keypath: keypath,
432-
ScriptConfigIndex: uint32(scriptConfigIndex),
433-
SilentPayment: silentPayment,
459+
Ours: isOurs,
460+
Type: msgOutputType,
461+
Value: uint64(txOut.Value),
462+
Payload: payload,
463+
Keypath: keypath,
464+
ScriptConfigIndex: uint32(scriptConfigIndex),
465+
SilentPayment: silentPayment,
466+
OutputScriptConfigIndex: outputScriptConfigIndex,
434467
}
435468
}
436469

@@ -489,6 +522,7 @@ func (keystore *keystore) signBTCTransaction(btcProposedTx *btc.ProposedTransact
489522
signatures, generatedOutputs, err := keystore.device.BTCSign(
490523
msgCoin,
491524
scriptConfigs,
525+
outputScriptConfigs,
492526
&firmware.BTCTx{
493527
Version: uint32(tx.Version),
494528
Inputs: inputs,

0 commit comments

Comments
 (0)