@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
269 lines (241 loc) • 8.05 kB
text/typescript
import { getEnv } from "@ledgerhq/live-env";
import network from "@ledgerhq/live-network";
import type { LiveNetworkResponse } from "@ledgerhq/live-network/network";
import BigNumber from "bignumber.js";
import invariant from "invariant";
import type {
ERC20TokenAccount,
ERC20TokenTransfer,
HgraphErcTokenAccountResponse,
HgraphErcTokenTransferResponse,
HgraphLatestIndexedConsensusTimestampResponse,
HgraphResponse,
} from "../types/hgraph";
const getPaginationDirection = (fetchAllPages: boolean, order: string) => {
if (fetchAllPages) return "_gt";
return order === "asc" ? "_gt" : "_lt";
};
const throwOnGraphQLErrors: <T>(
res: LiveNetworkResponse<HgraphResponse<T>>,
context: string,
) => asserts res is LiveNetworkResponse<{ data: T }> = (res, context) => {
if ("errors" in res.data) {
const reason = res.data.errors[0]?.message ?? "";
throw new Error(`hedera: failed to fetch ${context} from Hgraph: ${reason}`);
}
};
async function getLatestIndexedConsensusTimestamp(): Promise<BigNumber> {
const res = await network<HgraphLatestIndexedConsensusTimestampResponse>({
url: getEnv("API_HEDERA_HGRAPH"),
method: "POST",
data: {
query: `
query LatestTransaction {
ethereum_transaction(
limit: 1,
order_by: { consensus_timestamp: desc }
) {
consensus_timestamp
}
}
`,
},
});
throwOnGraphQLErrors(res, "latest indexed consensus timestamp");
const lastTransactionTimestamp = res.data.data.ethereum_transaction[0]?.consensus_timestamp;
invariant(lastTransactionTimestamp, "No transactions found in Hgraph");
return new BigNumber(lastTransactionTimestamp);
}
async function getERC20Balances({ address }: { address: string }): Promise<ERC20TokenAccount[]> {
const res = await network<HgraphErcTokenAccountResponse>({
url: getEnv("API_HEDERA_HGRAPH"),
method: "POST",
data: {
query: `
query GetAccountPortfolio($accountId: bigint!) {
erc_token_account(
where: {
account_id: { _eq: $accountId }
}
) {
token_id
balance
balance_timestamp
created_timestamp
}
}
`,
variables: {
accountId: address.split(".").pop(),
},
},
});
throwOnGraphQLErrors(res, "ERC20 balances");
return res.data.data.erc_token_account;
}
async function getERC20Transfers({
address,
tokenEvmAddresses,
timestamp,
limit = 100,
order = "desc",
fetchAllPages,
}: {
address: string;
tokenEvmAddresses: string[];
fetchAllPages: boolean;
timestamp?: string;
limit?: number;
order?: "asc" | "desc";
}): Promise<ERC20TokenTransfer[]> {
if (tokenEvmAddresses.length === 0) {
return [];
}
let hasMorePages = true;
let cursor = timestamp?.replace(".", "") ?? null;
const transfers: ERC20TokenTransfer[] = [];
const accountId = address.split(".").pop();
while (hasMorePages) {
const res = await network<HgraphErcTokenTransferResponse>({
url: getEnv("API_HEDERA_HGRAPH"),
method: "POST",
data: {
query: `
query GetAccountTransfers($accountId: bigint!, $tokenEvmAddresses: [String!]!, $cursor: bigint, $limit: Int!) {
erc_token_transfer(
where: {
transfer_type: { _in: ["transfer", "mint", "burn"] }
contract_type: { _eq: "ERC_20" }
token_evm_address: { _in: $tokenEvmAddresses }
${cursor ? `consensus_timestamp: { ${getPaginationDirection(fetchAllPages, order)}: $cursor }` : ""}
_or: [
{ sender_account_id: { _eq: $accountId } }
{ receiver_account_id: { _eq: $accountId } }
]
}
order_by: { consensus_timestamp: ${order} }
limit: $limit
) {
token_id
token_evm_address
sender_evm_address
sender_account_id
receiver_evm_address
receiver_account_id
payer_account_id
amount
transfer_type
consensus_timestamp
transaction_hash
}
}
`,
variables: {
accountId,
tokenEvmAddresses,
limit,
...(cursor && { cursor }),
},
},
});
throwOnGraphQLErrors(res, "ERC20 transfers");
const newTransfers = res.data.data.erc_token_transfer;
transfers.push(...newTransfers);
// stop fetching if pagination mode is used and we reached the limit
if (!fetchAllPages && transfers.length >= limit) {
hasMorePages = false;
}
// stop if no more results (empty array indicates no more data)
if (newTransfers.length === 0 || newTransfers.length < limit) {
hasMorePages = false;
}
if (hasMorePages) {
// update cursor to the last item's timestamp for next iteration
const lastTransfer = newTransfers[newTransfers.length - 1];
cursor = lastTransfer.consensus_timestamp.toString();
}
}
// ensure we don't exceed the limit when not fetching all pages
if (!fetchAllPages && transfers.length > limit) {
transfers.splice(limit);
}
return transfers;
}
async function getERC20TransfersByTimestampRange({
startTimestamp,
endTimestamp,
order = "desc",
limit = 100,
}: {
startTimestamp: string;
endTimestamp: string;
order?: "asc" | "desc";
limit?: number;
}): Promise<ERC20TokenTransfer[]> {
const transfers: ERC20TokenTransfer[] = [];
let hasMorePages = true;
let cursor: string | null = null;
const normalizedStartTimestamp = startTimestamp.replace(".", "");
const normalizedEndTimestamp = endTimestamp.replace(".", "");
while (hasMorePages) {
const res: LiveNetworkResponse<HgraphErcTokenTransferResponse> = await network({
url: getEnv("API_HEDERA_HGRAPH"),
method: "POST",
data: {
query: `
query GetAccountTransfers($startTimestamp: bigint!, $endTimestamp: bigint!, $cursor: bigint, $limit: Int!) {
erc_token_transfer(
where: {
transfer_type: { _in: ["transfer", "mint", "burn"] }
contract_type: { _eq: "ERC_20" }
consensus_timestamp: {
${cursor ? "_gt: $cursor" : "_gte: $startTimestamp"}
_lt: $endTimestamp
}
}
order_by: { consensus_timestamp: ${order} }
limit: $limit
) {
token_id
token_evm_address
sender_evm_address
sender_account_id
receiver_evm_address
receiver_account_id
payer_account_id
amount
transfer_type
consensus_timestamp
transaction_hash
}
}
`,
variables: {
startTimestamp: normalizedStartTimestamp,
endTimestamp: normalizedEndTimestamp,
limit,
...(cursor && { cursor }),
},
},
});
throwOnGraphQLErrors(res, "ERC20 transfers by timestamp range");
const newTransfers = res.data.data.erc_token_transfer;
transfers.push(...newTransfers);
// stop if no more results
if (newTransfers.length === 0 || newTransfers.length < limit) {
hasMorePages = false;
}
if (hasMorePages) {
// update cursor to the last item's timestamp for next iteration
const lastTransfer = newTransfers[newTransfers.length - 1];
cursor = lastTransfer.consensus_timestamp.toString();
}
}
return transfers;
}
export const hgraphClient = {
getLatestIndexedConsensusTimestamp,
getERC20Balances,
getERC20Transfers,
getERC20TransfersByTimestampRange,
};