UNPKG

@ledgerhq/coin-hedera

Version:
832 lines (736 loc) 28.2 kB
import type { BlockInfo } from "@ledgerhq/coin-module-framework/api/types"; import { getEnv } from "@ledgerhq/live-env"; import { BigNumber } from "bignumber.js"; import { HEDERA_TRANSACTION_NAMES, FINALITY_MS } from "../constants"; import { apiClient } from "../network/api"; import { hgraphClient } from "../network/hgraph"; import { enrichERC20Transfers } from "../network/utils"; import { getMockedEnrichedERC20Transfer } from "../test/fixtures/common.fixture"; import { getMockedERC20TokenCurrency } from "../test/fixtures/currency.fixture"; import { getMockedERC20TokenTransfer } from "../test/fixtures/hgraph.fixture"; import { getMockedMirrorAccount, getMockedMirrorContractCallResult, getMockedMirrorTransaction, } from "../test/fixtures/mirror.fixture"; import type { StakingAnalysis } from "../types"; import { getBlockV2 } from "./getBlock.v2"; import { getBlockInfo } from "./getBlockInfo"; import { analyzeStakingOperation, getDateRangeFromBlockHeight } from "./utils"; jest.mock("./getBlockInfo"); jest.mock("../network/api"); jest.mock("../network/hgraph"); jest.mock("../network/utils", () => ({ ...jest.requireActual("../network/utils"), enrichERC20Transfers: jest.fn(), })); jest.mock("./utils", () => ({ ...jest.requireActual("./utils"), getDateRangeFromBlockHeight: jest.fn(), analyzeStakingOperation: jest.fn().mockResolvedValue(null), fromEVMAddress: jest.fn().mockImplementation((evmAddress: string) => { const addressMap: Record<string, string> = { "0x0000000000000000000000000000000000000999": "0.0.999", "0x0000000000000000000000000000000000001001": "0.0.1001", }; return addressMap[evmAddress] || null; }), })); describe("getBlockV2", () => { const mockBlockInfo: BlockInfo = { height: 100, hash: "mock_hash", time: new Date("2024-01-01T00:00:00Z"), }; const mockDateRange = { start: new Date(1704067200123), end: new Date(1704067260456), }; beforeEach(() => { jest.clearAllMocks(); (getBlockInfo as jest.Mock).mockResolvedValue(mockBlockInfo); (getDateRangeFromBlockHeight as jest.Mock).mockReturnValue(mockDateRange); (analyzeStakingOperation as jest.Mock).mockResolvedValue(null); (enrichERC20Transfers as jest.Mock).mockReturnValue([]); (hgraphClient.getLatestIndexedConsensusTimestamp as jest.Mock).mockResolvedValue( new BigNumber("1704067200123000000000"), ); }); it("should return empty block when no transactions exist", async () => { (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([]); const result = await getBlockV2(100); expect(result).toEqual({ info: mockBlockInfo, transactions: [], }); }); it("should call dependencies with correct parameters", async () => { (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([]); await getBlockV2(42); expect(getDateRangeFromBlockHeight).toHaveBeenCalledWith(42); expect(getBlockInfo).toHaveBeenCalledWith(42); expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledTimes(1); expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalledWith({ startTimestamp: `gte:1704067200.123`, endTimestamp: `lt:1704067260.456`, limit: 100, order: "desc", }); expect(hgraphClient.getERC20TransfersByTimestampRange).toHaveBeenCalledTimes(1); expect(hgraphClient.getERC20TransfersByTimestampRange).toHaveBeenCalledWith({ startTimestamp: "1704067200.123000000", endTimestamp: "1704067260.456000000", limit: 100, order: "desc", }); expect(enrichERC20Transfers).toHaveBeenCalledTimes(1); expect(hgraphClient.getLatestIndexedConsensusTimestamp).toHaveBeenCalledTimes(1); }); it("should extract fee payer from transaction_id by default", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "SUCCESS", charged_tx_fee: 100000, staking_reward_transfers: [], transfers: [], token_transfers: [], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); expect(result.transactions[0].feesPayer).toBe("0.0.999"); }); it("should infer fee payer from transfers when initiator is not debited", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.10067173-1761755118-730000493", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "INSUFFICIENT_PAYER_BALANCE", charged_tx_fee: 40743, staking_reward_transfers: [], transfers: [ { account: "0.0.23", amount: -40743 }, { account: "0.0.801", amount: 40743 }, ], token_transfers: [], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); const payerOperation = result.transactions[0].operations.find(op => op.address === "0.0.23"); expect(result.transactions[0].feesPayer).toBe("0.0.23"); expect(payerOperation).toMatchObject({ address: "0.0.23", amount: BigInt(0), }); }); it("should exclude fee from payer's operation amount", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "SUCCESS", charged_tx_fee: 67179, staking_reward_transfers: [], transfers: [ { account: "0.0.999", amount: -567179, }, { account: "0.0.1001", amount: 500000, }, ], token_transfers: [], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); const senderOperation = result.transactions[0].operations.find(op => op.address === "0.0.999"); expect(senderOperation).toMatchObject({ address: "0.0.999", amount: BigInt(-567179 + 67179), }); }); it("should handle HTS token transfers", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "SUCCESS", charged_tx_fee: 100000, staking_reward_transfers: [], transfers: [], token_transfers: [ { token_id: "0.0.12345", account: "0.0.999", amount: -1000, }, { token_id: "0.0.12345", account: "0.0.1001", amount: 1000, }, ], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); expect(result.transactions[0].operations).toEqual([ { type: "transfer", address: "0.0.999", asset: { type: "hts", assetReference: "0.0.12345", }, amount: BigInt(-1000), }, { type: "transfer", address: "0.0.1001", asset: { type: "hts", assetReference: "0.0.12345", }, amount: BigInt(1000), }, ]); }); it("should handle ERC20 token transfers", async () => { const mockMirrorAccount = getMockedMirrorAccount({ account: "0.0.12345" }); const mockTokenERC20 = getMockedERC20TokenCurrency(); const mockMirrorTransaction = getMockedMirrorTransaction({ consensus_timestamp: "1625097600.000000000", transaction_hash: "erc20-transfer-hash", charged_tx_fee: 300000, result: "SUCCESS", name: "CONTRACTCALL", memo_base64: "xyz", staking_reward_transfers: [], token_transfers: [], transfers: [{ account: mockMirrorAccount.account, amount: -300000 }], }); const mockERC20Transfer = getMockedERC20TokenTransfer({ token_evm_address: mockTokenERC20.contractAddress, transaction_hash: mockMirrorTransaction.transaction_hash, consensus_timestamp: Number(mockMirrorTransaction.consensus_timestamp.split(".")[0]) * 10 ** 9, sender_account_id: 12345, receiver_account_id: 67890, sender_evm_address: mockMirrorAccount.evm_address, receiver_evm_address: "0xrecipient", payer_account_id: 12345, amount: 5000000, }); const mockContractCallResult = getMockedMirrorContractCallResult({ block_hash: "0xblockhash123", gas_consumed: 75000, gas_limit: 100000, gas_used: 75000, }); const mockEnrichedERC20Transfer = getMockedEnrichedERC20Transfer({ mirrorTransaction: mockMirrorTransaction, contractCallResult: mockContractCallResult, transfers: [mockERC20Transfer], }); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([ mockMirrorTransaction, ]); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([ mockERC20Transfer, ]); (enrichERC20Transfers as jest.Mock).mockReturnValue([mockEnrichedERC20Transfer]); const result = await getBlockV2(100); expect(result.transactions[0].operations).toEqual([ { type: "transfer", address: "0.0.12345", asset: { type: "native", }, amount: BigInt(0), }, { type: "transfer", address: "0.0.67890", asset: { type: "erc20", assetReference: mockTokenERC20.contractAddress, }, amount: BigInt(mockERC20Transfer.amount), }, { type: "transfer", address: "0.0.12345", asset: { type: "erc20", assetReference: mockTokenERC20.contractAddress, }, amount: BigInt(-mockERC20Transfer.amount), }, ]); }); it("should deduplicate CONTRACT_CALL when matching ERC20 transfer exists", async () => { const mockMirrorAccount = getMockedMirrorAccount({ account: "0.0.12345" }); const mockTokenERC20 = getMockedERC20TokenCurrency(); const sharedHash = "duplicate-hash"; const sharedTimestamp = "1625097600.000000000"; const mockMirrorContractCall = getMockedMirrorTransaction({ consensus_timestamp: sharedTimestamp, transaction_hash: sharedHash, transaction_id: "0.0.12345-1625097600-000000000", charged_tx_fee: 300000, result: "SUCCESS", name: "CONTRACTCALL", staking_reward_transfers: [], token_transfers: [], transfers: [{ account: mockMirrorAccount.account, amount: -300000 }], }); const mockERC20Transfer = getMockedERC20TokenTransfer({ token_evm_address: mockTokenERC20.contractAddress, transaction_hash: sharedHash, consensus_timestamp: Number(sharedTimestamp.split(".")[0]) * 10 ** 9, sender_account_id: 12345, receiver_account_id: 67890, sender_evm_address: mockMirrorAccount.evm_address, receiver_evm_address: "0xrecipient", payer_account_id: 12345, amount: 5000000, }); const mockContractCallResult = getMockedMirrorContractCallResult(); const mockEnrichedERC20Transfer = getMockedEnrichedERC20Transfer({ mirrorTransaction: mockMirrorContractCall, contractCallResult: mockContractCallResult, transfers: [mockERC20Transfer], }); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([ mockMirrorContractCall, ]); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([ mockERC20Transfer, ]); (enrichERC20Transfers as jest.Mock).mockReturnValue([mockEnrichedERC20Transfer]); const result = await getBlockV2(100); expect(result.transactions).toEqual([expect.objectContaining({ hash: sharedHash })]); }); it("should handle ERC20 transfer with null account_ids (using EVM addresses only)", async () => { const mockMirrorAccount = getMockedMirrorAccount({ account: "0.0.12345" }); const mockTokenERC20 = getMockedERC20TokenCurrency(); const mockMirrorTransaction = getMockedMirrorTransaction({ consensus_timestamp: "1625097600.000000000", transaction_hash: "erc20-transfer-hash", charged_tx_fee: 300000, result: "SUCCESS", name: "CONTRACTCALL", staking_reward_transfers: [], token_transfers: [], transfers: [{ account: mockMirrorAccount.account, amount: -300000 }], }); const mockERC20Transfer = getMockedERC20TokenTransfer({ token_evm_address: mockTokenERC20.contractAddress, transaction_hash: mockMirrorTransaction.transaction_hash, consensus_timestamp: Number(mockMirrorTransaction.consensus_timestamp.split(".")[0]) * 10 ** 9, sender_account_id: null, receiver_account_id: null, sender_evm_address: "0x0000000000000000000000000000000000000999", receiver_evm_address: "0x0000000000000000000000000000000000001001", payer_account_id: 12345, amount: 5000000, }); const mockContractCallResult = getMockedMirrorContractCallResult(); const mockEnrichedERC20Transfer = getMockedEnrichedERC20Transfer({ mirrorTransaction: mockMirrorTransaction, contractCallResult: mockContractCallResult, transfers: [mockERC20Transfer], }); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([ mockMirrorTransaction, ]); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([ mockERC20Transfer, ]); (enrichERC20Transfers as jest.Mock).mockReturnValue([mockEnrichedERC20Transfer]); const result = await getBlockV2(100); expect(result.transactions).toHaveLength(1); expect(result.transactions[0].operations).toEqual([ { address: mockMirrorAccount.account, amount: BigInt(0), asset: { type: "native", }, type: "transfer", }, { type: "transfer", address: mockERC20Transfer.receiver_evm_address, asset: { type: "erc20", assetReference: mockTokenERC20.contractAddress, }, amount: BigInt(mockERC20Transfer.amount), }, { type: "transfer", address: mockERC20Transfer.sender_evm_address, asset: { type: "erc20", assetReference: mockTokenERC20.contractAddress, }, amount: BigInt(-mockERC20Transfer.amount), }, ]); }); it.each([ { label: "sender evm address is null", transferOverrides: { sender_account_id: null, sender_evm_address: null }, }, { label: "recipient evm address is null", transferOverrides: { receiver_account_id: null, receiver_evm_address: null }, }, ])("should skip ERC20 operations when $label", async ({ transferOverrides }) => { const mockMirrorAccount = getMockedMirrorAccount({ account: "0.0.12345" }); const mockTokenERC20 = getMockedERC20TokenCurrency(); const mockMirrorTransaction = getMockedMirrorTransaction({ consensus_timestamp: "1625097600.000000000", transaction_hash: "erc20-transfer-hash", result: "SUCCESS", name: "CONTRACTCALL", transfers: [{ account: mockMirrorAccount.account, amount: -300000 }], }); const mockERC20Transfer = getMockedERC20TokenTransfer({ token_evm_address: mockTokenERC20.contractAddress, transaction_hash: mockMirrorTransaction.transaction_hash, ...transferOverrides, }); const mockContractCallResult = getMockedMirrorContractCallResult(); const mockEnrichedERC20Transfer = getMockedEnrichedERC20Transfer({ mirrorTransaction: mockMirrorTransaction, contractCallResult: mockContractCallResult, transfers: [mockERC20Transfer], }); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([ mockMirrorTransaction, ]); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([ mockERC20Transfer, ]); (enrichERC20Transfers as jest.Mock).mockReturnValue([mockEnrichedERC20Transfer]); const result = await getBlockV2(100); const operations = result.transactions[0]?.operations; const erc20Operations = operations?.filter( op => op.type === "transfer" && op.asset.type === "erc20", ); expect(erc20Operations).toEqual([]); }); it("should mark failed transactions", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "INSUFFICIENT_ACCOUNT_BALANCE", charged_tx_fee: 100000, staking_reward_transfers: [], transfers: [], token_transfers: [], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); expect(result.transactions[0].failed).toBe(true); }); it("should analyze CRYPTOUPDATEACCOUNT transactions for staking", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash_update", name: HEDERA_TRANSACTION_NAMES.UpdateAccount, result: "SUCCESS", charged_tx_fee: 22000, consensus_timestamp: "1704067210.123456789", staking_reward_transfers: [], transfers: [], token_transfers: [], }); const mockStakingAnalysis: StakingAnalysis = { operationType: "DELEGATE", targetStakingNodeId: 5, previousStakingNodeId: null, stakedAmount: BigInt(100), }; (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); (analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis); const result = await getBlockV2(100); expect(analyzeStakingOperation).toHaveBeenCalledTimes(1); expect(analyzeStakingOperation).toHaveBeenCalledWith("0.0.999", mockTx); expect(result.transactions[0].operations).toHaveLength(1); expect(result.transactions[0].operations[0]).toEqual({ type: "other", operationType: mockStakingAnalysis.operationType, stakedNodeId: mockStakingAnalysis.targetStakingNodeId, previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId, stakedAmount: mockStakingAnalysis.stakedAmount, }); }); it("should handle UNDELEGATE staking operation", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash_undelegate", name: HEDERA_TRANSACTION_NAMES.UpdateAccount, result: "SUCCESS", charged_tx_fee: 22000, consensus_timestamp: "1704067210.123456789", staking_reward_transfers: [], transfers: [], token_transfers: [], }); const mockStakingAnalysis: StakingAnalysis = { operationType: "UNDELEGATE", targetStakingNodeId: null, previousStakingNodeId: 3, stakedAmount: BigInt(100), }; (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); (analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis); const result = await getBlockV2(100); expect(result.transactions[0].operations[0]).toEqual({ type: "other", operationType: mockStakingAnalysis.operationType, stakedNodeId: mockStakingAnalysis.targetStakingNodeId, previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId, stakedAmount: mockStakingAnalysis.stakedAmount, }); }); it("should handle REDELEGATE staking operation", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash_redelegate", name: HEDERA_TRANSACTION_NAMES.UpdateAccount, result: "SUCCESS", charged_tx_fee: 22000, consensus_timestamp: "1704067210.123456789", staking_reward_transfers: [], transfers: [], token_transfers: [], }); const mockStakingAnalysis: StakingAnalysis = { operationType: "REDELEGATE", targetStakingNodeId: 10, previousStakingNodeId: 5, stakedAmount: BigInt(100), }; (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); (analyzeStakingOperation as jest.Mock).mockResolvedValue(mockStakingAnalysis); const result = await getBlockV2(100); expect(result.transactions[0].operations).toEqual([ { type: "other", operationType: mockStakingAnalysis.operationType, stakedNodeId: mockStakingAnalysis.targetStakingNodeId, previousStakedNodeId: mockStakingAnalysis.previousStakingNodeId, stakedAmount: mockStakingAnalysis.stakedAmount, }, ]); }); it("should create CLAIM_REWARDS operations for staking reward transfers", async () => { const account1 = "0.0.999"; const account2 = "0.0.1001"; const stakingRewardAccount = getEnv("HEDERA_STAKING_REWARD_ACCOUNT_ID"); const rewardAccount1 = 30313674; const rewardAccount2 = 191772; const chargedFee = 79874; const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash", name: "CRYPTOTRANSFER", result: "SUCCESS", charged_tx_fee: chargedFee, staking_reward_transfers: [ { account: account1, amount: rewardAccount1, }, { account: account2, amount: rewardAccount2, }, ], transfers: [ { account: "0.0.35", amount: 3235, }, { account: stakingRewardAccount, amount: -30505446, }, { account: "0.0.801", amount: 76639, }, { account: account1, amount: 29233800, }, { account: account2, amount: 1191772, }, ], token_transfers: [], }); const totalRewards = mockTx.staking_reward_transfers.reduce((acc, t) => acc + t.amount, 0); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); const result = await getBlockV2(100); expect(result.transactions[0].operations).toEqual([ { type: "transfer", address: "0.0.35", asset: { type: "native" }, amount: BigInt(3235), }, { type: "transfer", address: stakingRewardAccount, asset: { type: "native" }, amount: BigInt(-totalRewards), }, { type: "transfer", address: "0.0.801", asset: { type: "native" }, amount: BigInt(76639), }, { type: "transfer", address: account1, asset: { type: "native" }, amount: BigInt(29233800 + chargedFee - rewardAccount1), }, { type: "transfer", address: account2, asset: { type: "native" }, amount: BigInt(1191772 - rewardAccount2), }, { type: "transfer", address: account1, asset: { type: "native" }, amount: BigInt(rewardAccount1), }, { type: "transfer", address: account2, asset: { type: "native" }, amount: BigInt(rewardAccount2), }, ]); }); it("should handle CRYPTOUPDATEACCOUNT if it's not related to staking", async () => { const mockTx = getMockedMirrorTransaction({ transaction_id: "0.0.999-1234567890-000000000", transaction_hash: "hash_regular_update", name: HEDERA_TRANSACTION_NAMES.UpdateAccount, result: "SUCCESS", charged_tx_fee: 22000, consensus_timestamp: "1704067210.123456789", staking_reward_transfers: [], transfers: [ { account: "0.0.999", amount: -23000, }, { account: "0.0.1000", amount: 1000, }, ], token_transfers: [], }); (hgraphClient.getERC20TransfersByTimestampRange as jest.Mock).mockResolvedValue([]); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([mockTx]); (analyzeStakingOperation as jest.Mock).mockResolvedValue(null); const result = await getBlockV2(100); expect(result.transactions[0].operations).toEqual([ { type: "transfer", address: "0.0.999", asset: { type: "native" }, amount: BigInt(-1000), }, { type: "transfer", address: "0.0.1000", asset: { type: "native" }, amount: BigInt(1000), }, ]); }); it("should throw error when querying a block in the future", async () => { const now = Date.now(); const futureRange = { start: new Date(now + 60_000), end: new Date(now + 120_000), }; (getDateRangeFromBlockHeight as jest.Mock).mockReturnValue(futureRange); await expect(getBlockV2(999)).rejects.toThrow("Block 999 is not available yet"); expect(getBlockInfo).not.toHaveBeenCalled(); expect(apiClient.getTransactionsByTimestampRange).not.toHaveBeenCalled(); }); it("should throw error when querying a block overlapping the non-finalized window", async () => { const now = Date.now(); const overlappingRange = { start: new Date(now - FINALITY_MS - 1_000), end: new Date(now - FINALITY_MS / 2), }; (getDateRangeFromBlockHeight as jest.Mock).mockReturnValue(overlappingRange); await expect(getBlockV2(998)).rejects.toThrow("Block 998 is not available yet"); expect(getBlockInfo).not.toHaveBeenCalled(); expect(apiClient.getTransactionsByTimestampRange).not.toHaveBeenCalled(); }); it("should succeed when querying the finalized window", async () => { const now = Date.now(); const finalizedRange = { start: new Date(now - FINALITY_MS - 120_000), end: new Date(now - FINALITY_MS - 60_000), }; (getDateRangeFromBlockHeight as jest.Mock).mockReturnValue(finalizedRange); (apiClient.getTransactionsByTimestampRange as jest.Mock).mockResolvedValue([]); const result = await getBlockV2(100); expect(result).toEqual({ info: mockBlockInfo, transactions: [], }); expect(getBlockInfo).toHaveBeenCalledWith(100); expect(apiClient.getTransactionsByTimestampRange).toHaveBeenCalled(); }); it("should throw error when hgraph is not synced up to the end of the block range", async () => { const blockHeight = 100; // 1 ns behind the end of the block's date range const endTimestampNs = new BigNumber(mockDateRange.end.getTime()) .multipliedBy(10 ** 6) .minus(1); (hgraphClient.getLatestIndexedConsensusTimestamp as jest.Mock).mockResolvedValue( endTimestampNs, ); await expect(getBlockV2(blockHeight)).rejects.toThrow( `Block ${blockHeight} has no ERC20 synced yet (${endTimestampNs})`, ); expect(getBlockInfo).not.toHaveBeenCalled(); expect(apiClient.getTransactionsByTimestampRange).not.toHaveBeenCalled(); }); });