diff --git a/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts b/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts new file mode 100644 index 00000000..5060bad9 --- /dev/null +++ b/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts @@ -0,0 +1,113 @@ +import assert from 'assert'; +import { loadTests } from '../utils.js'; +import { Mnemonic, QiHDWallet, Zone, QiAddressInfo } from '../../index.js'; + +interface TestCaseQiAddressDerivation { + mnemonic: string; + externalAddresses: Array<{ + zone: string; + addresses: Array; + }>; + changeAddresses: Array<{ + zone: string; + addresses: Array; + }>; + paymentCodeAddresses: { + bobMnemonic: string; + sendAddresses: Array<{ + zone: string; + addresses: Array; + }>; + receiveAddresses: Array<{ + zone: string; + addresses: Array; + }>; + }; +} + +describe('QiHDWallet Address Derivation', function () { + this.timeout(2 * 60 * 1000); + const tests = loadTests('qi-address-derivation'); + + for (const test of tests) { + it('derives external addresses correctly', function () { + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const qiWallet = QiHDWallet.fromMnemonic(mnemonic); + + for (const externalAddressesInfo of test.externalAddresses) { + const zone = externalAddressesInfo.zone as Zone; + for (const expectedAddressInfo of externalAddressesInfo.addresses) { + const derivedAddressInfo = qiWallet.getNextAddressSync(0, zone); + assert.deepEqual( + derivedAddressInfo, + expectedAddressInfo, + `External address mismatch for zone ${zone}, expected: ${JSON.stringify(expectedAddressInfo)}, derived: ${JSON.stringify(derivedAddressInfo)}`, + ); + } + } + }); + + it('derives change addresses correctly', function () { + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const qiWallet = QiHDWallet.fromMnemonic(mnemonic); + + for (const changeAddressesInfo of test.changeAddresses) { + const zone = changeAddressesInfo.zone as Zone; + for (const expectedAddressInfo of changeAddressesInfo.addresses) { + const derivedAddressInfo = qiWallet.getNextChangeAddressSync(0, zone); + assert.deepEqual( + derivedAddressInfo, + expectedAddressInfo, + `Change address mismatch for zone ${zone}, expected: ${JSON.stringify(expectedAddressInfo)}, derived: ${JSON.stringify(derivedAddressInfo)}`, + ); + } + } + }); + + it('derives payment code send addresses correctly', function () { + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const qiWallet = QiHDWallet.fromMnemonic(mnemonic); + + const bobMnemonic = Mnemonic.fromPhrase(test.paymentCodeAddresses.bobMnemonic); + const bobQiWallet = QiHDWallet.fromMnemonic(bobMnemonic); + const bobPaymentCode = bobQiWallet.getPaymentCode(0); + + qiWallet.openChannel(bobPaymentCode); + + for (const sendAddressesInfo of test.paymentCodeAddresses.sendAddresses) { + const zone = sendAddressesInfo.zone as Zone; + for (const expectedAddressInfo of sendAddressesInfo.addresses) { + const derivedAddressInfo = qiWallet.getNextSendAddress(bobPaymentCode, zone); + assert.deepEqual( + derivedAddressInfo, + expectedAddressInfo, + `Payment code send address mismatch, expected: ${JSON.stringify(expectedAddressInfo)}, derived: ${JSON.stringify(derivedAddressInfo)}`, + ); + } + } + }); + + it('derives payment code receive addresses correctly', function () { + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const qiWallet = QiHDWallet.fromMnemonic(mnemonic); + + const bobMnemonic = Mnemonic.fromPhrase(test.paymentCodeAddresses.bobMnemonic); + const bobQiWallet = QiHDWallet.fromMnemonic(bobMnemonic); + const bobPaymentCode = bobQiWallet.getPaymentCode(0); + + qiWallet.openChannel(bobPaymentCode); + + for (const receiveAddressesInfo of test.paymentCodeAddresses.receiveAddresses) { + const zone = receiveAddressesInfo.zone as Zone; + for (const expectedAddressInfo of receiveAddressesInfo.addresses) { + const derivedAddressInfo = qiWallet.getNextReceiveAddress(bobPaymentCode, zone); + assert.deepEqual( + derivedAddressInfo, + expectedAddressInfo, + `Payment code receive address mismatch, expected: ${JSON.stringify(expectedAddressInfo)}, derived: ${JSON.stringify(derivedAddressInfo)}`, + ); + } + } + }); + } +}); diff --git a/src/quais.ts b/src/quais.ts index d5ddcce7..a7d0a14c 100644 --- a/src/quais.ts +++ b/src/quais.ts @@ -218,6 +218,8 @@ export { encryptKeystoreJsonSync, SerializedHDWallet, SerializedQiHDWallet, + QiAddressInfo, + NeuteredAddressInfo, } from './wallet/index.js'; // WORDLIST diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 85043b2d..dec13e6d 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -13,7 +13,7 @@ export { BaseWallet } from './base-wallet.js'; -export type { SerializedHDWallet } from './hdwallet.js'; +export type { SerializedHDWallet, NeuteredAddressInfo } from './hdwallet.js'; export { QuaiHDWallet } from './quai-hdwallet.js'; @@ -31,6 +31,6 @@ export { Wallet } from './wallet.js'; export type { KeystoreAccount, EncryptOptions } from './json-keystore.js'; -export { QiHDWallet, SerializedQiHDWallet } from './qi-hdwallet.js'; +export { QiHDWallet, SerializedQiHDWallet, QiAddressInfo } from './qi-hdwallet.js'; export { HDNodeVoidWallet, HDNodeWallet } from './hdnodewallet.js'; diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 46f20253..14eb915a 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -62,7 +62,7 @@ type DerivationPath = 'BIP44:external' | 'BIP44:change' | string; // string for * * @extends NeuteredAddressInfo */ -interface QiAddressInfo extends NeuteredAddressInfo { +export interface QiAddressInfo extends NeuteredAddressInfo { status: AddressStatus; derivationPath: DerivationPath; } diff --git a/testcases/qi-address-derivation.json.gz b/testcases/qi-address-derivation.json.gz new file mode 100644 index 00000000..02324774 Binary files /dev/null and b/testcases/qi-address-derivation.json.gz differ