diff --git a/src/_tests/types.ts b/src/_tests/types.ts
index 6dcec686..58158de0 100644
--- a/src/_tests/types.ts
+++ b/src/_tests/types.ts
@@ -277,17 +277,11 @@ export interface AddrParams {
     account: number;
     zone: Zone;
 }
-export type TestAddresses = Array<{
-    params: AddrParams;
-    expectedAddress: AddressInfo;
-}>;
-
 export interface AddressInfo {
     pubKey: string;
     address: string;
     account: number;
     index: number;
-    change: boolean;
     zone: Zone;
 }
 
@@ -312,12 +306,6 @@ export interface TestCaseQuaiTransaction {
     signed: string;
 }
 
-export interface TestCaseQuaiAddresses {
-    name: string;
-    mnemonic: string;
-    addresses: TestAddresses;
-}
-
 export interface TestCaseQuaiTypedData {
     name: string;
     mnemonic: string;
@@ -336,13 +324,6 @@ export interface TestCaseQuaiMessageSign {
     signature: string;
 }
 
-export interface TestCaseQiAddresses {
-    name: string;
-    mnemonic: string;
-    addresses: TestAddresses;
-    changeAddresses: TestAddresses;
-}
-
 export interface TxInput {
     txhash: string;
     index: number;
diff --git a/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts b/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts
index 5060bad9..748eaa02 100644
--- a/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts
+++ b/src/_tests/unit/qihdwallet-address-derivation.unit.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
 import assert from 'assert';
 import { loadTests } from '../utils.js';
 import { Mnemonic, QiHDWallet, Zone, QiAddressInfo } from '../../index.js';
@@ -111,3 +112,244 @@ describe('QiHDWallet Address Derivation', function () {
         });
     }
 });
+
+describe('QiHDWallet Address Getters', function () {
+    this.timeout(2 * 60 * 1000);
+    const tests = loadTests<TestCaseQiAddressDerivation>('qi-address-derivation');
+
+    for (const test of tests) {
+        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 _ of externalAddressesInfo.addresses) {
+                qiWallet.getNextAddressSync(0, zone);
+            }
+        }
+
+        for (const changeAddressesInfo of test.changeAddresses) {
+            const zone = changeAddressesInfo.zone as Zone;
+            for (const _ of changeAddressesInfo.addresses) {
+                qiWallet.getNextChangeAddressSync(0, zone);
+            }
+        }
+
+        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 _ of receiveAddressesInfo.addresses) {
+                qiWallet.getNextReceiveAddress(bobPaymentCode, zone);
+            }
+        }
+
+        it('getAddressInfo returns correct address info', function () {
+            for (const externalAddressesInfo of test.externalAddresses) {
+                for (const expectedAddressInfo of externalAddressesInfo.addresses) {
+                    const addressInfo = qiWallet.getAddressInfo(expectedAddressInfo.address);
+                    assert.deepEqual(
+                        addressInfo,
+                        expectedAddressInfo,
+                        `External address info mismatch for address ${expectedAddressInfo.address} (got ${JSON.stringify(addressInfo)}, expected ${JSON.stringify(expectedAddressInfo)})`,
+                    );
+                }
+            }
+        });
+
+        it('getChangeAddressInfo returns correct address info', function () {
+            for (const changeAddressesInfo of test.changeAddresses) {
+                for (const expectedAddressInfo of changeAddressesInfo.addresses) {
+                    const addressInfo = qiWallet.getChangeAddressInfo(expectedAddressInfo.address);
+                    assert.deepEqual(
+                        addressInfo,
+                        expectedAddressInfo,
+                        `Change address info mismatch for address ${expectedAddressInfo.address} (got ${JSON.stringify(addressInfo)}, expected ${JSON.stringify(expectedAddressInfo)})`,
+                    );
+                }
+            }
+        });
+
+        it('getAddressesForZone returns all addresses for specified zone', function () {
+            for (const externalAddressesInfo of test.externalAddresses) {
+                const zone = externalAddressesInfo.zone as Zone;
+                const addresses = qiWallet.getAddressesForZone(zone);
+                assert.deepEqual(
+                    addresses,
+                    externalAddressesInfo.addresses,
+                    `External addresses mismatch for zone ${zone} (got ${JSON.stringify(addresses)}, expected ${JSON.stringify(externalAddressesInfo.addresses)})`,
+                );
+            }
+        });
+
+        it('getChangeAddressesForZone returns all change addresses for specified zone', function () {
+            for (const changeAddressesInfo of test.changeAddresses) {
+                const zone = changeAddressesInfo.zone as Zone;
+                const addresses = qiWallet.getChangeAddressesForZone(zone);
+                assert.deepEqual(
+                    addresses,
+                    changeAddressesInfo.addresses,
+                    `Change addresses mismatch for zone ${zone} (got ${JSON.stringify(addresses)}, expected ${JSON.stringify(changeAddressesInfo.addresses)})`,
+                );
+            }
+        });
+
+        it('getPaymentChannelAddressesForZone returns correct addresses', function () {
+            for (const receiveAddressesInfo of test.paymentCodeAddresses.receiveAddresses) {
+                const zone = receiveAddressesInfo.zone as Zone;
+                const addresses = qiWallet.getPaymentChannelAddressesForZone(bobPaymentCode, zone);
+                assert.deepEqual(
+                    addresses,
+                    receiveAddressesInfo.addresses,
+                    `Payment channel addresses mismatch for zone ${zone} (got ${JSON.stringify(addresses)}, expected ${JSON.stringify(receiveAddressesInfo.addresses)})`,
+                );
+            }
+        });
+
+        it('getAddressesForAccount returns all addresses for specified account', function () {
+            // Test for account 0 (the one used in test data)
+            const allAddresses = [
+                ...test.externalAddresses.flatMap((info) => info.addresses),
+                ...test.changeAddresses.flatMap((info) => info.addresses),
+                ...test.paymentCodeAddresses.receiveAddresses.flatMap((info) => info.addresses),
+            ].filter((addr) => addr.account === 0);
+
+            const addresses = qiWallet.getAddressesForAccount(0);
+            assert.deepEqual(
+                addresses,
+                allAddresses,
+                `Addresses mismatch for account 0 (got ${JSON.stringify(addresses)}, expected ${JSON.stringify(allAddresses)})`,
+            );
+        });
+
+        it('returns empty arrays for non-existent zones and accounts', function () {
+            const nonExistentZone = '0x22' as Zone;
+            const nonExistentAccount = 999;
+
+            assert.deepEqual(qiWallet.getAddressesForZone(nonExistentZone), []);
+            assert.deepEqual(qiWallet.getChangeAddressesForZone(nonExistentZone), []);
+            assert.deepEqual(qiWallet.getPaymentChannelAddressesForZone(bobPaymentCode, nonExistentZone), []);
+            assert.deepEqual(qiWallet.getAddressesForAccount(nonExistentAccount), []);
+        });
+    }
+});
+
+describe('Basic Address Management', function () {
+    const tests = loadTests<TestCaseQiAddressDerivation>('qi-address-derivation');
+
+    for (const test of tests) {
+        const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
+        const qiWallet = QiHDWallet.fromMnemonic(mnemonic);
+
+        it('should add external addresses correctly', function () {
+            // Test with addresses from the first zone in test data
+            const zoneAddresses = test.externalAddresses[0].addresses;
+            const firstAddress = zoneAddresses[0];
+
+            // Add address using the same account and index from test data
+            const addedAddress = qiWallet.addAddress(firstAddress.account, firstAddress.index);
+
+            assert.deepEqual(
+                addedAddress,
+                firstAddress,
+                `Added address does not match expected address for index ${firstAddress.index}`,
+            );
+
+            // Verify the address was added correctly by retrieving it
+            const retrievedAddress = qiWallet.getAddressInfo(firstAddress.address);
+            assert.deepEqual(retrievedAddress, firstAddress, 'Retrieved address does not match added address');
+
+            // Test adding same address index again should throw error
+            assert.throws(
+                () => qiWallet.addAddress(firstAddress.account, firstAddress.index),
+                Error,
+                `Address index ${firstAddress.index} already exists in wallet under path BIP44:external`,
+            );
+        });
+
+        it('should add change addresses correctly', function () {
+            // Test with change addresses from the first zone in test data
+            const zoneChangeAddresses = test.changeAddresses[0].addresses;
+            const firstChangeAddress = zoneChangeAddresses[0];
+
+            // Add change address using the same account and index from test data
+            const addedChangeAddress = qiWallet.addChangeAddress(firstChangeAddress.account, firstChangeAddress.index);
+
+            assert.deepEqual(
+                addedChangeAddress,
+                firstChangeAddress,
+                `Added change address does not match expected address for index ${firstChangeAddress.index}`,
+            );
+
+            // Verify the change address was added correctly by retrieving it
+            const retrievedChangeAddress = qiWallet.getChangeAddressInfo(firstChangeAddress.address);
+            assert.deepEqual(
+                retrievedChangeAddress,
+                firstChangeAddress,
+                'Retrieved change address does not match added change address',
+            );
+
+            // Test adding same change address index again should throw error
+            assert.throws(
+                () => qiWallet.addChangeAddress(firstChangeAddress.account, firstChangeAddress.index),
+                Error,
+                `Address index ${firstChangeAddress.index} already exists in wallet under path BIP44:change`,
+            );
+        });
+
+        it('should handle invalid indices correctly', function () {
+            // Test with negative index
+            assert.throws(() => qiWallet.addAddress(0, -1), Error, 'Negative index should throw error');
+
+            assert.throws(() => qiWallet.addChangeAddress(0, -1), Error, 'Negative index should throw error');
+        });
+
+        it('should handle invalid accounts correctly', function () {
+            // Test with negative account
+            assert.throws(() => qiWallet.addAddress(-1, 0), Error, 'Negative account should throw error');
+
+            assert.throws(() => qiWallet.addChangeAddress(-1, 0), Error, 'Negative account should throw error');
+        });
+
+        it('should reject indices that derive invalid addresses', function () {
+            // For Cyprus1 (0x00) and account 0:
+            // - Index 384 derives an invalid address (wrong zone or ledger)
+            // - Index 385 derives the first valid external address
+            // - Index 4 derives an invalid change address
+            // - Index 5 derives the first valid change address
+
+            // Test invalid external address index
+            assert.throws(
+                () => qiWallet.addAddress(0, 384),
+                Error,
+                'Failed to derive a Qi valid address for the zone 0x00',
+            );
+
+            // Test invalid change address index
+            assert.throws(
+                () => qiWallet.addChangeAddress(0, 4),
+                Error,
+                'Failed to derive a Qi valid address for the zone 0x00',
+            );
+        });
+
+        it('should reject indices that derive duplicate addresses', function () {
+            // Test that adding an existing address index throws error
+            assert.throws(
+                () => qiWallet.addAddress(0, 385),
+                Error,
+                'Address index 385 already exists in wallet under path BIP44:external',
+            );
+
+            // Test that adding an existing change address index throws error
+            assert.throws(
+                () => qiWallet.addChangeAddress(0, 5),
+                Error,
+                'Address index 5 already exists in wallet under path BIP44:change',
+            );
+        });
+    }
+});
diff --git a/src/_tests/unit/qihdwallet-import-privkey.unit.test.ts b/src/_tests/unit/qihdwallet-import-privkey.unit.test.ts
new file mode 100644
index 00000000..725b0c58
--- /dev/null
+++ b/src/_tests/unit/qihdwallet-import-privkey.unit.test.ts
@@ -0,0 +1,151 @@
+import assert from 'assert';
+import { loadTests } from '../utils.js';
+import { QiHDWallet, Mnemonic, Zone } from '../../index.js';
+
+interface TestCaseImportPrivKey {
+    shouldSucceed: boolean;
+    privateKey: string;
+    error?: string;
+    pubKey?: string;
+    address?: string;
+    zone?: string;
+}
+
+describe('QiHDWallet Import Private Key', function () {
+    const tests = loadTests<TestCaseImportPrivKey>('qi-wallet-import-privkey');
+
+    let wallet: QiHDWallet;
+
+    beforeEach(function () {
+        const mnemonic = Mnemonic.fromPhrase('test test test test test test test test test test test junk');
+        wallet = QiHDWallet.fromMnemonic(mnemonic);
+    });
+
+    for (const test of tests) {
+        if (test.shouldSucceed) {
+            it(`should successfully import private key ${test.privateKey}`, async function () {
+                const addressInfo = await wallet.importPrivateKey(test.privateKey);
+
+                assert.strictEqual(
+                    addressInfo.pubKey,
+                    test.pubKey,
+                    `Public key mismatch, expected: ${test.pubKey}, got: ${addressInfo.pubKey}`,
+                );
+
+                assert.strictEqual(
+                    addressInfo.address,
+                    test.address,
+                    `Address mismatch, expected: ${test.address}, got: ${addressInfo.address}`,
+                );
+
+                assert.strictEqual(
+                    addressInfo.zone,
+                    test.zone,
+                    `Zone mismatch, expected: ${test.zone}, got: ${addressInfo.zone}`,
+                );
+
+                assert.strictEqual(
+                    addressInfo.derivationPath,
+                    test.privateKey,
+                    'Private key should be stored in derivationPath',
+                );
+            });
+        } else {
+            it(`should fail to import invalid private key ${test.privateKey}`, async function () {
+                await assert.rejects(
+                    async () => {
+                        await wallet.importPrivateKey(test.privateKey);
+                    },
+                    (error: Error) => {
+                        assert.ok(
+                            error.message.includes(test.error!),
+                            `Expected error message to include "${test.error}", got "${error.message}"`,
+                        );
+                        return true;
+                    },
+                );
+            });
+        }
+    }
+
+    it('should prevent duplicate imports of the same private key', async function () {
+        const validPrivateKey = tests.find((t) => t.shouldSucceed)!.privateKey;
+
+        // First import should succeed
+        await wallet.importPrivateKey(validPrivateKey);
+
+        // Second import should fail
+        await assert.rejects(
+            async () => {
+                await wallet.importPrivateKey(validPrivateKey);
+            },
+            (error: Error) => {
+                assert.ok(
+                    error.message.includes('already exists in wallet'),
+                    'Expected error message to indicate duplicate address',
+                );
+                return true;
+            },
+        );
+    });
+
+    it('should return all imported addresses when no zone specified', async function () {
+        const validTests = tests.filter((t) => t.shouldSucceed);
+        for (const test of validTests) {
+            await wallet.importPrivateKey(test.privateKey);
+        }
+
+        const importedAddresses = wallet.getImportedAddresses();
+
+        assert.strictEqual(importedAddresses.length, validTests.length, 'Should return all imported addresses');
+
+        for (let i = 0; i < validTests.length; i++) {
+            assert.strictEqual(
+                importedAddresses[i].address,
+                validTests[i].address,
+                'Imported address should match test data',
+            );
+        }
+    });
+
+    it('should return only addresses for specified zone', async function () {
+        const validTests = tests.filter((t) => t.shouldSucceed);
+        for (const test of validTests) {
+            await wallet.importPrivateKey(test.privateKey);
+        }
+
+        const testZone = validTests[0].zone;
+        const zoneAddresses = wallet.getImportedAddresses(testZone as Zone);
+
+        const expectedAddresses = validTests.filter((t) => t.zone === testZone);
+
+        assert.strictEqual(
+            zoneAddresses.length,
+            expectedAddresses.length,
+            `Should return only addresses for zone ${testZone}`,
+        );
+
+        for (let i = 0; i < expectedAddresses.length; i++) {
+            assert.strictEqual(
+                zoneAddresses[i].address,
+                expectedAddresses[i].address,
+                'Zone-filtered address should match test data',
+            );
+        }
+    });
+
+    it('should return empty array when no addresses imported', function () {
+        const addresses = wallet.getImportedAddresses();
+        assert.deepStrictEqual(addresses, [], 'Should return empty array when no addresses imported');
+    });
+
+    it('should return empty array when no addresses in specified zone', async function () {
+        const validTest = tests.find((t) => t.shouldSucceed)!;
+        await wallet.importPrivateKey(validTest.privateKey);
+
+        const differentZone = '0x22';
+        const addresses = wallet.getImportedAddresses(differentZone as Zone);
+
+        assert.deepStrictEqual(addresses, [], 'Should return empty array when no addresses in specified zone');
+    });
+});
diff --git a/src/_tests/unit/qihdwallet.unit.test.ts b/src/_tests/unit/qihdwallet.unit.test.ts
index 3582a5a1..9a7dbf3e 100644
--- a/src/_tests/unit/qihdwallet.unit.test.ts
+++ b/src/_tests/unit/qihdwallet.unit.test.ts
@@ -1,71 +1,13 @@
 import assert from 'assert';
 
