UNPKG

@ledgerhq/coin-tron

Version:
372 lines (339 loc) 10.9 kB
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers"; import { makeScanAccounts } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers"; import type { TokenCurrency } from "@ledgerhq/types-cryptoassets"; import { Account, AccountBridge, SyncConfig, TransactionCommon } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import { setupServer } from "msw/node"; import { firstValueFrom, reduce } from "rxjs"; import coinConfig, { TronCoinConfig } from "../config"; import * as tronNetwork from "../network"; import { mockServer, TRONGRID_BASE_URL_MOCKED } from "../network/index.mock"; import { AccountTronAPI } from "../network/types"; import { Transaction, TronAccount } from "../types"; import type { NetworkInfo } from "../types/bridge"; import accountFixture from "./fixtures/synchronization.account.fixture.json"; import { getAccountShape } from "./synchronization"; import { createBridges } from "./index"; // Create mock functions inside the factory to avoid hoisting issues jest.mock("../network", () => { const actual = jest.requireActual<typeof import("../network")>("../network"); // Create mocks that default to actual implementations const mocks = { getTronAccountNetwork: jest.fn(actual.getTronAccountNetwork), getLastBlock: jest.fn(actual.getLastBlock), fetchTronAccount: jest.fn(actual.fetchTronAccount), fetchTronAccountTxs: jest.fn(actual.fetchTronAccountTxs), getUnwithdrawnReward: jest.fn(actual.getUnwithdrawnReward), }; return { ...actual, ...mocks, // Export mocks for tests to access __mocks: mocks, }; }); // Import the mocked module to access the mocks // Get mock functions from the mocked module const mockFunctions = (tronNetwork as typeof tronNetwork & { __mocks: typeof mockFunctions }) .__mocks as { getTronAccountNetwork: jest.Mock; getLastBlock: jest.Mock; fetchTronAccount: jest.Mock; fetchTronAccountTxs: jest.Mock; getUnwithdrawnReward: jest.Mock; }; const currency = getCryptoCurrencyById("tron"); const defaultSyncConfig = { paginationConfig: {}, }; function syncAccount<T extends TransactionCommon, A extends Account = Account>( bridge: AccountBridge<T, A>, account: A, syncConfig: SyncConfig = defaultSyncConfig, ): Promise<A> { return firstValueFrom( bridge.sync(account, syncConfig).pipe(reduce((a, f: (arg0: A) => A) => f(a), account)), ); } const dummyAccount: TronAccount = { type: "Account", id: "", derivationMode: "", seedIdentifier: "", used: false, currency, index: 0, freshAddress: "", freshAddressPath: "", swapHistory: [], blockHeight: 0, balance: new BigNumber(0), spendableBalance: new BigNumber(0), operationsCount: 0, operations: [], pendingOperations: [], lastSyncDate: new Date(0), creationDate: new Date(), balanceHistoryCache: { HOUR: { balances: [], latestDate: undefined, }, DAY: { balances: [], latestDate: undefined, }, WEEK: { balances: [], latestDate: undefined, }, }, tronResources: {} as any, }; const reviver = (key: string, value: unknown) => { // Format date if ( [ "date", "creationDate", "expiredAt", "lastVotedDate", "lastWithdrawnRewardDate", // "latestDate", ].includes(key) === true ) { return value !== null ? typeof value === "string" ? new Date(value as string) : new Date(value as number) : new Date("1970-01-01T00:00:00.000Z"); } // BigNumber conversion if ( [ "unwithdrawnReward", "amount", "balance", "spendableBalance", "freeLimit", "freeUsed", "gainedLimit", "gainedUsed", "value", "frozenAmount", "unfreezeAmount", "energy", "fee", ].includes(key) === true ) { return typeof value === "string" ? new BigNumber(value as string) : value; } // Remove undesired properties as they always change //FIXME: balanceHistoryCache if (["lastSyncDate", "latestDate", "balanceHistoryCache"].includes(key) === true) { return undefined; } return value; }; describe("sync", () => { let bridge: ReturnType<typeof createBridges>; beforeAll(() => { // Mock the crypto assets store with the expected tokens const parentCurrency = { type: "CryptoCurrency", id: "tron", coinType: 195, name: "Tron", managerAppName: "Tron", ticker: "TRX", scheme: "tron", color: "#D9012C", family: "tron", blockAvgTime: 9, units: [{ name: "TRX", code: "TRX", magnitude: 6 }], explorerViews: [ { tx: "https://tronscan.org/#/transaction/$hash", address: "https://tronscan.org/#/address/$address", }, ], keywords: ["trx", "tron"], }; const mockTokens = { "tron/trc10/1002000": { type: "TokenCurrency", id: "tron/trc10/1002000", contractAddress: "TF5Bn4cJCT6GVeUgyCN4rBhDg42KBrpAjg", parentCurrency, tokenType: "trc10", name: "BitTorrent Old", ticker: "BTTOLD", delisted: true, disableCountervalue: false, ledgerSignature: "0a0a426974546f7272656e7410061a46304402202e2502f36b00e57be785fc79ec4043abcdd4fdd1b58d737ce123599dffad2cb602201702c307f009d014a553503b499591558b3634ceee4c054c61cedd8aca94c02b", units: [{ name: "BitTorrent Old", code: "BTTOLD", magnitude: 6 }], }, "tron/trc20/tla2f6vpqdgre67v1736s7bj8ray5wyju7": { type: "TokenCurrency", id: "tron/trc20/tla2f6vpqdgre67v1736s7bj8ray5wyju7", contractAddress: "TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7", parentCurrency, tokenType: "trc20", name: "WINK", ticker: "WIN", delisted: false, disableCountervalue: false, ledgerSignature: null, units: [{ name: "WINK", code: "WIN", magnitude: 6 }], }, "tron/trc20/tcfll5dx5zjdknwuesxxi1vpwjlvmwzzy9": { type: "TokenCurrency", id: "tron/trc20/tcfll5dx5zjdknwuesxxi1vpwjlvmwzzy9", contractAddress: "TCFLL5dx5ZJdKnWuesXxi1VPwjLVmWZZy9", parentCurrency, tokenType: "trc20", name: "JUST GOV", ticker: "JST", delisted: false, disableCountervalue: false, ledgerSignature: null, units: [{ name: "JUST GOV", code: "JST", magnitude: 18 }], }, }; setupMockCryptoAssetsStore({ findTokenById: async (id: string): Promise<TokenCurrency | undefined> => { return ( (mockTokens[id as keyof typeof mockTokens] as TokenCurrency | undefined) || undefined ); }, findTokenByAddressInCurrency: async ( address: string, currencyId: string, ): Promise<TokenCurrency | undefined> => { if (currencyId === "tron") { const token = Object.values(mockTokens).find( token => token.contractAddress.toLowerCase() === address.toLowerCase(), ); return (token as TokenCurrency | undefined) || undefined; } return undefined; }, }); const signer = jest.fn(); const coinConfig = (): TronCoinConfig => ({ status: { type: "active", }, explorer: { url: TRONGRID_BASE_URL_MOCKED, }, }); bridge = createBridges(signer, coinConfig); mockServer.listen({ onUnhandledRequest: "error" }); }); afterAll(() => { mockServer.close(); }); it.each([ { id: "TL24LCps5FKwp3PoU1MvrYrwhi5LU1tHre", expectedAccount: JSON.parse( JSON.stringify(accountFixture["TL24LCps5FKwp3PoU1MvrYrwhi5LU1tHre"]), reviver, ), }, ])("should always be sync without error for address %s", async ({ id, expectedAccount }) => { const account = await syncAccount<Transaction, TronAccount>(bridge.accountBridge, { ...dummyAccount, id: `js:2:tron:${id}:`, freshAddress: id, }); expect(account.id).toEqual(`js:2:tron:${id}:`); expect(account).toMatchObject(expectedAccount); }); }); describe("scanAccounts", () => { // Use the mock functions defined at module level const mockGetTronAccountNetwork = mockFunctions.getTronAccountNetwork; const mockGetLastBlock = mockFunctions.getLastBlock; const mockFetchTronAccount = mockFunctions.fetchTronAccount; const mockFetchTronAccountTxs = mockFunctions.fetchTronAccountTxs; const mockGetUnwithdrawnReward = mockFunctions.getUnwithdrawnReward; const localMockServer = setupServer(); const address = "TT2T17KZhoDu47i2E4FWxfG79zdkEWkU9N"; beforeAll(() => { setupMockCryptoAssetsStore(); coinConfig.setCoinConfig(() => ({ status: { type: "active", }, explorer: { url: TRONGRID_BASE_URL_MOCKED, }, })); localMockServer.listen({ onUnhandledRequest: "error" }); }); beforeEach(() => { // Reset mocks and set default implementations mockGetLastBlock.mockReset(); mockFetchTronAccount.mockReset(); mockFetchTronAccountTxs.mockReset(); mockGetUnwithdrawnReward.mockReset(); mockGetTronAccountNetwork.mockReset(); mockGetLastBlock.mockResolvedValueOnce({ height: 0, hash: "", time: new Date() }); mockFetchTronAccount.mockResolvedValueOnce([ { address, } as AccountTronAPI, ]); mockFetchTronAccountTxs.mockResolvedValueOnce([]); mockGetUnwithdrawnReward.mockResolvedValueOnce(BigNumber(0)); }); afterAll(() => { jest.clearAllMocks(); localMockServer.close(); }); it.each([ { freeNetLimit: new BigNumber(0), expectedUsed: false, }, { freeNetLimit: new BigNumber(100), expectedUsed: true, }, ])("returns an account flagged as used", async ({ freeNetLimit, expectedUsed }) => { // Given mockGetTronAccountNetwork.mockResolvedValueOnce({ family: "tron", freeNetUsed: BigNumber(0), freeNetLimit: BigNumber(freeNetLimit), netUsed: BigNumber(0), netLimit: BigNumber(0), energyUsed: BigNumber(0), energyLimit: BigNumber(0), } as NetworkInfo); const addressResolver = { address: "TT2T17KZhoDu47i2E4FWxfG79zdkEWkU9N", path: "path", publicKey: "publicKey", }; // When const scanAccounts = makeScanAccounts({ getAccountShape, getAddressFn: (_deviceId, _addressOpt) => Promise.resolve(addressResolver), }); const { account } = await firstValueFrom( scanAccounts({ currency, deviceId: "", syncConfig: defaultSyncConfig, }), ); // Then expect(mockGetTronAccountNetwork).toHaveBeenCalledTimes(1); expect(account.used).toEqual(expectedUsed); }); });