diff --git a/examples/transactions/send-qi-paymentcode.js b/examples/transactions/send-qi-paymentcode.js deleted file mode 100644 index ae7b93fb..00000000 --- a/examples/transactions/send-qi-paymentcode.js +++ /dev/null @@ -1,112 +0,0 @@ -const quais = require('../../lib/commonjs/quais'); -require('dotenv').config(); - -async function main() { - // Create provider - const options = {usePathing: false}; - const provider = new quais.JsonRpcProvider(process.env.RPC_URL, undefined, options); - - // Create wallet and connect to provider - console.log(process.env.RPC_URL) - const aliceMnemonic = quais.Mnemonic.fromPhrase(process.env.MNEMONIC); - const aliceWallet = quais.QiHDWallet.fromMnemonic(aliceMnemonic); - aliceWallet.connect(provider); - - // Get Alice payment code - const alicePaymentCode = aliceWallet.getPaymentCode(0); - console.log("Alice payment code: ", alicePaymentCode); - - // Create Bob wallet - const BOB_MNEMONIC = "innocent perfect bus miss prevent night oval position aspect nut angle usage expose grace juice"; - const bobMnemonic = quais.Mnemonic.fromPhrase(BOB_MNEMONIC); - const bobWallet = quais.QiHDWallet.fromMnemonic(bobMnemonic); - bobWallet.connect(provider); - - // Get Bob payment code - const bobPaymentCode = bobWallet.getPaymentCode(0); - console.log("Bob payment code: ", bobPaymentCode); - - // Open channel - aliceWallet.openChannel(bobPaymentCode); - bobWallet.openChannel(alicePaymentCode); - - // Scan Alice wallet - console.log("...scanning alice wallet"); - await aliceWallet.scan(quais.Zone.Cyprus1); - - // log alice change wallet addresses - console.log("Alice change wallet addresses: ", aliceWallet.getChangeAddressesForZone(quais.Zone.Cyprus1).map(a => a.address)); - // log alice external wallet addresses - console.log("Alice external wallet addresses: ", aliceWallet.getAddressesForZone(quais.Zone.Cyprus1).map(a => a.address)); - - // Scan Bob wallet - console.log("...scanning bob wallet"); - await bobWallet.scan(quais.Zone.Cyprus1); - - // Get Alice initial balance - console.log("...getting alice initial balance"); - const aliceInitialBalance = await aliceWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Alice initial balance: ", aliceInitialBalance); - - // Get Bob initial balance - console.log("...getting bob initial balance"); - const bobInitialBalance = await bobWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Bob initial balance: ", bobInitialBalance); - - // Send Qi - console.log("...sending qi to Bob"); - const amountToSendToBob = 25000; - const tx = await aliceWallet.sendTransaction(bobPaymentCode, amountToSendToBob, quais.Zone.Cyprus1, quais.Zone.Cyprus1); - console.log("... Alice transaction sent. Waiting for receipt..."); - - // Wait for tx to be mined - const txReceipt = await tx.wait(); - console.log("Alice's transaction receipt: ", txReceipt); - - // Sync wallets - console.log("...syncing wallets"); - await aliceWallet.sync(quais.Zone.Cyprus1); - await bobWallet.sync(quais.Zone.Cyprus1); - - // Get Alice updated balance - console.log("...getting alice updated balance"); - const aliceUpdatedBalance = await aliceWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Alice updated balance: ", aliceUpdatedBalance); - - // Get Bob updated balance - console.log("...getting bob updated balance"); - const bobUpdatedBalance = await bobWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Bob updated balance: ", bobUpdatedBalance); - - // Bob sends Qi back to Alice - console.log("...sending qi back to Alice"); - const amountToSendToAlice = 10000; - const tx2 = await bobWallet.sendTransaction(alicePaymentCode, amountToSendToAlice, quais.Zone.Cyprus1, quais.Zone.Cyprus1); - console.log("... Bob sent transaction. Waiting for receipt..."); - - // Wait for tx2 to be mined - const tx2Receipt = await tx2.wait(); - console.log("Bob's transaction receipt: ", tx2Receipt); - - // Sync wallets - await aliceWallet.sync(quais.Zone.Cyprus1); - await bobWallet.sync(quais.Zone.Cyprus1); - - // Get Alice updated balance - console.log("...getting alice updated balance"); - const aliceUpdatedBalance2 = await aliceWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Alice updated balance: ", aliceUpdatedBalance2); - - // Get Bob updated balance - console.log("...getting bob updated balance"); - const bobUpdatedBalance2 = await bobWallet.getBalanceForZone(quais.Zone.Cyprus1); - console.log("Bob updated balance: ", bobUpdatedBalance2); - -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/src/_tests/integration/providerdata.integration.test.ts b/src/_tests/integration/providerdata.integration.test.ts index b303cd3e..174e57d7 100644 --- a/src/_tests/integration/providerdata.integration.test.ts +++ b/src/_tests/integration/providerdata.integration.test.ts @@ -25,8 +25,8 @@ dotenv.config({ path: `.env`, override: false }); // import type { TestBlockchainNetwork } from "./blockchain-data.js"; //setupProviders(); - -const providerC1 = new quais.JsonRpcProvider(process.env.RPC_URL); +const options = { usePathing: false }; +const providerC1 = new quais.JsonRpcProvider(process.env.RPC_URL, undefined, options); const wallet = new quais.Wallet(process.env.CYPRUS1_PRIVKEY_1 || '', providerC1); const destinationC1 = '0x0047f9CEa7662C567188D58640ffC48901cde02a'; const destinationC2 = '0x011ae0a1Bd5B71b4F16F8FdD3AEF278C3D042449'; diff --git a/src/_tests/integration/qi-wallet-roundtrip.integration.test.ts b/src/_tests/integration/qi-wallet-roundtrip.integration.test.ts index 6b3ea005..1a32bc78 100644 --- a/src/_tests/integration/qi-wallet-roundtrip.integration.test.ts +++ b/src/_tests/integration/qi-wallet-roundtrip.integration.test.ts @@ -55,7 +55,8 @@ describe('QiHDWallet Roundtrip Transaction', function () { let bobWallet: QiHDWallet; let alicePaymentCode: string; let bobPaymentCode: string; - const provider = new JsonRpcProvider(process.env.RPC_URL); + const options = { usePathing: false }; + const provider = new JsonRpcProvider(process.env.RPC_URL, undefined, options); for (const test of tests) { this.timeout(1200000); diff --git a/src/_tests/integration/sendquai.integration.test.ts b/src/_tests/integration/sendquai.integration.test.ts index 93658c16..d57db482 100644 --- a/src/_tests/integration/sendquai.integration.test.ts +++ b/src/_tests/integration/sendquai.integration.test.ts @@ -38,7 +38,8 @@ describe('Test sending Quai', function () { before(async () => { const wsUrl = process.env.RPC_URL?.replace('https', 'wss')?.replace('http', 'ws'); - provider = new WebSocketProvider(wsUrl ?? ''); + const options = { usePathing: false }; + provider = new WebSocketProvider(wsUrl ?? '', undefined, options); wallet = new Wallet(process.env.CYPRUS1_PRIVKEY_1!, provider); const senderBalance = await provider.getBalance(wallet.address); // ensure balance is greater than 0.1 QUAI diff --git a/src/_tests/unit/qihdwallet-scan-and-convert-to-quai.unit.test.ts b/src/_tests/unit/qihdwallet-scan-and-convert-to-quai.unit.test.ts new file mode 100644 index 00000000..94e23d2a --- /dev/null +++ b/src/_tests/unit/qihdwallet-scan-and-convert-to-quai.unit.test.ts @@ -0,0 +1,211 @@ +import assert from 'assert'; +import { loadTests } from '../utils.js'; +import { + Mnemonic, + QiHDWallet, + QuaiHDWallet, + Zone, + OutpointInfo, + Block, + QiAddressInfo, + Network, + QiTransaction, + getBytes, + musigCrypto, + hexlify, +} from '../../index.js'; +import { Outpoint } from '../../transaction/utxo.js'; +import { QiPerformActionTransaction } from '../../providers/abstract-provider.js'; +import { MockProvider } from './mockProvider.js'; +import { schnorr } from '@noble/curves/secp256k1'; +import { MuSigFactory } from '@brandonblack/musig'; + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); +}); + +import dotenv from 'dotenv'; +const env = process.env.NODE_ENV || 'development'; +dotenv.config({ path: `.env.${env}` }); +dotenv.config({ path: `.env`, override: false }); + +interface ScanTestCase { + name: string; + mnemonic: string; + amount_to_convert: number; + provider_outpoints: Array<{ + address: string; + outpoints: Array; + }>; + provider_locked_balance: Array<{ + address: string; + balance: number; + }>; + provider_balance: Array<{ + address: string; + balance: number; + }>; + provider_blocks: Array<{ + key: string; + block: Block; + }>; + expected_external_addresses: Array; + expected_change_addresses: Array; + expected_outpoints_info: Array; + expected_balance: number; + provider_estimate_fee_for_qi: Array<{ + input: QiPerformActionTransaction; + output: number; + }>; + provider_network: Network; + provider_broadcast_transaction_receipt: string; + expected_signed_tx: string; +} + +describe('QiHDWallet scan and convert to Quai', async function () { + const tests = loadTests('qi-wallet-scan-and-convert-to-quai'); + + for (const test of tests) { + describe(test.name, async function () { + this.timeout(1200000); + + const mockProvider = new MockProvider(); + + // set the provider outpoints + for (const outpoint of test.provider_outpoints) { + mockProvider.setOutpoints(outpoint.address, outpoint.outpoints); + } + + // set the provider blocks + for (const block of test.provider_blocks) { + mockProvider.setBlock(block.key, block.block); + } + + // set the provider locked balace + for (const lockedBalance of test.provider_locked_balance) { + mockProvider.setLockedBalance(lockedBalance.address, BigInt(lockedBalance.balance)); + } + + // set the provider balance + for (const balance of test.provider_balance) { + mockProvider.setBalance(balance.address, BigInt(balance.balance)); + } + + // set the provider estimate fee for Qi + for (const estimateFeeForQi of test.provider_estimate_fee_for_qi) { + mockProvider.setEstimateFeeForQi(estimateFeeForQi.input, estimateFeeForQi.output); + } + + // set the provider network + mockProvider.setNetwork(test.provider_network); + + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const qiWallet = QiHDWallet.fromMnemonic(mnemonic); + qiWallet.connect(mockProvider); + + const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic); + quaiWallet.connect(mockProvider); + const quaiAddressInfo = quaiWallet.getNextAddressSync(0, Zone.Cyprus1); + + it('it scans Qi wallet with no errors', async function () { + try { + await qiWallet.scan(Zone.Cyprus1); + assert.ok(true, '====> TESTING: scan completed'); + } catch (error) { + console.error('====> TESTING: error: ', error); + assert.fail('====> TESTING: error: ', error); + } + }); + + it('validates expected Qi external addresses', async function () { + const externalAddresses = qiWallet.getAddressesForZone(Zone.Cyprus1); + const sortedExternalAddresses = externalAddresses.sort((a, b) => a.address.localeCompare(b.address)); + const sortedExpectedExternalAddresses = test.expected_external_addresses.sort((a, b) => + a.address.localeCompare(b.address), + ); + assert.deepEqual(sortedExternalAddresses, sortedExpectedExternalAddresses); + }); + + it('validates expected Alice change addresses', async function () { + const changeAddresses = qiWallet.getChangeAddressesForZone(Zone.Cyprus1); + const sortedChangeAddresses = changeAddresses.sort((a, b) => a.address.localeCompare(b.address)); + const sortedExpectedChangeAddresses = test.expected_change_addresses.sort((a, b) => + a.address.localeCompare(b.address), + ); + assert.deepEqual(sortedChangeAddresses, sortedExpectedChangeAddresses); + }); + + it('validates wallet balance', async function () { + const balance = await qiWallet.getBalanceForZone(Zone.Cyprus1); + assert.equal(balance.toString(), test.expected_balance.toString()); + }); + + it('validates expected outpoints info', async function () { + assert.deepEqual(qiWallet.getOutpoints(Zone.Cyprus1), test.expected_outpoints_info); + }); + + it('sends transaction to convert Qi to Quai', async function () { + const aliceToBobTx = await qiWallet.convertToQuai( + quaiAddressInfo.address, + BigInt(test.amount_to_convert), + ); + assert.ok(aliceToBobTx); + }); + + it('validate signed transaction', function () { + const signedTransaction = mockProvider.getSignedTransaction(); + const expectedSignedTx = test.expected_signed_tx; + + const tx = QiTransaction.from(signedTransaction); + const expectedTx = QiTransaction.from(expectedSignedTx); + + // compare everyhing but the hash and signature + assert.deepEqual(tx.txInputs, expectedTx.txInputs); + assert.deepEqual(tx.txOutputs, expectedTx.txOutputs); + assert.deepEqual(tx.type, expectedTx.type); + assert.deepEqual(tx.chainId, expectedTx.chainId); + + console.log(`\n ℹī¸ Transaction has ${tx.txInputs.length} input(s)`); + let valid: boolean; + if (tx.txInputs.length === 1) { + console.log(' 🔑 Validating Single-Key Schnorr Signature'); + valid = validateSchnorrSignature(tx); + } else { + console.log(' 🔑 Validating Multi-Key đŸ‘Ĩ MuSig Signature'); + console.log(` Number of inputs: ${tx.txInputs.length}`); + valid = validateMuSigSignature(tx); + } + assert.ok(valid); + }); + }); + } +}); + +function validateSchnorrSignature(tx: QiTransaction): boolean { + const digest = tx.digest; + const signature = tx.signature; + const pubkey = tx.txInputs[0].pubkey; + + const pubkeyBytes = getBytes('0x' + pubkey.slice(4)); + const signatureBytes = getBytes(signature); + const hashBytes = getBytes(digest); + + return schnorr.verify(signatureBytes, hashBytes, pubkeyBytes); +} + +function validateMuSigSignature(tx: QiTransaction): boolean { + const musig = MuSigFactory(musigCrypto); + const pubkeys = tx.txInputs.map((input) => getBytes(input.pubkey)); + const aggPublicKeyObj = musig.keyAgg(pubkeys); + + const aggPublicKey = hexlify(aggPublicKeyObj.aggPublicKey); + const compressedPubKey = aggPublicKey.slice(0, -64); + const pubkey = '0x' + compressedPubKey.slice(4); + + const signatureBytes = getBytes(tx.signature); + const hashBytes = getBytes(tx.digest); + const pubkeyBytes = getBytes(pubkey); + + return schnorr.verify(signatureBytes, hashBytes, pubkeyBytes); +} diff --git a/src/_tests/unit/qihdwallet-scan-and-send.unit.test.ts b/src/_tests/unit/qihdwallet-scan-and-send.unit.test.ts index 43750565..ff3401f6 100644 --- a/src/_tests/unit/qihdwallet-scan-and-send.unit.test.ts +++ b/src/_tests/unit/qihdwallet-scan-and-send.unit.test.ts @@ -10,11 +10,14 @@ import { Network, QiTransaction, getBytes, + musigCrypto, + hexlify, } from '../../index.js'; import { Outpoint } from '../../transaction/utxo.js'; import { QiPerformActionTransaction } from '../../providers/abstract-provider.js'; import { MockProvider } from './mockProvider.js'; import { schnorr } from '@noble/curves/secp256k1'; +import { MuSigFactory } from '@brandonblack/musig'; process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); @@ -64,116 +67,128 @@ describe('QiHDWallet scan and send transaction', async function () { const tests = loadTests('qi-wallet-scan-and-send'); for (const test of tests) { - this.timeout(1200000); - - const mockProvider = new MockProvider(); - - // set the provider outpoints - for (const outpoint of test.provider_outpoints) { - mockProvider.setOutpoints(outpoint.address, outpoint.outpoints); - } - - // set the provider blocks - for (const block of test.provider_blocks) { - mockProvider.setBlock(block.key, block.block); - } - - // set the provider locked balace - for (const lockedBalance of test.provider_locked_balance) { - mockProvider.setLockedBalance(lockedBalance.address, BigInt(lockedBalance.balance)); - } - - // set the provider balance - for (const balance of test.provider_balance) { - mockProvider.setBalance(balance.address, BigInt(balance.balance)); - } - - // set the provider estimate fee for Qi - for (const estimateFeeForQi of test.provider_estimate_fee_for_qi) { - mockProvider.setEstimateFeeForQi(estimateFeeForQi.input, estimateFeeForQi.output); - } - - // set the provider network - mockProvider.setNetwork(test.provider_network); - - const mnemonic = Mnemonic.fromPhrase(test.mnemonic); - const aliceWallet = QiHDWallet.fromMnemonic(mnemonic); - aliceWallet.connect(mockProvider); - - const bobMnemonic = Mnemonic.fromPhrase(test.bob_mnemonic); - const bobWallet = QiHDWallet.fromMnemonic(bobMnemonic); - - const alicePaymentCode = aliceWallet.getPaymentCode(0); - const bobPaymentCode = bobWallet.getPaymentCode(0); - aliceWallet.openChannel(bobPaymentCode); - bobWallet.openChannel(alicePaymentCode); - - it('it scans Alice wallet with no errors', async function () { - try { - await aliceWallet.scan(Zone.Cyprus1); - assert.ok(true, '====> TESTING: scan completed'); - } catch (error) { - console.error('====> TESTING: error: ', error); - assert.fail('====> TESTING: error: ', error); - } - }); - it('validates expected Alice external addresses', async function () { - const externalAddresses = aliceWallet.getAddressesForZone(Zone.Cyprus1); - const sortedExternalAddresses = externalAddresses.sort((a, b) => a.address.localeCompare(b.address)); - const sortedExpectedExternalAddresses = test.expected_external_addresses.sort((a, b) => - a.address.localeCompare(b.address), - ); - assert.deepEqual(sortedExternalAddresses, sortedExpectedExternalAddresses); - }); - - it('validates expected Alice change addresses', async function () { - const changeAddresses = aliceWallet.getChangeAddressesForZone(Zone.Cyprus1); - const sortedChangeAddresses = changeAddresses.sort((a, b) => a.address.localeCompare(b.address)); - const sortedExpectedChangeAddresses = test.expected_change_addresses.sort((a, b) => - a.address.localeCompare(b.address), - ); - assert.deepEqual(sortedChangeAddresses, sortedExpectedChangeAddresses); - }); + describe(test.name, async function () { + this.timeout(1200000); - it('validates wallet balance', async function () { - const balance = await aliceWallet.getBalanceForZone(Zone.Cyprus1); - assert.equal(balance.toString(), test.expected_balance.toString()); - }); + const mockProvider = new MockProvider(); - it('validates expected outpoints info', async function () { - assert.deepEqual(aliceWallet.getOutpoints(Zone.Cyprus1), test.expected_outpoints_info); - }); + // set the provider outpoints + for (const outpoint of test.provider_outpoints) { + mockProvider.setOutpoints(outpoint.address, outpoint.outpoints); + } - it('sends transaction', async function () { - const aliceToBobTx = await aliceWallet.sendTransaction( - bobPaymentCode, - BigInt(test.amount_to_send_to_bob), - Zone.Cyprus1, - Zone.Cyprus1, - ); - assert.ok(aliceToBobTx); - }); + // set the provider blocks + for (const block of test.provider_blocks) { + mockProvider.setBlock(block.key, block.block); + } - it('validate signed transaction', function () { - const signedTransaction = mockProvider.getSignedTransaction(); - const expectedSignedTx = test.expected_signed_tx; + // set the provider locked balace + for (const lockedBalance of test.provider_locked_balance) { + mockProvider.setLockedBalance(lockedBalance.address, BigInt(lockedBalance.balance)); + } - const tx = QiTransaction.from(signedTransaction); - const expectedTx = QiTransaction.from(expectedSignedTx); + // set the provider balance + for (const balance of test.provider_balance) { + mockProvider.setBalance(balance.address, BigInt(balance.balance)); + } - // compare everyhing but the hash and signature - assert.deepEqual(tx.txInputs, expectedTx.txInputs); - assert.deepEqual(tx.txOutputs, expectedTx.txOutputs); - assert.deepEqual(tx.type, expectedTx.type); - assert.deepEqual(tx.chainId, expectedTx.chainId); + // set the provider estimate fee for Qi + for (const estimateFeeForQi of test.provider_estimate_fee_for_qi) { + mockProvider.setEstimateFeeForQi(estimateFeeForQi.input, estimateFeeForQi.output); + } - const valid = validateSignature(tx); - assert.ok(valid); + // set the provider network + mockProvider.setNetwork(test.provider_network); + + const mnemonic = Mnemonic.fromPhrase(test.mnemonic); + const aliceWallet = QiHDWallet.fromMnemonic(mnemonic); + aliceWallet.connect(mockProvider); + + const bobMnemonic = Mnemonic.fromPhrase(test.bob_mnemonic); + const bobWallet = QiHDWallet.fromMnemonic(bobMnemonic); + + const alicePaymentCode = aliceWallet.getPaymentCode(0); + const bobPaymentCode = bobWallet.getPaymentCode(0); + aliceWallet.openChannel(bobPaymentCode); + bobWallet.openChannel(alicePaymentCode); + + it('it scans Alice wallet with no errors', async function () { + try { + await aliceWallet.scan(Zone.Cyprus1); + assert.ok(true, '====> TESTING: scan completed'); + } catch (error) { + console.error('====> TESTING: error: ', error); + assert.fail('====> TESTING: error: ', error); + } + }); + + it('validates expected Alice external addresses', async function () { + const externalAddresses = aliceWallet.getAddressesForZone(Zone.Cyprus1); + const sortedExternalAddresses = externalAddresses.sort((a, b) => a.address.localeCompare(b.address)); + const sortedExpectedExternalAddresses = test.expected_external_addresses.sort((a, b) => + a.address.localeCompare(b.address), + ); + assert.deepEqual(sortedExternalAddresses, sortedExpectedExternalAddresses); + }); + + it('validates expected Alice change addresses', async function () { + const changeAddresses = aliceWallet.getChangeAddressesForZone(Zone.Cyprus1); + const sortedChangeAddresses = changeAddresses.sort((a, b) => a.address.localeCompare(b.address)); + const sortedExpectedChangeAddresses = test.expected_change_addresses.sort((a, b) => + a.address.localeCompare(b.address), + ); + assert.deepEqual(sortedChangeAddresses, sortedExpectedChangeAddresses); + }); + + it('validates wallet balance', async function () { + const balance = await aliceWallet.getBalanceForZone(Zone.Cyprus1); + assert.equal(balance.toString(), test.expected_balance.toString()); + }); + + it('validates expected outpoints info', async function () { + assert.deepEqual(aliceWallet.getOutpoints(Zone.Cyprus1), test.expected_outpoints_info); + }); + + it('sends transaction', async function () { + const aliceToBobTx = await aliceWallet.sendTransaction( + bobPaymentCode, + BigInt(test.amount_to_send_to_bob), + Zone.Cyprus1, + Zone.Cyprus1, + ); + assert.ok(aliceToBobTx); + }); + + it('validate signed transaction', function () { + const signedTransaction = mockProvider.getSignedTransaction(); + const expectedSignedTx = test.expected_signed_tx; + + const tx = QiTransaction.from(signedTransaction); + const expectedTx = QiTransaction.from(expectedSignedTx); + + // compare everyhing but the hash and signature + assert.deepEqual(tx.txInputs, expectedTx.txInputs); + assert.deepEqual(tx.txOutputs, expectedTx.txOutputs); + assert.deepEqual(tx.type, expectedTx.type); + assert.deepEqual(tx.chainId, expectedTx.chainId); + + console.log(`\n ℹī¸ Transaction has ${tx.txInputs.length} input(s)`); + let valid: boolean; + if (tx.txInputs.length === 1) { + console.log(' 🔑 Validating Single-Key Schnorr Signature'); + valid = validateSchnorrSignature(tx); + } else { + console.log(' 🔑 Validating Multi-Key đŸ‘Ĩ MuSig Signature'); + console.log(` Number of inputs: ${tx.txInputs.length}`); + valid = validateMuSigSignature(tx); + } + assert.ok(valid); + }); }); } }); -function validateSignature(tx: QiTransaction): boolean { +function validateSchnorrSignature(tx: QiTransaction): boolean { const digest = tx.digest; const signature = tx.signature; const pubkey = tx.txInputs[0].pubkey; @@ -184,3 +199,19 @@ function validateSignature(tx: QiTransaction): boolean { return schnorr.verify(signatureBytes, hashBytes, pubkeyBytes); } + +function validateMuSigSignature(tx: QiTransaction): boolean { + const musig = MuSigFactory(musigCrypto); + const pubkeys = tx.txInputs.map((input) => getBytes(input.pubkey)); + const aggPublicKeyObj = musig.keyAgg(pubkeys); + + const aggPublicKey = hexlify(aggPublicKeyObj.aggPublicKey); + const compressedPubKey = aggPublicKey.slice(0, -64); + const pubkey = '0x' + compressedPubKey.slice(4); + + const signatureBytes = getBytes(tx.signature); + const hashBytes = getBytes(tx.digest); + const pubkeyBytes = getBytes(pubkey); + + return schnorr.verify(signatureBytes, hashBytes, pubkeyBytes); +} diff --git a/src/_tests/unit/qihdwallet-scan.unit.ts b/src/_tests/unit/qihdwallet-scan.unit.ts deleted file mode 100644 index cc7b9f47..00000000 --- a/src/_tests/unit/qihdwallet-scan.unit.ts +++ /dev/null @@ -1,109 +0,0 @@ -import assert from 'assert'; -import { loadTests } from '../utils.js'; -import { Mnemonic, QiHDWallet, Zone, OutpointInfo, Block, QiAddressInfo } from '../../index.js'; -import { Outpoint } from '../../transaction/utxo.js'; -import { MockProvider } from './mockProvider.js'; - -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); -}); - -import dotenv from 'dotenv'; -const env = process.env.NODE_ENV || 'development'; -dotenv.config({ path: `.env.${env}` }); -dotenv.config({ path: `.env`, override: false }); - -interface ScanTestCase { - name: string; - nemonic: string; - provider_outpoints: Array<{ - address: string; - outpoints: Array; - }>; - provider_locked_balance: Array<{ - address: string; - balance: number; - }>; - provider_balance: Array<{ - address: string; - balance: number; - }>; - provider_blocks: Array<{ - key: string; - block: Block; - }>; - expected_external_addresses: Array; - expected_change_addresses: Array; - expected_outpoints_info: Array; - expected_balance: number; -} - -describe('QiHDWallet scan', async function () { - const tests = loadTests('qi-wallet-scan'); - - for (const test of tests) { - this.timeout(1200000); - - const mockProvider = new MockProvider(); - - // set the provider outpoints - for (const outpoint of test.provider_outpoints) { - mockProvider.setOutpoints(outpoint.address, outpoint.outpoints); - } - - // set the provider blocks - for (const block of test.provider_blocks) { - mockProvider.setBlock(block.key, block.block); - } - - // set the provider locked balace - for (const lockedBalance of test.provider_locked_balance) { - mockProvider.setLockedBalance(lockedBalance.address, BigInt(lockedBalance.balance)); - } - - // set the provider balance - for (const balance of test.provider_balance) { - mockProvider.setBalance(balance.address, BigInt(balance.balance)); - } - - const mnemonic = Mnemonic.fromPhrase(test.nemonic); - const wallet = QiHDWallet.fromMnemonic(mnemonic); - wallet.connect(mockProvider); - it('it scans the wallet with no errors', async function () { - try { - await wallet.scan(Zone.Cyprus1); - assert.ok(true, '====> TESTING: scan completed'); - } catch (error) { - console.error('====> TESTING: error: ', error); - assert.fail('====> TESTING: error: ', error); - } - }); - it('validates expected external addresses', async function () { - const externalAddresses = wallet.getAddressesForZone(Zone.Cyprus1); - const sortedExternalAddresses = externalAddresses.sort((a, b) => a.address.localeCompare(b.address)); - const sortedExpectedExternalAddresses = test.expected_external_addresses.sort((a, b) => - a.address.localeCompare(b.address), - ); - assert.deepEqual(sortedExternalAddresses, sortedExpectedExternalAddresses); - }); - - it('validates expected change addresses', async function () { - const changeAddresses = wallet.getChangeAddressesForZone(Zone.Cyprus1); - const sortedChangeAddresses = changeAddresses.sort((a, b) => a.address.localeCompare(b.address)); - const sortedExpectedChangeAddresses = test.expected_change_addresses.sort((a, b) => - a.address.localeCompare(b.address), - ); - assert.deepEqual(sortedChangeAddresses, sortedExpectedChangeAddresses); - }); - - it('validates wallet balance', async function () { - const balance = await wallet.getBalanceForZone(Zone.Cyprus1); - assert.equal(balance.toString(), test.expected_balance.toString()); - }); - - it('validates expected outpoints info', async function () { - assert.deepEqual(wallet.getOutpoints(Zone.Cyprus1), test.expected_outpoints_info); - }); - } -}); diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index fa6a01a1..762847cc 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -694,7 +694,7 @@ export class QiHDWallet extends AbstractHDWallet { const fee = BigInt(1000); // temporary hardcode fee to 1 Qi const selection = aggregateCoinSelector.performSelection({ fee, maxDenomination: 6, includeLocked: false }); - const sendAddressesInfo = this._getUnusedBIP44Addresses(1, 0, 'BIP44:external', zone); + const sendAddressesInfo = this.getUnusedBIP44Addresses(1, 0, 'BIP44:external', zone); const sendAddresses = sendAddressesInfo.map((addressInfo) => addressInfo.address); const changeAddresses: string[] = []; const inputPubKeys = selection.inputs.map((input) => { @@ -773,50 +773,8 @@ export class QiHDWallet extends AbstractHDWallet { // 3. Generate as many unused addresses as required to populate the spend outputs const sendAddresses = await getDestinationAddresses(selection.spendOutputs.length); - const getChangeAddressesForOutputs = async (count: number): Promise => { - const currentChangeAddresses = this._addressesMap.get('BIP44:change') || []; - const outputChangeAddresses: QiAddressInfo[] = []; - - for (let i = 0; i < currentChangeAddresses.length; i++) { - if (currentChangeAddresses[i].status === AddressStatus.UNUSED) { - outputChangeAddresses.push(currentChangeAddresses[i]); - } - - if (outputChangeAddresses.length === count) break; - } - - // Generate the remaining number of change addresses if needed - const remainingAddressesNeeded = count - outputChangeAddresses.length; - if (remainingAddressesNeeded > 0) { - outputChangeAddresses.push( - ...Array(remainingAddressesNeeded) - .fill(0) - .map(() => this.getNextChangeAddressSync(0, originZone)), - ); - } - - // Combine the existing change addresses with the newly generated addresses and ensure they are unique and sorted by index - const mergedChangeAddresses = [ - // Not updated last synced block because we are not certain of the success of the transaction - // so we will want to get deltas from last **checked** block - ...outputChangeAddresses.map((address) => ({ - ...address, - status: AddressStatus.ATTEMPTED_USE, - })), - ...currentChangeAddresses, - ]; - const sortedAndFilteredChangeAddresses = mergedChangeAddresses - .filter((address, index, self) => self.findIndex((t) => t.address === address.address) === index) - .sort((a, b) => a.index - b.index); - - // Update the _addressesMap with the modified change addresses and statuses - this._addressesMap.set('BIP44:change', sortedAndFilteredChangeAddresses); - - return outputChangeAddresses.map((address) => address.address); - }; - // 4. Get change addresses - const changeAddresses = await getChangeAddressesForOutputs(selection.changeOutputs.length); + const changeAddresses = await this.getChangeAddressesForOutputs(selection.changeOutputs.length, originZone); // 5. Create the transaction and sign it using the signTransaction method let inputPubKeys = selection.inputs.map((input) => this.locateAddressInfo(input.address)?.pubKey); @@ -843,7 +801,7 @@ export class QiHDWallet extends AbstractHDWallet { const changeAddressesNeeded = selection.changeOutputs.length - changeAddresses.length; if (changeAddressesNeeded > 0) { // Need more change addresses - const newChangeAddresses = await getChangeAddressesForOutputs(changeAddressesNeeded); + const newChangeAddresses = await this.getChangeAddressesForOutputs(changeAddressesNeeded, originZone); changeAddresses.push(...newChangeAddresses); } else if (changeAddressesNeeded < 0) { // Have extra change addresses, remove the excess @@ -990,6 +948,37 @@ export class QiHDWallet extends AbstractHDWallet { }; } + /** + * Gets a set of unused change addresses for transaction outputs and updates their status in the wallet. This method + * retrieves unused BIP44 change addresses, marks them as attempted use, and maintains the wallet's address mapping + * state. + * + * @private + * @param {number} count - The number of change addresses needed + * @param {Zone} zone - The zone to get change addresses from + * @param {number} [account=0] - The account index to use (defaults to 0). Default is `0` + * @returns {Promise} A promise that resolves to an array of change addresses + */ + private async getChangeAddressesForOutputs(count: number, zone: Zone, account: number = 0): Promise { + // Get unused change addresses using existing helper + const unusedAddresses = this.getUnusedBIP44Addresses(count, account, 'BIP44:change', zone); + + // Update address statuses in wallet + const currentAddresses = this._addressesMap.get('BIP44:change') || []; + const updatedAddresses = [ + // Mark selected addresses as attempted use + ...unusedAddresses.map((addr) => ({ ...addr, status: AddressStatus.ATTEMPTED_USE })), + // Keep other existing addresses unchanged + ...currentAddresses.filter((addr) => !unusedAddresses.some((unused) => unused.address === addr.address)), + ].sort((a, b) => a.index - b.index); + + // Update wallet's address map + this._addressesMap.set('BIP44:change', updatedAddresses); + + // Return just the addresses + return unusedAddresses.map((addr) => addr.address); + } + /** * Gets a set of unused BIP44 addresses from the specified derivation path. It first checks if there are any unused * addresses available in the _addressesMap and uses those if possible. If there are not enough unused addresses, it @@ -1000,7 +989,7 @@ export class QiHDWallet extends AbstractHDWallet { * @param zone - The zone to get addresses from. * @returns An array of addresses. */ - private _getUnusedBIP44Addresses( + private getUnusedBIP44Addresses( amount: number, account: number, path: DerivationPath, diff --git a/testcases/qi-wallet-scan-and-convert-to-quai.json.gz b/testcases/qi-wallet-scan-and-convert-to-quai.json.gz new file mode 100644 index 00000000..c6522c65 Binary files /dev/null and b/testcases/qi-wallet-scan-and-convert-to-quai.json.gz differ diff --git a/testcases/qi-wallet-scan-and-send.json.gz b/testcases/qi-wallet-scan-and-send.json.gz index 43d174b1..3d8bd555 100644 Binary files a/testcases/qi-wallet-scan-and-send.json.gz and b/testcases/qi-wallet-scan-and-send.json.gz differ