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 tests for QiHDWallet #248

Merged
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
168 changes: 168 additions & 0 deletions src/_tests/test-qi-hdwallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import assert from 'assert';

import { loadTests, convertToZone } 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,
TestCaseQiSerialization,
AddressInfo,
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.getNextAddress(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.getNextChangeAddress(params.account, params.zone);
assert.deepEqual(addrInfo, expectedAddress);
}
});
}
});

describe('QiHDWallet: Test serialization and deserialization of QiHDWallet', function () {
this.timeout(10000);
const tests = loadTests<TestCaseQiSerialization>('qi-serialization');
for (const test of tests) {
const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
const qiWallet = QiHDWallet.fromMnemonic(mnemonic);
let serialized: any;
it(`tests serialization QuaiHDWallet: ${test.name}`, async function () {
for (const param of test.params) {
qiWallet.getNextAddress(param.account, param.zone);
qiWallet.getNextChangeAddress(param.account, param.zone);
}
qiWallet.importOutpoints(test.outpoints);
serialized = qiWallet.serialize();
assert.deepEqual(serialized, test.serialized);
});

it(`tests deserialization QiHDWallet: ${test.name}`, async function () {
const deserialized = await QiHDWallet.deserialize(serialized);
assert.deepEqual(deserialized.serialize(), serialized);
});
}
});

describe('QiHDWallet: Test transaction signing', function () {
const tests = loadTests<TestCaseQiTransaction>('qi-transaction');
for (const test of tests) {
it(`tests signing a Qi transaction: ${test.name}`, async function () {
const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
const qiWallet = QiHDWallet.fromMnemonic(mnemonic);
for (const param of test.params) {
qiWallet.getNextAddress(param.account, param.zone);
}
qiWallet.importOutpoints(test.outpoints);
const qiTx = createQiTransaction(
test.transaction.chainId,
test.transaction.txInputs,
test.transaction.txOutputs,
);
const digest = keccak_256(qiTx.unsignedSerialized);
const signedSerialized = await qiWallet.signTransaction(qiTx);

const signedTx = QiTransaction.from(signedSerialized);
const signature = signedTx.signature;
let verified = false;
if (signedTx.txInputs.length > 1) {
const publicKeysArray = signedTx.txInputs.map((txInput) => txInput.pubkey);
verified = verifyMusigSignature(signature, digest, publicKeysArray);
} else {
const pubkey = signedTx.txInputs[0].pubkey;
verified = verifySchnorrSignature(signature, digest, pubkey);
}
assert.equal(verified, true);
});
}
});

describe('QiHDWallet: Test sign personal menssage', function () {
const tests = loadTests<TestCaseQiSignMessage>('qi-sign-message');
for (const test of tests) {
it(`tests signing personal message: ${test.name}`, async function () {
const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
const qiWallet = QiHDWallet.fromMnemonic(mnemonic);
const addrInfo = qiWallet.getNextAddress(0, Zone.Cyprus1);
const signature = await qiWallet.signMessage(addrInfo.address, test.message);
const digest = keccak_256(test.message);
const verified = verifySchnorrSignature(signature, digest, addrInfo.pubKey);
assert.equal(verified, true);
});
}
});

function createQiTransaction(chainId: number, inputs: TxInput[], outputs: TxOutput[]): QiTransaction {
const tx = new QiTransaction();
tx.chainId = chainId;
tx.txInputs = inputs;
tx.txOutputs = outputs;
return tx;
}

function verifySchnorrSignature(signature: string, digest: Uint8Array, pubkey: string): boolean {
pubkey = '0x' + pubkey.slice(4);
return schnorr.verify(getBytes(signature), digest, getBytes(pubkey));
}

