Skip to content

Commit

Permalink
add unit test for qiwallet sendTransaction() method
Browse files Browse the repository at this point in the history
  • Loading branch information
alejoacosta74 authored and rileystephens28 committed Jan 21, 2025
1 parent 51441d4 commit 378f2d1
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 9 deletions.
56 changes: 47 additions & 9 deletions src/_tests/unit/mockProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export class MockProvider implements Provider {
private _balances: Map<string, bigint> = new Map();
private _lockedBalances: Map<string, bigint> = new Map();
private _outpoints: Map<string, Array<Outpoint>> = new Map();
private _estimateFeeForQi: Map<string, number> = new Map();
private _signedTransaction: string = '';
private _eventHandlers: Map<
string,
Array<{
Expand All @@ -47,11 +49,31 @@ export class MockProvider implements Provider {
if (config?.blockNumber) this._blockNumber = config.blockNumber;
}

// Helper methods to set up test state
// Helper methods to set the balanace of an address
public setBalance(address: string, balance: bigint): void {
this._balances.set(address.toLowerCase(), balance);
}

// mock getBalance method
async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
return this._balances.get(address.toString().toLowerCase()) ?? BigInt(0);
}

// helper method to set the estimated Qi fee for a transaction in the mock provider
public setEstimateFeeForQi(input: QiPerformActionTransaction, output: number): void {
const key = JSON.stringify(input, bigIntSerializer);
this._estimateFeeForQi.set(key, output);
}

// mock estimateFeeForQi method
async estimateFeeForQi(input: QiPerformActionTransaction): Promise<bigint> {
const key = JSON.stringify(input, bigIntSerializer);

const fee = this._estimateFeeForQi.get(key) ?? 0;
return BigInt(fee);
}

// Helper methods to set the transaction to be used by the mock provider
public setTransaction(hash: string, tx: TransactionResponse): void {
this._transactions.set(hash.toLowerCase(), tx);
}
Expand All @@ -60,11 +82,17 @@ export class MockProvider implements Provider {
this._outpoints.set(address.toLowerCase(), outpoints);
}

// Implementation of Provider interface methods
// Helper methods to set the network to be used by the mock provider
public setNetwork(network: Network): void {
this._network = network;
}

// mock getNetwork method
async getNetwork(): Promise<Network> {
return this._network;
}

// Helper methods to set the block number to be used by the mock provider
public setBlock(key: string, block: Block): void {
this._blocks.set(key, block);
}
Expand All @@ -81,11 +109,13 @@ export class MockProvider implements Provider {
return this._blockNumber;
}

async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
return this._balances.get(address.toString().toLowerCase()) ?? BigInt(0);
// helper method to inspect the signed transaction sent to the mock provider
public getSignedTransaction(): string {
return this._signedTransaction;
}

async broadcastTransaction(zone: Zone, signedTx: string, from?: AddressLike): Promise<TransactionResponse> {
this._signedTransaction = signedTx;
// Simulate transaction broadcast
const type = decodeProtoTransaction(getBytes(signedTx)).type;

Expand All @@ -109,11 +139,6 @@ export class MockProvider implements Provider {
return this._outpoints.get(address.toString().toLowerCase()) ?? [];
}

async estimateFeeForQi(tx: QiPerformActionTransaction): Promise<bigint> {
// Return a mock fee for testing
return BigInt(1000);
}

async on(event: ProviderEvent, listener: Listener, zone?: Zone): Promise<this> {
const eventKey = this._getEventKey(event, zone);
if (!this._eventHandlers.has(eventKey)) {
Expand Down Expand Up @@ -307,3 +332,16 @@ export class MockProvider implements Provider {
throw new Error('getLatestQuaiRate: Method not implemented.');
}
}

function bigIntSerializer(key: string, value: any): any {
// Handle null/undefined explicitly
if (value === null || value === undefined) {
return value;
}
// Handle bigint values
if (typeof value === 'bigint') {
return value.toString();
}
// Return other values unchanged
return value;
}
186 changes: 186 additions & 0 deletions src/_tests/unit/qihdwallet-scan-and-send.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import assert from 'assert';
import { loadTests } from '../utils.js';
import {
Mnemonic,
QiHDWallet,
Zone,
OutpointInfo,
Block,
QiAddressInfo,
Network,
QiTransaction,
getBytes,
} 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';

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;
bob_mnemonic: string;
amount_to_send_to_bob: number;
provider_outpoints: Array<{
address: string;
outpoints: Array<Outpoint>;
}>;
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<QiAddressInfo>;
expected_change_addresses: Array<QiAddressInfo>;
expected_outpoints_info: Array<OutpointInfo>;
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 send transaction', async function () {
const tests = loadTests<ScanTestCase>('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);
});

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);

const valid = validateSignature(tx);
assert.ok(valid);
});
}
});

function validateSignature(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);
}
Binary file added testcases/qi-wallet-scan-and-send.json.gz
Binary file not shown.

0 comments on commit 378f2d1

Please sign in to comment.