-import { loadTests, convertToZone } from '../utils.js';
+import { loadTests } from '../utils.js';
 import { schnorr } from '@noble/curves/secp256k1';
 import { keccak_256 } from '@noble/hashes/sha3';
 import { MuSigFactory } from '@brandonblack/musig';
-import {
-    TestCaseQiAddresses,
-    TestCaseQiSignMessage,
-    TestCaseQiTransaction,
-    AddressInfo,
-    TxInput,
-    TxOutput,
-    Zone,
-} from '../types.js';
+import { TestCaseQiSignMessage, TestCaseQiTransaction, TxInput, TxOutput, Zone } from '../types.js';
 
 import { Mnemonic, QiHDWallet, QiTransaction, getBytes, hexlify, musigCrypto } from '../../index.js';
 
-describe('QiHDWallet: Test address generation and retrieval', function () {
-    const tests = loadTests<TestCaseQiAddresses>('qi-addresses');
-    for (const test of tests) {
-        const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
-        const qiWallet = QiHDWallet.fromMnemonic(mnemonic);
-        it(`tests addresses generation and retrieval: ${test.name}`, function () {
-            const generatedAddresses: AddressInfo[] = [];
-            for (const { params, expectedAddress } of test.addresses) {
-                const addrInfo = qiWallet.getNextAddressSync(params.account, params.zone);
-                assert.deepEqual(addrInfo, expectedAddress);
-                generatedAddresses.push(addrInfo);
-
-                const retrievedAddrInfo = qiWallet.getAddressInfo(expectedAddress.address);
-                assert.deepEqual(retrievedAddrInfo, expectedAddress);
-
-                const accountMap = new Map<number, AddressInfo[]>();
-                for (const addrInfo of generatedAddresses) {
-                    if (!accountMap.has(addrInfo.account)) {
-                        accountMap.set(addrInfo.account, []);
-                    }
-                    accountMap.get(addrInfo.account)!.push(addrInfo);
-                }
-                for (const [account, expectedAddresses] of accountMap) {
-                    const retrievedAddresses = qiWallet.getAddressesForAccount(account);
-                    assert.deepEqual(retrievedAddresses, expectedAddresses);
-                }
-
-                const zoneMap = new Map<string, AddressInfo[]>();
-                for (const addrInfo of generatedAddresses) {
-                    if (!zoneMap.has(addrInfo.zone)) {
-                        zoneMap.set(addrInfo.zone, []);
-                    }
-                    zoneMap.get(addrInfo.zone)!.push(addrInfo);
-                }
-                for (const [zone, expectedAddresses] of zoneMap) {
-                    const zoneEnum = convertToZone(zone);
-                    const retrievedAddresses = qiWallet.getAddressesForZone(zoneEnum);
-                    assert.deepEqual(retrievedAddresses, expectedAddresses);
-                }
-            }
-        });
-        it(`tests change addresses generation and retrieval: ${test.name}`, function () {
-            for (const { params, expectedAddress } of test.changeAddresses) {
-                const addrInfo = qiWallet.getNextChangeAddressSync(params.account, params.zone);
-                assert.deepEqual(addrInfo, expectedAddress);
-            }
-        });
-    }
-});
-
 describe('QiHDWallet: Test transaction signing', function () {
     const tests = loadTests<TestCaseQiTransaction>('qi-transaction');
     for (const test of tests) {
diff --git a/src/_tests/unit/quaihdwallet-address-derivation.unit.test.ts b/src/_tests/unit/quaihdwallet-address-derivation.unit.test.ts
new file mode 100644
index 00000000..d2266554
--- /dev/null
+++ b/src/_tests/unit/quaihdwallet-address-derivation.unit.test.ts
@@ -0,0 +1,173 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import assert from 'assert';
+import { loadTests } from '../utils.js';
+import { Mnemonic, QuaiHDWallet, Zone, NeuteredAddressInfo } from '../../index.js';
+
+interface TestCaseQuaiAddressDerivation {
+    mnemonic: string;
+    addresses: Array<{
+        zone: string;
+        account: number;
+        addresses: Array<NeuteredAddressInfo>;
+    }>;
+}
+
+describe('QuaiHDWallet Address Derivation', function () {
+    this.timeout(2 * 60 * 1000);
+    const tests = loadTests<TestCaseQuaiAddressDerivation>('quai-address-derivation');
+
+    for (const test of tests) {
+        it('derives addresses correctly', function () {
+            const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
+            const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
+
+            for (const addressesInfo of test.addresses) {
+                const zone = addressesInfo.zone as Zone;
+                const account = addressesInfo.account;
+
+                for (const expectedAddressInfo of addressesInfo.addresses) {
+                    const derivedAddressInfo = quaiWallet.getNextAddressSync(account, zone);
+                    assert.deepEqual(
+                        derivedAddressInfo,
+                        expectedAddressInfo,
+                        `Address mismatch for zone ${zone}, account ${account}, expected: ${JSON.stringify(expectedAddressInfo)}, derived: ${JSON.stringify(derivedAddressInfo)}`,
+                    );
+                }
+            }
+        });
+    }
+});
+
+describe('QuaiHDWallet Address Getters', function () {
+    this.timeout(2 * 60 * 1000);
+    const tests = loadTests<TestCaseQuaiAddressDerivation>('quai-address-derivation');
+
+    for (const test of tests) {
+        const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
+        const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
+
+        // Generate all addresses first
+        for (const addressesInfo of test.addresses) {
+            const zone = addressesInfo.zone as Zone;
+            const account = addressesInfo.account;
+            for (const _ of addressesInfo.addresses) {
+                quaiWallet.getNextAddressSync(account, zone);
+            }
+        }
+
+        it('getAddressInfo returns correct address info', function () {
+            for (const addressesInfo of test.addresses) {
+                for (const expectedAddressInfo of addressesInfo.addresses) {
+                    const addressInfo = quaiWallet.getAddressInfo(expectedAddressInfo.address);
+                    assert.deepEqual(
+                        addressInfo,
+                        expectedAddressInfo,
+                        `Address info mismatch for address ${expectedAddressInfo.address}`,
+                    );
+                }
+            }
+        });
+
+        it('getAddressesForZone returns all addresses for specified zone', function () {
+            for (const addressesInfo of test.addresses) {
+                const zone = addressesInfo.zone as Zone;
+                const addresses = quaiWallet.getAddressesForZone(zone);
+                const expectedAddresses = test.addresses
+                    .filter((info) => info.zone === zone)
+                    .flatMap((info) => info.addresses);
+
+                assert.deepEqual(addresses, expectedAddresses, `Addresses mismatch for zone ${zone}`);
+            }
+        });
+
+        it('getAddressesForAccount returns all addresses for specified account', function () {
+            const accountMap = new Map<number, NeuteredAddressInfo[]>();
+
+            // Group expected addresses by account
+            for (const addressesInfo of test.addresses) {
+                const account = addressesInfo.account;
+                if (!accountMap.has(account)) {
+                    accountMap.set(account, []);
+                }
+                accountMap.get(account)!.push(...addressesInfo.addresses);
+            }
+
+            // Test each account
+            for (const [account, expectedAddresses] of accountMap) {
+                const addresses = quaiWallet.getAddressesForAccount(account);
+                assert.deepEqual(addresses, expectedAddresses, `Addresses mismatch for account ${account}`);
+            }
+        });
+
+        it('returns empty arrays for non-existent zones and accounts', function () {
+            const nonExistentZone = '0x22' as Zone;
+            const nonExistentAccount = 999;
+
+            assert.deepEqual(quaiWallet.getAddressesForZone(nonExistentZone), []);
+            assert.deepEqual(quaiWallet.getAddressesForAccount(nonExistentAccount), []);
+        });
+    }
+});
+
+describe('Basic Address Management', function () {
+    const tests = loadTests<TestCaseQuaiAddressDerivation>('quai-address-derivation');
+
+    for (const test of tests) {
+        const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
+        const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
+
+        it('should add addresses correctly', function () {
+            // Test with addresses from the first zone/account in test data
+            const firstZoneAddresses = test.addresses[0].addresses;
+            const firstAddress = firstZoneAddresses[0];
+
+            // Add address using the same account and index from test data
+            const addedAddress = quaiWallet.addAddress(firstAddress.account, firstAddress.index);
+
+            assert.deepEqual(
+                addedAddress,
+                firstAddress,
+                `Added address does not match expected address for index ${firstAddress.index}`,
+            );
+
+            // Verify the address was added correctly by retrieving it
+            const retrievedAddress = quaiWallet.getAddressInfo(firstAddress.address);
+            assert.deepEqual(retrievedAddress, firstAddress, 'Retrieved address does not match added address');
+
+            // Test adding same address index again should throw error
+            assert.throws(
+                () => quaiWallet.addAddress(firstAddress.account, firstAddress.index),
+                Error,
+                `Address for index ${firstAddress.index} already exists`,
+            );
+        });
+
+        it('should handle invalid indices correctly', function () {
+            // Test with negative index
+            assert.throws(() => quaiWallet.addAddress(0, -1), Error, 'Negative index should throw error');
+        });
+
+        it('should handle invalid accounts correctly', function () {
+            // Test with negative account
+            assert.throws(() => quaiWallet.addAddress(-1, 0), Error, 'Negative account should throw error');
+        });
+
+        it('should reject indices that derive invalid addresses', function () {
+            // For Cyprus1 (0x00) and account 0:
+            // Index 518 derives an invalid address (wrong zone or ledger)
+            // Index 519 derives the first valid address
+
+            // Test invalid address index
+            assert.throws(
+                () => quaiWallet.addAddress(0, 518),
+                Error,
+                'Failed to derive a valid address zone for the index 518',
+            );
+        });
+
+        it('should reject indices that derive duplicate addresses', function () {
+            // Test that adding an existing address index throws error
+            assert.throws(() => quaiWallet.addAddress(0, 519), Error, 'Address for index 519 already exists');
+        });
+    }
+});
diff --git a/src/_tests/unit/quaihdwallet.unit.test.ts b/src/_tests/unit/quaihdwallet.unit.test.ts
index 88c36c17..3d0afcea 100644
--- a/src/_tests/unit/quaihdwallet.unit.test.ts
+++ b/src/_tests/unit/quaihdwallet.unit.test.ts
@@ -1,13 +1,11 @@
 import assert from 'assert';
 
-import { loadTests, convertToZone } from '../utils.js';
+import { loadTests } from '../utils.js';
 
 import {
     TestCaseQuaiTransaction,
     TestCaseQuaiSerialization,
-    TestCaseQuaiAddresses,
     TestCaseQuaiTypedData,
-    AddressInfo,
     Zone,
     TestCaseQuaiMessageSign,
 } from '../types.js';
@@ -16,50 +14,6 @@ import { recoverAddress } from '../../index.js';
 
 import { Mnemonic, QuaiHDWallet } from '../../index.js';
 
-describe('Test address generation and retrieval', function () {
-    const tests = loadTests<TestCaseQuaiAddresses>('quai-addresses');
-    for (const test of tests) {
-        const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
-        const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
-        it(`tests addresses generation and retrieval: ${test.name}`, function () {
-            const generatedAddresses: AddressInfo[] = [];
-            for (const { params, expectedAddress } of test.addresses) {
-                const addrInfo = quaiWallet.getNextAddressSync(params.account, params.zone);
-                assert.deepEqual(addrInfo, expectedAddress);
-                generatedAddresses.push(addrInfo);
-
-                const retrievedAddrInfo = quaiWallet.getAddressInfo(expectedAddress.address);
-                assert.deepEqual(retrievedAddrInfo, expectedAddress);
-
-                const accountMap = new Map<number, AddressInfo[]>();
-                for (const addrInfo of generatedAddresses) {
-                    if (!accountMap.has(addrInfo.account)) {
-                        accountMap.set(addrInfo.account, []);
-                    }
-                    accountMap.get(addrInfo.account)!.push(addrInfo);
-                }
-                for (const [account, expectedAddresses] of accountMap) {
-                    const retrievedAddresses = quaiWallet.getAddressesForAccount(account);
-                    assert.deepEqual(retrievedAddresses, expectedAddresses);
-                }
-
-                const zoneMap = new Map<string, AddressInfo[]>();
-                for (const addrInfo of generatedAddresses) {
-                    if (!zoneMap.has(addrInfo.zone)) {
-                        zoneMap.set(addrInfo.zone, []);
-                    }
-                    zoneMap.get(addrInfo.zone)!.push(addrInfo);
-                }
-                for (const [zone, expectedAddresses] of zoneMap) {
-                    const zoneEnum = convertToZone(zone);
-                    const retrievedAddresses = quaiWallet.getAddressesForZone(zoneEnum);
-                    assert.deepEqual(retrievedAddresses, expectedAddresses);
-                }
-            }
-        });
-    }
-});
-
 describe('Test transaction signing', function () {
     const tests = loadTests<TestCaseQuaiTransaction>('quai-transaction');
     for (const test of tests) {
diff --git a/src/wallet/hdwallet.ts b/src/wallet/hdwallet.ts
index 6937c3bd..526a5fe9 100644
--- a/src/wallet/hdwallet.ts
+++ b/src/wallet/hdwallet.ts
@@ -19,7 +19,6 @@ export interface NeuteredAddressInfo {
     address: string;
     account: number;
     index: number;
-    change: boolean;
     zone: Zone;
 }
 
@@ -42,7 +41,7 @@ export const _guard = {};
 /**
  * Abstract class representing a Hierarchical Deterministic (HD) wallet.
  */
-export abstract class AbstractHDWallet {
+export abstract class AbstractHDWallet<T extends NeuteredAddressInfo = NeuteredAddressInfo> {
     protected static _version: number = 1;
 
     protected static _coinType?: AllowedCoinType;
@@ -142,146 +141,92 @@ export abstract class AbstractHDWallet {
     }
 
     /**
-     * Adds an address to the wallet.
+     * Adds an address to the wallet for a given account and address index.
      *
-     * @param {number} account - The account number.
-     * @param {number} addressIndex - The address index.
-     * @param {boolean} [isChange=false] - Whether the address is a change address. Default is `false`
-     * @returns {NeuteredAddressInfo} The added address info.
+     * @param {number} account - The account number to add the address to
+     * @param {number} addressIndex - The index of the address to add
+     * @returns {T | null} The address info object if successful, null otherwise
      */
-    public addAddress(account: number, addressIndex: number, isChange: boolean = false): NeuteredAddressInfo {
-        return this._addAddress(this._addresses, account, addressIndex, isChange);
-    }
+    abstract addAddress(account: number, addressIndex: number): T | null;
 
     /**
-     * Helper method to add an address to the wallet address map.
+     * Gets the next available address for a given account and zone.
      *
-     * @param {Map<string, NeuteredAddressInfo>} addressMap - The address map.
-     * @param {number} account - The account number.
-     * @param {number} addressIndex - The address index.
-     * @param {boolean} [isChange=false] - Whether the address is a change address. Default is `false`
-     * @returns {NeuteredAddressInfo} The added address info.
-     * @throws {Error} If the address for the index already exists.
+     * @param {number} account - The account number to get the next address for
+     * @param {Zone} zone - The zone to get the next address in
+     * @returns {Promise<T>} Promise that resolves to the next address info
      */
-    protected _addAddress(
-        addressMap: Map<string, NeuteredAddressInfo>,
-        account: number,
-        addressIndex: number,
-        isChange: boolean = false,
-    ): NeuteredAddressInfo {
-        // check if address already exists for the index
-        this._addresses.forEach((addressInfo) => {
-            if (addressInfo.index === addressIndex) {
-                throw new Error(`Address for index ${addressIndex} already exists`);
-            }
-        });
-
-        // derive the address node and validate the zone
-        const changeIndex = isChange ? 1 : 0;
-        const addressNode = this._root
-            .deriveChild(account + HARDENED_OFFSET)
-            .deriveChild(changeIndex)
-            .deriveChild(addressIndex);
-        const zone = getZoneForAddress(addressNode.address);
-        if (!zone) {
-            throw new Error(`Failed to derive a valid address zone for the index ${addressIndex}`);
-        }
+    abstract getNextAddress(account: number, zone: Zone): Promise<T>;
 
-        return this.createAndStoreAddressInfo(addressNode, account, zone, isChange, addressMap);
-    }
+    /**
+     * Synchronously gets the next available address for a given account and zone.
+     *
+     * @param {number} account - The account number to get the next address for
+     * @param {Zone} zone - The zone to get the next address in
+     * @returns {T} The next address info
+     */
+    abstract getNextAddressSync(account: number, zone: Zone): T;
 
     /**
-     * Promise that resolves to the next address for the specified account and zone.
+     * Gets the address info for a given address string.
      *
-     * @param {number} account - The index of the account for which to retrieve the next address.
-     * @param {Zone} zone - The zone in which to retrieve the next address.
-     * @returns {Promise<NeuteredAddressInfo>} The next neutered address information.
+     * @param {string} address - The address to get info for
+     * @returns {T | null} The address info if found, null otherwise
      */
-    public async getNextAddress(account: number, zone: Zone): Promise<NeuteredAddressInfo> {
-        return Promise.resolve(this._getNextAddress(account, zone, false, this._addresses));
-    }
+    abstract getAddressInfo(address: string): T | null;
 
     /**
-     * Synchronously retrieves the next address for the specified account and zone.
+     * Gets the private key for a given address.
      *
-     * @param {number} account - The index of the account for which to retrieve the next address.
-     * @param {Zone} zone - The zone in which to retrieve the next address.
-     * @returns {NeuteredAddressInfo} The next neutered address information.
+     * @param {string} address - The address to get the private key for
+     * @returns {string} The private key as a hex string
      */
-    public getNextAddressSync(account: number, zone: Zone): NeuteredAddressInfo {
-        return this._getNextAddress(account, zone, false, this._addresses);
-    }
+    abstract getPrivateKey(address: string): string;
 
     /**
-     * Derives and returns the next address information for the specified account and zone.
+     * Gets all addresses belonging to a specific zone.
      *
-     * @param {number} accountIndex - The index of the account for which the address is being generated.
-     * @param {Zone} zone - The zone in which the address is to be used.
-     * @param {boolean} isChange - A flag indicating whether the address is a change address.
-     * @param {Map<string, NeuteredAddressInfo>} addressMap - A map storing the neutered address information.
-     * @returns {NeuteredAddressInfo} The derived neutered address information.
-     * @throws {Error} If the zone is invalid.
+     * @param {Zone} zone - The zone to get addresses for
+     * @returns {T[]} Array of address info objects in the zone
      */
-    protected _getNextAddress(
-        accountIndex: number,
-        zone: Zone,
-        isChange: boolean,
-        addressMap: Map<string, NeuteredAddressInfo>,
-    ): NeuteredAddressInfo {
-        this.validateZone(zone);
-        const lastIndex = this.getLastAddressIndex(addressMap, zone, accountIndex, isChange);
-        const addressNode = this.deriveNextAddressNode(accountIndex, lastIndex + 1, zone, isChange);
-        return this.createAndStoreAddressInfo(addressNode, accountIndex, zone, isChange, addressMap);
-    }
+    abstract getAddressesForZone(zone: Zone): T[];
 
     /**
-     * Gets the address info for a given address.
+     * Gets all addresses belonging to a specific account.
      *
-     * @param {string} address - The address.
-     * @returns {NeuteredAddressInfo | null} The address info or null if not found.
+     * @param {number} account - The account number to get addresses for
+     * @returns {T[]} Array of address info objects in the account
      */
-    public getAddressInfo(address: string): NeuteredAddressInfo | null {
-        const addressInfo = this._addresses.get(address);
-        if (!addressInfo) {
-            return null;
-        }
-        return addressInfo;
-    }
+    abstract getAddressesForAccount(account: number): T[];
 
     /**
-     * Returns the private key for a given address. This method should be used with caution as it exposes the private
-     * key to the user.
+     * Finds the highest used index for a given account and zone.
      *
-     * @param {string} address - The address associated with the desired private key.
-     * @returns {string} The private key.
+     * @param {T[] | undefined} addresses - Array of address info objects to search
+     * @param {number} account - The account number to find the last index for
+     * @param {Zone} zone - The zone to find the last index in
+     * @returns {number} The highest used index, or -1 if none found
+     * @protected
      */
-    public getPrivateKey(address: string): string {
-        const hdNode = this._getHDNodeForAddress(address);
-        return hdNode.privateKey;
-    }
+    protected abstract _findLastUsedIndex(addresses: T[] | undefined, account: number, zone: Zone): number;
 
     /**
-     * Gets the addresses for a given account.
+     * Abstract method to sign a message using the private key associated with the given address.
      *
-     * @param {number} account - The account number.
-     * @returns {NeuteredAddressInfo[]} The addresses for the account.
+     * @param {string} address - The address for which the message is to be signed.
+     * @param {string | Uint8Array} message - The message to be signed, either as a string or Uint8Array.
+     * @returns {Promise<string>} A promise that resolves to the signature of the message in hexadecimal string format.
+     * @throws {Error} If the method is not implemented in the subclass.
      */
-    public getAddressesForAccount(account: number): NeuteredAddressInfo[] {
-        const addresses = this._addresses.values();
-        return Array.from(addresses).filter((addressInfo) => addressInfo.account === account);
-    }
+    abstract signMessage(address: string, message: string | Uint8Array): Promise<string>;
 
     /**
-     * Gets the addresses for a given zone.
+     * Abstract method to sign a transaction.
      *
-     * @param {Zone} zone - The zone.
-     * @returns {NeuteredAddressInfo[]} The addresses for the zone.
+     * @param {TransactionRequest} tx - The transaction request.
+     * @returns {Promise<string>} A promise that resolves to the signed transaction.
      */
-    public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
-        this.validateZone(zone);
-        const addresses = this._addresses.values();
-        return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
-    }
+    abstract signTransaction(tx: TransactionRequest): Promise<string>;
 
     /**
      * Creates an instance of the HD wallet.
@@ -361,22 +306,6 @@ export abstract class AbstractHDWallet {
         return (this as any).createInstance(mnemonic);
     }
 
-    /**
-     * Abstract method to sign a transaction.
-     *
-     * @param {TransactionRequest} tx - The transaction request.
-     * @returns {Promise<string>} A promise that resolves to the signed transaction.
-     */
-    abstract signTransaction(tx: TransactionRequest): Promise<string>;
-
-    // /**
-    //  * Abstract method to send a transaction.
-    //  *
-    //  * @param {TransactionRequest} tx - The transaction request.
-    //  * @returns {Promise<TransactionResponse>} A promise that resolves to the transaction response.
-    //  */
-    // abstract sendTransaction(tx: TransactionRequest): Promise<TransactionResponse>;
-
     /**
      * Connects the wallet to a provider.
      *
@@ -398,41 +327,6 @@ export abstract class AbstractHDWallet {
         }
     }
 
-    /**
-     * Derives and returns the Hierarchical Deterministic (HD) node wallet associated with a given address.
-     *
-     * This method fetches the account and address information from the wallet's internal storage, derives the
-     * appropriate change node based on whether the address is a change address, and further derives the final HD node
-     * using the address index.
-     *
-     * @param {string} addr - The address for which to derive the HD node.
-     * @returns {HDNodeWallet} The derived HD node wallet corresponding to the given address.
-     * @throws {Error} If the given address is not known to the wallet.
-     * @throws {Error} If the account associated with the address is not found.
-     */
-    protected _getHDNodeForAddress(addr: string): HDNodeWallet {
-        const addressInfo = this._addresses.get(addr);
-        if (!addressInfo) {
-            throw new Error(`Address ${addr} is not known to this wallet`);
-        }
-
-        const changeIndex = addressInfo.change ? 1 : 0;
-        return this._root
-            .deriveChild(addressInfo.account + HARDENED_OFFSET)
-            .deriveChild(changeIndex)
-            .deriveChild(addressInfo.index);
-    }
-
-    /**
-     * Abstract method to sign a message using the private key associated with the given address.
-     *
-     * @param {string} address - The address for which the message is to be signed.
-     * @param {string | Uint8Array} message - The message to be signed, either as a string or Uint8Array.
-     * @returns {Promise<string>} A promise that resolves to the signature of the message in hexadecimal string format.
-     * @throws {Error} If the method is not implemented in the subclass.
-     */
-    abstract signMessage(address: string, message: string | Uint8Array): Promise<string>;
-
     /**
      * Serializes the HD wallet state into a format suitable for storage or transmission.
      *
@@ -488,10 +382,6 @@ export abstract class AbstractHDWallet {
             throw new Error(`Invalid NeuteredAddressInfo: index must be a non-negative integer: ${info.index}`);
         }
 
-        if (typeof info.change !== 'boolean') {
-            throw new Error(`Invalid NeuteredAddressInfo: change must be a boolean: ${info.change}`);
-        }
-
         if (!Object.values(Zone).includes(info.zone)) {
             throw new Error(`Invalid NeuteredAddressInfo: zone '${info.zone}' is not a valid Zone`);
         }
@@ -513,103 +403,4 @@ export abstract class AbstractHDWallet {
             throw new Error(`Invalid coinType ${serialized.coinType} for wallet (expected ${(this as any)._coinType})`);
         }
     }
-
-    /**
-     * Imports addresses from a serialized wallet into the addresses map. Before adding the addresses, a validation is
-     * performed to ensure the address, public key, and zone match the expected values.
-     *
-     * @param {Map<string, NeuteredAddressInfo>} addressMap - The map where the addresses will be imported.
-     * @param {NeuteredAddressInfo[]} addresses - The array of addresses to be imported, each containing account, index,
-     *   change, address, pubKey, and zone information.
-     * @throws {Error} If there is a mismatch between the expected and actual address, public key, or zone.
-     * @protected
-     */
-    protected importSerializedAddresses(
-        addressMap: Map<string, NeuteredAddressInfo>,
-        addresses: NeuteredAddressInfo[],
-    ): void {
-        for (const addressInfo of addresses) {
-            const newAddressInfo = this._addAddress(
-                addressMap,
-                addressInfo.account,
-                addressInfo.index,
-                addressInfo.change,
-            );
-            // validate the address info
-            if (addressInfo.address !== newAddressInfo.address) {
-                throw new Error(`Address mismatch: ${addressInfo.address} != ${newAddressInfo.address}`);
-            }
-            if (addressInfo.pubKey !== newAddressInfo.pubKey) {
-                throw new Error(`Public key mismatch: ${addressInfo.pubKey} != ${newAddressInfo.pubKey}`);
-            }
-            if (addressInfo.zone !== newAddressInfo.zone) {
-                throw new Error(`Zone mismatch: ${addressInfo.zone} != ${newAddressInfo.zone}`);
-            }
-        }
-    }
-
-    /**
-     * Retrieves the highest address index from the given address map for a specified zone, account, and change type.
-     *
-     * This method filters the address map based on the provided zone, account, and change type, then determines the
-     * maximum address index from the filtered addresses.
-     *
-     * @param {Map<string, NeuteredAddressInfo>} addressMap - The map containing address information, where the key is
-     *   an address string and the value is a NeuteredAddressInfo object.
-     * @param {Zone} zone - The specific zone to filter the addresses by.
-     * @param {number} account - The account number to filter the addresses by.
-     * @param {boolean} isChange - A boolean indicating whether to filter for change addresses (true) or receiving
-     *   addresses (false).
-     * @returns {number} - The highest address index for the specified criteria, or -1 if no addresses match.
-     * @protected
-     */
-    protected getLastAddressIndex(
-        addressMap: Map<string, NeuteredAddressInfo>,
-        zone: Zone,
-        account: number,
-        isChange: boolean,
-    ): number {
-        const addresses = Array.from(addressMap.values()).filter(
-            (addressInfo) =>
-                addressInfo.account === account && addressInfo.zone === zone && addressInfo.change === isChange,
-        );
-        return addresses.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1);
-    }
-
-    /**
-     * Creates and stores address information in the address map for a specified account, zone, and change type.
-     *
-     * This method constructs a NeuteredAddressInfo object using the provided HDNodeWallet and other parameters, then
-     * stores this information in the provided address map.
-     *
-     * @param {HDNodeWallet} addressNode - The HDNodeWallet object containing the address and public key information.
-     * @param {number} account - The account number to associate with the address.
-     * @param {Zone} zone - The specific zone to associate with the address.
-     * @param {boolean} isChange - A boolean indicating whether the address is a change address (true) or a receiving
-     *   address (false).
-     * @param {Map<string, NeuteredAddressInfo>} addressMap - The map to store the created NeuteredAddressInfo, with the
-     *   address as the key.
-     * @returns {NeuteredAddressInfo} - The created NeuteredAddressInfo object.
-     * @protected
-     */
-    protected createAndStoreAddressInfo(
-        addressNode: HDNodeWallet,
-        account: number,
-        zone: Zone,
-        isChange: boolean,
-        addressMap: Map<string, NeuteredAddressInfo>,
-    ): NeuteredAddressInfo {
-        const neuteredAddressInfo: NeuteredAddressInfo = {
-            pubKey: addressNode.publicKey,
-            address: addressNode.address,
-            account,
-            index: addressNode.index,
-            change: isChange,
-            zone,
-        };
-
-        addressMap.set(neuteredAddressInfo.address, neuteredAddressInfo);
-
-        return neuteredAddressInfo;
-    }
 }
diff --git a/src/wallet/qi-hdwallet-legacy.ts b/src/wallet/qi-hdwallet-legacy.ts
deleted file mode 100644
index ee66c981..00000000
--- a/src/wallet/qi-hdwallet-legacy.ts
+++ /dev/null
@@ -1,1567 +0,0 @@
-import {
-    AbstractHDWallet,
-    NeuteredAddressInfo,
-    SerializedHDWallet,
-    _guard,
-    MAX_ADDRESS_DERIVATION_ATTEMPTS,
-    HARDENED_OFFSET,
-} from './hdwallet.js';
-import { HDNodeWallet } from './hdnodewallet.js';
-import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js';
-import { computeAddress, isQiAddress } from '../address/index.js';
-import { getBytes, getZoneForAddress, hexlify } from '../utils/index.js';
-import { TransactionLike, QiTransaction, TxInput, FewestCoinSelector } from '../transaction/index.js';
-import { MuSigFactory } from '@brandonblack/musig';
-import { schnorr } from '@noble/curves/secp256k1';
-import { keccak256, musigCrypto } from '../crypto/index.js';
-import { Outpoint, UTXO, denominations } from '../transaction/utxo.js';
-import { AllowedCoinType, Shard, toShard, Zone } from '../constants/index.js';
-import { Mnemonic } from './mnemonic.js';
-import { PaymentCodePrivate, PaymentCodePublic, PC_VERSION, validatePaymentCode } from './payment-codes.js';
-import { BIP32Factory } from './bip32/bip32.js';
-import { bs58check } from './bip32/crypto.js';
-import { type BIP32API, HDNodeBIP32Adapter } from './bip32/types.js';
-import ecc from '@bitcoinerlab/secp256k1';
-import { SelectedCoinsResult } from '../transaction/abstract-coinselector.js';
-
-/**
- * @property {Outpoint} outpoint - The outpoint object.
- * @property {string} address - The address associated with the outpoint.
- * @property {Zone} zone - The zone of the outpoint.
- * @property {number} [account] - The account number (optional).
- * @interface OutpointInfo
- */
-export interface OutpointInfo {
-    outpoint: Outpoint;
-    address: string;
-    zone: Zone;
-    account?: number;
-}
-
-// enum AddressUseStatus {
-//     USED, // Address has been used in a transaction and is not available for reuse
-//     UNUSED, // Address has not been used in any transaction and is available for reuse
-//     ATTEMPTED, // Address was attempted to be used in a transaction but tx status is unknown
-// }
-
-interface PaymentChannelAddressInfo {
-    address: string;
-    pubKey: string;
-    index: number;
-    isUsed: boolean;
-    zone: Zone;
-    account: number;
-}
-
-interface PaymentChannelAddressExtendedInfo extends PaymentChannelAddressInfo {
-    counterpartyPaymentCode: string;
-}
-
-/**
- * @extends SerializedHDWallet
- * @property {OutpointInfo[]} outpoints - Array of outpoint information.
- * @property {NeuteredAddressInfo[]} changeAddresses - Array of change addresses.
- * @property {NeuteredAddressInfo[]} gapAddresses - Array of gap addresses.
- * @property {NeuteredAddressInfo[]} gapChangeAddresses - Array of gap change addresses.
- * @interface SerializedQiHDWallet
- */
-export interface SerializedQiHDWallet extends SerializedHDWallet {
-    outpoints: OutpointInfo[];
-    pendingOutpoints: OutpointInfo[];
-    addresses: NeuteredAddressInfo[];
-    changeAddresses: NeuteredAddressInfo[];
-    gapAddresses: NeuteredAddressInfo[];
-    gapChangeAddresses: NeuteredAddressInfo[];
-    usedGapAddresses: NeuteredAddressInfo[];
-    usedGapChangeAddresses: NeuteredAddressInfo[];
-    receiverPaymentCodeInfo: { [key: string]: PaymentChannelAddressInfo[] };
-    senderPaymentCodeInfo: { [key: string]: PaymentChannelAddressInfo[] };
-}
-
-type AddressUsageCallback = (address: string) => Promise<boolean>;
-
-/**
- * Current known issues:
- *
- * - When generating send addresses we are not checking if the address has already been used before
- * - When syncing is seems like we are adding way too many change addresses
- * - Bip44 external and change address maps also have gap addresses in them
- * - It is unclear if we have checked if addresses have been used and if they are used
- * - We should always check all addresses that were previously included in a transaction to see if they have been used
- */
-
-/**
- * The Qi HD wallet is a BIP44-compliant hierarchical deterministic wallet used for managing a set of addresses in the
- * Qi ledger. This is wallet implementation is the primary way to interact with the Qi UTXO ledger on the Quai network.
- *
- * The Qi HD wallet supports:
- *
- * - Adding accounts to the wallet heierchy
- * - Generating addresses for a specific account in any {@link Zone}
- * - Signing and sending transactions for any address in the wallet
- * - Serializing the wallet to JSON and deserializing it back to a wallet instance.
- *
- * @category Wallet
- * @example
- *
- * ```ts
- * import { QiHDWallet, Zone } from 'quais';
- *
- * const wallet = new QiHDWallet();
- * const cyrpus1Address = await wallet.getNextAddress(0, Zone.Cyrpus1); // get the first address in the Cyrpus1 zone
- * await wallet.sendTransaction({ txInputs: [...], txOutputs: [...] }); // send a transaction
- * const serializedWallet = wallet.serialize(); // serialize current (account/address) state of the wallet
- * .
- * .
- * .
- * const deserializedWallet = QiHDWallet.deserialize(serializedWallet); // create a new wallet instance from the serialized data
- * ```
- */
-export class QiHDWalletLegacy extends AbstractHDWallet {
-    /**
-     * @ignore
-     * @type {number}
-     */
-    protected static _version: number = 1;
-
-    /**
-     * @ignore
-     * @type {number}
-     */
-    protected static _GAP_LIMIT: number = 5;
-
-    /**
-     * @ignore
-     * @type {AllowedCoinType}
-     */
-    protected static _coinType: AllowedCoinType = 969;
-
-    /**
-     * Map of change addresses to address info.
-     *
-     * @ignore
-     * @type {Map<string, NeuteredAddressInfo>}
-     */
-    protected _changeAddresses: Map<string, NeuteredAddressInfo> = new Map();
-
-    /**
-     * Array of gap addresses.
-     *
-     * @ignore
-     * @type {NeuteredAddressInfo[]}
-     */
-    protected _gapChangeAddresses: NeuteredAddressInfo[] = [];
-
-    /**
-     * Array of gap change addresses.
-     *
-     * @ignore
-     * @type {NeuteredAddressInfo[]}
-     */
-    protected _gapAddresses: NeuteredAddressInfo[] = [];
-
-    /**
-     * This array is used to keep track of gap addresses that have been included in a transaction, but whose outpoints
-     * have not been imported into the wallet.
-     *
-     * @ignore
-     * @type {NeuteredAddressInfo[]}
-     */
-    protected _usedGapAddresses: NeuteredAddressInfo[] = [];
-
-    /**
-     * This array is used to keep track of gap change addresses that have been included in a transaction, but whose
-     * outpoints have not been imported into the wallet.
-     *
-     * @ignore
-     * @type {NeuteredAddressInfo[]}
-     */
-    protected _usedGapChangeAddresses: NeuteredAddressInfo[] = [];
-
-    /**
-     * Array of outpoint information.
-     *
-     * @ignore
-     * @type {OutpointInfo[]}
-     */
-    protected _availableOutpoints: OutpointInfo[] = [];
-
-    /**
-     * Map of outpoints that are pending confirmation of being spent.
-     */
-    protected _pendingOutpoints: OutpointInfo[] = [];
-
-    /**
-     * @ignore
-     * @type {AddressUsageCallback}
-     */
-    protected _addressUseChecker: AddressUsageCallback | undefined;
-
-    /**
-     * Map of paymentcodes to PaymentChannelAddressInfo for the receiver
-     */
-    private _receiverPaymentCodeInfo: Map<string, PaymentChannelAddressInfo[]> = new Map();
-
-    /**
-     * Map of paymentcodes to PaymentChannelAddressInfo for the sender
-     */
-    private _senderPaymentCodeInfo: Map<string, PaymentChannelAddressInfo[]> = new Map();
-
-    /**
-     * @ignore
-     * @param {HDNodeWallet} root - The root HDNodeWallet.
-     * @param {Provider} [provider] - The provider (optional).
-     */
-    constructor(guard: any, root: HDNodeWallet, provider?: Provider) {
-        super(guard, root, provider);
-    }
-
-    /**
-     * Sets the address use checker. The provided callback function should accept an address as input and return a
-     * boolean indicating whether the address is in use. If the callback returns true, the address is considered used
-     * and if it returns false, the address is considered unused.
-     *
-     * @param {AddressUsageCallback} checker - The address use checker.
-     */
-    public setAddressUseChecker(checker: AddressUsageCallback): void {
-        this._addressUseChecker = checker;
-    }
-
-    // getters for the payment code info maps
-    public get receiverPaymentCodeInfo(): { [key: string]: PaymentChannelAddressInfo[] } {
-        return Object.fromEntries(this._receiverPaymentCodeInfo);
-    }
-
-    public get senderPaymentCodeInfo(): { [key: string]: PaymentChannelAddressInfo[] } {
-        return Object.fromEntries(this._senderPaymentCodeInfo);
-    }
-
-    /**
-     * Promise that resolves to the next change address for the specified account and zone.
-     *
-     * @param {number} account - The index of the account for which to retrieve the next change address.
-     * @param {Zone} zone - The zone in which to retrieve the next change address.
-     * @returns {Promise<NeuteredAddressInfo>} The next change neutered address information.
-     */
-    public async getNextChangeAddress(account: number, zone: Zone): Promise<NeuteredAddressInfo> {
-        return Promise.resolve(this._getNextAddress(account, zone, true, this._changeAddresses));
-    }
-
-    /**
-     * Synchronously retrieves the next change address for the specified account and zone.
-     *
-     * @param {number} account - The index of the account for which to retrieve the next change address.
-     * @param {Zone} zone - The zone in which to retrieve the next change address.
-     * @returns {NeuteredAddressInfo} The next change neutered address information.
-     */
-    public getNextChangeAddressSync(account: number, zone: Zone): NeuteredAddressInfo {
-        return this._getNextAddress(account, zone, true, this._changeAddresses);
-    }
-
-    /**
-     * Imports an array of outpoints.
-     *
-     * @param {OutpointInfo[]} outpoints - The outpoints to import.
-     */
-    public importOutpoints(outpoints: OutpointInfo[]): void {
-        this.validateOutpointInfo(outpoints);
-        this._availableOutpoints.push(...outpoints);
-    }
-
-    /**
-     * Gets the outpoints for the specified zone.
-     *
-     * @param {Zone} zone - The zone.
-     * @returns {OutpointInfo[]} The outpoints for the zone.
-     */
-    public getOutpoints(zone: Zone): OutpointInfo[] {
-        this.validateZone(zone);
-        return this._availableOutpoints.filter((outpoint) => outpoint.zone === zone);
-    }
-
-    /**
-     * Signs a Qi transaction and returns the serialized transaction.
-     *
-     * @param {QiTransactionRequest} tx - The transaction to sign.
-     * @returns {Promise<string>} The serialized transaction.
-     * @throws {Error} If the UTXO transaction is invalid.
-     */
-    public async signTransaction(tx: QiTransactionRequest): Promise<string> {
-        const txobj = QiTransaction.from(<TransactionLike>tx);
-        if (!txobj.txInputs || txobj.txInputs.length == 0 || !txobj.txOutputs)
-            throw new Error('Invalid UTXO transaction, missing inputs or outputs');
-
-        const hash = getBytes(keccak256(txobj.unsignedSerialized));
-
-        let signature: string;
-        if (txobj.txInputs.length === 1) {
-            signature = this.createSchnorrSignature(txobj.txInputs[0], hash);
-        } else {
-            signature = this.createMuSigSignature(txobj, hash);
-        }
-        txobj.signature = signature;
-        return txobj.serialized;
-    }
-
-    /**
-     * Gets the payment channel address info for a given address.
-     *
-     * @param {string} address - The address to look up.
-     * @returns {PaymentChannelAddressInfo | null} The address info or null if not found.
-     */
-    public getPaymentChannelAddressInfo(address: string): PaymentChannelAddressExtendedInfo | null {
-        for (const [paymentCode, pcInfoArray] of this._receiverPaymentCodeInfo.entries()) {
-            const pcInfo = pcInfoArray.find((info) => info.address === address);
-            if (pcInfo) {
-                return { ...pcInfo, counterpartyPaymentCode: paymentCode };
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Locates the address information for the given address, searching through standard addresses, change addresses,
-     * and payment channel addresses.
-     *
-     * @param {string} address - The address to locate.
-     * @returns {NeuteredAddressInfo | PaymentChannelAddressInfo | null} The address info or null if not found.
-     */
-    public locateAddressInfo(address: string): NeuteredAddressInfo | PaymentChannelAddressExtendedInfo | null {
-        // First, try to get standard address info
-        let addressInfo = this.getAddressInfo(address);
-        if (addressInfo) {
-            return addressInfo;
-        }
-
-        // Next, try to get change address info
-        addressInfo = this.getChangeAddressInfo(address);
-        if (addressInfo) {
-            return addressInfo;
-        }
-
-        // Finally, try to get payment channel address info
-        const pcAddressInfo = this.getPaymentChannelAddressInfo(address);
-        if (pcAddressInfo) {
-            return pcAddressInfo;
-        }
-
-        // Address not found
-        return null;
-    }
-
-    /**
-     * Gets the **total** balance for the specified zone, including locked UTXOs.
-     *
-     * @param {Zone} zone - The zone to get the balance for.
-     * @returns {bigint} The total balance for the zone.
-     */
-    public getBalanceForZone(zone: Zone): bigint {
-        this.validateZone(zone);
-
-        return this._availableOutpoints
-            .filter((outpoint) => outpoint.zone === zone)
-            .reduce((total, outpoint) => {
-                const denominationValue = denominations[outpoint.outpoint.denomination];
-                return total + denominationValue;
-            }, BigInt(0));
-    }
-
-    /**
-     * Gets the locked balance for the specified zone.
-     *
-     * @param {Zone} zone - The zone to get the locked balance for.
-     * @returns {bigint} The locked balance for the zone.
-     */
-    public async getSpendableBalanceForZone(zone: Zone, blockNumber?: number): Promise<bigint> {
-        this.validateZone(zone);
-        if (!this.provider) {
-            throw new Error('Provider is not set');
-        }
-        if (!blockNumber) {
-            blockNumber = await this.provider.getBlockNumber(toShard(zone));
-        }
-        return this._availableOutpoints
-            .filter((utxo) => utxo.outpoint.lock === 0 || utxo.outpoint.lock! < blockNumber!)
-            .reduce((total, utxo) => {
-                const denominationValue = denominations[utxo.outpoint.denomination];
-                return total + denominationValue;
-            }, BigInt(0));
-    }
-
-    /**
-     * Gets the locked balance for the specified zone.
-     *
-     * @param {Zone} zone - The zone to get the locked balance for.
-     * @returns {bigint} The locked balance for the zone.
-     */
-    public async getLockedBalanceForZone(zone: Zone, blockNumber?: number): Promise<bigint> {
-        this.validateZone(zone);
-        if (!this.provider) {
-            throw new Error('Provider is not set');
-        }
-        if (!blockNumber) {
-            blockNumber = await this.provider.getBlockNumber(toShard(zone));
-        }
-        return this._availableOutpoints
-            .filter((utxo) => utxo.outpoint.lock !== 0 && blockNumber! < utxo.outpoint.lock!)
-            .reduce((total, utxo) => {
-                const denominationValue = denominations[utxo.outpoint.denomination];
-                return total + denominationValue;
-            }, BigInt(0));
-    }
-
-    /**
-     * Converts outpoints for a specific zone to UTXO format.
-     *
-     * @param {Zone} zone - The zone to filter outpoints for.
-     * @returns {UTXO[]} An array of UTXO objects.
-     */
-    private outpointsToUTXOs(zone: Zone): UTXO[] {
-        this.validateZone(zone);
-        return this._availableOutpoints
-            .filter((outpointInfo) => outpointInfo.zone === zone)
-            .map((outpointInfo) => {
-                const utxo = new UTXO();
-                utxo.txhash = outpointInfo.outpoint.txhash;
-                utxo.index = outpointInfo.outpoint.index;
-                utxo.address = outpointInfo.address;
-                utxo.denomination = outpointInfo.outpoint.denomination;
-                utxo.lock = outpointInfo.outpoint.lock ?? null;
-                return utxo;
-            });
-    }
-
-    private async prepareAndSendTransaction(
-        amount: bigint,
-        originZone: Zone,
-        getDestinationAddresses: (count: number) => Promise<string[]>,
-    ): Promise<TransactionResponse> {
-        if (!this.provider) {
-            throw new Error('Provider is not set');
-        }
-
-        // 1. Check the wallet has enough balance in the originating zone to send the transaction
-        const currentBlock = await this.provider.getBlockNumber(originZone as unknown as Shard);
-        const balance = await this.getSpendableBalanceForZone(originZone, currentBlock);
-        if (balance < amount) {
-            throw new Error(`Insufficient balance in the originating zone: want ${amount} Qi got ${balance} Qi`);
-        }
-
-        // 2. Select the UXTOs from the specified zone to use as inputs, and generate the spend and change outputs
-        const zoneUTXOs = this.outpointsToUTXOs(originZone);
-        const unlockedUTXOs = zoneUTXOs.filter((utxo) => utxo.lock === 0 || utxo.lock! < currentBlock);
-        const fewestCoinSelector = new FewestCoinSelector(unlockedUTXOs);
-
-        const spendTarget: bigint = amount;
-        let selection = fewestCoinSelector.performSelection(spendTarget);
-
-        // 3. Generate as many unused addresses as required to populate the spend outputs
-        const sendAddresses = await getDestinationAddresses(selection.spendOutputs.length);
-
-        const getChangeAddresses = async (count: number): Promise<string[]> => {
-            const addresses: string[] = [];
-            for (let i = 0; i < count; i++) {
-                if (this._gapChangeAddresses.length > 0) {
-                    const nextChangeAddressInfo = this._gapChangeAddresses.shift()!;
-                    addresses.push(nextChangeAddressInfo.address);
-                    this._usedGapChangeAddresses.push(nextChangeAddressInfo);
-                } else {
-                    addresses.push((await this.getNextChangeAddress(0, originZone)).address);
-                }
-            }
-            return addresses;
-        };
-
-        // 4. Get change addresses
-        const changeAddresses = await getChangeAddresses(selection.changeOutputs.length);
-
-        // 5. Create the transaction and sign it using the signTransaction method
-        let inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);
-        if (inputPubKeys.some((pubkey) => !pubkey)) {
-            throw new Error('Missing public key for input address');
-        }
-
-        const chainId = (await this.provider.getNetwork()).chainId;
-        let tx = await this.prepareTransaction(
-            selection,
-            inputPubKeys.map((pubkey) => pubkey!),
-            sendAddresses,
-            changeAddresses,
-            Number(chainId),
-        );
-
-        const gasLimit = await this.provider.estimateGas(tx);
-        const gasPrice = denominations[1]; // 0.005 Qi
-        const minerTip = (gasLimit * gasPrice) / 100n; // 1% extra as tip
-        // const feeData = await this.provider.getFeeData(originZone, true);
-        // const conversionRate = await this.provider.getLatestQuaiRate(originZone, feeData.gasPrice!);
-
-        // 5.6 Calculate total fee for the transaction using the gasLimit, gasPrice, and minerTip
-        const totalFee = gasLimit * gasPrice + minerTip;
-
-        // Get new selection with fee
-        selection = fewestCoinSelector.performSelection(spendTarget, totalFee);
-
-        // Determine if new addresses are needed for the change and spend outputs
-        const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length;
-        if (changeAddressesNeeded > 0) {
-            const newChangeAddresses = await getChangeAddresses(changeAddressesNeeded);
-            changeAddresses.push(...newChangeAddresses);
-        }
-
-        const spendAddressesNeeded = selection.spendOutputs.length - sendAddresses.length;
-        if (spendAddressesNeeded > 0) {
-            const newSendAddresses = await getDestinationAddresses(spendAddressesNeeded);
-            sendAddresses.push(...newSendAddresses);
-        }
-
-        inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey);
-
-        tx = await this.prepareTransaction(
-            selection,
-            inputPubKeys.map((pubkey) => pubkey!),
-            sendAddresses,
-            changeAddresses,
-            Number(chainId),
-        );
-
-        // Move used outpoints to pendingOutpoints
-        this.moveOutpointsToPending(tx.txInputs);
-
-        // Sign the transaction
-        const signedTx = await this.signTransaction(tx);
-
-        // Broadcast the transaction to the network using the provider
-        return this.provider.broadcastTransaction(originZone, signedTx);
-    }
-
-    /**
-     * Converts an amount of Qi to Quai and sends it to a specified Quai address.
-     *
-     * @param {string} destinationAddress - The Quai address to send the converted Quai to.
-     * @param {bigint} amount - The amount of Qi to convert to Quai.
-     * @returns {Promise<TransactionResponse>} A promise that resolves to the transaction response.
-     * @throws {Error} If the destination address is invalid, the amount is zero, or the conversion fails.
-     */
-    public async convertToQuai(destinationAddress: string, amount: bigint): Promise<TransactionResponse> {
-        const zone = getZoneForAddress(destinationAddress);
-        if (!zone) {
-            throw new Error(`Invalid zone for Quai address: ${destinationAddress}`);
-        }
-
-        if (isQiAddress(destinationAddress)) {
-            throw new Error(`Invalid Quai address: ${destinationAddress}`);
-        }
-
-        if (amount <= 0) {
-            throw new Error('Amount must be greater than 0');
-        }
-
-        const getDestinationAddresses = async (count: number): Promise<string[]> => {
-            return Array(count).fill(destinationAddress);
-        };
-
-        return this.prepareAndSendTransaction(amount, zone, getDestinationAddresses);
-    }
-
-    /**
-     * Sends a transaction to a specified recipient payment code in a specified zone.
-     *
-     * @param {string} recipientPaymentCode - The payment code of the recipient.
-     * @param {bigint} amount - The amount of Qi to send.
-     * @param {Zone} originZone - The zone where the transaction originates.
-     * @param {Zone} destinationZone - The zone where the transaction is sent.
-     * @returns {Promise<TransactionResponse>} A promise that resolves to the transaction response.
-     * @throws {Error} If the payment code is invalid, the amount is zero, or the zones are invalid.
-     */
-    public async sendTransaction(
-        recipientPaymentCode: string,
-        amount: bigint,
-        originZone: Zone,
-        destinationZone: Zone,
-    ): Promise<TransactionResponse> {
-        if (!validatePaymentCode(recipientPaymentCode)) {
-            throw new Error('Invalid payment code');
-        }
-        if (amount <= 0) {
-            throw new Error('Amount must be greater than 0');
-        }
-        this.validateZone(originZone);
-        this.validateZone(destinationZone);
-
-        const getDestinationAddresses = async (count: number): Promise<string[]> => {
-            const addresses: string[] = [];
-            while (addresses.length < count) {
-                const address = this.getNextSendAddress(recipientPaymentCode, destinationZone).address;
-                const { isUsed } = await this.checkAddressUse(address);
-                if (!isUsed) {
-                    addresses.push(address);
-                }
-            }
-            return addresses;
-        };
-
-        return this.prepareAndSendTransaction(amount, originZone, getDestinationAddresses);
-    }
-
-    private async prepareTransaction(
-        selection: SelectedCoinsResult,
-        inputPubKeys: string[],
-        sendAddresses: string[],
-        changeAddresses: string[],
-        chainId: number,
-    ): Promise<QiTransaction> {
-        const tx = new QiTransaction();
-        tx.txInputs = selection.inputs.map((input, index) => ({
-            txhash: input.txhash!,
-            index: input.index!,
-            pubkey: inputPubKeys[index],
-        }));
-        // 5.3 Create the "sender" outputs
-        const senderOutputs = selection.spendOutputs.map((output, index) => ({
-            address: sendAddresses[index],
-            denomination: output.denomination,
-        }));
-
-        // 5.4 Create the "change" outputs
-        const changeOutputs = selection.changeOutputs.map((output, index) => ({
-            address: changeAddresses[index],
-            denomination: output.denomination,
-        }));
-
-        tx.txOutputs = [...senderOutputs, ...changeOutputs].map((output) => ({
-            address: output.address,
-            denomination: output.denomination!,
-        }));
-        tx.chainId = chainId;
-        return tx;
-    }
-
-    /**
-     * Checks the status of pending outpoints and updates the wallet's UTXO set accordingly.
-     *
-     * @param zone The zone in which to check the pending outpoints.
-     */
-    private async checkPendingOutpoints(zone: Zone): Promise<void> {
-        // Create a copy to iterate over, as we'll be modifying the _pendingOutpoints array
-        const pendingOutpoints = [...this._pendingOutpoints.filter((info) => info.zone === zone)];
-
-        const uniqueAddresses = new Set<string>(pendingOutpoints.map((info) => info.address));
-        let outpointsByAddress: Outpoint[] = [];
-        try {
-            outpointsByAddress = (
-                await Promise.all(Array.from(uniqueAddresses).map((address) => this.getOutpointsByAddress(address)))
-            ).flat();
-        } catch (error) {
-            console.error('Error getting outpoints by address', error);
-        }
-
-        const allOutpointsByAddress = outpointsByAddress.flat();
-
-        for (const outpointInfo of pendingOutpoints) {
-            const isSpent = !allOutpointsByAddress.some(
-                (outpoint) =>
-                    outpoint.txhash === outpointInfo.outpoint.txhash && outpoint.index === outpointInfo.outpoint.index,
-            );
-
-            if (isSpent) {
-                // Outpoint has been spent; remove it from pendingOutpoints
-                this.removeOutpointFromPending(outpointInfo.outpoint);
-            } else {
-                // Outpoint is still unspent; move it back to available outpoints
-                this.moveOutpointToAvailable(outpointInfo);
-            }
-        }
-    }
-
-    /**
-     * Moves specified inputs to pending outpoints.
-     *
-     * @param inputs List of inputs used in the transaction.
-     */
-    private moveOutpointsToPending(inputs: TxInput[]): void {
-        inputs.forEach((input) => {
-            const index = this._availableOutpoints.findIndex(
-                (outpointInfo) =>
-                    outpointInfo.outpoint.txhash === input.txhash && outpointInfo.outpoint.index === input.index,
-            );
-            if (index !== -1) {
-                const [outpointInfo] = this._availableOutpoints.splice(index, 1);
-                this._pendingOutpoints.push(outpointInfo);
-            }
-        });
-    }
-
-    /**
-     * Removes an outpoint from the pending outpoints.
-     *
-     * @param outpoint The outpoint to remove.
-     */
-    private removeOutpointFromPending(outpoint: Outpoint): void {
-        this._pendingOutpoints = this._pendingOutpoints.filter(
-            (info) => !(info.outpoint.txhash === outpoint.txhash && info.outpoint.index === outpoint.index),
-        );
-    }
-
-    /**
-     * Moves an outpoint from pending back to available outpoints.
-     *
-     * @param outpointInfo The outpoint info to move.
-     */
-    private moveOutpointToAvailable(outpointInfo: OutpointInfo): void {
-        this.removeOutpointFromPending(outpointInfo.outpoint);
-        this._availableOutpoints.push(outpointInfo);
-    }
-
-    /**
-     * Returns a schnorr signature for the given message and private key.
-     *
-     * @ignore
-     * @param {TxInput} input - The transaction input.
-     * @param {Uint8Array} hash - The hash of the message.
-     * @returns {string} The schnorr signature.
-     */
-    private createSchnorrSignature(input: TxInput, hash: Uint8Array): string {
-        const privKey = this.getPrivateKeyForTxInput(input);
-        const signature = schnorr.sign(hash, getBytes(privKey));
-        return hexlify(signature);
-    }
-
-    /**
-     * Returns a MuSig signature for the given message and private keys corresponding to the input addresses.
-     *
-     * @ignore
-     * @param {QiTransaction} tx - The Qi transaction.
-     * @param {Uint8Array} hash - The hash of the message.
-     * @returns {string} The MuSig signature.
-     */
-    private createMuSigSignature(tx: QiTransaction, hash: Uint8Array): string {
-        const musig = MuSigFactory(musigCrypto);
-
-        // Collect private keys corresponding to the pubkeys found on the inputs
-        const privKeys = tx.txInputs.map((input) => this.getPrivateKeyForTxInput(input));
-
-        // Create an array of public keys corresponding to the private keys for musig aggregation
-        const pubKeys: Uint8Array[] = privKeys
-            .map((privKey) => musigCrypto.getPublicKey(getBytes(privKey!), true))
-            .filter((pubKey) => pubKey !== null) as Uint8Array[];
-
-        // Generate nonces for each public key
-        const nonces = pubKeys.map((pk) => musig.nonceGen({ publicKey: getBytes(pk!) }));
-        const aggNonce = musig.nonceAgg(nonces);
-
-        const signingSession = musig.startSigningSession(aggNonce, hash, pubKeys);
-
-        // Create partial signatures for each private key
-        const partialSignatures = privKeys.map((sk, index) =>
-            musig.partialSign({
-                secretKey: getBytes(sk || ''),
-                publicNonce: nonces[index],
-                sessionKey: signingSession,
-                verify: true,
-            }),
-        );
-
-        // Aggregate the partial signatures into a final aggregated signature
-        const finalSignature = musig.signAgg(partialSignatures, signingSession);
-
-        return hexlify(finalSignature);
-    }
-
-    /**
-     * Retrieves the private key for a given transaction input.
-     *
-     * This method derives the private key for a transaction input by locating the address info and then deriving the
-     * private key based on where the address info was found:
-     *
-     * - For BIP44 addresses (standard or change), it uses the HD wallet to derive the private key.
-     * - For payment channel addresses (BIP47), it uses PaymentCodePrivate to derive the private key.
-     *
-     * @param {TxInput} input - The transaction input containing the public key.
-     * @returns {string} The private key corresponding to the transaction input.
-     * @throws {Error} If the input does not contain a public key or if the address information cannot be found.
-     */
-    private getPrivateKeyForTxInput(input: TxInput): string {
-        if (!input.pubkey) throw new Error('Missing public key for input');
-
-        const address = computeAddress(input.pubkey);
-        const addressInfo = this.locateAddressInfo(address);
-
-        if (!addressInfo) {
-            throw new Error(`Address not found: ${address}`);
-        }
-
-        if ('change' in addressInfo) {
-            // NeuteredAddressInfo (BIP44 addresses)
-            const changeIndex = addressInfo.change ? 1 : 0;
-            const addressNode = this._root
-                .deriveChild(addressInfo.account + HARDENED_OFFSET)
-                .deriveChild(changeIndex)
-                .deriveChild(addressInfo.index);
-            return addressNode.privateKey;
-        } else {
-            // PaymentChannelAddressInfo (BIP47 addresses)
-            const pcAddressInfo = addressInfo as PaymentChannelAddressExtendedInfo;
-            const account = pcAddressInfo.account;
-            const index = pcAddressInfo.index - 1;
-
-            const counterpartyPaymentCode = pcAddressInfo.counterpartyPaymentCode;
-            if (!counterpartyPaymentCode) {
-                throw new Error('Counterparty payment code not found for payment channel address');
-            }
-
-            const bip32 = BIP32Factory(ecc);
-            const buf = bs58check.decode(counterpartyPaymentCode);
-            const version = buf[0];
-            if (version !== PC_VERSION) throw new Error('Invalid payment code version');
-
-            const counterpartyPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
-            const paymentCodePrivate = this._getPaymentCodePrivate(account);
-            const paymentPrivateKey = paymentCodePrivate.derivePaymentPrivateKey(counterpartyPCodePublic, index);
-            return hexlify(paymentPrivateKey);
-        }
-    }
-
-    /**
-     * Scans the specified zone for addresses with unspent outputs. Starting at index 0, it will generate new addresses
-     * until the gap limit is reached for external and change BIP44 addresses and payment channel addresses.
-     *
-     * @param {Zone} zone - The zone in which to scan for addresses.
-     * @param {number} [account=0] - The index of the account to scan. Default is `0`
-     * @returns {Promise<void>} A promise that resolves when the scan is complete.
-     * @throws {Error} If the zone is invalid.
-     */
-    public async scan(zone: Zone, account: number = 0): Promise<void> {
-        this.validateZone(zone);
-        // flush the existing addresses and outpoints
-        this._addresses = new Map();
-        this._changeAddresses = new Map();
-        this._gapAddresses = [];
-        this._gapChangeAddresses = [];
-        this._availableOutpoints = [];
-
-        // Reset each map so that all keys have empty array values but keys are preserved
-        const resetSenderPaymentCodeInfo = new Map(
-            Array.from(this._senderPaymentCodeInfo.keys()).map((key) => [key, []]),
-        );
-        const resetReceiverPaymentCodeInfo = new Map(
-            Array.from(this._receiverPaymentCodeInfo.keys()).map((key) => [key, []]),
-        );
-
-        this._senderPaymentCodeInfo = resetSenderPaymentCodeInfo;
-        this._receiverPaymentCodeInfo = resetReceiverPaymentCodeInfo;
-
-        await this._scan(zone, account);
-    }
-
-    /**
-     * Scans the specified zone for addresses with unspent outputs. Starting at the last address index, it will generate
-     * new addresses until the gap limit is reached for external and change BIP44 addresses and payment channel
-     * addresses.
-     *
-     * @param {Zone} zone - The zone in which to sync addresses.
-     * @param {number} [account=0] - The index of the account to sync. Default is `0`
-     * @returns {Promise<void>} A promise that resolves when the sync is complete.
-     * @throws {Error} If the zone is invalid.
-     */
-    public async sync(zone: Zone, account: number = 0): Promise<void> {
-        this.validateZone(zone);
-        await this._scan(zone, account);
-        await this.checkPendingOutpoints(zone);
-    }
-
-    /**
-     * Internal method to scan the specified zone for addresses with unspent outputs. This method handles the actual
-     * scanning logic, generating new addresses until the gap limit is reached for both gap and change addresses.
-     *
-     * @param {Zone} zone - The zone in which to scan for addresses.
-     * @param {number} [account=0] - The index of the account to scan. Default is `0`
-     * @returns {Promise<void>} A promise that resolves when the scan is complete.
-     * @throws {Error} If the provider is not set.
-     */
-    private async _scan(zone: Zone, account: number = 0): Promise<void> {
-        if (!this.provider) throw new Error('Provider not set');
-
-        // Start scanning processes for each derivation tree
-        const scans = [
-            this.scanBIP44Addresses(zone, account, false), // External addresses
-            this.scanBIP44Addresses(zone, account, true), // Change addresses
-        ];
-
-        // Add scanning processes for each payment channel
-        for (const paymentCode of this._receiverPaymentCodeInfo.keys()) {
-            scans.push(this.scanPaymentChannel(zone, account, paymentCode));
-        }
-
-        // Run all scans in parallel
-        await Promise.all(scans);
-    }
-
-    /**
-     * Scans for the next address in the specified zone and account, checking for associated outpoints, and updates the
-     * address count and gap addresses accordingly.
-     *
-     * @param {Zone} zone - The zone in which the address is being scanned.
-     * @param {number} account - The index of the account for which the address is being scanned.
-     * @param {boolean} isChange - A flag indicating whether the address is a change address.
-     * @returns {Promise<void>} A promise that resolves when the scan is complete.
-     * @throws {Error} If an error occurs during the address scanning or outpoints retrieval process.
-     */
-    private async scanBIP44Addresses(zone: Zone, account: number, isChange: boolean): Promise<void> {
-        const addressMap = isChange ? this._changeAddresses : this._addresses;
-        const gapAddresses = isChange ? this._gapChangeAddresses : this._gapAddresses;
-        const usedGapAddresses = isChange ? this._usedGapChangeAddresses : this._usedGapAddresses;
-
-        // First, add all used gap addresses to the address map and import their outpoints
-        for (const addressInfo of usedGapAddresses) {
-            this._addAddress(addressMap, account, addressInfo.index, isChange);
-        }
-
-        // Scan outpoints for every address in the address map
-        for (const addressInfo of addressMap.values()) {
-            const outpoints = await this.getOutpointsByAddress(addressInfo.address);
-            if (outpoints.length > 0) {
-                this.importOutpoints(
-                    outpoints.map((outpoint) => ({
-                        outpoint,
-                        address: addressInfo.address,
-                        zone,
-                        account,
-                    })),
-                );
-            }
-        }
-
-        let gapCount = 0;
-        // Second, re-examine existing gap addresses
-        const newlyUsedAddresses: NeuteredAddressInfo[] = [];
-        for (let i: number = 0; i < gapAddresses.length; ) {
-            const addressInfo = gapAddresses[i];
-            const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
-            if (isUsed) {
-                // Address has been used since last scan
-                this._addAddress(addressMap, account, addressInfo.index, isChange);
-                if (outpoints.length > 0) {
-                    this.importOutpoints(
-                        outpoints.map((outpoint) => ({
-                            outpoint,
-                            address: addressInfo.address,
-                            zone,
-                            account,
-                        })),
-                    );
-                }
-                // Remove from gap addresses
-                newlyUsedAddresses.push(addressInfo);
-                gapCount = 0;
-            } else {
-                gapCount++;
-                i++;
-            }
-        }
-
-        // remove addresses that have been used from the gap addresses
-        const updatedGapAddresses = gapAddresses.filter(
-            (addressInfo) => !newlyUsedAddresses.some((usedAddress) => usedAddress.address === addressInfo.address),
-        );
-
-        // Scan for new gap addresses
-        const newGapAddresses: NeuteredAddressInfo[] = [];
-        while (gapCount < QiHDWalletLegacy._GAP_LIMIT) {
-            const addressInfo = this._getNextAddress(account, zone, isChange, addressMap);
-            const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
-            if (isUsed) {
-                if (outpoints.length > 0) {
-                    this.importOutpoints(
-                        outpoints.map((outpoint) => ({
-                            outpoint,
-                            address: addressInfo.address,
-                            zone,
-                            account,
-                        })),
-                    );
-                }
-                gapCount = 0;
-            } else {
-                gapCount++;
-                // check if the address is already in the updated gap addresses array
-                if (!updatedGapAddresses.some((usedAddress) => usedAddress.address === addressInfo.address)) {
-                    newGapAddresses.push(addressInfo);
-                }
-            }
-        }
-
-        // update the gap addresses
-        if (isChange) {
-            this._gapChangeAddresses = [...updatedGapAddresses, ...newGapAddresses];
-        } else {
-            this._gapAddresses = [...updatedGapAddresses, ...newGapAddresses];
-        }
-    }
-
-    /**
-     * Scans the specified payment channel for addresses with unspent outputs. Starting at the last address index, it
-     * will generate new addresses until the gap limit is reached.
-     *
-     * @param {Zone} zone - The zone in which to scan for addresses.
-     * @param {number} account - The index of the account to scan.
-     * @param {string} paymentCode - The payment code to scan.
-     * @returns {Promise<void>} A promise that resolves when the scan is complete.
-     * @throws {Error} If the zone is invalid.
-     */
-    private async scanPaymentChannel(zone: Zone, account: number, paymentCode: string): Promise<void> {
-        let gapCount = 0;
-
-        const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(paymentCode);
-        if (!paymentCodeInfoArray) {
-            throw new Error(`Payment code ${paymentCode} not found`);
-        }
-        // first, re-examine existing unused addresses
-        const newlyUsedAddresses: PaymentChannelAddressInfo[] = [];
-        const unusedAddresses = paymentCodeInfoArray.filter((info) => !info.isUsed);
-        for (let i: number = 0; i < unusedAddresses.length; ) {
-            const addressInfo = unusedAddresses[i];
-            const { isUsed, outpoints } = await this.checkAddressUse(addressInfo.address);
-            if (outpoints.length > 0 || isUsed) {
-                // Address has been used since last scan
-                addressInfo.isUsed = true;
-                const pcAddressInfoIndex = paymentCodeInfoArray.findIndex((info) => info.index === addressInfo.index);
-                paymentCodeInfoArray[pcAddressInfoIndex] = addressInfo;
-                this.importOutpoints(
-                    outpoints.map((outpoint) => ({
-                        outpoint,
-                        address: addressInfo.address,
-                        zone,
-                        account,
-                    })),
-                );
-                // Remove from gap addresses
-                newlyUsedAddresses.push(addressInfo);
-                gapCount = 0;
-            } else {
-                // Address is still unused
-                gapCount++;
-                i++;
-            }
-        }
-        // remove the addresses that have been used from the payment code info array
-        const updatedPaymentCodeInfoArray = paymentCodeInfoArray.filter(
-            (addressInfo: PaymentChannelAddressInfo) =>
-                !newlyUsedAddresses.some((usedAddress) => usedAddress.index === addressInfo.index),
-        );
-        // Then, scan for new gap addresses
-        while (gapCount < QiHDWalletLegacy._GAP_LIMIT) {
-            const pcAddressInfo = this.getNextReceiveAddress(paymentCode, zone, account);
-            const outpoints = await this.getOutpointsByAddress(pcAddressInfo.address);
-
-            let isUsed = false;
-            if (outpoints.length > 0) {
-                isUsed = true;
-                this.importOutpoints(
-                    outpoints.map((outpoint) => ({
-                        outpoint,
-                        address: pcAddressInfo.address,
-                        zone,
-                        account,
-                    })),
-                );
-                gapCount = 0;
-            } else if (
-                this._addressUseChecker !== undefined &&
-                (await this._addressUseChecker(pcAddressInfo.address))
-            ) {
-                // address checker returned true, so the address is used
-                isUsed = true;
-                gapCount = 0;
-            } else {
-                gapCount++;
-            }
-
-            if (isUsed) {
-                // update the payment code info array if the address has been used
-                pcAddressInfo.isUsed = isUsed;
-                const pcAddressInfoIndex = updatedPaymentCodeInfoArray.findIndex(
-                    (info) => info.index === pcAddressInfo.index,
-                );
-                if (pcAddressInfoIndex !== -1) {
-                    updatedPaymentCodeInfoArray[pcAddressInfoIndex] = pcAddressInfo;
-                } else {
-                    updatedPaymentCodeInfoArray.push(pcAddressInfo);
-                }
-            }
-        }
-
-        // update the payment code info map
-        this._receiverPaymentCodeInfo.set(paymentCode, updatedPaymentCodeInfoArray);
-    }
-
-    /**
-     * Queries the network node for the outpoints of the specified address.
-     *
-     * @ignore
-     * @param {string} address - The address to query.
-     * @returns {Promise<Outpoint[]>} The outpoints for the address.
-     * @throws {Error} If the query fails.
-     */
-    private async getOutpointsByAddress(address: string): Promise<Outpoint[]> {
-        try {
-            return await this.provider!.getOutpointsByAddress(address);
-        } catch (error) {
-            throw new Error(`Failed to get outpoints for address: ${address} - error: ${error}`);
-        }
-    }
-
-    private async checkAddressUse(address: string): Promise<{ isUsed: boolean; outpoints: Outpoint[] }> {
-        let isUsed = false;
-        let outpoints: Outpoint[] = [];
-        try {
-            outpoints = await this.getOutpointsByAddress(address);
-            if (outpoints.length > 0) {
-                isUsed = true;
-            } else if (this._addressUseChecker !== undefined && (await this._addressUseChecker(address))) {
-                // address checker returned true, so the address is used
-                isUsed = true;
-            }
-        } catch (error) {
-            throw new Error(`Failed to get outpoints for address: ${address} - error: ${error}`);
-        }
-        return { isUsed, outpoints };
-    }
-
-    /**
-     * Gets the change addresses for the specified zone.
-     *
-     * @param {Zone} zone - The zone.
-     * @returns {NeuteredAddressInfo[]} The change addresses for the zone.
-     */
-    public getChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
-        this.validateZone(zone);
-        const changeAddresses = this._changeAddresses.values();
-        return Array.from(changeAddresses).filter((addressInfo) => addressInfo.zone === zone);
-    }
-
-    /**
-     * Gets the gap addresses for the specified zone.
-     *
-     * @param {Zone} zone - The zone.
-     * @returns {NeuteredAddressInfo[]} The gap addresses for the zone.
-     */
-    public getGapAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
-        this.validateZone(zone);
-        const gapAddresses = this._gapAddresses.filter((addressInfo) => addressInfo.zone === zone);
-        return gapAddresses;
-    }
-
-    /**
-     * Gets the gap change addresses for the specified zone.
-     *
-     * @param {Zone} zone - The zone.
-     * @returns {NeuteredAddressInfo[]} The gap change addresses for the zone.
-     */
-    public getGapChangeAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
-        this.validateZone(zone);
-        const gapChangeAddresses = this._gapChangeAddresses.filter((addressInfo) => addressInfo.zone === zone);
-        return gapChangeAddresses;
-    }
-
-    /**
-     * Signs a message using the private key associated with the given address.
-     *
-     * @param {string} address - The address for which the message is to be signed.
-     * @param {string | Uint8Array} message - The message to be signed, either as a string or Uint8Array.
-     * @returns {Promise<string>} A promise that resolves to the signature of the message in hexadecimal string format.
-     * @throws {Error} If the address does not correspond to a valid HD node or if signing fails.
-     */
-    public async signMessage(address: string, message: string | Uint8Array): Promise<string> {
-        const addrNode = this._getHDNodeForAddress(address);
-        const privKey = addrNode.privateKey;
-        const digest = keccak256(message);
-        const signature = schnorr.sign(digest, getBytes(privKey));
-        return hexlify(signature);
-    }
-
-    /**
-     * Serializes the HD wallet state into a format suitable for storage or transmission.
-     *
-     * @returns {SerializedQiHDWallet} An object representing the serialized state of the HD wallet, including
-     *   outpoints, change addresses, gap addresses, and other inherited properties.
-     */
-    public serialize(): SerializedQiHDWallet {
-        const hdwalletSerialized = super.serialize();
-        return {
-            outpoints: this._availableOutpoints,
-            pendingOutpoints: this._pendingOutpoints,
-            addresses: Array.from(this._addresses.values()),
-            changeAddresses: Array.from(this._changeAddresses.values()),
-            gapAddresses: this._gapAddresses,
-            gapChangeAddresses: this._gapChangeAddresses,
-            usedGapAddresses: this._usedGapAddresses,
-            usedGapChangeAddresses: this._usedGapChangeAddresses,
-            receiverPaymentCodeInfo: Object.fromEntries(this._receiverPaymentCodeInfo),
-            senderPaymentCodeInfo: Object.fromEntries(this._senderPaymentCodeInfo),
-            ...hdwalletSerialized,
-        };
-    }
-
-    /**
-     * Deserializes a serialized QiHDWallet object and reconstructs the wallet instance.
-     *
-     * @param {SerializedQiHDWallet} serialized - The serialized object representing the state of a QiHDWallet.
-     * @returns {Promise<QiHDWallet>} A promise that resolves to a reconstructed QiHDWallet instance.
-     * @throws {Error} If the serialized data is invalid or if any addresses in the gap addresses or gap change
-     *   addresses do not exist in the wallet.
-     */
-    public static async deserialize(serialized: SerializedQiHDWallet): Promise<QiHDWalletLegacy> {
-        super.validateSerializedWallet(serialized);
-        // create the wallet instance
-        const mnemonic = Mnemonic.fromPhrase(serialized.phrase);
-        const path = (this as any).parentPath(serialized.coinType);
-        const root = HDNodeWallet.fromMnemonic(mnemonic, path);
-        const wallet = new this(_guard, root);
-
-        // import the addresses
-        wallet.importSerializedAddresses(wallet._addresses, serialized.addresses);
-        // import the change addresses
-        wallet.importSerializedAddresses(wallet._changeAddresses, serialized.changeAddresses);
-
-        // import the gap addresses, verifying they already exist in the wallet
-        for (const gapAddressInfo of serialized.gapAddresses) {
-            const gapAddress = gapAddressInfo.address;
-            if (!wallet._addresses.has(gapAddress)) {
-                throw new Error(`Address ${gapAddress} not found in wallet`);
-            }
-            wallet._gapAddresses.push(gapAddressInfo);
-        }
-        // import the gap change addresses, verifying they already exist in the wallet
-        for (const gapChangeAddressInfo of serialized.gapChangeAddresses) {
-            const gapChangeAddress = gapChangeAddressInfo.address;
-            if (!wallet._changeAddresses.has(gapChangeAddress)) {
-                throw new Error(`Address ${gapChangeAddress} not found in wallet`);
-            }
-            wallet._gapChangeAddresses.push(gapChangeAddressInfo);
-        }
-
-        // validate the used gap addresses and import them
-        for (const usedGapAddressInfo of serialized.usedGapAddresses) {
-            if (!wallet._addresses.has(usedGapAddressInfo.address)) {
-                throw new Error(`Address ${usedGapAddressInfo.address} not found in wallet`);
-            }
-            wallet._usedGapAddresses.push(usedGapAddressInfo);
-        }
-
-        // validate the used gap change addresses and import them
-        for (const usedGapChangeAddressInfo of serialized.usedGapChangeAddresses) {
-            if (!wallet._changeAddresses.has(usedGapChangeAddressInfo.address)) {
-                throw new Error(`Address ${usedGapChangeAddressInfo.address} not found in wallet`);
-            }
-            wallet._usedGapChangeAddresses.push(usedGapChangeAddressInfo);
-        }
-
-        // validate and import the payment code info
-        wallet.validateAndImportPaymentCodeInfo(serialized.receiverPaymentCodeInfo, 'receiver');
-        wallet.validateAndImportPaymentCodeInfo(serialized.senderPaymentCodeInfo, 'sender');
-
-        // validate the available outpoints and import them
-        wallet.validateOutpointInfo(serialized.outpoints);
-        wallet._availableOutpoints.push(...serialized.outpoints);
-
-        // validate the pending outpoints and import them
-        wallet.validateOutpointInfo(serialized.pendingOutpoints);
-        wallet._pendingOutpoints.push(...serialized.pendingOutpoints);
-
-        return wallet;
-    }
-
-    /**
-     * Validates and imports a map of payment code info.
-     *
-     * @param {Map<string, PaymentChannelAddressInfo[]>} paymentCodeInfoMap - The map of payment code info to validate
-     *   and import.
-     * @param {'receiver' | 'sender'} target - The target map to update ('receiver' or 'sender').
-     * @throws {Error} If any of the payment code info is invalid.
-     */
-    private validateAndImportPaymentCodeInfo(
-        paymentCodeInfoMap: { [key: string]: PaymentChannelAddressInfo[] },
-        target: 'receiver' | 'sender',
-    ): void {
-        const targetMap = target === 'receiver' ? this._receiverPaymentCodeInfo : this._senderPaymentCodeInfo;
-
-        for (const [paymentCode, paymentCodeInfoArray] of Object.entries(paymentCodeInfoMap)) {
-            if (!validatePaymentCode(paymentCode)) {
-                throw new Error(`Invalid payment code: ${paymentCode}`);
-            }
-            for (const pcInfo of paymentCodeInfoArray) {
-                this.validatePaymentCodeInfo(pcInfo);
-            }
-            targetMap.set(paymentCode, paymentCodeInfoArray);
-        }
-    }
-
-    /**
-     * Validates a payment code info object.
-     *
-     * @param {PaymentChannelAddressInfo} pcInfo - The payment code info to validate.
-     * @throws {Error} If the payment code info is invalid.
-     */
-    private validatePaymentCodeInfo(pcInfo: PaymentChannelAddressInfo): void {
-        if (!/^(0x)?[0-9a-fA-F]{40}$/.test(pcInfo.address)) {
-            throw new Error('Invalid payment code info: address must be a 40-character hexadecimal string');
-        }
-        if (!Number.isInteger(pcInfo.index) || pcInfo.index < 0) {
-            throw new Error('Invalid payment code info: index must be a non-negative integer');
-        }
-        if (typeof pcInfo.isUsed !== 'boolean') {
-            throw new Error('Invalid payment code info: isUsed must be a boolean');
-        }
-        if (!Object.values(Zone).includes(pcInfo.zone)) {
-            throw new Error(`Invalid payment code info: zone '${pcInfo.zone}' is not a valid Zone`);
-        }
-        if (!Number.isInteger(pcInfo.account) || pcInfo.account < 0) {
-            throw new Error('Invalid payment code info: account must be a non-negative integer');
-        }
-    }
-
-    /**
-     * Validates an array of OutpointInfo objects. This method checks the validity of each OutpointInfo object by
-     * performing the following validations:
-     *
-     * - Validates the zone using the `validateZone` method.
-     * - Checks if the address exists in the wallet.
-     * - Checks if the account (if provided) exists in the wallet.
-     * - Validates the Outpoint by ensuring that `Txhash`, `Index`, and `Denomination` are not null.
-     *
-     * @ignore
-     * @param {OutpointInfo[]} outpointInfo - An array of OutpointInfo objects to be validated.
-     * @throws {Error} If any of the validations fail, an error is thrown with a descriptive message.
-     */
-    private validateOutpointInfo(outpointInfo: OutpointInfo[]): void {
-        outpointInfo.forEach((info) => {
-            // validate zone
-            this.validateZone(info.zone);
-
-            // validate address and account
-            this.validateAddressAndAccount(info.address, info.account);
-
-            // validate Outpoint
-            if (info.outpoint.txhash == null || info.outpoint.index == null || info.outpoint.denomination == null) {
-                throw new Error(`Invalid Outpoint: ${JSON.stringify(info)} `);
-            }
-        });
-    }
-
-    private validateAddressAndAccount(address: string, account?: number): void {
-        const addressInfo = this.locateAddressInfo(address);
-        if (!addressInfo) {
-            throw new Error(`Address ${address} not found in wallet`);
-        }
-        if (account && account !== addressInfo.account) {
-            throw new Error(`Address ${address} does not match account ${account}`);
-        }
-    }
-
-    /**
-     * Creates a new BIP47 payment code for the specified account. The payment code is derived from the account's BIP32
-     * root key.
-     *
-     * @param {number} account - The account index to derive the payment code from.
-     * @returns {Promise<string>} A promise that resolves to the Base58-encoded BIP47 payment code.
-     */
-    public getPaymentCode(account: number = 0): string {
-        const privatePcode = this._getPaymentCodePrivate(account);
-        return privatePcode.toBase58();
-    }
-
-    // helper method to get a bip32 API instance
-    private _getBIP32API(): BIP32API {
-        return BIP32Factory(ecc) as BIP32API;
-    }
-
-    // helper method to decode a base58 string into a Uint8Array
-    private _decodeBase58(base58: string): Uint8Array {
-        return bs58check.decode(base58);
-    }
-
-    /**
-     * Generates a BIP47 private payment code for the specified account. The payment code is created by combining the
-     * account's public key and chain code.
-     *
-     * @private
-     * @param {number} account - The account index for which to generate the private payment code.
-     * @returns {Promise<PaymentCodePrivate>} A promise that resolves to the PaymentCodePrivate instance.
-     */
-    private _getPaymentCodePrivate(account: number): PaymentCodePrivate {
-        const bip32 = this._getBIP32API();
-
-        const accountNode = this._root.deriveChild(account + HARDENED_OFFSET);
-
-        // payment code array
-        const pc = new Uint8Array(80);
-
-        // set version + options
-        pc.set([1, 0]);
-
-        // set the public key
-        const pubKey = accountNode.publicKey;
-        pc.set(getBytes(pubKey), 2);
-
-        // set the chain code
-        const chainCode = accountNode.chainCode;
-        pc.set(getBytes(chainCode), 35);
-
-        const adapter = new HDNodeBIP32Adapter(accountNode);
-
-        return new PaymentCodePrivate(adapter, ecc, bip32, pc);
-    }
-
-    /**
-     * Generates a payment address for sending funds to the specified receiver's BIP47 payment code. Uses Diffie-Hellman
-     * key exchange to derive the address from the receiver's public key and sender's private key.
-     *
-     * @param {string} receiverPaymentCode - The Base58-encoded BIP47 payment code of the receiver.
-     * @returns {Promise<string>} A promise that resolves to the payment address for sending funds.
-     * @throws {Error} Throws an error if the payment code version is invalid.
-     */
-    public getNextSendAddress(receiverPaymentCode: string, zone: Zone, account: number = 0): PaymentChannelAddressInfo {
-        const bip32 = this._getBIP32API();
-        const buf = this._decodeBase58(receiverPaymentCode);
-        const version = buf[0];
-        if (version !== PC_VERSION) throw new Error('Invalid payment code version');
-
-        const receiverPCodePrivate = this._getPaymentCodePrivate(account);
-        const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
-
-        const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(receiverPaymentCode);
-        const filteredPaymentCodeInfoArray = paymentCodeInfoArray?.filter(
-            (addressInfo) => addressInfo.account === account && addressInfo.zone === zone,
-        );
-        const lastIndex =
-            filteredPaymentCodeInfoArray && filteredPaymentCodeInfoArray.length > 0
-                ? filteredPaymentCodeInfoArray.reduce(
-                      (maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index),
-                      -1,
-                  )
-                : 0;
-
-        let addrIndex = lastIndex;
-        for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) {
-            const address = senderPCodePublic.getPaymentAddress(receiverPCodePrivate, addrIndex++);
-            if (this.isValidAddressForZone(address, zone)) {
-                const pubkey = senderPCodePublic.derivePaymentPublicKey(receiverPCodePrivate, addrIndex - 1);
-                const pcInfo: PaymentChannelAddressInfo = {
-                    address,
-                    pubKey: hexlify(pubkey),
-                    index: addrIndex,
-                    account,
-                    zone,
-                    isUsed: false,
-                };
-                if (paymentCodeInfoArray) {
-                    paymentCodeInfoArray.push(pcInfo);
-                } else {
-                    this._senderPaymentCodeInfo.set(receiverPaymentCode, [pcInfo]);
-                }
-                return pcInfo;
-            }
-        }
-
-        throw new Error(
-            `Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`,
-        );
-    }
-
-    /**
-     * Generates a payment address for receiving funds from the specified sender's BIP47 payment code. Uses
-     * Diffie-Hellman key exchange to derive the address from the sender's public key and receiver's private key.
-     *
-     * @param {string} senderPaymentCode - The Base58-encoded BIP47 payment code of the sender.
-     * @returns {Promise<string>} A promise that resolves to the payment address for receiving funds.
-     * @throws {Error} Throws an error if the payment code version is invalid.
-     */
-    public getNextReceiveAddress(
-        senderPaymentCode: string,
-        zone: Zone,
-        account: number = 0,
-    ): PaymentChannelAddressInfo {
-        const bip32 = this._getBIP32API();
-        const buf = this._decodeBase58(senderPaymentCode);
-        const version = buf[0];
-        if (version !== PC_VERSION) throw new Error('Invalid payment code version');
-
-        const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1));
-        const receiverPCodePrivate = this._getPaymentCodePrivate(account);
-
-        const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(senderPaymentCode);
-        const filteredPaymentCodeInfoArray = paymentCodeInfoArray?.filter(
-            (addressInfo) => addressInfo.account === account && addressInfo.zone === zone,
-        );
-        const lastIndex =
-            filteredPaymentCodeInfoArray && filteredPaymentCodeInfoArray.length > 0
-                ? filteredPaymentCodeInfoArray.reduce(
-                      (maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index),
-                      -1,
-                  )
-                : 0;
-
-        let addrIndex = lastIndex;
-        for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) {
-            const address = receiverPCodePrivate.getPaymentAddress(senderPCodePublic, addrIndex++);
-            if (this.isValidAddressForZone(address, zone)) {
-                const pubkey = receiverPCodePrivate.derivePaymentPublicKey(senderPCodePublic, addrIndex - 1);
-                const pcInfo: PaymentChannelAddressInfo = {
-                    address,
-                    pubKey: hexlify(pubkey),
-                    index: addrIndex,
-                    account,
-                    zone,
-                    isUsed: false,
-                };
-                if (paymentCodeInfoArray) {
-                    paymentCodeInfoArray.push(pcInfo);
-                } else {
-                    this._receiverPaymentCodeInfo.set(senderPaymentCode, [pcInfo]);
-                }
-                return pcInfo;
-            }
-        }
-
-        throw new Error(
-            `Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`,
-        );
-    }
-
-    /**
-     * Receives a payment code and stores it in the wallet for future use. If the payment code is already in the wallet,
-     * it will be ignored.
-     *
-     * @param {string} paymentCode - The payment code to store.
-     * @param {'receiver' | 'sender'} type - The type of payment code ('receiver' or 'sender').
-     */
-    public openChannel(paymentCode: string, type: 'receiver' | 'sender'): void {
-        if (!validatePaymentCode(paymentCode)) {
-            throw new Error(`Invalid payment code: ${paymentCode}`);
-        }
-        if (type === 'receiver') {
-            if (!this._receiverPaymentCodeInfo.has(paymentCode)) {
-                this._receiverPaymentCodeInfo.set(paymentCode, []);
-            }
-        } else {
-            if (!this._senderPaymentCodeInfo.has(paymentCode)) {
-                this._senderPaymentCodeInfo.set(paymentCode, []);
-            }
-        }
-    }
-
-    /**
-     * Gets the address info for a given address.
-     *
-     * @param {string} address - The address.
-     * @returns {NeuteredAddressInfo | null} The address info or null if not found.
-     */
-    public getChangeAddressInfo(address: string): NeuteredAddressInfo | null {
-        const changeAddressInfo = this._changeAddresses.get(address);
-        if (!changeAddressInfo) {
-            return null;
-        }
-        return changeAddressInfo;
-    }
-}
diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts
index bef5d5d9..2709dfcd 100644
--- a/src/wallet/qi-hdwallet.ts
+++ b/src/wallet/qi-hdwallet.ts
@@ -9,11 +9,11 @@ import {
 import { HDNodeWallet } from './hdnodewallet.js';
 import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js';
 import { computeAddress, isQiAddress } from '../address/index.js';
-import { getBytes, getZoneForAddress, hexlify, toQuantity } from '../utils/index.js';
+import { getBytes, getZoneForAddress, hexlify, isHexString, toQuantity } from '../utils/index.js';
 import { TransactionLike, QiTransaction, TxInput, FewestCoinSelector } from '../transaction/index.js';
 import { MuSigFactory } from '@brandonblack/musig';
 import { schnorr } from '@noble/curves/secp256k1';
-import { keccak256, musigCrypto } from '../crypto/index.js';
+import { keccak256, musigCrypto, SigningKey } from '../crypto/index.js';
 import { Outpoint, UTXO, denominations } from '../transaction/utxo.js';
 import { AllowedCoinType, Shard, toShard, Zone } from '../constants/index.js';
 import { Mnemonic } from './mnemonic.js';
@@ -64,6 +64,7 @@ type DerivationPath = 'BIP44:external' | 'BIP44:change' | string; // string for
  * @extends NeuteredAddressInfo
  */
 export interface QiAddressInfo extends NeuteredAddressInfo {
+    change: boolean;
     status: AddressStatus;
     derivationPath: DerivationPath;
 }
@@ -122,7 +123,7 @@ type AddressUsageCallback = (address: string) => Promise<boolean>;
  * const deserializedWallet = QiHDWallet.deserialize(serializedWallet); // create a new wallet instance from the serialized data
  * ```
  */
-export class QiHDWallet extends AbstractHDWallet {
+export class QiHDWallet extends AbstractHDWallet<QiAddressInfo> {
     /**
      * @ignore
      * @type {number}
@@ -141,6 +142,12 @@ export class QiHDWallet extends AbstractHDWallet {
      */
     protected static _coinType: AllowedCoinType = 969;
 
+    /**
+     * @ignore
+     * @type {string}
+     */
+    private static readonly PRIVATE_KEYS_PATH: string = 'privateKeys' as const;
+
     /**
      * A map containing address information for all addresses known to the wallet. This includes:
      *
@@ -196,6 +203,7 @@ export class QiHDWallet extends AbstractHDWallet {
         super(guard, root, provider);
         this._addressesMap.set('BIP44:external', []);
         this._addressesMap.set('BIP44:change', []);
+        this._addressesMap.set(QiHDWallet.PRIVATE_KEYS_PATH, []);
     }
 
     /**
@@ -224,7 +232,7 @@ export class QiHDWallet extends AbstractHDWallet {
      * @param {QiAddressInfo[]} addresses - The array of QiAddressInfo objects.
      * @returns {number} The last used index.
      */
-    private _findLastUsedIndex(addresses: QiAddressInfo[] | undefined, account: number, zone: Zone): number {
+    protected _findLastUsedIndex(addresses: QiAddressInfo[] | undefined, account: number, zone: Zone): number {
         const filteredAddresses = addresses?.filter(
             (addressInfo) => addressInfo.account === account && addressInfo.zone === zone,
         );
@@ -243,6 +251,14 @@ export class QiHDWallet extends AbstractHDWallet {
         const addresses = this._addressesMap.get(isChange ? 'BIP44:change' : 'BIP44:external') || [];
         const lastIndex = this._findLastUsedIndex(addresses, account, zone);
         const addressNode = this.deriveNextAddressNode(account, lastIndex + 1, zone, isChange);
+
+        const privateKeysArray = this._addressesMap.get(QiHDWallet.PRIVATE_KEYS_PATH) || [];
+        const existingPrivateKeyIndex = privateKeysArray.findIndex((info) => info.address === addressNode.address);
+        if (existingPrivateKeyIndex !== -1) {
+            privateKeysArray.splice(existingPrivateKeyIndex, 1);
+            this._addressesMap.set(QiHDWallet.PRIVATE_KEYS_PATH, privateKeysArray);
+        }
+
         const newAddrInfo = {
             pubKey: addressNode.publicKey,
             address: addressNode.address,
@@ -934,6 +950,11 @@ export class QiHDWallet extends AbstractHDWallet {
             throw new Error(`Address not found: ${address}`);
         }
 
+        // Handle imported private keys
+        if (isHexString(addressInfo.derivationPath, 32)) {
+            return addressInfo.derivationPath;
+        }
+
         if (addressInfo.derivationPath === 'BIP44:external' || addressInfo.derivationPath === 'BIP44:change') {
             // (BIP44 addresses)
             const changeIndex = addressInfo.change ? 1 : 0;
@@ -1231,8 +1252,7 @@ export class QiHDWallet extends AbstractHDWallet {
      * @throws {Error} If the address does not correspond to a valid HD node or if signing fails.
      */
     public async signMessage(address: string, message: string | Uint8Array): Promise<string> {
-        const addrNode = this._getHDNodeForAddress(address);
-        const privKey = addrNode.privateKey;
+        const privKey = this.getPrivateKey(address);
         const digest = keccak256(message);
         const signature = schnorr.sign(digest, getBytes(privKey));
         return hexlify(signature);
@@ -1251,9 +1271,8 @@ export class QiHDWallet extends AbstractHDWallet {
             ...hdwalletSerialized,
             outpoints: this._availableOutpoints,
             pendingOutpoints: this._pendingOutpoints,
-            addresses: Array.from(this._addressesMap.entries()).flatMap(([key, addresses]) =>
-                addresses.map((address) => ({ ...address, derivationPath: key })),
-            ),
+            // eslint-disable-next-line @typescript-eslint/no-unused-vars
+            addresses: Array.from(this._addressesMap.entries()).flatMap(([_, addresses]) => addresses),
             senderPaymentCodeInfo: Object.fromEntries(
                 Array.from(this._paymentCodeSendAddressMap.entries()).map(([key, value]) => [key, Array.from(value)]),
             ),
@@ -1296,8 +1315,10 @@ export class QiHDWallet extends AbstractHDWallet {
         // validate and import all the wallet addresses
         for (const addressInfo of serialized.addresses) {
             validateQiAddressInfo(addressInfo);
-            const key = addressInfo.derivationPath;
-            if (!wallet._addressesMap.has(key)) {
+            let key = addressInfo.derivationPath;
+            if (isHexString(key, 32)) {
+                key = QiHDWallet.PRIVATE_KEYS_PATH;
+            } else if (!key.startsWith('BIP44:')) {
                 wallet._addressesMap.set(key, []);
             }
             wallet._addressesMap.get(key)!.push(addressInfo);
@@ -1530,6 +1551,20 @@ export class QiHDWallet extends AbstractHDWallet {
         return this._addressesMap.has(paymentCode) && this._paymentCodeSendAddressMap.has(paymentCode);
     }
 
+    /**
+     * Gets the address info for a given address.
+     *
+     * @param {string} address - The address.
+     * @returns {QiAddressInfo | null} The address info or null if not found.
+     */
+    public getAddressInfo(address: string): QiAddressInfo | null {
+        const externalAddressInfo = this._addressesMap.get('BIP44:external')?.find((addr) => addr.address === address);
+        if (!externalAddressInfo) {
+            return null;
+        }
+        return externalAddressInfo;
+    }
+
     /**
      * Gets the address info for a given address.
      *
@@ -1543,4 +1578,144 @@ export class QiHDWallet extends AbstractHDWallet {
         }
         return changeAddressInfo;
     }
+
+    /**
+     * Imports a private key and adds it to the wallet.
+     *
+     * @param {string} privateKey - The private key to import (hex string)
+     * @returns {Promise<QiAddressInfo>} The address information for the imported key
+     * @throws {Error} If the private key is invalid or the address is already in use
+     */
+    public async importPrivateKey(privateKey: string): Promise<QiAddressInfo> {
+        if (!isHexString(privateKey, 32)) {
+            throw new Error(`Invalid private key format: must be 32-byte hex string (got ${privateKey})`);
+        }
+
+        const pubKey = SigningKey.computePublicKey(privateKey, true);
+        const address = computeAddress(pubKey);
+
+        // Validate address is for correct zone and ledger
+        const addressZone = getZoneForAddress(address);
+        if (!addressZone) {
+            throw new Error(`Private key does not correspond to a valid address for any zone (got ${address})`);
+        }
+        if (!isQiAddress(address)) {
+            throw new Error(`Private key does not correspond to a valid Qi address (got ${address})`);
+        }
+
+        for (const [path, addresses] of this._addressesMap.entries()) {
+            if (addresses.some((info) => info.address === address)) {
+                throw new Error(`Address ${address} already exists in wallet under path ${path}`);
+            }
+        }
+
+        const addressInfo: QiAddressInfo = {
+            pubKey,
+            address,
+            account: 0,
+            index: -1,
+            change: false,
+            zone: addressZone,
+            status: AddressStatus.UNUSED,
+            derivationPath: privateKey, // Store private key in derivationPath
+        };
+
+        this._addressesMap.get(QiHDWallet.PRIVATE_KEYS_PATH)!.push(addressInfo);
+
+        return addressInfo;
+    }
+
+    /**
+     * Gets all addresses that were imported via private keys.
+     *
+     * @param {Zone} [zone] - Optional zone to filter addresses by
+     * @returns {QiAddressInfo[]} Array of address info objects for imported addresses
+     */
+    public getImportedAddresses(zone?: Zone): QiAddressInfo[] {
+        const importedAddresses = this._addressesMap.get(QiHDWallet.PRIVATE_KEYS_PATH) || [];
+
+        if (zone !== undefined) {
+            this.validateZone(zone);
+            return importedAddresses.filter((info) => info.zone === zone);
+        }
+
+        return [...importedAddresses];
+    }
+
+    /**
+     * Adds a new address to the wallet.
+     *
+     * @param {number} account - The account number.
+     * @param {number} addressIndex - The address index.
+     * @returns {QiAddressInfo} The address info for the new address.
+     */
+    public addAddress(account: number, addressIndex: number): QiAddressInfo {
+        return this._addAddress(account, addressIndex, false);
+    }
+
+    /**
+     * Adds a new change address to the wallet.
+     *
+     * @param {number} account - The account number.
+     * @param {number} addressIndex - The address index.
+     * @returns {QiAddressInfo} The address info for the new address.
+     */
+    public addChangeAddress(account: number, addressIndex: number): QiAddressInfo {
+        return this._addAddress(account, addressIndex, true);
+    }
+
+    private _addAddress(account: number, addressIndex: number, isChange: boolean): QiAddressInfo {
+        const derivationPath = isChange ? 'BIP44:change' : 'BIP44:external';
+
+        const existingAddresses = this._addressesMap.get(derivationPath) || [];
+        if (existingAddresses.some((info) => info.index === addressIndex)) {
+            throw new Error(`Address index ${addressIndex} already exists in wallet under path ${derivationPath}`);
+        }
+
+        const addressNode = this._root
+            .deriveChild(account + HARDENED_OFFSET)
+            .deriveChild(isChange ? 1 : 0)
+            .deriveChild(addressIndex);
+        const zone = getZoneForAddress(addressNode.address);
+        if (!zone) {
+            throw new Error(`Failed to derive a Qi valid address zone for the index ${addressIndex}`);
+        }
+
+        if (!isQiAddress(addressNode.address)) {
+            throw new Error(`Address ${addressNode.address} is not a valid Qi address`);
+        }
+
+        const addressInfo: QiAddressInfo = {
+            pubKey: addressNode.publicKey,
+            address: addressNode.address,
+            account,
+            index: addressIndex,
+            change: isChange,
+            zone,
+            status: AddressStatus.UNUSED,
+            derivationPath,
+        };
+
+        const addresses = this._addressesMap.get(derivationPath);
+        if (!addresses) {
+            this._addressesMap.set(derivationPath, [addressInfo]);
+        } else {
+            addresses.push(addressInfo);
+        }
+
+        return addressInfo;
+    }
+
+    /**
+     * Gets the addresses for a given account.
+     *
+     * @param {number} account - The account number.
+     * @returns {QiAddressInfo[]} The addresses for the account.
+     */
+    public getAddressesForAccount(account: number): QiAddressInfo[] {
+        const addresses = this._addressesMap.values();
+        return Array.from(addresses)
+            .flat()
+            .filter((info) => info.account === account);
+    }
 }
diff --git a/src/wallet/quai-hdwallet.ts b/src/wallet/quai-hdwallet.ts
index eb0067e6..2e742bc7 100644
--- a/src/wallet/quai-hdwallet.ts
+++ b/src/wallet/quai-hdwallet.ts
@@ -1,11 +1,12 @@
 import { AbstractHDWallet, NeuteredAddressInfo, _guard } from './hdwallet.js';
 import { HDNodeWallet } from './hdnodewallet.js';
 import { QuaiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js';
-import { resolveAddress } from '../address/index.js';
-import { AllowedCoinType } from '../constants/index.js';
-import { SerializedHDWallet } from './hdwallet.js';
+import { isQuaiAddress, resolveAddress } from '../address/index.js';
+import { AllowedCoinType, Zone } from '../constants/index.js';
+import { SerializedHDWallet, HARDENED_OFFSET } from './hdwallet.js';
 import { Mnemonic } from './mnemonic.js';
 import { TypedDataDomain, TypedDataField } from '../hash/index.js';
+import { getZoneForAddress } from '../utils/index.js';
 
 export interface SerializedQuaiHDWallet extends SerializedHDWallet {
     addresses: Array<NeuteredAddressInfo>;
@@ -40,7 +41,7 @@ export interface SerializedQuaiHDWallet extends SerializedHDWallet {
  * const deserializedWallet = QuaiHDWallet.deserialize(serializedWallet); // create a new wallet instance from the serialized data
  * ```
  */
-export class QuaiHDWallet extends AbstractHDWallet {
+export class QuaiHDWallet extends AbstractHDWallet<NeuteredAddressInfo> {
     /**
      * The version of the wallet.
      *
@@ -115,7 +116,7 @@ export class QuaiHDWallet extends AbstractHDWallet {
      * This method extends the serialization from the parent class (AbstractHDWallet) and includes additional
      * QuaiHDWallet-specific data, such as the addresses.
      *
-     * @example const wallet = new QuaiHDWallet(); const serializedData = wallet.serialize(); // serializedData can now
+     * @example Const wallet = new QuaiHDWallet(); const serializedData = wallet.serialize(); // serializedData can now
      * be stored or transmitted
      *
      * @returns {SerializedQuaiHDWallet} An object representing the serialized state of the QuaiHDWallet, including
@@ -149,7 +150,7 @@ export class QuaiHDWallet extends AbstractHDWallet {
         const wallet = new this(_guard, root);
 
         // import the addresses
-        wallet.importSerializedAddresses(wallet._addresses, serialized.addresses);
+        wallet.importSerializedAddresses(serialized.addresses);
 
         return wallet;
     }
@@ -174,4 +175,223 @@ export class QuaiHDWallet extends AbstractHDWallet {
         const addrNode = this._getHDNodeForAddress(address);
         return addrNode.signTypedData(domain, types, value);
     }
+
+    /**
+     * Adds an address to the wallet.
+     *
+     * @param {number} account - The account number.
+     * @param {number} addressIndex - The address index.
+     * @returns {NeuteredAddressInfo} The added address info.
+     */
+    public addAddress(account: number, addressIndex: number): NeuteredAddressInfo {
+        return this._addAddress(account, addressIndex) as NeuteredAddressInfo;
+    }
+
+    /**
+     * Helper method to add an address to the wallet address map.
+     *
+     * @param {Map<string, NeuteredAddressInfo>} addressMap - The address map.
+     * @param {number} account - The account number.
+     * @param {number} addressIndex - The address index.
+     * @returns {NeuteredAddressInfo} The added address info.
+     * @throws {Error} If the address for the index already exists.
+     */
+    protected _addAddress(account: number, addressIndex: number): NeuteredAddressInfo {
+        // check if address already exists for the index
+        this._addresses.forEach((addressInfo) => {
+            if (addressInfo.index === addressIndex) {
+                throw new Error(`Address for index ${addressIndex} already exists`);
+            }
+        });
+
+        // derive the address node and validate the zone
+        const changeIndex = 0;
+        const addressNode = this._root
+            .deriveChild(account + HARDENED_OFFSET)
+            .deriveChild(changeIndex)
+            .deriveChild(addressIndex);
+        const zone = getZoneForAddress(addressNode.address);
+        if (!zone) {
+            throw new Error(`Failed to derive a valid address zone for the index ${addressIndex}`);
+        }
+
+        if (!isQuaiAddress(addressNode.address)) {
+            throw new Error(`Address ${addressNode.address} is not a valid Quai address`);
+        }
+
+        return this.createAndStoreAddressInfo(addressNode, account, zone);
+    }
+
+    /**
+     * Imports addresses from a serialized wallet into the addresses map. Before adding the addresses, a validation is
+     * performed to ensure the address, public key, and zone match the expected values.
+     *
+     * @param {Map<string, NeuteredAddressInfo>} addressMap - The map where the addresses will be imported.
+     * @param {NeuteredAddressInfo[]} addresses - The array of addresses to be imported, each containing account, index,
+     *   address, pubKey, and zone information.
+     * @throws {Error} If there is a mismatch between the expected and actual address, public key, or zone.
+     * @protected
+     */
+    protected importSerializedAddresses(addresses: NeuteredAddressInfo[]): void {
+        for (const addressInfo of addresses) {
+            const newAddressInfo = this._addAddress(addressInfo.account, addressInfo.index);
+            // validate the address info
+            if (addressInfo.address !== newAddressInfo.address) {
+                throw new Error(`Address mismatch: ${addressInfo.address} != ${newAddressInfo.address}`);
+            }
+            if (addressInfo.pubKey !== newAddressInfo.pubKey) {
+                throw new Error(`Public key mismatch: ${addressInfo.pubKey} != ${newAddressInfo.pubKey}`);
+            }
+            if (addressInfo.zone !== newAddressInfo.zone) {
+                throw new Error(`Zone mismatch: ${addressInfo.zone} != ${newAddressInfo.zone}`);
+            }
+        }
+    }
+
+    /**
+     * Promise that resolves to the next address for the specified account and zone.
+     *
+     * @param {number} account - The index of the account for which to retrieve the next address.
+     * @param {Zone} zone - The zone in which to retrieve the next address.
+     * @returns {Promise<T>} The next neutered address information.
+     */
+    public async getNextAddress(account: number, zone: Zone): Promise<NeuteredAddressInfo> {
+        return Promise.resolve(this._getNextAddress(account, zone));
+    }
+
+    /**
+     * Synchronously retrieves the next address for the specified account and zone.
+     *
+     * @param {number} account - The index of the account for which to retrieve the next address.
+     * @param {Zone} zone - The zone in which to retrieve the next address.
+     * @returns {T} The next neutered address information.
+     */
+    public getNextAddressSync(account: number, zone: Zone): NeuteredAddressInfo {
+        return this._getNextAddress(account, zone);
+    }
+
+    /**
+     * Derives and returns the next address information for the specified account and zone.
+     *
+     * @param {number} accountIndex - The index of the account for which the address is being generated.
+     * @param {Zone} zone - The zone in which the address is to be used.
+     * @param {Map<string, NeuteredAddressInfo>} addressMap - A map storing the neutered address information.
+     * @returns {T} The derived neutered address information.
+     * @throws {Error} If the zone is invalid.
+     */
+    protected _getNextAddress(accountIndex: number, zone: Zone): NeuteredAddressInfo {
+        this.validateZone(zone);
+        const lastIndex = this._findLastUsedIndex(Array.from(this._addresses.values()), accountIndex, zone);
+        const addressNode = this.deriveNextAddressNode(accountIndex, lastIndex + 1, zone, false);
+        return this.createAndStoreAddressInfo(addressNode, accountIndex, zone);
+    }
+
+    /**
+     * Creates and stores address information in the address map for a specified account, zone, and change type.
+     *
+     * This method constructs a NeuteredAddressInfo object using the provided HDNodeWallet and other parameters, then
+     * stores this information in the provided address map.
+     *
+     * @param {HDNodeWallet} addressNode - The HDNodeWallet object containing the address and public key information.
+     * @param {number} account - The account number to associate with the address.
+     * @param {Zone} zone - The specific zone to associate with the address.
+     * @param {Map<string, NeuteredAddressInfo>} addressMap - The map to store the created NeuteredAddressInfo, with the
+     *   address as the key.
+     * @returns {NeuteredAddressInfo} - The created NeuteredAddressInfo object.
+     * @protected
+     */
+    protected createAndStoreAddressInfo(addressNode: HDNodeWallet, account: number, zone: Zone): NeuteredAddressInfo {
+        const neuteredAddressInfo: NeuteredAddressInfo = {
+            pubKey: addressNode.publicKey,
+            address: addressNode.address,
+            account,
+            index: addressNode.index,
+            zone,
+        };
+
+        this._addresses.set(neuteredAddressInfo.address, neuteredAddressInfo);
+
+        return neuteredAddressInfo;
+    }
+
+    /**
+     * Gets the address info for a given address.
+     *
+     * @param {string} address - The address.
+     * @returns {T | null} The address info or null if not found.
+     */
+    public getAddressInfo(address: string): NeuteredAddressInfo | null {
+        const addressInfo = this._addresses.get(address);
+        if (!addressInfo) {
+            return null;
+        }
+        return addressInfo;
+    }
+
+    /**
+     * Returns the private key for a given address. This method should be used with caution as it exposes the private
+     * key to the user.
+     *
+     * @param {string} address - The address associated with the desired private key.
+     * @returns {string} The private key.
+     */
+    public getPrivateKey(address: string): string {
+        const hdNode = this._getHDNodeForAddress(address);
+        return hdNode.privateKey;
+    }
+
+    /**
+     * Derives and returns the Hierarchical Deterministic (HD) node wallet associated with a given address.
+     *
+     * This method fetches the account and address information from the wallet's internal storage, derives the
+     * appropriate change node based on whether the address is a change address, and further derives the final HD node
+     * using the address index.
+     *
+     * @param {string} addr - The address for which to derive the HD node.
+     * @returns {HDNodeWallet} The derived HD node wallet corresponding to the given address.
+     * @throws {Error} If the given address is not known to the wallet.
+     * @throws {Error} If the account associated with the address is not found.
+     */
+    protected _getHDNodeForAddress(addr: string): HDNodeWallet {
+        const addressInfo = this._addresses.get(addr);
+        if (!addressInfo) {
+            throw new Error(`Address ${addr} is not known to this wallet`);
+        }
+
+        const changeIndex = 0;
+        return this._root
+            .deriveChild(addressInfo.account + HARDENED_OFFSET)
+            .deriveChild(changeIndex)
+            .deriveChild(addressInfo.index);
+    }
+
+    /**
+     * Gets the addresses for a given zone.
+     *
+     * @param {Zone} zone - The zone.
+     * @returns {NeuteredAddressInfo[]} The addresses for the zone.
+     */
+    public getAddressesForZone(zone: Zone): NeuteredAddressInfo[] {
+        this.validateZone(zone);
+        const addresses = this._addresses.values();
+        return Array.from(addresses).filter((addressInfo) => addressInfo.zone === zone);
+    }
+
+    /**
+     * Gets the addresses for a given account.
+     *
+     * @param {number} account - The account number.
+     * @returns {NeuteredAddressInfo[]} The addresses for the account.
+     */
+    public getAddressesForAccount(account: number): NeuteredAddressInfo[] {
+        const addresses = this._addresses.values();
+        return Array.from(addresses).filter((addressInfo) => addressInfo.account === account);
+    }
+
+    protected _findLastUsedIndex(addresses: NeuteredAddressInfo[] | undefined, account: number, zone: Zone): number {
+        const filteredAddresses = addresses?.filter(
+            (addressInfo) => addressInfo.account === account && addressInfo.zone === zone,
+        );
+        return filteredAddresses?.reduce((maxIndex, addressInfo) => Math.max(maxIndex, addressInfo.index), -1) || -1;
+    }
 }
diff --git a/testcases/qi-addresses.json.gz b/testcases/qi-addresses.json.gz
deleted file mode 100644
index 22e43036..00000000
Binary files a/testcases/qi-addresses.json.gz and /dev/null differ
diff --git a/testcases/qi-wallet-import-privkey.json.gz b/testcases/qi-wallet-import-privkey.json.gz
new file mode 100644
index 00000000..4de7f901
Binary files /dev/null and b/testcases/qi-wallet-import-privkey.json.gz differ
diff --git a/testcases/quai-address-derivation.json.gz b/testcases/quai-address-derivation.json.gz
new file mode 100644
index 00000000..84a20dc0
Binary files /dev/null and b/testcases/quai-address-derivation.json.gz differ
diff --git a/testcases/quai-addresses.json.gz b/testcases/quai-addresses.json.gz
deleted file mode 100644
index 3ac8cf8b..00000000
Binary files a/testcases/quai-addresses.json.gz and /dev/null differ