diff --git a/src/_tests/types.ts b/src/_tests/types.ts index 3d620e10..6dcec686 100644 --- a/src/_tests/types.ts +++ b/src/_tests/types.ts @@ -376,23 +376,6 @@ export interface TestCaseQiTransaction { signed: string; } -export interface TestCaseQiSerialization { - name: string; - mnemonic: string; - params: Array; - outpoints: Array; - serialized: { - version: number; - phrase: string; - coinType: number; - addresses: Array; - changeAddresses: Array; - gapAddresses: Array; - changeGapAddresses: Array; - outpoints: Array; - }; -} - export interface TestCaseQiSignMessage { name: string; mnemonic: string; diff --git a/src/_tests/unit/qihdwallet-serialization.unit.test.ts b/src/_tests/unit/qihdwallet-serialization.unit.test.ts new file mode 100644 index 00000000..2d8ee969 --- /dev/null +++ b/src/_tests/unit/qihdwallet-serialization.unit.test.ts @@ -0,0 +1,187 @@ +import assert from 'assert'; +import { loadTests } from '../utils.js'; +import { QiHDWallet, SerializedQiHDWallet, Zone, AddressStatus } from '../../index.js'; + +describe('QiHDWallet Serialization/Deserialization', function () { + this.timeout(10000); + const tests = loadTests('qi-wallet-serialization'); + + for (const test of tests) { + it('should correctly deserialize and reserialize wallet state', async function () { + // First deserialize the wallet from test data + const deserializedWallet = await QiHDWallet.deserialize(test); + + // Now serialize it back + const serializedWallet = deserializedWallet.serialize(); + + // Verify all properties match the original test data + assert.strictEqual(serializedWallet.version, test.version, 'Version mismatch'); + assert.strictEqual(serializedWallet.phrase, test.phrase, 'Phrase mismatch'); + assert.strictEqual(serializedWallet.coinType, test.coinType, 'Coin type mismatch'); + + // Compare outpoints + assert.deepStrictEqual(serializedWallet.outpoints, test.outpoints, 'Outpoints mismatch'); + + // Compare pending outpoints + assert.deepStrictEqual( + serializedWallet.pendingOutpoints, + test.pendingOutpoints, + 'Pending outpoints mismatch', + ); + + // Compare addresses + assert.deepStrictEqual( + serializedWallet.addresses.sort((a, b) => a.index - b.index), + test.addresses.sort((a, b) => a.index - b.index), + 'Addresses mismatch', + ); + + // Compare sender payment code info + assert.deepStrictEqual( + serializedWallet.senderPaymentCodeInfo, + test.senderPaymentCodeInfo, + 'Sender payment code info mismatch', + ); + + // Finally compare the entire serialized object + assert.deepStrictEqual( + serializedWallet, + test, + 'Complete serialized wallet does not match original test data', + ); + }); + + it('should maintain wallet functionality after deserialization', async function () { + const deserializedWallet = await QiHDWallet.deserialize(test); + const zone = Zone.Cyprus1; + + // Verify the wallet has the correct number of addresses + const externalAddresses = deserializedWallet.getAddressesForZone(zone); + assert.strictEqual( + externalAddresses.length, + test.addresses.filter((addr) => addr.derivationPath === 'BIP44:external' && addr.zone === zone).length, + 'External addresses count mismatch', + ); + + // Verify the wallet has the correct number of change addresses + const changeAddresses = deserializedWallet.getChangeAddressesForZone(zone); + assert.strictEqual( + changeAddresses.length, + test.addresses.filter((addr) => addr.derivationPath === 'BIP44:change' && addr.zone === zone).length, + 'Change addresses count mismatch', + ); + + // Verify gap addresses + const gapAddresses = deserializedWallet.getGapAddressesForZone(zone); + assert.strictEqual( + gapAddresses.length, + test.addresses.filter( + (addr) => + addr.derivationPath === 'BIP44:external' && + addr.zone === zone && + addr.status === AddressStatus.UNUSED, + ).length, + 'Gap addresses count mismatch', + ); + + // Verify outpoints were correctly imported + const zoneOutpoints = deserializedWallet.getOutpoints(zone); + assert.strictEqual( + zoneOutpoints.length, + test.outpoints.filter((outpoint) => outpoint.zone === zone).length, + 'Outpoints count mismatch', + ); + + // Verify payment channels were correctly restored + const paymentCodes = Object.keys(test.senderPaymentCodeInfo); + for (const paymentCode of paymentCodes) { + // Verify channel is open + assert.strictEqual( + deserializedWallet.channelIsOpen(paymentCode), + true, + `Payment channel ${paymentCode} not restored`, + ); + + // Verify payment channel addresses for zone + const paymentChannelAddresses = deserializedWallet.getPaymentChannelAddressesForZone(paymentCode, zone); + assert.strictEqual( + paymentChannelAddresses.length, + test.addresses.filter((addr) => addr.derivationPath === paymentCode && addr.zone === zone).length, + 'Payment channel addresses count mismatch', + ); + + // Verify gap payment channel addresses + const gapPaymentChannelAddresses = deserializedWallet.getGapPaymentChannelAddresses(paymentCode); + assert.strictEqual( + gapPaymentChannelAddresses.length, + test.addresses.filter( + (addr) => addr.derivationPath === paymentCode && addr.status === AddressStatus.UNUSED, + ).length, + 'Gap payment channel addresses count mismatch', + ); + + // Verify the addresses match the expected ones + const expectedPaymentChannelAddresses = test.addresses + .filter((addr) => addr.derivationPath === paymentCode && addr.zone === zone) + .sort((a, b) => a.index - b.index); + + assert.deepStrictEqual( + paymentChannelAddresses.sort((a, b) => a.index - b.index), + expectedPaymentChannelAddresses, + 'Payment channel addresses do not match expected addresses', + ); + } + }); + + it('should correctly handle gap addresses and payment channel addresses', async function () { + const deserializedWallet = await QiHDWallet.deserialize(test); + const zone = '0x00' as Zone; + + // Test gap addresses functionality + const gapAddresses = deserializedWallet.getGapAddressesForZone(zone); + for (const addr of gapAddresses) { + assert.strictEqual(addr.status, AddressStatus.UNUSED, 'Gap address should be unused'); + assert.strictEqual(addr.derivationPath, 'BIP44:external', 'Gap address should be external'); + assert.strictEqual(addr.zone, zone, 'Gap address should be in correct zone'); + } + + // Test payment channel functionality for each payment code + const paymentCodes = Object.keys(test.senderPaymentCodeInfo); + for (const paymentCode of paymentCodes) { + // Test gap payment channel addresses + const gapPaymentAddresses = deserializedWallet.getGapPaymentChannelAddresses(paymentCode); + for (const addr of gapPaymentAddresses) { + assert.strictEqual(addr.status, AddressStatus.UNUSED, 'Gap payment address should be unused'); + assert.strictEqual( + addr.derivationPath, + paymentCode, + 'Gap payment address should have correct payment code', + ); + } + + // Test zone-specific payment channel addresses + const zonePaymentAddresses = deserializedWallet.getPaymentChannelAddressesForZone(paymentCode, zone); + for (const addr of zonePaymentAddresses) { + assert.strictEqual( + addr.derivationPath, + paymentCode, + 'Payment address should have correct payment code', + ); + assert.strictEqual(addr.zone, zone, 'Payment address should be in correct zone'); + } + + // Verify all payment addresses are in the original test data + const allTestAddresses = test.addresses + .filter((addr) => addr.derivationPath === paymentCode) + .map((addr) => addr.address); + + for (const addr of zonePaymentAddresses) { + assert.ok( + allTestAddresses.includes(addr.address), + 'Payment address should exist in original test data', + ); + } + } + }); + } +}); diff --git a/src/_tests/unit/qihdwallet.unit.test.ts b/src/_tests/unit/qihdwallet.unit.test.ts index 57967ba4..3582a5a1 100644 --- a/src/_tests/unit/qihdwallet.unit.test.ts +++ b/src/_tests/unit/qihdwallet.unit.test.ts @@ -8,7 +8,6 @@ import { TestCaseQiAddresses, TestCaseQiSignMessage, TestCaseQiTransaction, - TestCaseQiSerialization, AddressInfo, TxInput, TxOutput, @@ -67,74 +66,6 @@ describe('QiHDWallet: Test address generation and retrieval', function () { } }); -describe('QiHDWallet: Test serialization and deserialization of QiHDWallet', function () { - this.timeout(10000); - const tests = loadTests('qi-serialization'); - for (const test of tests) { - const mnemonic = Mnemonic.fromPhrase(test.mnemonic); - const qiWallet = QiHDWallet.fromMnemonic(mnemonic); - let serialized: any; - it(`tests serialization QuaiHDWallet: ${test.name}`, async function () { - for (const param of test.params) { - qiWallet.getNextAddressSync(param.account, param.zone); - qiWallet.getNextChangeAddressSync(param.account, param.zone); - } - qiWallet.importOutpoints(test.outpoints); - serialized = qiWallet.serialize(); - assert.deepEqual(serialized, test.serialized); - }); - - it(`tests deserialization QiHDWallet: ${test.name}`, async function () { - const deserialized = await QiHDWallet.deserialize(serialized); - assert.deepEqual(deserialized.serialize(), serialized); - }); - } -}); - -describe('QiHDWallet: Test serialization and deserialization of QiHDWallet with payment codes', function () { - this.timeout(10000); - it('tests serialization and deserialization with payment codes and addresses', async function () { - // Create Alice's wallet - const aliceMnemonic = Mnemonic.fromPhrase( - 'empower cook violin million wool twelve involve nice donate author mammal salt royal shiver birth olympic embody hello beef suit isolate mixed text spot', - ); - const aliceQiWallet = QiHDWallet.fromMnemonic(aliceMnemonic); - - // Create Bob's wallet - const bobMnemonic = Mnemonic.fromPhrase( - 'innocent perfect bus miss prevent night oval position aspect nut angle usage expose grace juice', - ); - const bobQiWallet = QiHDWallet.fromMnemonic(bobMnemonic); - - // Get payment codes - const alicePaymentCode = await aliceQiWallet.getPaymentCode(0); - const bobPaymentCode = await bobQiWallet.getPaymentCode(0); - - // Generate addresses - await aliceQiWallet.getNextSendAddress(bobPaymentCode, Zone.Cyprus1); - await aliceQiWallet.getNextReceiveAddress(bobPaymentCode, Zone.Cyprus1); - - // Serialize Alice's wallet - const serializedAliceWallet = aliceQiWallet.serialize(); - - // Deserialize Alice's wallet - const deserializedAliceWallet = await QiHDWallet.deserialize(serializedAliceWallet); - - // Assertions - assert.strictEqual( - deserializedAliceWallet.getPaymentCode(0), - alicePaymentCode, - 'Payment code should match after deserialization', - ); - - assert.equal( - deserializedAliceWallet.channelIsOpen(alicePaymentCode), - aliceQiWallet.channelIsOpen(alicePaymentCode), - 'Channel should be open', - ); - }); -}); - describe('QiHDWallet: Test transaction signing', function () { const tests = loadTests('qi-transaction'); for (const test of tests) { diff --git a/src/quais.ts b/src/quais.ts index a0727af3..bc2a3a7e 100644 --- a/src/quais.ts +++ b/src/quais.ts @@ -221,6 +221,7 @@ export { QiAddressInfo, NeuteredAddressInfo, OutpointInfo, + AddressStatus, } from './wallet/index.js'; // WORDLIST diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 7c230c18..c8922df2 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -31,6 +31,6 @@ export { Wallet } from './wallet.js'; export type { KeystoreAccount, EncryptOptions } from './json-keystore.js'; -export { QiHDWallet, SerializedQiHDWallet, QiAddressInfo, OutpointInfo } from './qi-hdwallet.js'; +export { QiHDWallet, SerializedQiHDWallet, QiAddressInfo, OutpointInfo, AddressStatus } 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 e1b38589..14dfe292 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -44,7 +44,7 @@ export interface OutpointInfo { * * @enum {string} */ -enum AddressStatus { +export enum AddressStatus { USED = 'USED', UNUSED = 'UNUSED', ATTEMPTED_USE = 'ATTEMPTED_USE', diff --git a/testcases/qi-wallet-serialization.json.gz b/testcases/qi-wallet-serialization.json.gz new file mode 100644 index 00000000..319abfbc Binary files /dev/null and b/testcases/qi-wallet-serialization.json.gz differ