Skip to content

Commit

Permalink
Merge pull request #7536 from LedgerHQ/chore/coin-bitcoin-xpub-unit-t…
Browse files Browse the repository at this point in the history
…ests

LIVE-12807 Chore/coin bitcoin xpub unit tests
  • Loading branch information
Wozacosta authored Aug 14, 2024
2 parents 1f26de1 + 527fb15 commit f895f64
Show file tree
Hide file tree
Showing 10 changed files with 802 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-guests-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/coin-bitcoin": patch
---

adds some unit tests in coin-bitcoin coin-module
1 change: 1 addition & 0 deletions libs/coin-modules/coin-bitcoin/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testPathIgnorePatterns: ["lib/", "lib-es/", ".integration.test.ts"],
modulePathIgnorePatterns: ["__tests__/fixtures"],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import BigNumber from "bignumber.js";
import { listCryptoCurrencies } from "@ledgerhq/cryptoassets/currencies";
import { emptyHistoryCache } from "@ledgerhq/coin-framework/account/index";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { BitcoinAccount, BitcoinResources, NetworkInfoRaw } from "../../types";
import {
AddressFormat,
BitcoinAddress,
BitcoinSignature,
BitcoinSigner,
BitcoinXPub,
CreateTransaction,
SignerTransaction,
} from "../../signer";

export const networkInfo: NetworkInfoRaw = {
family: "bitcoin",
feeItems: {
items: [
{
key: "0",
speed: "high",
feePerByte: "3",
},
{
key: "1",
speed: "standard",
feePerByte: "2",
},
{
key: "2",
speed: "low",
feePerByte: "1",
},
],
defaultFeePerByte: "1",
},
};

export function createFixtureAccount(account?: Partial<BitcoinAccount>): BitcoinAccount {
const currency = listCryptoCurrencies(true).find(c => c.id === "bitcoin")!;

const bitcoinResources: BitcoinResources = account?.bitcoinResources || {
utxos: [],
};

const freshAddress = {
address: "1fMK6i7CMDES1GNGDEMX5ddDaxbkjWPw1M",
derivationPath: "derivation_path",
};

return {
type: "Account",
id: "E0A538D5-5EE7-4E37-BB57-F373B08B8580",
seedIdentifier: "FD5EAFE3-8C7F-4565-ADFA-2A1A2067322A",
derivationMode: "",
index: 0,
freshAddress: freshAddress.address,
freshAddressPath: freshAddress.derivationPath,
used: true,
balance: account?.balance || new BigNumber(0),
spendableBalance: account?.spendableBalance || new BigNumber(0),
creationDate: new Date(),
blockHeight: 100_000,
currency,
operationsCount: 0,
operations: [],
pendingOperations: [],
lastSyncDate: new Date(),
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],

bitcoinResources,
};
}

export const mockXpubPubKey = {
xpub: [
"xpub6DEHKg8fgKcb5iYGPLtpBYD9gm7nvym3wwhHVnH3TtogvJGTcApj71K8iTpL7CzdZWAxwyjkZEFUrnLK24zKqgj3EVH7Vg1CD1ujibwiHuy",
],
pubKey: [
{
publicKey:
"xpub6DEHKg8fgKcb5iYGPLtpBYD9gm7nvym3wwhHVnH3TtogvJGTcApj71K8iTpL7CzdZWAxwyjkZEFUrnLK24zKqgj3EVH7Vg1CD1ujibwiHuy",
bitcoinAddress: "bc1qhh568mfmwu7ymvwhu5e4mttpfg4ehxfpvhjs64",
chainCode: "",
},
],
};

export const mockSigner: BitcoinSigner = {
getWalletXpub: (_arg: { path: string; xpubVersion: number }): Promise<BitcoinXPub> =>
Promise.resolve(""),
getWalletPublicKey: (
_path: string,
_opts?: {
verify?: boolean;
format?: AddressFormat;
},
): Promise<BitcoinAddress> => {
return Promise.resolve({
publicKey: "",
bitcoinAddress: "",
chainCode: "",
});
},
signMessage: (_path: string, _messageHex: string): Promise<BitcoinSignature> =>
Promise.resolve({
v: 0,
r: "123",
s: "456",
}),
splitTransaction: (
_transactionHex: string,
_isSegwitSupported: boolean | null | undefined,
_hasExtraData: boolean | null | undefined,
_additionals: Array<string> | null | undefined,
): SignerTransaction => ({
version: Buffer.from(""),
inputs: [
{
prevout: Buffer.from(""),
script: Buffer.from(""),
sequence: Buffer.from(""),
},
],
}),
createPaymentTransaction: (_arg: CreateTransaction): Promise<string> =>
Promise.resolve("createPaymentTransactionReturn"),
};

