Skip to content
143 changes: 105 additions & 38 deletions __tests__/integration/hathorwallet_others.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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);
});
});
6 changes: 5 additions & 1 deletion src/new/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
WalletType,
HistorySyncMode,
getDefaultLogger,
isSingleScanPolicy,
} from '../types';
import { TokenVersion } from '../models/enum';
import transactionUtils from '../utils/transaction';
Expand Down Expand Up @@ -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;
}
Expand Down
22 changes: 20 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ export interface IWalletAccessData {
export enum SCANNING_POLICY {
GAP_LIMIT = 'gap-limit',
INDEX_LIMIT = 'index-limit',
SINGLE = 'single',
}

export interface IGapLimitAddressScanPolicy {
Expand All @@ -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.
*/
Expand All @@ -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
Expand All @@ -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;
Expand Down
16 changes: 15 additions & 1 deletion src/utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import {
WalletType,
IUtxo,
ITokenData,
ISingleAddressScanPolicy,
IIndexLimitAddressScanPolicy,
AddressScanPolicyData,
TokenVersion,
} from '../types';
import walletApi from '../api/wallet';
Expand Down Expand Up @@ -257,7 +260,8 @@ export async function scanPolicyStartAddresses(
storage: IStorage
): Promise<IScanPolicyLoadAddresses> {
const scanPolicy = await storage.getScanningPolicy();
let limits;
let limits: Omit<IIndexLimitAddressScanPolicy, 'policy'> | null;
let policyData: AddressScanPolicyData;
switch (scanPolicy) {
case SCANNING_POLICY.INDEX_LIMIT:
limits = await storage.getIndexLimit();
Expand All @@ -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 {
Expand All @@ -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;
}
Expand Down
21 changes: 20 additions & 1 deletion src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
WalletType,
ITokenData,
TokenVersion,
SCANNING_POLICY,
AddressScanPolicyData,
} from '../types';
import { Fee } from '../utils/fee';

Expand Down Expand Up @@ -163,6 +165,8 @@
// Flag to indicate if the websocket connection is enabled
private readonly _isWsEnabled: boolean;

private scanPolicy: AddressScanPolicyData | null;

public storage: IStorage;

constructor({
Expand All @@ -175,6 +179,7 @@
passphrase = '',
enableWs = true,
storage = null,
scanPolicy = null,
}: {
requestPassword: () => Promise<string>;
seed?: string | null;
Expand All @@ -185,6 +190,7 @@
passphrase?: string;
enableWs?: boolean;
storage?: IStorage | null;
scanPolicy?: AddressScanPolicyData | null;
}) {
super();

Expand Down Expand Up @@ -246,7 +252,7 @@

this.newAddresses = [];
this.indexToUse = -1;
// TODO should we have a debug mode?
this.scanPolicy = scanPolicy;
}

/**
Expand Down Expand Up @@ -878,6 +884,19 @@
// 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 => ({

Check warning on line 891 in src/wallet/wallet.ts

View check run for this annotation

Codecov / codecov/patch

src/wallet/wallet.ts#L890-L891

Added lines #L890 - L891 were not covered by tests
address: addr.address,
index: addr.index,
addressPath: `m/44'/280'/0'/0/${addr.index}`,
}));
this.indexToUse = 0;
return;

Check warning on line 897 in src/wallet/wallet.ts

View check run for this annotation

Codecov / codecov/patch

src/wallet/wallet.ts#L896-L897

Added lines #L896 - L897 were not covered by tests
}

const data = await walletApi.getNewAddresses(this);
this.newAddresses = data.addresses;
this.indexToUse = 0;
Expand Down
Loading