From d31615734bbd48d7ac35b6a51733e01509745856 Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Thu, 21 Oct 2021 09:40:56 -0500 Subject: [PATCH] reconfig bug. CoinNotFoundError return value bug --- client/asset/btc/btc.go | 1 + client/asset/btc/spv.go | 58 +++++++++---- client/asset/btc/spv_test.go | 5 +- client/asset/interface.go | 4 + client/core/core.go | 86 +++++++++++++++---- client/core/trade.go | 6 +- client/webserver/live_test.go | 14 +-- client/webserver/site/src/html/wallets.tmpl | 2 +- client/webserver/site/src/js/app.js | 12 ++- client/webserver/site/src/js/forms.js | 4 +- client/webserver/site/src/js/wallets.js | 32 ++++--- .../src/localized_html/en-US/wallets.tmpl | 2 +- .../src/localized_html/pt-BR/wallets.tmpl | 2 +- .../src/localized_html/zh-CN/wallets.tmpl | 2 +- 14 files changed, 158 insertions(+), 72 deletions(-) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 31d9ab7785..e3474f4d88 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -172,6 +172,7 @@ var ( spvWalletDefinition, rpcWalletDefinition, }, + LegacyWalletIndex: 1, } ) diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index 2932e4bea3..3cff950995 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -193,6 +193,7 @@ func logNeutrino(netDir string) error { rotatorMtx.Lock() defer rotatorMtx.Unlock() if loggerCount > 0 { + loggerCount++ return nil } @@ -477,10 +478,19 @@ func (w *spvWallet) syncStatus() (*syncStatus, error) { return nil, err } + target := w.syncHeight() + currentHeight := blk.Height + synced := w.wallet.ChainSynced() + // Sometimes the wallet doesn't report the chain as synced right away. + // Seems to be a bug. + if !synced && target > 0 && target == currentHeight { + synced = true + } + return &syncStatus{ - Target: w.syncHeight(), - Height: blk.Height, - Syncing: !w.wallet.ChainSynced(), + Target: target, + Height: currentHeight, + Syncing: !synced, }, nil } @@ -677,7 +687,8 @@ func (w *spvWallet) sendToAddress(address string, value, feeRate uint64, subtrac } func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*chainhash.Hash, error) { - const unfundedTxSize = dexbtc.MinimumTxOverhead + 2*dexbtc.P2WPKHOutputSize + txOutSize := dexbtc.TxOutOverhead + uint64(len(pkScript)) // send-to address + var unfundedTxSize uint64 = dexbtc.MinimumTxOverhead + dexbtc.P2WPKHOutputSize /* change */ + txOutSize unspents, err := w.listUnspent() if err != nil { @@ -689,7 +700,9 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c return nil, fmt.Errorf("error converting unspent outputs: %w", err) } - enough := func(inputsSize, inputsVal uint64) bool { + // With sendWithSubtract, fees are subtracted from the sent amount, so we + // target an input sum, not an output value. Makes the math easy. + enough := func(_, inputsVal uint64) bool { return inputsVal >= value } @@ -699,8 +712,16 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c } fees := (unfundedTxSize + uint64(inputsSize)) * feeRate - if fees > sum { + send := value - fees + extra := sum - send + + switch { + case fees > sum: + return nil, fmt.Errorf("fees > sum") + case fees > value: return nil, fmt.Errorf("fees > value") + case send > sum: + return nil, fmt.Errorf("send > sum") } tx := wire.NewMsgTx(wire.TxVersion) @@ -710,9 +731,6 @@ func (w *spvWallet) sendWithSubtract(pkScript []byte, value, feeRate uint64) (*c tx.AddTxIn(txIn) } - send := value - fees - extra := sum - send - change := extra - fees changeAddr, err := w.changeAddress() if err != nil { @@ -918,6 +936,11 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error { // *Rescan supplied with a QuitChan-type RescanOption. // Actually, should use btcwallet.Wallet.NtfnServer ? + // notes := make(<-chan interface{}) + // if w.chainClient != nil { + // notes = w.chainClient.Notifications() + // } + // Nanny for the caches checkpoints and txBlocks caches. wg.Add(1) go func() { @@ -926,6 +949,7 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error { defer w.stop() ticker := time.NewTicker(time.Minute * 20) + defer ticker.Stop() expiration := time.Hour * 2 for { select { @@ -945,6 +969,8 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error { } } w.checkpointMtx.Unlock() + // case note := <-notes: + // fmt.Printf("--Notification received: %T: %+v \n", note, note) case <-ctx.Done(): return } @@ -1290,14 +1316,10 @@ func (w *spvWallet) getTxOut(txHash *chainhash.Hash, vout uint32, pkScript []byt return nil, 0, err } - if utxo == nil || utxo.spend != nil { + if utxo == nil || utxo.spend != nil || utxo.blockHash == nil { return nil, 0, nil } - if utxo.blockHash == nil { - return nil, 0, fmt.Errorf("output %s:%v not found", txHash, vout) - } - tip, err := w.cl.BestBlock() if err != nil { return nil, 0, fmt.Errorf("BestBlock error: %v", err) @@ -1335,11 +1357,12 @@ search: if err != nil { return nil, fmt.Errorf("error getting block hash for height %d: %w", height, err) } - res.checkpoint = *blockHash matched, err := w.matchPkScript(blockHash, [][]byte{pkScript}) if err != nil { return nil, fmt.Errorf("matchPkScript error: %w", err) } + + res.checkpoint = *blockHash if !matched { continue search } @@ -1401,6 +1424,11 @@ func (w *spvWallet) matchPkScript(blockHash *chainhash.Hash, scripts [][]byte) ( if err != nil { return false, fmt.Errorf("GetCFilter error: %w", err) } + + if filter.N() == 0 { + return false, fmt.Errorf("unexpected empty filter for %s", blockHash) + } + var filterKey [gcs.KeySize]byte copy(filterKey[:], blockHash[:gcs.KeySize]) diff --git a/client/asset/btc/spv_test.go b/client/asset/btc/spv_test.go index 7678609096..fbee237db2 100644 --- a/client/asset/btc/spv_test.go +++ b/client/asset/btc/spv_test.go @@ -13,6 +13,7 @@ import ( "time" "decred.org/dcrdex/dex" + "decred.org/dcrdex/dex/encode" dexbtc "decred.org/dcrdex/dex/networks/btc" "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcjson" @@ -319,7 +320,9 @@ func (c *tNeutrinoClient) GetBlockHeader(blkHash *chainhash.Hash) (*wire.BlockHe func (c *tNeutrinoClient) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) { var key [gcs.KeySize]byte copy(key[:], blockHash.CloneBytes()[:]) - return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, c.getCFilterScripts[blockHash]) + scripts := c.getCFilterScripts[blockHash] + scripts = append(scripts, encode.RandomBytes(10)) + return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, scripts) } func (c *tNeutrinoClient) GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) { diff --git a/client/asset/interface.go b/client/asset/interface.go index 57db407f97..d9f2682fe5 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -57,6 +57,10 @@ type WalletInfo struct { // be the initial form offered to the user for configuration, with others // available to select. AvailableWallets []*WalletDefinition `json:"availablewallets"` + // LegacyWalletIndex should be set for assets that existed before wallets + // were typed. The index should point to the WalletDefinition that should + // be assumed when the type is provided as an empty string. + LegacyWalletIndex int `json:"emptyidx"` // UnitInfo is the information about unit names and conversion factors for // the asset. UnitInfo dex.UnitInfo `json:"unitinfo"` diff --git a/client/core/core.go b/client/core/core.go index 6ccb560245..34a6d3f72f 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -1797,27 +1797,15 @@ func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error { // derived deterministically from the app seed and a random password. The // password is returned for encrypting and storing. func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form *WalletForm) ([]byte, error) { - creds := c.creds() - if creds == nil { - return nil, errors.New("no v2 credentials stored") - } - - appSeed, err := crypter.Decrypt(creds.EncSeed) + seed, pw, err := c.assetSeedAndPass(assetID, crypter) if err != nil { - return nil, fmt.Errorf("app seed decryption error: %w", err) + return nil, err } c.log.Infof("Initializing a built-in %s wallet", unbip(assetID)) - - b := make([]byte, len(appSeed)+4) - copy(b, appSeed) - binary.BigEndian.PutUint32(b[len(appSeed):], assetID) - - seed := blake256.Sum256(b) - pw := encode.RandomBytes(32) if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{ Type: form.Type, - Seed: seed[:], + Seed: seed, Pass: pw, Settings: form.Config, DataDir: c.assetDataDirectory(assetID), @@ -1830,6 +1818,26 @@ func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form return pw, nil } +func (c *Core) assetSeedAndPass(assetID uint32, crypter encrypt.Crypter) (seed, pass []byte, err error) { + creds := c.creds() + if creds == nil { + return nil, nil, errors.New("no v2 credentials stored") + } + + appSeed, err := crypter.Decrypt(creds.EncSeed) + if err != nil { + return nil, nil, fmt.Errorf("app seed decryption error: %w", err) + } + + b := make([]byte, len(appSeed)+4) + copy(b, appSeed) + binary.BigEndian.PutUint32(b[len(appSeed):], assetID) + + s := blake256.Sum256(b) + p := blake256.Sum256(seed) + return s[:], p[:], nil +} + // assetDataDirectory is a directory for a wallet to use for local storage. func (c *Core) assetDataDirectory(assetID uint32) string { return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb", unbip(assetID)) @@ -2026,7 +2034,7 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er assetID, unbip(assetID)) } seeded := oldWallet.Info().Seeded - if seeded && len(newWalletPW) > 0 { + if seeded && newWalletPW != nil { return newError(passwordErr, "cannot set a password on a built-in wallet") } oldDepositAddr := oldWallet.currentDepositAddress() @@ -2039,10 +2047,23 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er Address: oldDepositAddr, } - fmt.Printf("--ReconfigureWallet.0 %+v \n", walletDef) + var restartOnFail bool + + defer func() { + if restartOnFail { + if _, err := c.connectWallet(oldWallet); err != nil { + c.log.Errorf("Failed to reconnect wallet after a failed reconfiguration attempt: %v", err) + } + } + }() + + oldDef, err := walletDefinition(assetID, oldWallet.walletType) + if err != nil { + return fmt.Errorf("failed to locate old wallet definition: %v", err) + } if walletDef.Seeded { - if len(newWalletPW) > 0 { + if newWalletPW != nil { return newError(passwordErr, "cannot set a password on a seeded wallet") } @@ -2056,8 +2077,27 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er if err != nil { return newError(createWalletErr, "error creating new %q-type %s wallet: %v", form.Type, unbip(assetID), err) } - // return newError(existenceCheckErr, "cannot reconfigure wallet that doesn't exist") + } else if oldDef.Seeded { + _, newWalletPW, err = c.assetSeedAndPass(assetID, crypter) + if err != nil { + return newError(authErr, "error retrieving wallet password: %v", err) + } + } + + if oldWallet.connected() { + oldDef, err := walletDefinition(assetID, oldWallet.walletType) + // Error can be normal if the wallet was created before wallet types + // were a thing. Just assume this is an old wallet and therefore not + // seeded. + if err == nil && oldDef.Seeded { + oldWallet.Disconnect() + restartOnFail = true + } } + } else if newWalletPW == nil && oldDef.Seeded { + // If we're switching from a seeded wallet and no password was provided, + // use empty string = wallet not encrypted. + newWalletPW = []byte{} } // Reload the wallet with the new settings. @@ -2159,6 +2199,8 @@ func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) er c.wallets[assetID] = wallet c.walletMtx.Unlock() + restartOnFail = false + if oldWallet.connected() { // NOTE: Cannot lock the wallet backend because it may be the same as // the one just connected. @@ -6235,6 +6277,12 @@ func walletDefinition(assetID uint32, walletType string) (*asset.WalletDefinitio if err != nil { return nil, newError(assetSupportErr, "asset.Info error: %v", err) } + if walletType == "" { + if len(winfo.AvailableWallets) <= winfo.LegacyWalletIndex { + return nil, fmt.Errorf("legacy wallet index out of range") + } + return winfo.AvailableWallets[winfo.LegacyWalletIndex], nil + } var walletDef *asset.WalletDefinition for _, def := range winfo.AvailableWallets { if def.Type == walletType { diff --git a/client/core/trade.go b/client/core/trade.go index 6a392b8760..044b228b40 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -2029,7 +2029,7 @@ func (t *trackedTrade) processAuditMsg(msgID uint64, audit *msgjson.Audit) error err := t.auditContract(match, audit.CoinID, audit.Contract, audit.TxData) if err != nil { contractID := coinIDString(t.wallets.toAsset.ID, audit.CoinID) - t.dc.log.Error("Failed to audit contract coin %v (%s) for match %s: %v", + t.dc.log.Errorf("Failed to audit contract coin %v (%s) for match %s: %v", contractID, t.wallets.toAsset.Symbol, match, err) // Don't revoke in case server sends a revised audit request before // the match is revoked. @@ -2075,6 +2075,7 @@ func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contr var auditInfo *asset.AuditInfo var tries int contractID, contractSymb := coinIDString(t.wallets.toAsset.ID, coinID), t.wallets.toAsset.Symbol + tLastWarning := time.Now() t.latencyQ.Wait(&wait.Waiter{ Expiration: time.Now().Add(24 * time.Hour), // effectively forever TryFunc: func() bool { @@ -2093,7 +2094,8 @@ func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contr "Check your internet and wallet connections!", contractID, contractSymb)) return wait.DontTryAgain } - if tries > 0 && tries%12 == 0 { + if time.Since(tLastWarning) > 30*time.Minute { + tLastWarning = time.Now() subject, detail := t.formatDetails(TopicAuditTrouble, contractID, contractSymb, match) t.notify(newOrderNote(TopicAuditTrouble, subject, detail, db.WarningLevel, t.coreOrder())) } diff --git a/client/webserver/live_test.go b/client/webserver/live_test.go index 0e6f145acc..c80a772ec2 100644 --- a/client/webserver/live_test.go +++ b/client/webserver/live_test.go @@ -1099,6 +1099,7 @@ func (c *TCore) walletState(assetID uint32) *core.WalletState { Balance: c.balances[assetID], Units: winfos[assetID].UnitInfo.AtomicUnit, Encrypted: true, + Synced: true, } } @@ -1224,13 +1225,12 @@ func (c *TCore) SupportedAssets() map[uint32]*core.SupportedAsset { c.mtx.RLock() defer c.mtx.RUnlock() return map[uint32]*core.SupportedAsset{ - 0: mkSupportedAsset("btc", c.wallets[0], c.balances[0]), - 42: mkSupportedAsset("dcr", c.wallets[42], c.balances[42]), - 2: mkSupportedAsset("ltc", c.wallets[2], c.balances[2]), - 22: mkSupportedAsset("mona", c.wallets[22], c.balances[22]), - 3: mkSupportedAsset("doge", c.wallets[3], c.balances[3]), - 28: mkSupportedAsset("vtc", c.wallets[28], c.balances[28]), - 141: mkSupportedAsset("kmd", c.wallets[141], c.balances[141]), + 0: mkSupportedAsset("btc", c.wallets[0], c.balances[0]), + 42: mkSupportedAsset("dcr", c.wallets[42], c.balances[42]), + 2: mkSupportedAsset("ltc", c.wallets[2], c.balances[2]), + 22: mkSupportedAsset("mona", c.wallets[22], c.balances[22]), + 3: mkSupportedAsset("doge", c.wallets[3], c.balances[3]), + 28: mkSupportedAsset("vtc", c.wallets[28], c.balances[28]), } } diff --git a/client/webserver/site/src/html/wallets.tmpl b/client/webserver/site/src/html/wallets.tmpl index 1b1cb90018..cc879e8cd1 100644 --- a/client/webserver/site/src/html/wallets.tmpl +++ b/client/webserver/site/src/html/wallets.tmpl @@ -168,7 +168,7 @@ {{- /* RECONFIGURE WALLET */ -}} -
+
[[[Reconfigure]]] diff --git a/client/webserver/site/src/js/app.js b/client/webserver/site/src/js/app.js index ad7214f23f..03802ece36 100644 --- a/client/webserver/site/src/js/app.js +++ b/client/webserver/site/src/js/app.js @@ -681,10 +681,14 @@ export default class Application { return encRate / RateEncodingFactor * r } - walletDefinition (assetID) { - const asset = this.assets[assetID] - const walletType = asset.wallet.type || asset.info.availablewallets[0].type - return asset.info.availablewallets.filter(def => def.type === walletType)[0] + walletDefinition (assetID, walletType) { + const assetInfo = this.assets[assetID].info + if (walletType === '') return assetInfo.availablewallets[assetInfo.emptyidx] + return assetInfo.availablewallets.filter(def => def.type === walletType)[0] + } + + currentWalletDefinition (assetID) { + return this.walletDefinition(assetID, this.assets[assetID].wallet.type) } /* diff --git a/client/webserver/site/src/js/forms.js b/client/webserver/site/src/js/forms.js index 293f4870de..15c397567a 100644 --- a/client/webserver/site/src/js/forms.js +++ b/client/webserver/site/src/js/forms.js @@ -73,7 +73,6 @@ export class NewWalletForm { const page = this.page const asset = app().assets[assetID] const tabs = page.walletTypeTabs - const asset = app().assets[assetID] if (this.currentAsset && this.currentAsset.id === asset.id) return this.currentAsset = asset page.nwAssetLogo.src = Doc.logoPath(asset.symbol) @@ -138,8 +137,7 @@ export class NewWalletForm { */ async loadDefaults () { // No default config files for seeded assets right now. - const asset = app().assets[this.currentAsset.id] - const walletDef = asset.info.availablewallets.filter(def => def.type === this.currentWalletType)[0] + const walletDef = app().walletDefinition(this.currentAsset.id, this.currentWalletType) if (walletDef.seeded) return const loaded = app().loading(this.form) const res = await postJSON('/api/defaultwalletcfg', { diff --git a/client/webserver/site/src/js/wallets.js b/client/webserver/site/src/js/wallets.js index 7ca611737c..f47b417efc 100644 --- a/client/webserver/site/src/js/wallets.js +++ b/client/webserver/site/src/js/wallets.js @@ -62,7 +62,7 @@ export default class WalletsPage extends BasePage { this.walletForm = new NewWalletForm(page.walletForm, () => { this.createWalletSuccess() }) // Bind the wallet reconfig form. - this.walletReconfig = new WalletConfigForm(page.reconfigInputs, false) + this.reconfigForm = new WalletConfigForm(page.reconfigInputs, false) // Bind the wallet unlock form. this.unlockForm = new UnlockWalletForm(page.openForm, () => { this.openWalletSuccess() }) @@ -71,7 +71,7 @@ export default class WalletsPage extends BasePage { bindForm(page.withdrawForm, page.submitWithdraw, () => { this.withdraw() }) // Bind the wallet reconfiguration submission. - bindForm(page.walletReconfig, page.submitReconfig, () => this.reconfig()) + bindForm(page.reconfigForm, page.submitReconfig, () => this.reconfig()) // Bind the row clicks, which shows the available markets for the asset. for (const rowInfo of Object.values(rowInfos)) { @@ -278,7 +278,7 @@ export default class WalletsPage extends BasePage { this.setPWSettingViz(this.changeWalletPW) const asset = app().assets[assetID] - const currentDef = app().walletDefinition(assetID) + const currentDef = app().currentWalletDefinition(assetID) if (asset.info.availablewallets.length > 1) { Doc.empty(page.changeWalletTypeSelect) @@ -297,8 +297,8 @@ export default class WalletsPage extends BasePage { page.recfgAssetLogo.src = Doc.logoPath(asset.symbol) page.recfgAssetName.textContent = asset.info.name await this.hideBox() - this.animation = this.showBox(page.walletReconfig) - const loaded = app().loading(page.walletReconfig) + this.animation = this.showBox(page.reconfigForm) + const loaded = app().loading(page.reconfigForm) const res = await postJSON('/api/walletsettings', { assetID: assetID }) @@ -308,22 +308,20 @@ export default class WalletsPage extends BasePage { Doc.show(page.reconfigErr) return } - this.walletReconfig.setConfig(res.map) - - this.walletReconfig.update(currentDef.configopts || []) - this.update(currentDef) + this.reconfigForm.update(currentDef.configopts || []) + this.reconfigForm.setConfig(res.map) + this.updateDisplayedReconfigFields(currentDef) } changeWalletType () { const page = this.page const walletType = page.changeWalletTypeSelect.value - const asset = app().assets[this.reconfigAsset] - const walletDef = asset.info.availablewallets.filter(def => def.type === walletType)[0] - this.update(walletDef) - this.walletReconfig.update(walletDef.configopts || []) + const walletDef = app().walletDefinition(this.reconfigAsset, walletType) + this.reconfigForm.update(walletDef.configopts || []) + this.updateDisplayedReconfigFields(walletDef) } - update (walletDef) { + updateDisplayedReconfigFields (walletDef) { if (walletDef.seeded) { Doc.hide(this.page.showChangePW) this.changeWalletPW = false @@ -460,15 +458,15 @@ export default class WalletsPage extends BasePage { return } - let walletType = app().walletDefinition(this.reconfigAsset).type + let walletType = app().currentWalletDefinition(this.reconfigAsset).type if (!Doc.isHidden(page.changeWalletType)) { walletType = page.changeWalletTypeSelect.value } - const loaded = app().loading(page.walletReconfig) + const loaded = app().loading(page.reconfigForm) const req = { assetID: this.reconfigAsset, - config: this.walletReconfig.map(), + config: this.reconfigForm.map(), appPW: page.appPW.value, walletType: walletType } diff --git a/client/webserver/site/src/localized_html/en-US/wallets.tmpl b/client/webserver/site/src/localized_html/en-US/wallets.tmpl index 2c70b066ae..047d50e0d0 100644 --- a/client/webserver/site/src/localized_html/en-US/wallets.tmpl +++ b/client/webserver/site/src/localized_html/en-US/wallets.tmpl @@ -168,7 +168,7 @@ {{- /* RECONFIGURE WALLET */ -}} -
+
Reconfigure diff --git a/client/webserver/site/src/localized_html/pt-BR/wallets.tmpl b/client/webserver/site/src/localized_html/pt-BR/wallets.tmpl index a6c29522be..6bac6f590d 100644 --- a/client/webserver/site/src/localized_html/pt-BR/wallets.tmpl +++ b/client/webserver/site/src/localized_html/pt-BR/wallets.tmpl @@ -168,7 +168,7 @@ {{- /* RECONFIGURE WALLET */ -}} -
+
Reconfigurar diff --git a/client/webserver/site/src/localized_html/zh-CN/wallets.tmpl b/client/webserver/site/src/localized_html/zh-CN/wallets.tmpl index 21cf80ccd6..00c1bcc47f 100644 --- a/client/webserver/site/src/localized_html/zh-CN/wallets.tmpl +++ b/client/webserver/site/src/localized_html/zh-CN/wallets.tmpl @@ -168,7 +168,7 @@ {{- /* RECONFIGURE WALLET */ -}} -
+
重新配置