export const mockSignerContext = <T>(
_deviceId: string,
_crypto: CryptoCurrency,
fn: (signer: BitcoinSigner) => Promise<T>,
): Promise<T> =>
fn({
getWalletXpub: (_arg: { path: string; xpubVersion: number }): Promise<BitcoinXPub> =>
Promise.resolve(""),
getWalletPublicKey: (
_path: string,
_opts?: {
verify?: boolean;
format?: AddressFormat;
},
): Promise<BitcoinAddress> => {
return Promise.resolve({
publicKey: "",
bitcoinAddress: "",
chainCode: "",
});
},
signMessage: (_path: string, _messageHex: string): Promise<BitcoinSignature> =>
Promise.resolve({
v: 0,
r: "123",
s: "456",
}),
splitTransaction: (
_transactionHex: string,
_isSegwitSupported: boolean | null | undefined,
_hasExtraData: boolean | null | undefined,
_additionals: Array<string> | null | undefined,
): SignerTransaction => ({
version: Buffer.from(""),
inputs: [
{
prevout: Buffer.from(""),
script: Buffer.from(""),
sequence: Buffer.from(""),
},
],
}),
createPaymentTransaction: (_arg: CreateTransaction): Promise<string> => Promise.resolve(""),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { FeeNotLoaded } from "@ledgerhq/errors";

import { bitcoinPickingStrategy } from "../../types";
import wallet, { TransactionInfo } from "../../wallet-btc";
import { fromTransactionRaw } from "../../transaction";
import { buildTransaction } from "../../buildTransaction";

import { createFixtureAccount, networkInfo } from "../fixtures/common.fixtures";

jest.mock("../../wallet-btc", () => ({
...jest.requireActual("../../wallet-btc"),
getWalletAccount: jest.fn().mockReturnValue({
xpub: {
crypto: "bitcoin",
},
}),
}));

describe("buildTransaction", () => {
const mockAccount = createFixtureAccount();
const maxSpendable = 100000;
const txInfo = {
inputs: [],
outputs: [],
fee: 0,
associatedDerivations: [],
changeAddress: { address: "change-address", account: 1, index: 1 },
} as TransactionInfo;

beforeEach(() => {
jest.clearAllMocks();
wallet.estimateAccountMaxSpendable = jest.fn().mockResolvedValue(maxSpendable);
wallet.buildAccountTx = jest.fn().mockResolvedValue(txInfo);
});
it("should throw FeeNotLoaded if feePerByte is not provided", async () => {
const transaction = fromTransactionRaw({
family: "bitcoin",
recipient: "1Cz2ZXb6Y6AacXJTpo4RBjQMLEmscuxD8e",
amount: "999",
feePerByte: null,
networkInfo,
rbf: false,
utxoStrategy: {
strategy: bitcoinPickingStrategy.MERGE_OUTPUTS,
excludeUTXOs: [],
},
});

await expect(buildTransaction(mockAccount, transaction)).rejects.toThrow(FeeNotLoaded);
});

it("should call getWalletAccount with the provided account", async () => {
const transaction = fromTransactionRaw({
family: "bitcoin",
recipient: "1Cz2ZXb6Y6AacXJTpo4RBjQMLEmscuxD8e",
amount: "999",
feePerByte: "1",
networkInfo,
rbf: false,
utxoStrategy: {
strategy: bitcoinPickingStrategy.MERGE_OUTPUTS,
excludeUTXOs: [],
},
});

const res = await buildTransaction(mockAccount, transaction);

// eslint-disable-next-line @typescript-eslint/no-var-requires
expect(require("../../wallet-btc").getWalletAccount).toHaveBeenCalledWith(mockAccount);
expect(res).toEqual(txInfo);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
import { makeGetAccountShape } from "../../synchronisation";

import { createFixtureAccount, mockSignerContext } from "../fixtures/common.fixtures";

const mockStartSpan = jest.fn().mockReturnValue({
finish: jest.fn(),
});
describe("synchronisation", () => {
it("should return a function", () => {
const result = makeGetAccountShape(mockSignerContext, mockStartSpan);
expect(typeof result).toBe("function");
});

it("should return an account shape with the correct properties", async () => {
const getAccountShape = makeGetAccountShape(mockSignerContext, mockStartSpan);
const mockAccount = createFixtureAccount();
mockAccount.id =
"js:2:bitcoin:xpub6DM4oxVnZiePFvQMu1RJLQwWUzZQP3UNaLqrGcbJQkAJZYdiRoRivHULWoYN3zBYU4mJRpM3WrGaqo1kS8Q2XFfd9E3QEc9P3MKHwbHz9LB:native_segwit";
const result = await getAccountShape(
{
currency: getCryptoCurrencyById("bitcoin"),
address: "0x123",
index: 1,
derivationPath: "m/44'/0'/0'/0/1",
derivationMode: "taproot",
initialAccount: mockAccount,
},
{ paginationConfig: {} },
);
expect(result).toMatchObject({
bitcoinResources: {
utxos: [],
walletAccount: {
params: {
currency: "bitcoin",
derivationMode: "Taproot",
index: 1,
network: "mainnet",
path: "m/44'",
xpub: "xpub6DM4oxVnZiePFvQMu1RJLQwWUzZQP3UNaLqrGcbJQkAJZYdiRoRivHULWoYN3zBYU4mJRpM3WrGaqo1kS8Q2XFfd9E3QEc9P3MKHwbHz9LB",
},
xpub: {
GAP: 20,
OUTPUT_VALUE_MAX: 9007199254740991,
derivationMode: "Taproot",
explorer: { baseUrl: "https://explorers.api.live.ledger.com/blockchain/v4/btc" },
freshAddress: "bc1pusjmg6xjpym8t8rvdw5gyx2mxvqj0l439acqzy2ssv546k857svqdnth09",
freshAddressIndex: 0,
txsSyncArraySize: 1000,
xpub: "xpub6DM4oxVnZiePFvQMu1RJLQwWUzZQP3UNaLqrGcbJQkAJZYdiRoRivHULWoYN3zBYU4mJRpM3WrGaqo1kS8Q2XFfd9E3QEc9P3MKHwbHz9LB",
},
},
},
freshAddress: "bc1pusjmg6xjpym8t8rvdw5gyx2mxvqj0l439acqzy2ssv546k857svqdnth09",
freshAddressPath: "m/44'/1'/0/0",
id: "js:2:bitcoin:xpub6DM4oxVnZiePFvQMu1RJLQwWUzZQP3UNaLqrGcbJQkAJZYdiRoRivHULWoYN3zBYU4mJRpM3WrGaqo1kS8Q2XFfd9E3QEc9P3MKHwbHz9LB:taproot",
operations: [],
operationsCount: 0,
xpub: "xpub6DM4oxVnZiePFvQMu1RJLQwWUzZQP3UNaLqrGcbJQkAJZYdiRoRivHULWoYN3zBYU4mJRpM3WrGaqo1kS8Q2XFfd9E3QEc9P3MKHwbHz9LB",
});
});
});
2 changes: 1 addition & 1 deletion libs/coin-modules/coin-bitcoin/src/getTransactionStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const getTransactionStatus: AccountBridge<
Transaction,
Account,
TransactionStatus
>["getTransactionStatus"] = async (account, transaction) => {
>["getTransactionStatus"] = async (account: Account, transaction: Transaction) => {
const errors: Record<string, Error> = {};
const warnings: Record<string, Error> = {};
const useAllAmount = !!transaction.useAllAmount;
Expand Down
42 changes: 1 addition & 41 deletions libs/coin-modules/coin-bitcoin/src/hw-signMessage.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import BigNumber from "bignumber.js";
import { emptyHistoryCache } from "@ledgerhq/coin-framework/account/index";
import { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { listCryptoCurrencies } from "@ledgerhq/cryptoassets/index";
import { signMessage } from "./hw-signMessage";
import { BitcoinSigner } from "./signer";
import { BitcoinAccount, BitcoinResources } from "./types";
import { createFixtureAccount } from "./__tests__/fixtures/common.fixtures";

describe("signMessage", () => {
it("returns a base64 format string", async () => {
Expand Down Expand Up @@ -43,40 +40,3 @@ describe("signMessage", () => {
});
});
});

function createFixtureAccount(account?: Partial<BitcoinAccount>): BitcoinAccount {
const currency = listCryptoCurrencies(true).find(c => c.id === "bitcoin")!;

const bitcoinResources: BitcoinResources = account?.bitcoinResources || {
utxos: [],
};

const freshAddress = {
address: "1fMK6i7CMDES1GNGDEMX5ddDaxbkjWPw1M",
derivationPath: "derivation_path",
};

return {
type: "Account",
id: "E0A538D5-5EE7-4E37-BB57-F373B08B8580",
seedIdentifier: "FD5EAFE3-8C7F-4565-ADFA-2A1A2067322A",
derivationMode: "",
index: 0,
freshAddress: freshAddress.address,
freshAddressPath: freshAddress.derivationPath,
used: true,
balance: account?.balance || new BigNumber(0),
spendableBalance: account?.spendableBalance || new BigNumber(0),
creationDate: new Date(),
blockHeight: 100_000,
currency,
operationsCount: 0,
operations: [],
pendingOperations: [],
lastSyncDate: new Date(),
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],

bitcoinResources,
};
}
Loading

0 comments on commit f895f64

Please sign in to comment.