diff --git a/__tests__/integration/hathorwallet_others.test.ts b/__tests__/integration/hathorwallet_others.test.ts index b2235ebe5..4ad2276c5 100644 --- a/__tests__/integration/hathorwallet_others.test.ts +++ b/__tests__/integration/hathorwallet_others.test.ts @@ -1614,6 +1614,111 @@ describe('getAuthorityUtxos', () => { }); }); +describe('index-limit address scanning policy', () => { + /** @type HathorWallet */ + let hWallet; + beforeAll(async () => { + const walletData = precalculationHelpers.test.getPrecalculatedWallet(); + hWallet = await generateWalletHelper({ + seed: walletData.words, + addresses: walletData.addresses, + scanPolicy: { + policy: 'index-limit', + startIndex: 0, + endIndex: 9, + }, + }); + }); + + afterAll(async () => { + await hWallet.stop(); + }); + + it('should start a wallet configured to index-limit', async () => { + // 0-9 addresses = 10 + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(10); + + // 0-14 addresses = 15 + await hWallet.indexLimitLoadMore(5); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(15); + + // 0-24 addresses = 25 + await hWallet.indexLimitSetEndIndex(24); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(25); + + // Setting below current loaded index will be a no-op + await hWallet.indexLimitSetEndIndex(5); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(25); + }); +}); + +describe('single address scanning policy', () => { + /** @type HathorWallet */ + let hWallet; + beforeAll(async () => { + const walletData = precalculationHelpers.test.getPrecalculatedWallet(); + hWallet = await generateWalletHelper({ + seed: walletData.words, + addresses: walletData.addresses, + scanPolicy: { + policy: 'single', + index: 5, + }, + }); + }); + + afterAll(async () => { + await hWallet.stop(); + }); + + it('should start a wallet configured to single address', async () => { + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(1); + + // Send tokens to address 5 (the loaded one) + const address5 = await hWallet.getAddressAtIndex(5); + await GenesisWalletHelper.injectFunds(hWallet, address5, 10n); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(1); + + // Send more transactions from the same wallet to the same address + const tx1 = await hWallet.sendTransaction(address5, 1n); + await waitForTxReceived(hWallet, tx1.hash); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(1); + await expect(hWallet.getBalance('00')).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + balance: expect.objectContaining({ unlocked: 10n }), + }), + ]) + ); + + // Send a tx to an unloaded address before the current one + const address0 = await hWallet.getAddressAtIndex(0); + const tx2 = await hWallet.sendTransaction(address0, 1n); + await waitForTxReceived(hWallet, tx2.hash); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(1); + await expect(hWallet.getBalance('00')).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + balance: expect.objectContaining({ unlocked: 9n }), + }), + ]) + ); + + // Send a tx to an unloaded address before the current one + const address10 = await hWallet.getAddressAtIndex(10); + const tx3 = await hWallet.sendTransaction(address10, 1n); + await waitForTxReceived(hWallet, tx3.hash); + await expect(hWallet.storage.store.addressCount()).resolves.toEqual(1); + await expect(hWallet.getBalance('00')).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + balance: expect.objectContaining({ unlocked: 8n }), + }), + ]) + ); + }); +}); + // This section tests methods that have side effects impacting the whole wallet. Executing it last. describe('internal methods', () => { /** @type HathorWallet */ @@ -1701,41 +1806,3 @@ describe('internal methods', () => { expect(spy).toHaveBeenCalledTimes(1); }); }); - -describe('index-limit address scanning policy', () => { - /** @type HathorWallet */ - let hWallet; - beforeAll(async () => { - const walletData = precalculationHelpers.test.getPrecalculatedWallet(); - hWallet = await generateWalletHelper({ - seed: walletData.words, - addresses: walletData.addresses, - scanPolicy: { - policy: 'index-limit', - startIndex: 0, - endIndex: 9, - }, - }); - }); - - afterAll(async () => { - await hWallet.stop(); - }); - - it('should start a wallet configured to index-limit', async () => { - // 0-9 addresses = 10 - await expect(hWallet.storage.store.addressCount()).resolves.toEqual(10); - - // 0-14 addresses = 15 - await hWallet.indexLimitLoadMore(5); - await expect(hWallet.storage.store.addressCount()).resolves.toEqual(15); - - // 0-24 addresses = 25 - await hWallet.indexLimitSetEndIndex(24); - await expect(hWallet.storage.store.addressCount()).resolves.toEqual(25); - - // Setting below current loaded index will be a no-op - await hWallet.indexLimitSetEndIndex(5); - await expect(hWallet.storage.store.addressCount()).resolves.toEqual(25); - }); -}); diff --git a/src/new/wallet.ts b/src/new/wallet.ts index 762bb9f74..86e67b207 100644 --- a/src/new/wallet.ts +++ b/src/new/wallet.ts @@ -59,6 +59,7 @@ import { WalletType, HistorySyncMode, getDefaultLogger, + isSingleScanPolicy, } from '../types'; import { TokenVersion } from '../models/enum'; import transactionUtils from '../utils/transaction'; @@ -659,7 +660,10 @@ class HathorWallet extends EventEmitter { } else { address = await deriveAddressP2SH(index, this.storage); } - await this.storage.saveAddress(address); + const policyData = await this.storage.getScanningPolicyData(); + if (!isSingleScanPolicy(policyData)) { + await this.storage.saveAddress(address); + } } return address.base58; } diff --git a/src/types.ts b/src/types.ts index 5661d31e9..aa0311ec8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -364,6 +364,7 @@ export interface IWalletAccessData { export enum SCANNING_POLICY { GAP_LIMIT = 'gap-limit', INDEX_LIMIT = 'index-limit', + SINGLE = 'single', } export interface IGapLimitAddressScanPolicy { @@ -377,6 +378,11 @@ export interface IIndexLimitAddressScanPolicy { endIndex: number; } +export interface ISingleAddressScanPolicy { + policy: SCANNING_POLICY.SINGLE; + index?: number; +} + /** * This is a request from the scanning policy to load `count` addresses starting from nextIndex. */ @@ -385,9 +391,15 @@ export interface IScanPolicyLoadAddresses { count: number; } -export type AddressScanPolicy = SCANNING_POLICY.GAP_LIMIT | SCANNING_POLICY.INDEX_LIMIT; +export type AddressScanPolicy = + | SCANNING_POLICY.GAP_LIMIT + | SCANNING_POLICY.INDEX_LIMIT + | SCANNING_POLICY.SINGLE; -export type AddressScanPolicyData = IGapLimitAddressScanPolicy | IIndexLimitAddressScanPolicy; +export type AddressScanPolicyData = + | IGapLimitAddressScanPolicy + | IIndexLimitAddressScanPolicy + | ISingleAddressScanPolicy; export function isGapLimitScanPolicy( scanPolicyData: AddressScanPolicyData @@ -401,6 +413,12 @@ export function isIndexLimitScanPolicy( return scanPolicyData.policy === SCANNING_POLICY.INDEX_LIMIT; } +export function isSingleScanPolicy( + scanPolicyData: AddressScanPolicyData +): scanPolicyData is ISingleAddressScanPolicy { + return scanPolicyData.policy === SCANNING_POLICY.SINGLE; +} + export interface IWalletData { lastLoadedAddressIndex: number; lastUsedAddressIndex: number; diff --git a/src/utils/storage.ts b/src/utils/storage.ts index d61c6f042..4dc26fd11 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -24,6 +24,9 @@ import { WalletType, IUtxo, ITokenData, + ISingleAddressScanPolicy, + IIndexLimitAddressScanPolicy, + AddressScanPolicyData, TokenVersion, } from '../types'; import walletApi from '../api/wallet'; @@ -257,7 +260,8 @@ export async function scanPolicyStartAddresses( storage: IStorage ): Promise { const scanPolicy = await storage.getScanningPolicy(); - let limits; + let limits: Omit | null; + let policyData: AddressScanPolicyData; switch (scanPolicy) { case SCANNING_POLICY.INDEX_LIMIT: limits = await storage.getIndexLimit(); @@ -269,6 +273,13 @@ export async function scanPolicyStartAddresses( nextIndex: limits.startIndex, count: limits.endIndex - limits.startIndex + 1, }; + case SCANNING_POLICY.SINGLE: + // Single scanning always loads 1 address only + policyData = (await storage.getScanningPolicyData()) as ISingleAddressScanPolicy; + return { + nextIndex: policyData.index ?? 0, + count: 1, + }; case SCANNING_POLICY.GAP_LIMIT: default: return { @@ -293,6 +304,9 @@ export async function checkScanningPolicy( return checkIndexLimit(storage); case SCANNING_POLICY.GAP_LIMIT: return checkGapLimit(storage); + case SCANNING_POLICY.SINGLE: + // Single scanning policy never needs to load another address. + return null; default: return null; } diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 07bcf58ef..52b44a440 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -97,6 +97,8 @@ import { WalletType, ITokenData, TokenVersion, + SCANNING_POLICY, + AddressScanPolicyData, } from '../types'; import { Fee } from '../utils/fee'; @@ -163,6 +165,8 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { // Flag to indicate if the websocket connection is enabled private readonly _isWsEnabled: boolean; + private scanPolicy: AddressScanPolicyData | null; + public storage: IStorage; constructor({ @@ -175,6 +179,7 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { passphrase = '', enableWs = true, storage = null, + scanPolicy = null, }: { requestPassword: () => Promise; seed?: string | null; @@ -185,6 +190,7 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { passphrase?: string; enableWs?: boolean; storage?: IStorage | null; + scanPolicy?: AddressScanPolicyData | null; }) { super(); @@ -246,7 +252,7 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { this.newAddresses = []; this.indexToUse = -1; - // TODO should we have a debug mode? + this.scanPolicy = scanPolicy; } /** @@ -878,6 +884,19 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { // asynchronous, so we will get an empty or partial array of addresses if they are not all loaded. this.failIfWalletNotReady(); } + + if (this.scanPolicy?.policy === SCANNING_POLICY.SINGLE) { + const addressIndex = this.scanPolicy.index ?? 0; + const data = await walletApi.getAddresses(this, addressIndex); + this.newAddresses = data.addresses.map(addr => ({ + address: addr.address, + index: addr.index, + addressPath: `m/44'/280'/0'/0/${addr.index}`, + })); + this.indexToUse = 0; + return; + } + const data = await walletApi.getNewAddresses(this); this.newAddresses = data.addresses; this.indexToUse = 0;