@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
495 lines (447 loc) • 15.3 kB
text/typescript
import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers";
import { encodeTokenAccountId } from "@ledgerhq/ledger-wallet-framework/account/accountId";
import { encodeOperationId } from "@ledgerhq/ledger-wallet-framework/operation";
import { getEnv } from "@ledgerhq/live-env";
import BigNumber from "bignumber.js";
import { apiClient } from "../network/api";
import { getMockedCurrency } from "../test/fixtures/currency.fixture";
import type { HederaMirrorTransaction } from "../types";
import { listOperations } from "./listOperations";
import * as utils from "./utils";
setupMockCryptoAssetsStore();
jest.mock("@ledgerhq/ledger-wallet-framework/account/accountId");
jest.mock("@ledgerhq/ledger-wallet-framework/operation");
jest.mock("../network/api");
jest.mock("./utils");
describe("listOperations", () => {
beforeEach(() => {
jest.clearAllMocks();
(utils.extractFeesPayer as jest.Mock).mockImplementation(input =>
typeof input === "string"
? input.split("-")[0]
: (input.transaction_id?.split("-")[0] ?? "0.0.0"),
);
(utils.getMemoFromBase64 as jest.Mock).mockImplementation(memo =>
memo ? `decoded-${memo}` : null,
);
(utils.createStakingRewardOperationHash as jest.Mock).mockImplementation(
hash => `${hash}-reward`,
);
(encodeOperationId as jest.Mock).mockImplementation(
(accountId, hash, type) => `${accountId}-${hash}-${type}`,
);
(encodeTokenAccountId as jest.Mock).mockImplementation(
(accountId, token) => `${accountId}-${token.id}`,
);
});
it("should return empty arrays when no transactions are found", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: [],
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "asc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(apiClient.getAccountTransactions).toHaveBeenCalledTimes(1);
expect(apiClient.getAccountTransactions).toHaveBeenCalledWith({
address,
fetchAllPages: true,
pagingToken: null,
order: "asc",
limit: 10,
});
expect(result.coinOperations).toEqual([]);
expect(result.tokenOperations).toEqual([]);
});
it("should parse HBAR transfer transactions correctly", async () => {
const address = "0.0.1234567";
const mockCurrency = getMockedCurrency();
const mockTransactions: Partial<HederaMirrorTransaction>[] = [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.1234567-1625097600-000000000",
charged_tx_fee: 500000,
result: "SUCCESS",
memo_base64: "test-memo",
token_transfers: [],
staking_reward_transfers: [],
transfers: [
{ account: address, amount: -1000000 },
{ account: "0.0.67890", amount: 1000000 },
],
name: "CRYPTOTRANSFER",
},
];
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: mockTransactions,
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.tokenOperations).toEqual([]);
expect(result.coinOperations).toHaveLength(1);
expect(result.coinOperations).toMatchObject([
{
type: "OUT",
value: expect.any(Object),
hash: "hash1",
fee: expect.any(Object),
date: expect.any(Date),
senders: [address],
recipients: ["0.0.67890"],
extra: {
pagingToken: "1625097600.000000000",
consensusTimestamp: "1625097600.000000000",
transactionId: "0.0.1234567-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
memo: "decoded-test-memo",
},
},
]);
});
it("should parse token transfer transactions correctly", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
const tokenId = "0.0.7890";
const mockToken = {
id: "token1",
contractAddress: tokenId,
standard: "hts",
name: "Test Token",
units: [{ name: "TT", code: "tt", magnitude: 6 }],
};
const mockTransactions: Partial<HederaMirrorTransaction>[] = [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.12345-1625097600-000000000",
charged_tx_fee: 500000,
result: "SUCCESS",
token_transfers: [
{ token_id: tokenId, account: address, amount: -1000 },
{ token_id: tokenId, account: "0.0.67890", amount: 1000 },
],
staking_reward_transfers: [],
transfers: [],
name: "CRYPTOTRANSFER",
},
];
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: mockTransactions,
nextCursor: null,
});
const findTokenByAddressInCurrencyMock = jest.fn().mockResolvedValue(mockToken);
setupMockCryptoAssetsStore({
findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.coinOperations).toMatchObject([
{
type: "FEES",
fee: expect.any(Object),
},
]);
expect(result.tokenOperations).toMatchObject([
{
type: "OUT",
value: expect.any(Object),
hash: "hash1",
contract: tokenId,
standard: "hts",
senders: [address],
recipients: ["0.0.67890"],
extra: {
pagingToken: "1625097600.000000000",
consensusTimestamp: "1625097600.000000000",
transactionId: "0.0.12345-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
},
},
]);
});
it("should parse token associate transactions correctly", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
const mockTransactions: Partial<HederaMirrorTransaction>[] = [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.12345-1625097600-000000000",
charged_tx_fee: 500000,
result: "SUCCESS",
token_transfers: [],
staking_reward_transfers: [],
transfers: [{ account: address, amount: -500000 }],
name: "TOKENASSOCIATE",
},
];
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: mockTransactions,
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.tokenOperations).toEqual([]);
expect(result.coinOperations).toMatchObject([
{
type: "ASSOCIATE_TOKEN",
value: expect.any(Object),
hash: "hash1",
fee: expect.any(Object),
senders: [address],
recipients: [],
extra: {
pagingToken: "1625097600.000000000",
consensusTimestamp: "1625097600.000000000",
transactionId: "0.0.12345-1625097600-000000000", // <-- the transactionId is used in upstream layers to identify the fees payer
},
},
]);
});
it("should skip token operations when token is not found in cryptoassets", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
const tokenId = "0.0.7890";
const mockTransactions: Partial<HederaMirrorTransaction>[] = [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
charged_tx_fee: 500000,
result: "SUCCESS",
token_transfers: [
{ token_id: tokenId, account: address, amount: -1000 },
{ token_id: tokenId, account: "0.0.67890", amount: 1000 },
],
staking_reward_transfers: [],
transfers: [],
name: "CRYPTOTRANSFER",
},
];
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: mockTransactions,
nextCursor: null,
});
const findTokenByAddressInCurrencyMock = jest.fn().mockResolvedValue(null);
setupMockCryptoAssetsStore({
findTokenByAddressInCurrency: findTokenByAddressInCurrencyMock,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.coinOperations).toEqual([]);
expect(result.tokenOperations).toEqual([]);
});
it("should use pagination parameters correctly", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: [],
nextCursor: null,
});
await listOperations({
currency: mockCurrency,
address,
limit: 20,
order: "asc",
cursor: "1625097500.000000000",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(apiClient.getAccountTransactions).toHaveBeenCalledTimes(1);
expect(apiClient.getAccountTransactions).toHaveBeenCalledWith({
address,
fetchAllPages: true,
pagingToken: "1625097500.000000000",
order: "asc",
limit: 20,
});
});
it("should handle failed transactions", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
const mockTransactions: Partial<HederaMirrorTransaction>[] = [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.12345-1625097600-000000000",
charged_tx_fee: 500000,
result: "INVALID_SIGNATURE",
memo_base64: "",
token_transfers: [],
staking_reward_transfers: [],
transfers: [
{ account: address, amount: -1000000 },
{ account: "0.0.67890", amount: 1000000 },
],
name: "CRYPTOTRANSFER",
},
];
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: mockTransactions,
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.coinOperations).toMatchObject([
{
hasFailed: true,
extra: {
transactionId: "0.0.12345-1625097600-000000000",
feesPayer: "0.0.12345",
},
},
]);
});
it("should include inferred fees payer in operation extra", async () => {
const address = "0.0.12345";
const mockCurrency = getMockedCurrency();
(utils.extractFeesPayer as jest.Mock).mockReturnValue("0.0.23");
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: [
{
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.10067173-1761755118-730000493",
charged_tx_fee: 40743,
result: "INSUFFICIENT_PAYER_BALANCE",
memo_base64: "",
token_transfers: [],
staking_reward_transfers: [],
transfers: [
{ account: "0.0.23", amount: -40743 },
{ account: "0.0.801", amount: 40743 },
],
name: "CRYPTOTRANSFER",
},
],
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
expect(result.coinOperations).toMatchObject([
{
extra: {
transactionId: "0.0.10067173-1761755118-730000493",
feesPayer: "0.0.23",
},
},
]);
});
it("should create REWARD operation when staking rewards are present", async () => {
const address = "0.0.1234567";
const mockCurrency = getMockedCurrency();
const mockTransaction: Partial<HederaMirrorTransaction> = {
consensus_timestamp: "1625097600.000000000",
transaction_hash: "hash1",
transaction_id: "0.0.1234567-1625097600-000000000",
charged_tx_fee: 500000,
result: "SUCCESS",
memo_base64: "",
token_transfers: [],
staking_reward_transfers: [{ account: address, amount: 1000000 }],
transfers: [{ account: address, amount: -500000 }],
name: "CRYPTOTRANSFER",
};
(apiClient.getAccountTransactions as jest.Mock).mockResolvedValue({
transactions: [mockTransaction],
nextCursor: null,
});
const result = await listOperations({
currency: mockCurrency,
address,
limit: 10,
order: "desc",
mirrorTokens: [],
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: false,
useSyntheticBlocks: false,
});
const rewardTimestamp = result.coinOperations[0].date.getTime();
const mainTimestamp = result.coinOperations[1].date.getTime();
expect(result.tokenOperations).toEqual([]);
expect(rewardTimestamp).toBe(mainTimestamp + 1);
expect(result.coinOperations).toMatchObject([
{
type: "REWARD",
hash: utils.createStakingRewardOperationHash(mockTransaction.transaction_hash ?? ""),
value: new BigNumber(1000000),
fee: new BigNumber(0),
senders: [getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID")],
recipients: [address],
extra: { transactionId: "0.0.1234567-1625097600-000000000" }, // <-- the transactionId is used in upstream layers to identify the fees payer
},
{
type: "OUT",
hash: mockTransaction.transaction_hash,
extra: { transactionId: "0.0.1234567-1625097600-000000000" }, // <-- the transactionId is used in upstream layers to identify the fees payer
},
]);
});
});