Skip to content

Commit 2daf8ae

Browse files
committed
Merge branch 'fixnotes'
2 parents 0da83b3 + ba609e1 commit 2daf8ae

File tree

6 files changed

+278
-27
lines changed

6 files changed

+278
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Changelog
22

33
## [Unreleased]
4+
- Restore lost transaction notes when ugprading to v4.28.0.
45
- Improve error message when EtherScan responds with a rate limit error.
56

67
## 4.28.0 [released 2021-05-27]

backend/accounts/account.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020

2121
"github.com/btcsuite/btcd/wire"
2222
"github.com/btcsuite/btcutil"
23-
"github.com/digitalbitbox/bitbox-wallet-app/backend/accounts/notes"
2423
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin"
2524
"github.com/digitalbitbox/bitbox-wallet-app/backend/signing"
2625
"github.com/digitalbitbox/bitbox-wallet-app/util/observable"
@@ -84,7 +83,7 @@ type Interface interface {
8483
CanVerifyAddresses() (bool, bool, error)
8584
VerifyAddress(addressID string) (bool, error)
8685

87-
Notes() *notes.Notes
86+
TxNote(txID string) string
8887
// ProposeTxnote stores a note. The note is is persisted in the notes database upon calling
8988
// SendTx(). This function must be called before `SendTx()`.
9089
ProposeTxNote(string)

backend/accounts/baseaccount.go

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"encoding/csv"
1919
"fmt"
2020
"io"
21+
"os"
2122
"path"
2223
"sync"
2324
"time"
@@ -65,17 +66,26 @@ type BaseAccount struct {
6566
synced bool
6667
offline error
6768

68-
notes *notes.Notes
69+
// notes handles transaction notes.
70+
//
71+
// It is a slice for migration purposes: from v4.27.0 to v4.28.0, the account identifiers
72+
// changed. The slice contains all possible instances of where notes are stored. The first
73+
// element is the newest, and other elements are notes stored under legacy names. After
74+
// `Initialize()`, this will always have at least one element.
75+
notes []*notes.Notes
6976

7077
proposedTxNote string
7178
proposedTxNoteMu sync.Mutex
79+
80+
log *logrus.Entry
7281
}
7382

7483
// NewBaseAccount creates a new Account instance.
7584
func NewBaseAccount(config *AccountConfig, coin coin.Coin, log *logrus.Entry) *BaseAccount {
7685
account := &BaseAccount{
7786
config: config,
7887
coin: coin,
88+
log: log,
7989
}
8090
account.Synchronizer = synchronizer.NewSynchronizer(
8191
func() { config.OnEvent(EventSyncStarted) },
@@ -131,20 +141,65 @@ func (account *BaseAccount) SetOffline(offline error) {
131141
// Initialize initializes the account. `accountIdentifier` is used as part of the filename of
132142
// account databases.
133143
func (account *BaseAccount) Initialize(accountIdentifier string) error {
134-
notes, err := notes.LoadNotes(path.Join(
144+
txNotes, err := notes.LoadNotes(path.Join(
135145
account.config.NotesFolder,
136146
fmt.Sprintf("%s.json", accountIdentifier),
137147
))
138148
if err != nil {
139149
return err
140150
}
141-
account.notes = notes
142-
return nil
143-
}
151+
account.notes = []*notes.Notes{txNotes}
152+
153+
// Append legacy notes (notes stored in files based on obsolete account identifiers). Account
154+
// identifiers changed from v4.27.0 to v4.28.0.
155+
if len(account.Config().SigningConfigurations) == 0 {
156+
return nil
157+
}
158+
accountNumber, err := account.Config().SigningConfigurations[0].AccountNumber()
159+
if err != nil {
160+
return nil
161+
}
162+
if accountNumber != 0 {
163+
// Up to v4.27.0, we only had one account per coin.
164+
return nil
165+
}
144166

145-
// Notes implements Interface.
146-
func (account *BaseAccount) Notes() *notes.Notes {
147-
return account.notes
167+
legacyConfigurations := signing.ConvertToLegacyConfigurations(account.Config().SigningConfigurations)
168+
var legacyAccountIdentifiers []string
169+
switch account.coin.Code() {
170+
case coin.CodeBTC, coin.CodeTBTC, coin.CodeLTC, coin.CodeTLTC:
171+
legacyAccountIdentifiers = []string{fmt.Sprintf("account-%s-%s", legacyConfigurations.Hash(), account.coin.Code())}
172+
// Also consider split accounts:
173+
for _, cfg := range account.Config().SigningConfigurations {
174+
legacyConfigurations := signing.ConvertToLegacyConfigurations(signing.Configurations{cfg})
175+
legacyAccountIdentifier := fmt.Sprintf("account-%s-%s-%s", legacyConfigurations.Hash(), account.coin.Code(), cfg.ScriptType())
176+
legacyAccountIdentifiers = append(
177+
legacyAccountIdentifiers,
178+
legacyAccountIdentifier,
179+
)
180+
}
181+
default:
182+
legacyAccountIdentifiers = []string{
183+
fmt.Sprintf("account-%s-%s", legacyConfigurations[0].Hash(), account.coin.Code()),
184+
}
185+
}
186+
for _, identifier := range legacyAccountIdentifiers {
187+
notesFile := path.Join(account.config.NotesFolder, fmt.Sprintf("%s.json", identifier))
188+
if _, err := os.Stat(notesFile); os.IsNotExist(err) {
189+
continue // skip nonexistent legacy notes file
190+
}
191+
192+
legacyNotes, err := notes.LoadNotes(path.Join(
193+
account.config.NotesFolder,
194+
fmt.Sprintf("%s.json", identifier),
195+
))
196+
if err != nil {
197+
return err
198+
}
199+
account.notes = append(account.notes, legacyNotes)
200+
}
201+
202+
return nil
148203
}
149204

150205
// ProposeTxNote implements accounts.Account.
@@ -168,14 +223,33 @@ func (account *BaseAccount) GetAndClearProposedTxNote() string {
168223

169224
// SetTxNote implements accounts.Account.
170225
func (account *BaseAccount) SetTxNote(txID string, note string) error {
171-
if err := account.notes.SetTxNote(txID, note); err != nil {
226+
// The notes slice is guaranteed to have at least one element by BaseAccount.Initialize.
227+
if err := account.notes[0].SetTxNote(txID, note); err != nil {
172228
return err
173229
}
230+
// Delete the notes in legacy files. Don't really care if it fails.
231+
for i, notes := range account.notes[1:] {
232+
if err := notes.SetTxNote(txID, ""); err != nil {
233+
account.log.WithError(err).Errorf("Can't delete a note from a legacy file idx=%d", i)
234+
}
235+
}
174236
// Prompt refresh.
175237
account.config.OnEvent(EventStatusChanged)
176238
return nil
177239
}
178240

241+
// TxNote fetches a note for a transcation. Returns the empty string if no note was found.
242+
func (account *BaseAccount) TxNote(txID string) string {
243+
// Take the first note we can find. The first slice element is the regular location of notes,
244+
// the other elements lookup notes in legacy locations, so they are not lost when upgrading.
245+
for _, notes := range account.notes {
246+
if note := notes.TxNote(txID); note != "" {
247+
return note
248+
}
249+
}
250+
return ""
251+
}
252+
179253
// ExportCSV implements accounts.Account.
180254
func (account *BaseAccount) ExportCSV(w io.Writer, transactions []*TransactionData) error {
181255
writer := csv.NewWriter(w)
@@ -221,7 +295,7 @@ func (account *BaseAccount) ExportCSV(w io.Writer, transactions []*TransactionDa
221295
feeString,
222296
addressAndAmount.Address,
223297
transaction.TxID,
224-
account.Notes().TxNote(transaction.InternalID),
298+
account.TxNote(transaction.InternalID),
225299
})
226300
if err != nil {
227301
return errp.WithStack(err)

backend/accounts/baseaccount_test.go

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ package accounts
1717
import (
1818
"bytes"
1919
"errors"
20+
"io/ioutil"
21+
"path"
2022
"testing"
2123
"time"
2224

25+
"github.com/btcsuite/btcutil/hdkeychain"
2326
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin"
2427
"github.com/digitalbitbox/bitbox-wallet-app/backend/coins/coin/mocks"
28+
"github.com/digitalbitbox/bitbox-wallet-app/backend/signing"
2529
"github.com/digitalbitbox/bitbox-wallet-app/util/logging"
2630
"github.com/digitalbitbox/bitbox-wallet-app/util/test"
2731
"github.com/stretchr/testify/require"
@@ -38,20 +42,65 @@ func TestBaseAccount(t *testing.T) {
3842
}
3943
panic("can't reach")
4044
}
45+
46+
derivationPath, err := signing.NewAbsoluteKeypath("m/84'/1'/0'")
47+
require.NoError(t, err)
48+
const xpub = "tpubDCxoQyC5JaGydxN3yprM6sgqgu65LruN3JBm1fnSmGxXR3AcuNwr" +
49+
"E7J2CVaCvuLPJtJNySjshNsYbR96Y7yfEdcywYqWubzUQLVGh2b4mF9"
50+
extendedPublicKey, err := hdkeychain.NewKeyFromString(xpub)
51+
require.NoError(t, err)
52+
4153
const accountIdentifier = "test-account-identifier"
4254
cfg := &AccountConfig{
43-
Code: "test",
44-
Name: "Test",
45-
DBFolder: test.TstTempDir("baseaccount_test_dbfolder"),
46-
NotesFolder: test.TstTempDir("baseaccount_test_notesfolder"),
47-
Keystores: nil,
48-
OnEvent: func(event Event) { events <- event },
49-
RateUpdater: nil,
50-
SigningConfigurations: nil,
51-
GetNotifier: nil,
55+
Code: "test",
56+
Name: "Test",
57+
DBFolder: test.TstTempDir("baseaccount_test_dbfolder"),
58+
NotesFolder: test.TstTempDir("baseaccount_test_notesfolder"),
59+
Keystores: nil,
60+
OnEvent: func(event Event) { events <- event },
61+
RateUpdater: nil,
62+
SigningConfigurations: signing.Configurations{
63+
signing.NewBitcoinConfiguration(signing.ScriptTypeP2PKH, []byte{1, 2, 3, 4}, derivationPath, extendedPublicKey),
64+
signing.NewBitcoinConfiguration(signing.ScriptTypeP2WPKH, []byte{1, 2, 3, 4}, derivationPath, extendedPublicKey),
65+
signing.NewBitcoinConfiguration(signing.ScriptTypeP2WPKHP2SH, []byte{1, 2, 3, 4}, derivationPath, extendedPublicKey),
66+
},
67+
GetNotifier: nil,
5268
}
69+
// The long ID in the filename is the legacy hash of the configurations above (unified and split).
70+
// This tests notes migration from v4.27.0 to v4.28.0.
71+
require.NoError(t,
72+
ioutil.WriteFile(
73+
path.Join(cfg.NotesFolder, "account-54b4597c3a5c48177ef2b12c97e0cb30b6fef0b431e7821675b17704c330ce5d-tbtc.json"),
74+
[]byte(`{"transactions": { "legacy-1": "legacy note in unified account" }}`),
75+
0666,
76+
),
77+
)
78+
require.NoError(t,
79+
ioutil.WriteFile(
80+
path.Join(cfg.NotesFolder, "account-989b84ec36f0b84e7926f9f5715e4b59a0592993b1fa4b70836addbcb0cb6e09-tbtc-p2pkh.json"),
81+
[]byte(`{"transactions": { "legacy-2": "legacy note in split account, p2pkh" }}`),
82+
0666,
83+
),
84+
)
85+
require.NoError(t,
86+
ioutil.WriteFile(
87+
path.Join(cfg.NotesFolder, "account-e60b99507ba983d522f15932dbe1214e99de13c56e2bea75ed9c285f7c013117-tbtc-p2wpkh.json"),
88+
[]byte(`{"transactions": { "legacy-3": "legacy note in split account, p2wpkh" }}`),
89+
0666,
90+
),
91+
)
92+
require.NoError(t,
93+
ioutil.WriteFile(
94+
path.Join(cfg.NotesFolder, "account-9e779e0d49e77236f0769e0bab2fd656958d3fd023dce2388525ee66fead88bb-tbtc-p2wpkh-p2sh.json"),
95+
[]byte(`{"transactions": { "legacy-4": "legacy note in split account, p2wpkh-p2sh" }}`),
96+
0666,
97+
),
98+
)
5399

54100
mockCoin := &mocks.CoinMock{
101+
CodeFunc: func() coin.Code {
102+
return coin.CodeTBTC
103+
},
55104
SmallestUnitFunc: func() string {
56105
return "satoshi"
57106
},
@@ -103,12 +152,26 @@ func TestBaseAccount(t *testing.T) {
103152
// Was cleared by the previous call.
104153
require.Equal(t, "", account.GetAndClearProposedTxNote())
105154

106-
notes := account.Notes()
107-
require.NotNil(t, notes)
108-
109155
require.NoError(t, account.SetTxNote("test-tx-id", "another test note"))
110156
require.Equal(t, EventStatusChanged, checkEvent())
111-
require.Equal(t, "another test note", notes.TxNote("test-tx-id"))
157+
require.Equal(t, "another test note", account.TxNote("test-tx-id"))
158+
159+
// Test notes migration from v4.27.0 to v4.28.0
160+
require.Equal(t, "legacy note in unified account", account.TxNote("legacy-1"))
161+
require.Equal(t, "legacy note in split account, p2pkh", account.TxNote("legacy-2"))
162+
require.Equal(t, "legacy note in split account, p2wpkh", account.TxNote("legacy-3"))
163+
require.Equal(t, "legacy note in split account, p2wpkh-p2sh", account.TxNote("legacy-4"))
164+
// Setting a note sets it in the main notes file, and wipes it out in legacy note files.
165+
require.NoError(t, account.SetTxNote("legacy-1", "updated legacy note"))
166+
require.Equal(t, "updated legacy note", account.TxNote("legacy-1"))
167+
contents, err := ioutil.ReadFile(path.Join(cfg.NotesFolder, "account-54b4597c3a5c48177ef2b12c97e0cb30b6fef0b431e7821675b17704c330ce5d-tbtc.json"))
168+
require.NoError(t, err)
169+
require.JSONEq(t, `{"transactions":{}}`, string(contents))
170+
171+
// Test that the notes were persisted under the right file name with the right contents.
172+
contents, err = ioutil.ReadFile(path.Join(cfg.NotesFolder, "test-account-identifier.json"))
173+
require.NoError(t, err)
174+
require.JSONEq(t, `{"transactions":{"legacy-1": "updated legacy note", "test-tx-id": "another test note"}}`, string(contents))
112175
})
113176

114177
t.Run("exportCSV", func(t *testing.T) {
@@ -122,7 +185,7 @@ func TestBaseAccount(t *testing.T) {
122185

123186
require.Equal(t, header, export(nil))
124187

125-
require.NoError(t, account.notes.SetTxNote("some-internal-tx-id", "some note, with a comma"))
188+
require.NoError(t, account.SetTxNote("some-internal-tx-id", "some note, with a comma"))
126189
fee := coin.NewAmountFromInt64(101)
127190
timestamp := time.Date(2020, 2, 30, 16, 44, 20, 0, time.UTC)
128191
require.Equal(t,

backend/coins/btc/handlers/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (handlers *Handlers) getAccountTransactions(_ *http.Request) (interface{},
175175
Fee: feeString,
176176
Time: formattedTime,
177177
Addresses: addresses,
178-
Note: handlers.account.Notes().TxNote(txInfo.InternalID),
178+
Note: handlers.account.TxNote(txInfo.InternalID),
179179
}
180180
switch handlers.account.Coin().(type) {
181181
case *btc.Coin:

0 commit comments

Comments
 (0)