function verifyMusigSignature(signature: string, digest: Uint8Array, publicKeysArray: string[]) {
const musig = MuSigFactory(musigCrypto);

const pubkeysUintArray = publicKeysArray.map((pubkey) => getBytes(pubkey));

const aggPublicKeyObj = musig.keyAgg(pubkeysUintArray);

const aggPublicKey = hexlify(aggPublicKeyObj.aggPublicKey);

// remove the last 32 bytes (64 hex) from the aggPublicKey
const compressedPubKey = aggPublicKey.slice(0, -64);

return verifySchnorrSignature(signature, digest, compressedPubKey);
}
80 changes: 36 additions & 44 deletions src/_tests/test-quai-hdwallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TestCaseQuaiSerialization,
TestCaseQuaiAddresses,
TestCaseQuaiTypedData,
addressInfo,
AddressInfo,
Zone,
TestCaseQuaiMessageSign,
} from './types.js';
Expand All @@ -21,45 +21,40 @@ describe('Test address generation and retrieval', function () {
for (const test of tests) {
const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
it(`tests method 'getNextAddress()': ${test.name}`, function () {
const newAddresses: addressInfo[] = [];
for (const param of test.params) {
const addrInfo = quaiWallet.getNextAddress(param.account, param.zone);
newAddresses.push(addrInfo);
}
assert.deepEqual(newAddresses, test.expectedAddresses);
});
it(`tests method 'getAddressInfo()': ${test.name}`, function () {
for (const addrInfo of test.expectedAddresses) {
const retrievedAddrInfo = quaiWallet.getAddressInfo(addrInfo.address);
assert.deepEqual(retrievedAddrInfo, addrInfo);
}
});
it(`tests method 'getAddressesForAccount()': ${test.name}`, function () {
const expectedAddressesMap = new Map<number, addressInfo[]>();
for (const addrInfo of test.expectedAddresses) {
if (!expectedAddressesMap.has(addrInfo.account)) {
expectedAddressesMap.set(addrInfo.account, []);
it(`tests addresses generation and retrieval: ${test.name}`, function () {
const generatedAddresses: AddressInfo[] = [];
for (const { params, expectedAddress } of test.addresses) {
const addrInfo = quaiWallet.getNextAddress(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);
}
expectedAddressesMap.get(addrInfo.account)!.push(addrInfo);
}
for (const [account, expectedAddresses] of expectedAddressesMap) {
const retrievedAddresses = quaiWallet.getAddressesForAccount(account);
assert.deepEqual(retrievedAddresses, expectedAddresses);
}
});
it(`tests method 'getAddressesForZone()': ${test.name}`, function () {
const expectedAddressesMap = new Map<string, addressInfo[]>();
for (const addrInfo of test.expectedAddresses) {
if (!expectedAddressesMap.has(addrInfo.zone)) {
expectedAddressesMap.set(addrInfo.zone, []);
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);
}
expectedAddressesMap.get(addrInfo.zone)!.push(addrInfo);
}
for (const [zone, expectedAddresses] of expectedAddressesMap) {
const zoneEnum = convertToZone(zone);
const retrievedAddresses = quaiWallet.getAddressesForZone(zoneEnum);
assert.deepEqual(retrievedAddresses, expectedAddresses);
}
});
}
Expand All @@ -68,13 +63,10 @@ describe('Test address generation and retrieval', function () {
describe('Test transaction signing', function () {
const tests = loadTests<TestCaseQuaiTransaction>('quai-transaction');
for (const test of tests) {
if (!test.signed) {
continue;
}
it(`tests signing an EIP-155 transaction: ${test.name}`, async function () {
const mnemonic = Mnemonic.fromPhrase(test.mnemonic);
const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
quaiWallet.getNextAddress(test.account, test.zone);
quaiWallet.getNextAddress(test.params.account, test.params.zone);
const txData = test.transaction;
const signed = await quaiWallet.signTransaction(txData);
assert.equal(signed, test.signed, 'signed');
Expand All @@ -89,8 +81,8 @@ describe('Test serialization and deserialization of QuaiHDWallet', function () {
const quaiWallet = QuaiHDWallet.fromMnemonic(mnemonic);
let serialized: any;
it(`tests serialization QuaiHDWallet: ${test.name}`, async function () {
for (let i = 0; i < test.totalAddresses; i++) {
quaiWallet.getNextAddress(test.account, test.zone);
for (const param of test.params) {
quaiWallet.getNextAddress(param.account, param.zone);
}
serialized = quaiWallet.serialize();
assert.deepEqual(serialized, test.serialized);
Expand Down
Loading
Loading