From c9c6f4b7f2ad5ce5019a698dd3acbf605cc8eb29 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Wed, 14 Feb 2024 20:57:07 +0400 Subject: [PATCH] wallet: Use `interactive scan` on initial sync and rescan. Check issue #872 --- CHANGELOG.md | 8 +- lib/wallet/client.js | 18 +++ lib/wallet/nodeclient.js | 28 ++++ lib/wallet/nullclient.js | 13 +- lib/wallet/txdb.js | 5 +- lib/wallet/wallet.js | 2 +- lib/wallet/walletdb.js | 100 ++++++++++++- test/wallet-rescan-test.js | 296 +++++++++++++++++++++++++------------ test/wallet-unit-test.js | 2 +- 9 files changed, 361 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4478c72e1..db884e263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ process and allows parallel rescans. - expects ws hook for `block rescan interactive abort` param `message`. ### Wallet Changes +- Add migration that recalculates txdb balances to fix any inconsistencies. +- Wallet will now use `interactive scan` for initial sync(on open) and rescan. + #### Configuration - Wallet now has option `wallet-migrate-no-rescan`/`migrate-no-rescan` if you want to disable rescan when migration recommends it. It may result in the @@ -51,7 +54,6 @@ process and allows parallel rescans. #### Wallet API -- Add migration that recalculates txdb balances to fix any inconsistencies. - WalletNode now emits `open` and `close` events. - WalletDB Now emits events for: `open`, `close`, `connect`, `disconnect`. - WalletDB @@ -59,12 +61,14 @@ process and allows parallel rescans. - `open()` no longer calls scan, instead only rollbacks and waits for sync to do the rescan. - emits events for: `open`, `close`, `connect`, `disconnect`, `sync done`. -- HTTP Changes: + +#### Wallet HTTP - All transaction creating endpoints now accept `hardFee` for specifying the exact fee. - All transaction sending endpoints now fundlock/queue tx creation. (no more conflicting transactions) + ## v6.0.0 ### Node and Wallet HTTP API diff --git a/lib/wallet/client.js b/lib/wallet/client.js index b67891ee5..0f7e3a729 100644 --- a/lib/wallet/client.js +++ b/lib/wallet/client.js @@ -17,6 +17,7 @@ const parsers = { 'block connect': (entry, txs) => parseBlock(entry, txs), 'block disconnect': entry => [parseEntry(entry)], 'block rescan': (entry, txs) => parseBlock(entry, txs), + 'block rescan interactive': (entry, txs) => parseBlock(entry, txs), 'chain reset': entry => [parseEntry(entry)], 'tx': tx => [TX.decode(tx)] }; @@ -75,10 +76,27 @@ class WalletClient extends NodeClient { return super.setFilter(filter.encode()); } + /** + * Rescan for any missed transactions. + * @param {Number|Hash} start - Start block. + * @returns {Promise} + */ + async rescan(start) { return super.rescan(start); } + /** + * Rescan interactive for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Boolean} [fullLock=false] + * @returns {Promise} + */ + + async rescanInteractive(start, fullLock) { + return super.rescanInteractive(start, null, fullLock); + } + async getNameStatus(nameHash) { const json = await super.getNameStatus(nameHash); return NameState.fromJSON(json); diff --git a/lib/wallet/nodeclient.js b/lib/wallet/nodeclient.js index a477ebf8a..5f181bcbc 100644 --- a/lib/wallet/nodeclient.js +++ b/lib/wallet/nodeclient.js @@ -262,6 +262,34 @@ class NodeClient extends AsyncEmitter { }); } + /** + * Rescan interactive for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Boolean} [fullLock=false] + * @returns {Promise} + */ + + async rescanInteractive(start, fullLock = true) { + if (this.node.spv) + return this.node.chain.reset(start); + + const iter = async (entry, txs) => { + return await this.handleCall('block rescan interactive', entry, txs); + }; + + try { + return await this.node.scanInteractive( + start, + this.filter, + iter, + fullLock + ); + } catch (e) { + await this.handleCall('block rescan interactive abort', e.message); + throw e; + } + } + /** * Get name state. * @param {Buffer} nameHash diff --git a/lib/wallet/nullclient.js b/lib/wallet/nullclient.js index abc30f266..0dcdde0b7 100644 --- a/lib/wallet/nullclient.js +++ b/lib/wallet/nullclient.js @@ -165,8 +165,6 @@ class NullClient extends EventEmitter { /** * Rescan for any missed transactions. * @param {Number|Hash} start - Start block. - * @param {Bloom} filter - * @param {Function} iter - Iterator. * @returns {Promise} */ @@ -174,6 +172,17 @@ class NullClient extends EventEmitter { ; } + /** + * Rescan interactive for any missed transactions. + * @param {Number|Hash} start - Start block. + * @param {Boolean} [fullLock=false] + * @returns {Promise} + */ + + async rescanInteractive(start, fullLock) { + ; + } + /** * Get opening bid height. * @param {Buffer} nameHash diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index a1e53ab4f..ec4869647 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -14,14 +14,13 @@ const Amount = require('../ui/amount'); const CoinView = require('../coins/coinview'); const Coin = require('../primitives/coin'); const Outpoint = require('../primitives/outpoint'); -const records = require('./records'); const layout = require('./layout').txdb; const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const rules = require('../covenants/rules'); const NameState = require('../covenants/namestate'); const NameUndo = require('../covenants/undo'); -const {TXRecord} = records; +const {TXRecord} = require('./records'); const {types} = rules; /* @@ -1484,7 +1483,7 @@ class TXDB { /** * Revert a block. * @param {Number} height - * @returns {Promise} - blocks length + * @returns {Promise} - number of txs removed. */ async revert(height) { diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 9c23aa88b..0423eeb4f 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -4785,7 +4785,7 @@ class Wallet extends EventEmitter { /** * Revert a block. * @param {Number} height - * @returns {Promise} + * @returns {Promise} - number of txs removed. */ async revert(height) { diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 7173bb348..1bad159e6 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -29,8 +29,10 @@ const WalletMigrator = require('./migrations'); const layout = layouts.wdb; const tlayout = layouts.txdb; const {states} = require('../covenants/namestate'); +const {scanActions} = require('../blockchain/common'); /** @typedef {import('../primitives/tx')} TX */ +/** @typedef {import('../blockchain/common').ScanAction} ScanAction */ const { ChainState, @@ -189,6 +191,21 @@ class WalletDB extends EventEmitter { } }); + this.client.hook('block rescan interactive', async (entry, txs) => { + try { + return await this.rescanBlockInteractive(entry, txs); + } catch (e) { + this.emit('error', e); + return { + type: scanActions.ABORT + }; + } + }); + + this.client.hook('block rescan interactive abort', async (message) => { + this.emit('error', new Error(message)); + }); + this.client.bind('tx', async (tx) => { try { await this.addTX(tx); @@ -522,7 +539,7 @@ class WalletDB extends EventEmitter { } // syncNode sets the rescanning to true. - return this.scan(height); + return this.scanInteractive(height); } /** @@ -534,11 +551,17 @@ class WalletDB extends EventEmitter { */ async scan(height) { + assert(this.rescanning, 'WDB: Rescanning guard not set.'); + if (height == null) height = this.state.startHeight; assert((height >>> 0) === height, 'WDB: Must pass in a height.'); + this.logger.info( + 'Rolling back %d blocks.', + this.height - height + 1); + await this.rollback(height); this.logger.info( @@ -550,6 +573,38 @@ class WalletDB extends EventEmitter { return this.client.rescan(tip.hash); } + /** + * Interactive scan blockchain from a given height. + * Expect this.rescanning to be set to true. + * @private + * @param {Number} [height=this.state.startHeight] + * @param {Boolean} [fullLock=true] + * @returns {Promise} + */ + + async scanInteractive(height, fullLock = true) { + assert(this.rescanning, 'WDB: Rescanning guard not set.'); + + if (height == null) + height = this.state.startHeight; + + assert((height >>> 0) === height, 'WDB: Must pass in a height.'); + + this.logger.info( + 'Rolling back %d blocks.', + this.height - height + 1); + + await this.rollback(height); + + this.logger.info( + 'WalletDB is scanning %d blocks.', + this.state.height - height + 1); + + const tip = await this.getTip(); + + return this.client.rescanInteractive(tip.hash, fullLock); + } + /** * Deep Clean: * Keep all keys, account data, wallet maps (name and path). @@ -660,7 +715,7 @@ class WalletDB extends EventEmitter { this.rescanning = true; try { - return await this.scan(height); + return await this.scanInteractive(height); } finally { this.rescanning = false; } @@ -2132,7 +2187,7 @@ class WalletDB extends EventEmitter { */ async addOutpointMap(b, hash, index, wid) { - await this.addOutpoint(hash, index); + this.addOutpoint(hash, index); return this.addMap(b, layout.o.encode(hash, index), wid); } @@ -2431,7 +2486,7 @@ class WalletDB extends EventEmitter { * Unconfirm a block's transactions * and write the new best hash (SPV version). * @param {ChainEntry} entry - * @returns {Promise} + * @returns {Promise} - number of txs removed. */ async removeBlock(entry) { @@ -2447,7 +2502,7 @@ class WalletDB extends EventEmitter { * Unconfirm a block's transactions. * @private * @param {ChainEntry} entry - * @returns {Promise} + * @returns {Promise} - number of txs removed. */ async _removeBlock(entry) { @@ -2524,6 +2579,41 @@ class WalletDB extends EventEmitter { } } + /** + * Rescan a block interactively. + * @param {ChainEntry} entry + * @param {TX[]} txs + * @returns {Promise} - interactive action + */ + + async rescanBlockInteractive(entry, txs) { + if (!this.rescanning) + throw new Error(`WDB: Unsolicited rescan block: ${entry.height}.`); + + if (entry.height > this.state.height + 1) + throw new Error(`WDB: Rescan block too high: ${entry.height}.`); + + const blockAdded = await this._addBlock(entry, txs); + + if (!blockAdded) + throw new Error('WDB: Block not added.'); + + if (blockAdded.filterUpdated) { + // We remove block, because adding the same block twice, will ignore + // already indexed transactions. This handles the case where single + // transaction has undiscovered outputs. + await this._removeBlock(entry); + + return { + type: scanActions.REPEAT + }; + } + + return { + type: scanActions.NEXT + }; + } + /** * Add a transaction to the database, map addresses * to wallet IDs, potentially store orphans, resolve diff --git a/test/wallet-rescan-test.js b/test/wallet-rescan-test.js index 920e0ae37..b5b810b69 100644 --- a/test/wallet-rescan-test.js +++ b/test/wallet-rescan-test.js @@ -38,10 +38,9 @@ const combinations = [ ]; const noSPVcombinations = combinations.filter(c => !c.SPV); +const regtest = Network.get('regtest'); describe('Wallet rescan/addBlock', function() { - const network = Network.get('regtest'); - // TODO: Add SPV tests. for (const {SPV, STANDALONE, name} of noSPVcombinations) { describe(`rescan/addBlock gapped addresses (${name} Integration)`, function() { @@ -54,66 +53,16 @@ describe('Wallet rescan/addBlock', function() { const WALLET_NAME = 'test'; const ACCOUNT = 'default'; - const network = Network.get('regtest'); + const regtest = Network.get('regtest'); /** @type {NodesContext} */ let nodes; let minerWallet, minerAddress; let main, addBlock, rescan; - const deriveAddresses = async (wallet, depth) => { - const accInfo = await wallet.getAccount('default'); - let currentDepth = accInfo.receiveDepth; - - if (depth <= currentDepth) - return; - - while (currentDepth !== depth) { - const addr = await wallet.createAddress('default'); - currentDepth = addr.index; - } - }; - - const getAddress = async (wallet, depth = -1) => { - const accInfo = await wallet.getAccount('default'); - const {accountKey, lookahead} = accInfo; - - if (depth === -1) - depth = accInfo.receiveDepth; - - const XPUBKey = HDPublicKey.fromBase58(accountKey, network); - const key = XPUBKey.derive(0).derive(depth).publicKey; - const address = Address.fromPubkey(key); - - const gappedDepth = depth + lookahead + 1; - return {address, depth, gappedDepth}; - }; - - const generateGappedAddresses = async (wallet, count) => { - let depth = -1; - - const addresses = []; - - // generate gapped addresses. - for (let i = 0; i < count; i++) { - const addrInfo = await getAddress(wallet, depth); - - addresses.push({ - address: addrInfo.address, - depth: addrInfo.depth, - gappedDepth: addrInfo.gappedDepth - }); - - await deriveAddresses(wallet, depth); - depth = addrInfo.gappedDepth; - } - - return addresses; - }; - before(async () => { // Initial node is the one that progresses the network. - nodes = new NodesContext(network, 1); + nodes = new NodesContext(regtest, 1); // MAIN_WALLET = 0 nodes.init({ wallet: true, @@ -199,7 +148,8 @@ describe('Wallet rescan/addBlock', function() { // 1 address per block, all of them gapped. // Start after first gap, make sure rescan has no clue. - const all = await generateGappedAddresses(main.client, blocks + 1); + const all = await generateGappedAddresses(main.client, blocks + 1, regtest); + await deriveAddresses(main.client, all[all.length - 1].depth); const addresses = all.slice(1); // give addBlock first address. await deriveAddresses(addBlock.client, addresses[0].depth - TEST_LOOKAHEAD); @@ -211,7 +161,7 @@ describe('Wallet rescan/addBlock', function() { for (let i = 0; i < blocks; i++) { await minerWallet.send({ outputs: [{ - address: addresses[i].address.toString(network), + address: addresses[i].address.toString(regtest), value: 1e6 }] }); @@ -259,7 +209,8 @@ describe('Wallet rescan/addBlock', function() { const expectedRescanBalance = await getBalance(rescan.client, ACCOUNT); const txCount = 5; - const all = await generateGappedAddresses(main.client, txCount + 1); + const all = await generateGappedAddresses(main.client, txCount + 1, regtest); + await deriveAddresses(main.client, all[all.length - 1].depth); const addresses = all.slice(1); // give addBlock first address. @@ -272,7 +223,7 @@ describe('Wallet rescan/addBlock', function() { for (const {address} of addresses) { await minerWallet.send({ outputs: [{ - address: address.toString(network), + address: address.toString(regtest), value: 1e6 }] }); @@ -302,7 +253,7 @@ describe('Wallet rescan/addBlock', function() { assert.deepStrictEqual(addBlockInfo, mainInfo); }); - it.skip('should receive gapped txs in the same block (rescan)', async () => { + it('should receive gapped txs in the same block (rescan)', async () => { const expectedBalance = await getBalance(main.client, ACCOUNT); const expectedInfo = await main.client.getAccount(ACCOUNT); @@ -319,7 +270,8 @@ describe('Wallet rescan/addBlock', function() { const expectedRescanBalance = await getBalance(rescan.client, ACCOUNT); const outCount = 5; - const all = await generateGappedAddresses(main.client, outCount + 1); + const all = await generateGappedAddresses(main.client, outCount + 1, regtest); + await deriveAddresses(main.client, all[all.length - 1].depth); const addresses = all.slice(1); // give addBlock first address. @@ -330,7 +282,7 @@ describe('Wallet rescan/addBlock', function() { const rescanWalletBlocks = forEvent(rescan.wdb, 'block connect'); const outputs = addresses.map(({address}) => ({ - address: address.toString(network), + address: address.toString(regtest), value: 1e6 })); @@ -359,7 +311,7 @@ describe('Wallet rescan/addBlock', function() { assert.deepStrictEqual(addBlockInfo, mainInfo); }); - it.skip('should receive gapped outputs in the same tx (rescan)', async () => { + it('should receive gapped outputs in the same tx (rescan)', async () => { const expectedBalance = await getBalance(main.client, ACCOUNT); const expectedInfo = await main.client.getAccount(ACCOUNT); @@ -389,10 +341,10 @@ describe('Wallet rescan/addBlock', function() { let nodes; let wnodeCtx, noWnodeCtx; let minerWallet, minerAddress; - let testAddress; + let testWallet, testAddress; before(async () => { - nodes = new NodesContext(network, 1); + nodes = new NodesContext(regtest, 1); // MINER = 0 nodes.init({ @@ -432,7 +384,7 @@ describe('Wallet rescan/addBlock', function() { minerWallet = nodes.context(MINER).wclient.wallet('primary'); minerAddress = (await minerWallet.createAddress('default')).address; - const testWallet = wnodeCtx.wclient.wallet('primary'); + testWallet = wnodeCtx.wclient.wallet('primary'); testAddress = (await testWallet.createAddress('default')).address; await nodes.close(WALLET); @@ -501,29 +453,23 @@ describe('Wallet rescan/addBlock', function() { // Disable wallet await noWnodeCtx.close(); - // sync node. - let eventsToWait; - wnodeCtx.init(); + const eventsToWait = []; // For spv we don't wait for sync done, as it will do the full rescan // and reset the SPVNode as well. It does not depend on the accumulated // blocks. if (SPV) { - eventsToWait = [ - // This will happen right away, as scan will just call reset - forEvent(wnodeCtx.wdb, 'sync done'), - // This is what matters for the rescan. - forEventCondition(wnodeCtx.wdb, 'block connect', (entry) => { - return entry.height === nodes.height(MINER); - }), + // This will happen right away, as scan will just call reset + eventsToWait.push(forEvent(wnodeCtx.wdb, 'sync done')); + // This is what matters for the rescan. + eventsToWait.push(forEventCondition(wnodeCtx.wdb, 'block connect', (entry) => { + return entry.height === nodes.height(MINER); + })); // Make sure node gets resets. - forEvent(wnodeCtx.node, 'reset') - ]; + eventsToWait.push(forEvent(wnodeCtx.node, 'reset')); } else { - eventsToWait = [ - forEvent(wnodeCtx.wdb, 'sync done') - ]; + eventsToWait.push(forEvent(wnodeCtx.wdb, 'sync done')); } await wnodeCtx.open(); @@ -584,24 +530,23 @@ describe('Wallet rescan/addBlock', function() { wnodeCtx.init(); // initial sync - let eventsToWait; + const eventsToWait = []; + if (SPV) { - eventsToWait = [ - // This will happen right away, as scan will just call reset - forEvent(wnodeCtx.wdb, 'sync done'), - // This is what matters for the rescan. - forEventCondition(wnodeCtx.wdb, 'block connect', (entry) => { - return entry.height === nodes.height(MINER); - }), - // Make sure node gets resets. - forEvent(wnodeCtx.node, 'reset'), - forEvent(wnodeCtx.wdb, 'unconfirmed') - ]; + // This will happen right away, as scan will just call reset + eventsToWait.push(forEvent(wnodeCtx.wdb, 'sync done')); + + // This is what matters for the rescan. + eventsToWait.push(forEventCondition(wnodeCtx.wdb, 'block connect', (entry) => { + return entry.height === nodes.height(MINER); + })); + + // Make sure node gets resets. + eventsToWait.push(forEvent(wnodeCtx.node, 'reset')); + eventsToWait.push(forEvent(wnodeCtx.wdb, 'unconfirmed')); } else { - eventsToWait = [ - forEvent(wnodeCtx.wdb, 'sync done'), - forEvent(wnodeCtx.wdb, 'unconfirmed') - ]; + eventsToWait.push(forEvent(wnodeCtx.wdb, 'sync done')); + eventsToWait.push(forEvent(wnodeCtx.wdb, 'unconfirmed')); } await wnodeCtx.open(); await Promise.all(eventsToWait); @@ -621,13 +566,111 @@ describe('Wallet rescan/addBlock', function() { await wnodeCtx.close(); }); + + it('should rescan/resync after wallet was off and received gapped txs in the same block', async () => { + if (SPV) + this.skip(); + + const txCount = 5; + await wnodeCtx.open(); + const startingBalance = await getBalance(testWallet, 'default'); + const all = await generateGappedAddresses(testWallet, txCount, regtest); + await wnodeCtx.close(); + + await noWnodeCtx.open(); + + for (const {address} of all) { + await minerWallet.send({ + outputs: [{ + address: address.toString(regtest), + value: 1e6 + }] + }); + } + + const waitHeight = nodes.height(MINER) + 1; + const nodeSync = forEventCondition(noWnodeCtx.node, 'connect', (entry) => { + return entry.height === waitHeight; + }); + + await nodes.generate(MINER, 1, minerAddress); + + await nodeSync; + await noWnodeCtx.close(); + + wnodeCtx.init(); + + const syncDone = forEvent(wnodeCtx.wdb, 'sync done'); + await wnodeCtx.open(); + await syncDone; + assert.strictEqual(wnodeCtx.wdb.height, nodes.height(MINER)); + + const balance = await getBalance(testWallet, 'default'); + const diff = balance.diff(startingBalance); + assert.deepStrictEqual(diff, new Balance({ + tx: txCount, + coin: txCount, + confirmed: 1e6 * txCount, + unconfirmed: 1e6 * txCount + })); + + await wnodeCtx.close(); + }); + + it('should rescan/resync after wallet was off and received gapped coins in the same tx', async () => { + if (SPV) + this.skip(); + + const outCount = 5; + await wnodeCtx.open(); + const startingBalance = await getBalance(testWallet, 'default'); + const all = await generateGappedAddresses(testWallet, outCount, regtest); + await wnodeCtx.close(); + + await noWnodeCtx.open(); + + const outputs = all.map(({address}) => ({ + address: address.toString(regtest), + value: 1e6 + })); + + await minerWallet.send({outputs}); + + const waitHeight = nodes.height(MINER) + 1; + const nodeSync = forEventCondition(noWnodeCtx.node, 'connect', (entry) => { + return entry.height === waitHeight; + }); + + await nodes.generate(MINER, 1, minerAddress); + + await nodeSync; + await noWnodeCtx.close(); + + wnodeCtx.init(); + + const syncDone = forEvent(wnodeCtx.wdb, 'sync done'); + await wnodeCtx.open(); + await syncDone; + assert.strictEqual(wnodeCtx.wdb.height, nodes.height(MINER)); + + const balance = await getBalance(testWallet, 'default'); + const diff = balance.diff(startingBalance); + assert.deepStrictEqual(diff, new Balance({ + tx: 1, + coin: outCount, + confirmed: 1e6 * outCount, + unconfirmed: 1e6 * outCount + })); + + await wnodeCtx.close(); + }); }); } for (const {STANDALONE, name} of noSPVcombinations) { describe(`Deadlock (${name} Integration)`, function() { this.timeout(10000); - const nodes = new NodesContext(network, 1); + const nodes = new NodesContext(regtest, 1); let minerCtx; let nodeCtx, address, node, wdb; @@ -767,6 +810,10 @@ describe('Wallet rescan/addBlock', function() { // We start rescan only after first disconnect is detected to ensure // wallet guard is set. await forEvent(node.chain, 'disconnect'); + + // abort should also report reason as an error. + const errorEvents = forEvent(wdb, 'error', 1); + let err; try { // Because we are rescanning within the rescan blocks, @@ -780,8 +827,63 @@ describe('Wallet rescan/addBlock', function() { assert(err); assert.strictEqual(err.message, 'Cannot rescan an alternate chain.'); + const errors = await errorEvents; + assert.strictEqual(errors.length, 1); + const errEv = errors[0].values[0]; + assert(errEv); + assert.strictEqual(errEv.message, 'Cannot rescan an alternate chain.'); + await mineBlocks; }); }); } }); + +async function deriveAddresses(walletClient, depth) { + const accInfo = await walletClient.getAccount('default'); + let currentDepth = accInfo.receiveDepth; + + if (depth <= currentDepth) + return; + + while (currentDepth !== depth) { + const addr = await walletClient.createAddress('default'); + currentDepth = addr.index; + } +} + +async function getAddress(walletClient, depth = -1, network = regtest) { + const accInfo = await walletClient.getAccount('default'); + const {accountKey, lookahead} = accInfo; + + if (depth === -1) + depth = accInfo.receiveDepth; + + const XPUBKey = HDPublicKey.fromBase58(accountKey, network); + const key = XPUBKey.derive(0).derive(depth).publicKey; + const address = Address.fromPubkey(key); + + const gappedDepth = depth + lookahead + 1; + return {address, depth, gappedDepth}; +} + +async function generateGappedAddresses(walletClient, count, network = regtest) { + let depth = -1; + + const addresses = []; + + // generate gapped addresses. + for (let i = 0; i < count; i++) { + const addrInfo = await getAddress(walletClient, depth, network); + + addresses.push({ + address: addrInfo.address, + depth: addrInfo.depth, + gappedDepth: addrInfo.gappedDepth + }); + + depth = addrInfo.gappedDepth; + } + + return addresses; +} diff --git a/test/wallet-unit-test.js b/test/wallet-unit-test.js index d991085b8..1e6a55302 100644 --- a/test/wallet-unit-test.js +++ b/test/wallet-unit-test.js @@ -520,7 +520,7 @@ describe('Wallet Unit Tests', () => { let rescan = false; let rescanHash = null; - wdb.client.rescan = async (hash) => { + wdb.client.rescanInteractive = async (hash) => { rescan = true; rescanHash = hash; };