@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
1,113 lines (953 loc) • 35.3 kB
text/typescript
import {
bitcoinFamilyAccountGetAddressLogic,
bitcoinFamilyAccountGetPublicKeyLogic,
bitcoinFamilyAccountGetXPubLogic,
broadcastTransactionLogic,
completeExchangeLogic,
receiveOnAccountLogic,
signMessageLogic,
WalletAPIContext,
} from "./logic";
import { AppManifest, WalletAPITransaction } from "./types";
import {
createFixtureAccount,
createFixtureCryptoCurrency,
createFixtureTokenAccount,
} from "../mock/fixtures/cryptoCurrencies";
import { Transaction as EvmTransaction } from "@ledgerhq/coin-evm/types/index";
import { OperationType, SignedOperation, TokenAccount } from "@ledgerhq/types-live";
import BigNumber from "bignumber.js";
import * as converters from "./converters";
import * as signMessage from "../hw/signMessage/index";
import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets";
import { TrackingAPI } from "./tracking";
import { cryptocurrenciesById } from "@ledgerhq/cryptoassets/currencies";
import { setSupportedCurrencies } from "../currencies";
import { initialState as walletState } from "@ledgerhq/live-wallet/store";
describe("receiveOnAccountLogic", () => {
// Given
const mockWalletAPIReceiveRequested = jest.fn();
const mockWalletAPIReceiveFail = jest.fn();
const context = createContextContainingAccountId({
tracking: {
receiveRequested: mockWalletAPIReceiveRequested,
receiveFail: mockWalletAPIReceiveFail,
},
accountsParams: [{ id: "11" }, { id: "12" }],
});
const uiNavigation = jest.fn();
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockWalletAPIReceiveRequested.mockClear();
mockWalletAPIReceiveFail.mockClear();
uiNavigation.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
describe("when nominal case", () => {
// Given
const accountId = "js:2:ethereum:0x012:";
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
const expectedResult = "Function called";
beforeEach(() => {
uiNavigation.mockResolvedValueOnce(expectedResult);
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
});
it("calls uiNavigation callback with an accountAddress", async () => {
// Given
const convertedAccount = {
...createWalletAPIAccount(),
address: "Converted address",
};
jest.spyOn(converters, "accountToWalletAPIAccount").mockReturnValueOnce(convertedAccount);
// When
const result = await receiveOnAccountLogic(
walletState,
context,
walletAccountId,
uiNavigation,
);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
expect(uiNavigation.mock.calls[0][2]).toEqual("Converted address");
expect(result).toEqual(expectedResult);
});
it("calls the tracking for success", async () => {
// When
await receiveOnAccountLogic(walletState, context, walletAccountId, uiNavigation);
// Then
expect(mockWalletAPIReceiveRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPIReceiveFail).toHaveBeenCalledTimes(0);
});
});
describe("when account cannot be found", () => {
// Given
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
beforeEach(() => {
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(undefined);
});
it("returns an error", async () => {
// When
await expect(async () => {
await receiveOnAccountLogic(walletState, context, walletAccountId, uiNavigation);
}).rejects.toThrow(`accountId ${walletAccountId} unknown`);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
it("calls the tracking for error", async () => {
// When
await expect(async () => {
await receiveOnAccountLogic(walletState, context, walletAccountId, uiNavigation);
}).rejects.toThrow();
// Then
expect(mockWalletAPIReceiveRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPIReceiveFail).toHaveBeenCalledTimes(1);
});
});
});
function createWalletAPIEtherumTransaction(): WalletAPITransaction {
return {
family: "ethereum",
amount: BigNumber(1000000000),
recipient: "0x0123456",
nonce: 8,
data: Buffer.from("Some data..."),
gasPrice: BigNumber(700000),
gasLimit: BigNumber(1200000),
};
}
function createWalletAPIBitcoinTransaction(): WalletAPITransaction {
return {
family: "bitcoin",
amount: BigNumber(1000000000),
recipient: "0x0123456",
feePerByte: BigNumber(900000),
};
}
describe("completeExchangeLogic", () => {
// Given
const mockWalletAPICompleteExchangeRequested = jest.fn();
const context = createContextContainingAccountId({
tracking: {
completeExchangeRequested: mockWalletAPICompleteExchangeRequested,
},
accountsParams: [{ id: "11" }, { id: "12" }],
});
const uiNavigation = jest.fn();
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeAll(() => {
setSupportedCurrencies(["bitcoin", "ethereum"]);
});
afterAll(() => {
setSupportedCurrencies([]);
});
beforeEach(() => {
mockWalletAPICompleteExchangeRequested.mockClear();
uiNavigation.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
describe("when nominal case", () => {
// Given
const expectedResult = "Function called";
beforeEach(() => uiNavigation.mockResolvedValueOnce(expectedResult));
it("calls uiNavigation callback (token)", async () => {
// Given
const fromAccountId = "js:2:ethereum:0x16:+ethereum%2Ferc20%2Fusd_tether__erc20_";
const toAccountId = "js:2:ethereum:0x042:";
const fromAccount = createFixtureTokenAccount("16");
const fromParentAccount = createFixtureAccount("16");
context.accounts = [...context.accounts, fromAccount, fromParentAccount];
const transaction = createWalletAPIEtherumTransaction();
const completeExchangeRequest = {
provider: "provider",
fromAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f1",
toAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f2",
transaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
};
const expectedTransaction: EvmTransaction = {
family: "evm",
amount: new BigNumber("1000000000"),
subAccountId: fromAccountId,
recipient: "0x0123456",
nonce: 8,
data: Buffer.from("Some data..."),
type: 0,
gasPrice: new BigNumber("700000"),
maxFeePerGas: undefined,
maxPriorityFeePerGas: undefined,
gasLimit: new BigNumber("1200000"),
customGasLimit: new BigNumber("1200000"),
feesStrategy: "medium",
mode: "send",
useAllAmount: false,
chainId: 1,
};
getAccountIdFromWalletAccountIdSpy
.mockReturnValueOnce(fromAccountId)
.mockReturnValueOnce(toAccountId);
// When
const result = await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
expect(uiNavigation.mock.calls[0][0]).toEqual({
provider: "provider",
exchange: {
fromAccount,
fromParentAccount,
fromCurrency: fromAccount.token,
toAccount: undefined,
toParentAccount: undefined,
toCurrency: undefined,
},
transaction: expectedTransaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
swapId: undefined,
rate: undefined,
});
expect(result).toEqual(expectedResult);
});
it("calls uiNavigation callback (coin)", async () => {
// Given
const fromAccountId = "js:2:ethereum:0x017:";
const toAccountId = "js:2:ethereum:0x042:";
const fromAccount = createFixtureAccount("17");
context.accounts = [...context.accounts, fromAccount];
const transaction = createWalletAPIEtherumTransaction();
const completeExchangeRequest = {
provider: "provider",
fromAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f1",
toAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f2",
transaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
};
const expectedTransaction: EvmTransaction = {
family: "evm",
amount: new BigNumber("1000000000"),
recipient: "0x0123456",
nonce: 8,
data: Buffer.from("Some data..."),
gasPrice: new BigNumber("700000"),
gasLimit: new BigNumber("1200000"),
customGasLimit: new BigNumber("1200000"),
feesStrategy: "medium",
mode: "send",
useAllAmount: false,
chainId: 1,
subAccountId: undefined,
type: 0,
maxFeePerGas: undefined,
maxPriorityFeePerGas: undefined,
};
getAccountIdFromWalletAccountIdSpy
.mockReturnValueOnce(fromAccountId)
.mockReturnValueOnce(toAccountId);
// When
const result = await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
expect(uiNavigation.mock.calls[0][0]).toEqual({
provider: "provider",
exchange: {
fromAccount,
fromParentAccount: undefined,
fromCurrency: fromAccount.currency,
toAccount: undefined,
toParentAccount: undefined,
toCurrency: undefined,
},
transaction: expectedTransaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
swapId: undefined,
rate: undefined,
});
expect(result).toEqual(expectedResult);
});
it.each(["slow", "medium", "fast", "custom"])(
"calls uiNavigation with a transaction that has the %s feeStrategy",
async expectedFeeStrategy => {
// Given
const fromAccountId = "js:2:ethereum:0x017:";
const toAccountId = "js:2:ethereum:0x042:";
const fromAccount = createFixtureAccount("17");
context.accounts = [...context.accounts, fromAccount];
const transaction = createWalletAPIEtherumTransaction();
const completeExchangeRequest = {
provider: "provider",
fromAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f1",
toAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f2",
transaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: expectedFeeStrategy,
exchangeType: 8,
swapId: "1234",
rate: 1,
};
getAccountIdFromWalletAccountIdSpy
.mockReturnValueOnce(fromAccountId)
.mockReturnValueOnce(toAccountId);
// When
await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
expect(uiNavigation.mock.calls[0][0]["transaction"].feesStrategy).toEqual(
expectedFeeStrategy,
);
},
);
it("calls the tracking for success", async () => {
// Given
const fromAccountId = "js:2:ethereum:0x012:";
const toAccountId = "js:2:ethereum:0x042:";
const completeExchangeRequest = {
provider: "provider",
fromAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f1",
toAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f2",
transaction: createWalletAPIEtherumTransaction(),
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
swapId: "1234",
rate: 1,
};
getAccountIdFromWalletAccountIdSpy
.mockReturnValueOnce(fromAccountId)
.mockReturnValueOnce(toAccountId);
// When
await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
// Then
expect(mockWalletAPICompleteExchangeRequested).toHaveBeenCalledTimes(1);
});
});
describe("when Account is from a different family than the transaction", () => {
// Given
const expectedResult = "Function called";
beforeEach(() => uiNavigation.mockResolvedValueOnce(expectedResult));
it("returns an error", async () => {
// Given
const fromAccountId = "js:2:ethereum:0x012:";
const toAccountId = "js:2:ethereum:0x042:";
const fromAccount = createFixtureAccount("17");
context.accounts = [...context.accounts, fromAccount];
const transaction = createWalletAPIBitcoinTransaction();
const completeExchangeRequest = {
provider: "provider",
fromAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f1",
toAccountId: "806ea21d-f5f0-425a-add3-39d4b78209f2",
transaction,
binaryPayload: "binaryPayload",
signature: "signature",
feesStrategy: "medium",
exchangeType: 8,
swapId: "1234",
rate: 1,
};
getAccountIdFromWalletAccountIdSpy
.mockReturnValueOnce(fromAccountId)
.mockReturnValueOnce(toAccountId);
// When
await expect(async () => {
await completeExchangeLogic(context, completeExchangeRequest, uiNavigation);
}).rejects.toThrow("Account and transaction must be from the same family");
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
});
});
describe("broadcastTransactionLogic", () => {
// Given
const mockWalletAPIBroadcastFail = jest.fn();
const context = createContextContainingAccountId({
tracking: {
broadcastFail: mockWalletAPIBroadcastFail,
},
accountsParams: [{ id: "11" }, { id: "12" }],
});
const uiNavigation = jest.fn();
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockWalletAPIBroadcastFail.mockClear();
uiNavigation.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
describe("when nominal case", () => {
// Given
const accountId = "js:2:ethereum:0x012:";
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
const signedTransaction = createSignedOperation();
beforeEach(() => {
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
});
it("calls uiNavigation callback with a signedOperation", async () => {
// Given
const expectedResult = "Function called";
// const signedOperation = createSignedOperation();
// jest
// .spyOn(serializers, "deserializeWalletAPISignedTransaction")
// .mockReturnValueOnce(signedOperation);
uiNavigation.mockResolvedValueOnce(expectedResult);
// When
const result = await broadcastTransactionLogic(
context,
walletAccountId,
signedTransaction,
uiNavigation,
);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
// expect(uiNavigation.mock.calls[0][2]).toEqual(signedOperation);
expect(result).toEqual(expectedResult);
});
it("calls the tracking for success", async () => {
// When
await broadcastTransactionLogic(context, walletAccountId, signedTransaction, uiNavigation);
// Then
expect(mockWalletAPIBroadcastFail).toHaveBeenCalledTimes(0);
});
});
describe("when account cannot be found", () => {
// Given
const nonFoundAccountId = "js:2:ethereum:0x010:";
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
const signedTransaction = createSignedOperation();
beforeEach(() => {
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(nonFoundAccountId);
});
it("returns an error", async () => {
// Given
const expectedResult = "Function called";
// const signedOperation = createSignedOperation();
// jest
// .spyOn(serializers, "deserializeWalletAPISignedTransaction")
// .mockReturnValueOnce(signedOperation);
uiNavigation.mockResolvedValueOnce(expectedResult);
// When
await expect(async () => {
await broadcastTransactionLogic(context, walletAccountId, signedTransaction, uiNavigation);
}).rejects.toThrow("Account required");
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
it("calls the tracking for error", async () => {
// When
await expect(async () => {
await broadcastTransactionLogic(context, walletAccountId, signedTransaction, uiNavigation);
}).rejects.toThrow();
// Then
expect(mockWalletAPIBroadcastFail).toHaveBeenCalledTimes(1);
});
});
});
describe("signMessageLogic", () => {
// Given
const mockWalletAPISignMessageRequested = jest.fn();
const mockWalletAPISignMessageFail = jest.fn();
const context = createContextContainingAccountId({
tracking: {
signMessageRequested: mockWalletAPISignMessageRequested,
signMessageFail: mockWalletAPISignMessageFail,
},
accountsParams: [{ id: "11" }, { id: "12" }],
});
const uiNavigation = jest.fn();
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockWalletAPISignMessageRequested.mockClear();
mockWalletAPISignMessageFail.mockClear();
uiNavigation.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
describe("when nominal case", () => {
// Given
const accountId = "js:2:ethereum:0x012:";
const messageToSign = "Message to sign";
const spyPrepareMessageToSign = jest.spyOn(signMessage, "prepareMessageToSign");
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
beforeEach(() => {
spyPrepareMessageToSign.mockClear();
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
});
it("calls uiNavigation callback with a signedOperation", async () => {
// Given
const expectedResult = "Function called";
const formattedMessage = createMessageData();
spyPrepareMessageToSign.mockReturnValueOnce(formattedMessage);
uiNavigation.mockResolvedValueOnce(expectedResult);
// When
const result = await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
// Then
expect(uiNavigation).toHaveBeenCalledTimes(1);
expect(uiNavigation.mock.calls[0][1]).toEqual(formattedMessage);
expect(result).toEqual(expectedResult);
});
it("calls the tracking for success", async () => {
// When
await signMessageLogic(context, accountId, messageToSign, uiNavigation);
// Then
expect(mockWalletAPISignMessageRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPISignMessageFail).toHaveBeenCalledTimes(0);
});
});
describe("when account cannot be found", () => {
// Given
const nonFoundAccountId = "js:2:ethereum:0x010:";
const messageToSign = "Message to sign";
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
beforeEach(() => {
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(nonFoundAccountId);
});
it("returns an error", async () => {
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow("account not found");
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
it("calls the tracking for error", async () => {
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow();
// Then
expect(mockWalletAPISignMessageRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPISignMessageFail).toHaveBeenCalledTimes(1);
});
});
describe("when account found is not of type 'Account'", () => {
// Given
const tokenAccountId = "15";
const messageToSign = "Message to sign";
context.accounts = [createTokenAccount(tokenAccountId), ...context.accounts];
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
beforeEach(() => {
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(tokenAccountId);
});
it("returns an error", async () => {
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow("account provided should be the main one");
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
it("calls the tracking for error", async () => {
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow();
// Then
expect(mockWalletAPISignMessageRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPISignMessageFail).toHaveBeenCalledTimes(1);
});
});
describe("when inner call prepareMessageToSign raise an error", () => {
// Given
const accountId = "js:2:ethereum:0x012:";
const messageToSign = "Message to sign";
const spyPrepareMessageToSign = jest.spyOn(signMessage, "prepareMessageToSign");
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
beforeEach(() => {
spyPrepareMessageToSign.mockClear();
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
});
it("returns an error", async () => {
// Given
spyPrepareMessageToSign.mockImplementationOnce(() => {
throw new Error("Some error");
});
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow("Some error");
// Then
expect(uiNavigation).toHaveBeenCalledTimes(0);
});
it("calls the tracking for error", async () => {
// Given
spyPrepareMessageToSign.mockImplementationOnce(() => {
throw new Error("Some error");
});
// When
await expect(async () => {
await signMessageLogic(context, walletAccountId, messageToSign, uiNavigation);
}).rejects.toThrow();
// Then
expect(mockWalletAPISignMessageRequested).toHaveBeenCalledTimes(1);
expect(mockWalletAPISignMessageFail).toHaveBeenCalledTimes(1);
});
});
});
jest.mock("@ledgerhq/coin-bitcoin/lib/wallet-btc/index", () => ({
...jest.requireActual("@ledgerhq/coin-bitcoin/lib/wallet-btc/index"),
getWalletAccount: jest.fn().mockReturnValue({
xpub: {
crypto: {
getAddress: jest.fn().mockReturnValue("0x01"),
getPubkeyAt: jest.fn().mockReturnValue(Buffer.from("testPubkey")),
},
},
}),
}));
describe("bitcoinFamilyAccountGetAddressLogic", () => {
// Given
const mockBitcoinFamilyAccountAddressRequested = jest.fn();
const mockBitcoinFamilyAccountAddressFail = jest.fn();
const mockBitcoinFamilyAccountAddressSuccess = jest.fn();
const bitcoinCrypto = cryptocurrenciesById["bitcoin"];
const context = createContextContainingAccountId({
tracking: {
bitcoinFamilyAccountAddressRequested: mockBitcoinFamilyAccountAddressRequested,
bitcoinFamilyAccountAddressFail: mockBitcoinFamilyAccountAddressFail,
bitcoinFamilyAccountAddressSuccess: mockBitcoinFamilyAccountAddressSuccess,
},
accountsParams: [{ id: "11" }, { id: "12" }, { id: "13", currency: bitcoinCrypto }],
});
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockBitcoinFamilyAccountAddressRequested.mockClear();
mockBitcoinFamilyAccountAddressFail.mockClear();
mockBitcoinFamilyAccountAddressSuccess.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
it.each([
{
desc: "receive unkown accountId",
accountId: undefined,
errorMessage: `accountId ${walletAccountId} unknown`,
},
{
desc: "account not found",
accountId: "js:2:ethereum:0x010:",
errorMessage: "account not found",
},
{
desc: "account is not a bitcoin family account",
accountId: "js:2:ethereum:0x012:",
errorMessage: "not a bitcoin family account",
},
])("returns an error when $desc", async ({ accountId, errorMessage }) => {
// Given
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
await expect(async () => {
await bitcoinFamilyAccountGetAddressLogic(context, walletAccountId);
}).rejects.toThrow(errorMessage);
// Then
expect(mockBitcoinFamilyAccountAddressRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountAddressFail).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountAddressSuccess).toHaveBeenCalledTimes(0);
});
it("should return the address", async () => {
// Given
const accountId = "js:2:bitcoin:0x013:";
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
const result = await bitcoinFamilyAccountGetAddressLogic(context, walletAccountId);
// Then
expect(result).toEqual("0x01");
expect(mockBitcoinFamilyAccountAddressRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountAddressFail).toHaveBeenCalledTimes(0);
expect(mockBitcoinFamilyAccountAddressSuccess).toHaveBeenCalledTimes(1);
});
it("should return the address with a derivationPath", async () => {
// Given
const accountId = "js:2:bitcoin:0x013:";
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
const result = await bitcoinFamilyAccountGetAddressLogic(context, walletAccountId, "0/1");
// Then
expect(result).toEqual("0x01");
expect(mockBitcoinFamilyAccountAddressRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountAddressFail).toHaveBeenCalledTimes(0);
expect(mockBitcoinFamilyAccountAddressSuccess).toHaveBeenCalledTimes(1);
});
});
describe("bitcoinFamilyAccountGetPublicKeyLogic", () => {
// Given
const mockBitcoinFamilyAccountPublicKeyRequested = jest.fn();
const mockBitcoinFamilyAccountPublicKeyFail = jest.fn();
const mockBitcoinFamilyAccountPublicKeySuccess = jest.fn();
const bitcoinCrypto = cryptocurrenciesById["bitcoin"];
const context = createContextContainingAccountId({
tracking: {
bitcoinFamilyAccountPublicKeyRequested: mockBitcoinFamilyAccountPublicKeyRequested,
bitcoinFamilyAccountPublicKeyFail: mockBitcoinFamilyAccountPublicKeyFail,
bitcoinFamilyAccountPublicKeySuccess: mockBitcoinFamilyAccountPublicKeySuccess,
},
accountsParams: [{ id: "11" }, { id: "12" }, { id: "13", currency: bitcoinCrypto }],
});
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockBitcoinFamilyAccountPublicKeyRequested.mockClear();
mockBitcoinFamilyAccountPublicKeyFail.mockClear();
mockBitcoinFamilyAccountPublicKeySuccess.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
it.each([
{
desc: "receive unkown accountId",
accountId: undefined,
errorMessage: `accountId ${walletAccountId} unknown`,
},
{
desc: "account not found",
accountId: "js:2:ethereum:0x010:",
errorMessage: "account not found",
},
{
desc: "account is not a bitcoin family account",
accountId: "js:2:ethereum:0x012:",
errorMessage: "not a bitcoin family account",
},
])("returns an error when $desc", async ({ accountId, errorMessage }) => {
// Given
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
await expect(async () => {
await bitcoinFamilyAccountGetPublicKeyLogic(context, walletAccountId);
}).rejects.toThrow(errorMessage);
// Then
expect(mockBitcoinFamilyAccountPublicKeyRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountPublicKeyFail).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountPublicKeySuccess).toHaveBeenCalledTimes(0);
});
it("should return the PublicKey", async () => {
// Given
const accountId = "js:2:bitcoin:0x013:";
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
const result = await bitcoinFamilyAccountGetPublicKeyLogic(context, walletAccountId);
// Then
expect(result).toEqual(Buffer.from("testPubkey").toString("hex"));
expect(mockBitcoinFamilyAccountPublicKeyRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountPublicKeyFail).toHaveBeenCalledTimes(0);
expect(mockBitcoinFamilyAccountPublicKeySuccess).toHaveBeenCalledTimes(1);
});
it("should return the PublicKey with a derivationPath", async () => {
// Given
const accountId = "js:2:bitcoin:0x013:";
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
const result = await bitcoinFamilyAccountGetPublicKeyLogic(context, walletAccountId, "0/1");
// Then
expect(result).toEqual(Buffer.from("testPubkey").toString("hex"));
expect(mockBitcoinFamilyAccountPublicKeyRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountPublicKeyFail).toHaveBeenCalledTimes(0);
expect(mockBitcoinFamilyAccountPublicKeySuccess).toHaveBeenCalledTimes(1);
});
});
describe("bitcoinFamilyAccountGetXPubLogic", () => {
// Given
const mockBitcoinFamilyAccountXpubRequested = jest.fn();
const mockBitcoinFamilyAccountXpubFail = jest.fn();
const mockBitcoinFamilyAccountXpubSuccess = jest.fn();
const bitcoinCrypto = cryptocurrenciesById["bitcoin"];
const context = createContextContainingAccountId({
tracking: {
bitcoinFamilyAccountXpubRequested: mockBitcoinFamilyAccountXpubRequested,
bitcoinFamilyAccountXpubFail: mockBitcoinFamilyAccountXpubFail,
bitcoinFamilyAccountXpubSuccess: mockBitcoinFamilyAccountXpubSuccess,
},
accountsParams: [{ id: "11" }, { id: "12" }, { id: "13", currency: bitcoinCrypto }],
});
const getAccountIdFromWalletAccountIdSpy = jest.spyOn(
converters,
"getAccountIdFromWalletAccountId",
);
beforeEach(() => {
mockBitcoinFamilyAccountXpubRequested.mockClear();
mockBitcoinFamilyAccountXpubFail.mockClear();
mockBitcoinFamilyAccountXpubSuccess.mockClear();
getAccountIdFromWalletAccountIdSpy.mockClear();
});
const walletAccountId = "806ea21d-f5f0-425a-add3-39d4b78209f1";
it.each([
{
desc: "receive unkown accountId",
accountId: undefined,
errorMessage: `accountId ${walletAccountId} unknown`,
},
{
desc: "account not found",
accountId: "js:2:ethereum:0x010:",
errorMessage: "account not found",
},
{
desc: "account is not a bitcoin family account",
accountId: "js:2:ethereum:0x012:",
errorMessage: "not a bitcoin family account",
},
])("returns an error when $desc", async ({ accountId, errorMessage }) => {
// Given
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
await expect(async () => {
await bitcoinFamilyAccountGetXPubLogic(context, walletAccountId);
}).rejects.toThrow(errorMessage);
// Then
expect(mockBitcoinFamilyAccountXpubRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountXpubFail).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountXpubSuccess).toHaveBeenCalledTimes(0);
});
it("should return the xpub", async () => {
// Given
const accountId = "js:2:bitcoin:0x013:";
getAccountIdFromWalletAccountIdSpy.mockReturnValueOnce(accountId);
// When
const result = await bitcoinFamilyAccountGetXPubLogic(context, walletAccountId);
// Then
expect(result).toEqual("testxpub");
expect(mockBitcoinFamilyAccountXpubRequested).toHaveBeenCalledTimes(1);
expect(mockBitcoinFamilyAccountXpubFail).toHaveBeenCalledTimes(0);
expect(mockBitcoinFamilyAccountXpubSuccess).toHaveBeenCalledTimes(1);
});
});
function createAppManifest(id = "1"): AppManifest {
return {
id,
private: false,
name: "New App Manifest",
url: "https://www.ledger.com",
homepageUrl: "https://www.ledger.com",
supportUrl: "https://www.ledger.com",
icon: null,
platforms: ["ios", "android", "desktop"],
apiVersion: "1.0.0",
manifestVersion: "1.0.0",
branch: "debug",
params: undefined,
categories: [],
currencies: "*",
content: {
shortDescription: {
en: "short description",
},
description: {
en: "description",
},
},
permissions: [],
domains: [],
visibility: "complete",
};
}
function createContextContainingAccountId({
tracking,
accountsParams,
}: {
tracking: Partial<TrackingAPI>;
accountsParams: Array<{ id: string; currency?: CryptoCurrency }>;
}): WalletAPIContext {
return {
manifest: createAppManifest(),
accounts: accountsParams
.map(({ id, currency }) => createFixtureAccount(id, currency))
.concat([createFixtureAccount()]),
tracking: tracking as TrackingAPI,
};
}
function createSignedOperation(): SignedOperation {
const operation = {
id: "42",
hash: "hashed",
type: "IN" as OperationType,
value: new BigNumber(0),
fee: new BigNumber(0),
senders: [],
recipients: [],
blockHeight: null,
blockHash: null,
accountId: "14",
date: new Date(),
extra: {},
};
return {
operation,
signature: "Signature",
};
}
function createWalletAPIAccount() {
return {
id: "12",
name: "",
address: "",
currency: "",
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
blockHeight: 0,
lastSyncDate: new Date(),
};
}
function createMessageData() {
return {
account: createFixtureAccount("17"),
message: "default message",
};
}
function createTokenAccount(id = "32"): TokenAccount {
return {
type: "TokenAccount",
id,
parentId: "whatever",
token: createTokenCurrency(),
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
creationDate: new Date(),
operationsCount: 0,
operations: [],
pendingOperations: [],
balanceHistoryCache: {
WEEK: { latestDate: null, balances: [] },
HOUR: { latestDate: null, balances: [] },
DAY: { latestDate: null, balances: [] },
},
swapHistory: [],
};
}
function createTokenCurrency(): TokenCurrency {
return {
type: "TokenCurrency",
id: "3",
contractAddress: "",
parentCurrency: createFixtureCryptoCurrency("eth"),
tokenType: "",
//-- CurrencyCommon
name: "",
ticker: "",
units: [],
};
}