Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add unit test for qiwallet scan() method (for 2 outpoints) #381

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading
Loading