Skip to content

Commit

Permalink
test(hd-wallet): update unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
javadkh2 committed Nov 13, 2023
1 parent 8c8387b commit e22c5f6
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 628 deletions.
4 changes: 2 additions & 2 deletions packages/libs/hd-wallet/src/bip44/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './kadenaGenKeypairFromSeed';
export * from './kadenaGetPublic';
export * from './kadenaKeyPairsFromRandom';
export * from './mnemonic';
export * from './sign';
export * from './kadenaMnemonic';
export * from './kadenaSign';
21 changes: 11 additions & 10 deletions packages/libs/hd-wallet/src/bip44/kadenaGenKeypairFromSeed.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt, kadenaEncrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

Expand All @@ -6,7 +7,7 @@ function genKeypairFromSeed(
seedBuffer: Uint8Array,
index: number,
derivationPathTemplate: string,
): [string, string] {
): [string, EncryptedString] {
const derivationPath = derivationPathTemplate.replace(
'<index>',
index.toString(),
Expand All @@ -15,26 +16,26 @@ function genKeypairFromSeed(
const { publicKey, privateKey } = deriveKeyPair(seedBuffer, derivationPath);

const encryptedPrivateKey = kadenaEncrypt(
Buffer.from(privateKey, 'hex'),
password,
Buffer.from(privateKey, 'hex'),
);

return [publicKey, encryptedPrivateKey];
}

export function kadenaGenKeypairFromSeed(
password: string,
seed: string,
seed: EncryptedString,
index: number,
derivationPathTemplate?: string,
): [string, string];
): [string, EncryptedString];

export function kadenaGenKeypairFromSeed(
password: string,
seed: string,
seed: EncryptedString,
indexRange: [number, number],
derivationPathTemplate?: string,
): Array<[string, string]>;
): Array<[string, EncryptedString]>;

