@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
331 lines (294 loc) • 11.6 kB
text/typescript
import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
import { LedgerAPI4xx } from "@ledgerhq/errors";
import type { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import BigNumber from "bignumber.js";
import hederaCoinConfig from "../config";
import { HederaAddAccountError } from "../errors";
import { apiClient } from "../network/api";
import * as networkUtils from "../network/utils";
import { getMockedConfig } from "../test/fixtures/config.fixture";
import {
getMockedCurrency,
getMockedERC20TokenCurrency,
getMockedHTSTokenCurrency,
} from "../test/fixtures/currency.fixture";
import type { HederaERC20TokenBalance } from "../types";
import { getBalance } from "./getBalance";
jest.mock("../config");
jest.mock("../network/api");
jest.mock("../network/utils");
const mockHederaConfig = jest.mocked(hederaCoinConfig);
describe("getBalance", () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
const mockConfig = { ...getMockedConfig(), useHgraphForErc20: true };
const mockMirrorAccount = {
balance: {
balance: "1000000000",
},
};
beforeEach(() => {
jest.clearAllMocks();
mockHederaConfig.getCoinConfig.mockReturnValue(mockConfig);
});
it("should return native balance when only HBAR is present", async () => {
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue([]);
const result = await getBalance(mockCurrency, address);
expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
expect(apiClient.getAccount).toHaveBeenCalledWith(address);
expect(apiClient.getAccountTokens).toHaveBeenCalledTimes(1);
expect(apiClient.getAccountTokens).toHaveBeenCalledWith(address);
// non-staking account: no node lookup should happen
expect(apiClient.getNode).not.toHaveBeenCalled();
expect(apiClient.getNodes).not.toHaveBeenCalled();
expect(result).toEqual([
{
asset: { type: "native" },
value: BigInt("1000000000"),
},
]);
});
it("should return native balance and token balances", async () => {
const mockMirrorTokens = [
{
token_id: "0.0.7890",
balance: "5000",
},
];
const mockTokenHTS = getMockedHTSTokenCurrency({ contractAddress: "0.0.7890" });
const mockTokenERC20 = getMockedERC20TokenCurrency({ contractAddress: "0x12345" });
const mockERC20Balances: HederaERC20TokenBalance[] = [
{
balance: new BigNumber(100),
token: mockTokenERC20,
},
];
const findTokenByAddressInCurrencyMock = jest
.fn()
.mockImplementation(
async (tokenId: string, _currencyId: string): Promise<TokenCurrency | undefined> => {
if (tokenId === mockTokenHTS.contractAddress) return mockTokenHTS;
if (tokenId === mockTokenERC20.contractAddress) return mockTokenERC20;
return undefined;
},
);
setupMockCryptoAssetsStore({
findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
});
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue(mockMirrorTokens);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue(mockERC20Balances);
const result = await getBalance(mockCurrency, address);
expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
expect(apiClient.getAccount).toHaveBeenCalledWith(address);
expect(apiClient.getAccountTokens).toHaveBeenCalledTimes(1);
expect(apiClient.getAccountTokens).toHaveBeenCalledWith(address);
expect(networkUtils.getERC20BalancesForAccountV2).toHaveBeenCalledTimes(1);
expect(networkUtils.getERC20BalancesForAccountV2).toHaveBeenCalledWith(address);
expect(findTokenByAddressInCurrencyMock).toHaveBeenCalledTimes(2);
expect(findTokenByAddressInCurrencyMock).toHaveBeenCalledWith("0.0.7890", "hedera");
expect(findTokenByAddressInCurrencyMock).toHaveBeenCalledWith("0x12345", "hedera");
expect(result).toEqual(
expect.arrayContaining([
{
asset: { type: "native" },
value: BigInt("1000000000"),
},
{
value: BigInt("5000"),
asset: {
type: mockTokenHTS.tokenType,
assetReference: mockTokenHTS.contractAddress,
assetOwner: address,
name: mockTokenHTS.name,
unit: mockTokenHTS.units[0],
},
},
{
value: BigInt("100"),
asset: {
type: mockTokenERC20.tokenType,
assetReference: mockTokenERC20.contractAddress,
assetOwner: address,
name: mockTokenERC20.name,
unit: mockTokenERC20.units[0],
},
},
]),
);
});
it("should return stake", async () => {
const mockMirrorAccount = {
account: address,
staked_node_id: 5,
balance: {
balance: 100,
},
pending_reward: 100,
};
const mockMirrorNode = {
node_id: 5,
node_account_id: "0.0.5",
description: "Hosted for Wipro | Amsterdam, Netherlands",
max_stake: 45000000000000000,
stake: 45000000000000000,
};
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
(apiClient.getNode as jest.Mock).mockResolvedValue(mockMirrorNode);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue([]);
const result = await getBalance(mockCurrency, address);
expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
expect(apiClient.getAccount).toHaveBeenCalledWith(address);
expect(apiClient.getNode).toHaveBeenCalledTimes(1);
expect(apiClient.getNode).toHaveBeenCalledWith(mockMirrorAccount.staked_node_id);
expect(apiClient.getNodes).not.toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
asset: { type: "native" },
value: BigInt(mockMirrorAccount.balance.balance),
stake: {
uid: address,
address,
asset: { type: "native" },
state: "active",
amount: BigInt(mockMirrorAccount.balance.balance + mockMirrorAccount.pending_reward),
amountDeposited: BigInt(mockMirrorAccount.balance.balance),
amountRewarded: BigInt(mockMirrorAccount.pending_reward),
delegate: mockMirrorNode.node_account_id,
},
});
});
it("should skip tokens without units or not found in CAL", async () => {
const mockMirrorTokens = [
{
token_id: "0.0.7890",
balance: "5000",
},
{
token_id: "0.0.9876",
balance: "10000",
},
];
const mockTokenHTS1: TokenCurrency = {
type: "TokenCurrency",
id: "token1",
contractAddress: "0.0.7890",
tokenType: "hts",
name: "Test Token 1",
ticker: "TT1",
parentCurrency: mockCurrency,
units: [{ name: "TT1", code: "tt1", magnitude: 6 }],
delisted: false,
disableCountervalue: false,
};
const mockTokenHTS2: TokenCurrency = {
type: "TokenCurrency",
id: "token2",
contractAddress: "0.0.1111",
tokenType: "hts",
name: "Test Token 2",
ticker: "TT1",
parentCurrency: mockCurrency,
units: [],
delisted: false,
disableCountervalue: false,
};
const mockTokenERC20 = getMockedERC20TokenCurrency({ contractAddress: "0x12345" });
const mockERC20Balances: HederaERC20TokenBalance[] = [
{
balance: new BigNumber(100),
token: mockTokenERC20,
},
{
balance: new BigNumber(200),
token: getMockedERC20TokenCurrency({ contractAddress: "0x54321" }),
},
];
const findTokenByAddressInCurrencyMock = jest
.fn()
.mockImplementation(
async (tokenId: string, _currencyId: string): Promise<TokenCurrency | undefined> => {
if (tokenId === mockTokenHTS1.contractAddress) return mockTokenHTS1;
if (tokenId === mockTokenHTS2.contractAddress) return mockTokenHTS2;
if (tokenId === mockTokenERC20.contractAddress) return mockTokenERC20;
return undefined;
},
);
setupMockCryptoAssetsStore({
findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
});
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue(mockMirrorTokens);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue(mockERC20Balances);
const result = await getBalance(mockCurrency, address);
expect(result).toEqual([
{
asset: { type: "native" },
value: BigInt("1000000000"),
},
{
value: BigInt("5000"),
asset: {
type: mockTokenHTS1.tokenType,
assetReference: mockTokenHTS1.contractAddress,
assetOwner: address,
name: mockTokenHTS1.name,
unit: mockTokenHTS1.units[0],
},
},
{
value: BigInt("100"),
asset: {
type: mockTokenERC20.tokenType,
assetReference: mockTokenERC20.contractAddress,
assetOwner: address,
name: mockTokenERC20.name,
unit: mockTokenERC20.units[0],
},
},
]);
});
it("should throw when failing to getAccount data", async () => {
const error = new Error("Network error");
(apiClient.getAccount as jest.Mock).mockRejectedValue(error);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue([]);
await expect(getBalance(mockCurrency, address)).rejects.toThrow(error);
});
it("should throw when failing to getAccountTokens data", async () => {
const error = new Error("Network error");
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockRejectedValue(error);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue([]);
await expect(getBalance(mockCurrency, address)).rejects.toThrow(error);
});
it("should throw when failing to fetch ERC20 balances", async () => {
const error = new Error("Network error");
(apiClient.getAccount as jest.Mock).mockResolvedValue(mockMirrorAccount);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockRejectedValue(error);
await expect(getBalance(mockCurrency, address)).rejects.toThrow(error);
});
it.each([
{
name: "HederaAddAccountError",
error: new HederaAddAccountError(),
},
{
name: "404 error",
error: new LedgerAPI4xx("", { status: 404, url: undefined, method: "GET" }),
},
])("should return empty results on $name", async ({ error }) => {
const address = "0.0.0";
(apiClient.getAccount as jest.Mock).mockRejectedValue(error);
(apiClient.getAccountTokens as jest.Mock).mockResolvedValue([]);
(networkUtils.getERC20BalancesForAccountV2 as jest.Mock).mockResolvedValue([]);
const result = await getBalance(mockCurrency, address);
expect(apiClient.getAccount).toHaveBeenCalledTimes(1);
expect(apiClient.getAccount).toHaveBeenCalledWith(address);
expect(result).toEqual([]);
});
});