/**
* Generates a key pair from a seed buffer and an index or range of indices, and optionally encrypts the private key.
Expand All @@ -48,15 +49,15 @@ export function kadenaGenKeypairFromSeed(
*/
export function kadenaGenKeypairFromSeed(
password: string,
seed: string,
seed: EncryptedString,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'/0'/0'`,
): [string, string] | Array<[string, string]> {
): [string, EncryptedString] | Array<[string, EncryptedString]> {
if (typeof seed !== 'string' || seed === '') {
throw new Error('No seed provided.');
}

const seedBuffer = kadenaDecrypt(seed, password);
const seedBuffer = kadenaDecrypt(password, seed);

if (typeof indexOrRange === 'number') {
return genKeypairFromSeed(
Expand All @@ -72,7 +73,7 @@ export function kadenaGenKeypairFromSeed(
throw new Error('The start index must be less than the end index.');
}

const keyPairs: [string, string][] = [];
const keyPairs: [string, EncryptedString][] = [];

for (let index = startIndex; index <= endIndex; index++) {
const [publicKey, encryptedPrivateKey] = genKeypairFromSeed(
Expand Down
9 changes: 5 additions & 4 deletions packages/libs/hd-wallet/src/bip44/kadenaGetPublic.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import { deriveKeyPair } from './utils/sign';

Expand All @@ -18,14 +19,14 @@ function genPublicKeyFromSeed(

export function kadenaGetPublic(
password: string,
seed: string,
seed: EncryptedString,
index: number,
derivationPathTemplate?: string,
): string;

export function kadenaGetPublic(
password: string,
seed: string,
seed: EncryptedString,
indexRange: [number, number],
derivationPathTemplate?: string,
): string[];
Expand All @@ -42,15 +43,15 @@ export function kadenaGetPublic(
*/
export function kadenaGetPublic(
password: string,
seed: string,
seed: EncryptedString,
indexOrRange: number | [number, number],
derivationPathTemplate: string = `m'/44'/626'/<index>'/0'/0'`,
): string | string[] {
if (typeof seed !== 'string' || seed === '') {
throw new Error('No seed provided.');
}

const seedBuffer = kadenaDecrypt(seed, password);
const seedBuffer = kadenaDecrypt(password, seed);

if (typeof indexOrRange === 'number') {
return genPublicKeyFromSeed(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaEncrypt } from '../utils/kadenaEncryption';
/**
* Generates a mnemonic phrase using the BIP39 protocol with a specified wordlist.
Expand All @@ -23,12 +24,12 @@ export async function kadenaMnemonicToSeed(
password: string,
mnemonic: string,
// wordList: string[] = wordlist,
): Promise<string> {
): Promise<EncryptedString> {
if (bip39.validateMnemonic(mnemonic, wordlist) === false) {
throw Error('Invalid mnemonic.');
}

const seedBuffer = await bip39.mnemonicToSeed(mnemonic);

return kadenaEncrypt(seedBuffer, password);
return kadenaEncrypt(password, seedBuffer);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import type { IUnsignedCommand } from '@kadena/client';
import { verifySig } from '@kadena/cryptography-utils';
import type { EncryptedString } from '../utils/kadenaEncryption';
import { kadenaDecrypt } from '../utils/kadenaEncryption';
import { signWithKeyPair, signWithSeed } from './utils/sign';

/**
* Signs a Kadena transaction with a given public and private key pair.
*
* @param {string} publicKey - The public key to be used for signing the transaction.
* @param {string} privateKey - The private key to be used for signing the transaction.
* @param {string} encryptedPrivateKey - The private key to be used for signing the transaction.
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithKeyPair(
password: string,
publicKey: string,
privateKey: string,
encryptedPrivateKey: EncryptedString,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
return signWithKeyPair(publicKey, privateKey);
return signWithKeyPair(
publicKey,
Buffer.from(kadenaDecrypt(password, encryptedPrivateKey)).toString('hex'),
);
}

/**
Expand All @@ -24,12 +30,13 @@ export function kadenaSignWithKeyPair(
* @returns {Function} A function that takes an unsigned command (`IUnsignedCommand`) and returns an object with an array of signatures.
*/
export function kadenaSignWithSeed(
seed: Uint8Array,
password: string,
seed: EncryptedString,
index: number,
derivationPathTemplate: string = `m'/44'/626'/<index>'/0'/0'`,
): (tx: IUnsignedCommand) => { sigs: { sig: string }[] } {
return signWithSeed(
seed,
kadenaDecrypt(password, seed),
derivationPathTemplate.replace('<index>', index.toString()),
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest';

import {
kadenaGenKeypairFromSeed,
kadenaGenMnemonic,
kadenaMnemonicToSeed,
} from '../';

import { kadenaDecrypt } from '../../utils/kadenaEncryption';

describe('kadenaGenKeypairFromSeed', () => {
it('should generate an encrypted keypair from the seedBuffer when a password is provided', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const [publicKey, encryptedPrivateKey] = kadenaGenKeypairFromSeed(
password,
seed,
0,
);

expect(publicKey).toHaveLength(64);
expect(typeof encryptedPrivateKey).toBe('string'); // Checks if privateKey is a string, thus encrypted
});

it('should generate a range of keypairs from the seed', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const keyPairs = kadenaGenKeypairFromSeed(password, seed, [0, 3]);
expect(keyPairs).toHaveLength(4);
keyPairs.forEach(([publicKey, privateKey]) => {
expect(publicKey).toHaveLength(64);
expect(
Buffer.from(kadenaDecrypt(password, privateKey)).toString('hex'),
).toHaveLength(64);
});
});

it('should throw an error for out-of-bounds index values', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const outOfBoundsIndex = -1;

expect(() => {
kadenaGenKeypairFromSeed(password, seed, outOfBoundsIndex);
}).toThrowError('Invalid child index: -1');
});

it('returns an encrypted private key that can be decrypted with the password', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const [, encryptedPrivateKey] = kadenaGenKeypairFromSeed(password, seed, 0);
const decryptedPrivateKey = kadenaDecrypt(password, encryptedPrivateKey);
expect(decryptedPrivateKey).toBeTruthy();
expect(Buffer.from(decryptedPrivateKey).toString('hex')).toHaveLength(64);
});

// it('should handle the highest non-hardened index without throwing errors', async () => {
// const mnemonic = kadenaGenMnemonic();
// const { seedBuffer } = await kadenaMnemonicToSeed(mnemonic);

// /*
// * HD wallets as per BIP32 spec define two types of indices:
// * - Non-hardened (ranging from 0 to 2^31 - 1)
// * - Hardened (ranging from 2^31 to 2^32 - 1).
// * The highest non-hardened index is therefore 2^31 - 1,
// * which is the largest 32-bit integer that can be used for generating non-hardened keys.
// */

// const highestNonHardenedIndex = 2 ** 31 - 1;
// expect(() => {
// kadenaGenKeypairFromSeed(seedBuffer, highestNonHardenedIndex);
// }).not.toThrow();
// });
});
57 changes: 57 additions & 0 deletions packages/libs/hd-wallet/src/bip44/tests/kadenaGetPublic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';

import { kadenaGenMnemonic, kadenaGetPublic, kadenaMnemonicToSeed } from '..';

describe('kadenaGetPublic', () => {
it('should retrieve the public key from seed and the given index', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);

const publicKeyIndex0 = kadenaGetPublic(password, seed, 0);
const publicKeyIndex1 = kadenaGetPublic(password, seed, 1);

expect(publicKeyIndex0).toHaveLength(64);
expect(publicKeyIndex1).toHaveLength(64);
expect(publicKeyIndex1).not.toBe(publicKeyIndex0);
});

it('should retrieve distinct public keys from seedBuffer for different indexes', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);

const indexes = [0, 1, 2, 3, 4];
const publicKeys = indexes.map((index) =>
kadenaGetPublic(password, seed, index),
);

publicKeys.forEach((publicKey) => {
expect(publicKey).toHaveLength(64);
});

const uniquePublicKeys = new Set(publicKeys); // Check that all public keys are unique
expect(uniquePublicKeys.size).toBe(indexes.length);
});

it('should get the similar public keys as Enkrypt for the same path', async () => {
const password = 'pass';
const seed = await kadenaMnemonicToSeed(
password,
// this mnemonic is generated by Enkrypt wallet
'coyote utility final warfare thumb symbol mule scale final nominee behave crumble',
);
let publicKey = kadenaGetPublic(password, seed, 0);
expect(publicKey).toBe(
'43726c4a2e7b03fa5d23635307e5b130baf8b261e1081c099a2b43db1d4554cc',
);
publicKey = kadenaGetPublic(password, seed, 1);
expect(publicKey).toBe(
'3f53dfad097fdf8501c32b275e109980ed7121866a63ca34bb035c4a2e41a265',
);
publicKey = kadenaGetPublic(password, seed, 2);
expect(publicKey).toBe(
'3021bcfa703cc4fac007ab4c5050df5c0b8ca7d655ea80c84af9ea5e43ecf0ff',
);
});
});
45 changes: 45 additions & 0 deletions packages/libs/hd-wallet/src/bip44/tests/kadenaMnemonic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';

import { kadenaGenMnemonic, kadenaMnemonicToSeed } from '../';

import { kadenaDecrypt } from '../../utils/kadenaEncryption';

describe('kadenaGenMnemonic', () => {
it('should generate a valid mnemonic', () => {
const mnemonic = kadenaGenMnemonic();
expect(mnemonic.split(' ')).toHaveLength(12);
});
});

describe('kadenaMnemonicToSeed', () => {
it('should convert mnemonic to encrypt seed with a password', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
expect(typeof seed).toBe('string'); // Check if the seed is a string, indicating it has been encrypted
});

it('returns encrypted seed that can be decrypted with the password', async () => {
const mnemonic = kadenaGenMnemonic();
const password = 'password';
const seed = await kadenaMnemonicToSeed(password, mnemonic);
const decryptedSeed = kadenaDecrypt(password, seed);
expect(decryptedSeed).toBeTruthy();
});

it('should throw an error for an invalid mnemonic', async () => {
const invalidMnemonic = 'this is not a valid mnemonic';

await expect(
kadenaMnemonicToSeed('password', invalidMnemonic),
).rejects.toThrowError('Invalid mnemonic.');
});

it('should throw an error when mnemonic is empty', async () => {
const emptyMnemonic = '';

await expect(
kadenaMnemonicToSeed('password', emptyMnemonic),
).rejects.toThrowError('Invalid mnemonic.');
});
});
Loading

0 comments on commit e22c5f6

Please sign in to comment.