@ledgerhq/coin-hedera
Version:
Ledger Hedera Coin integration
150 lines (132 loc) • 5.7 kB
text/typescript
import { encodeAccountId, getSyncHash } from "@ledgerhq/ledger-wallet-framework/account";
import type {
GetAccountShape,
IterateResultBuilder,
} from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers";
import { mergeOps } from "@ledgerhq/ledger-wallet-framework/bridge/jsHelpers";
import type { GetAddressResult } from "@ledgerhq/ledger-wallet-framework/derivation";
import {
getDerivationScheme,
runDerivationScheme,
} from "@ledgerhq/ledger-wallet-framework/derivation";
import { BigNumber } from "bignumber.js";
import invariant from "invariant";
import { HARDCODED_BLOCK_HEIGHT } from "../constants";
import { listOperationsV2 } from "../logic";
import { resolveConfig } from "../logic/utils";
import { apiClient } from "../network/api";
import { getERC20BalancesForAccountV2, toEVMAddress } from "../network/utils";
import type { HederaAccount } from "../types";
import { getSubAccounts, prepareOperations, applyPendingExtras, mergeSubAccounts } from "./utils";
export const getAccountShape: GetAccountShape<HederaAccount> = async (
info,
{ blacklistedTokenIds },
): Promise<Partial<HederaAccount>> => {
const { currency, derivationMode, address, initialAccount } = info;
const config = resolveConfig(currency.id);
invariant(address, "hedera: address is expected");
const evmAddress = await toEVMAddress({ configOrCurrencyId: config, accountId: address });
invariant(evmAddress, `hedera: evm address is missing for ${address}`);
const liveAccountId = encodeAccountId({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: address,
derivationMode,
});
// get current account balance and tokens
// tokens are fetched with separate requests to get "created_timestamp" for each token
// based on this, ASSOCIATE_TOKEN operations can be connected with tokens
const [mirrorAccount, mirrorTokens, erc20Tokens] = await Promise.all([
apiClient.getAccount({ configOrCurrencyId: config, address }),
apiClient.getAccountTokens({ configOrCurrencyId: config, address }),
getERC20BalancesForAccountV2({ configOrCurrencyId: config, address }),
]);
const accountBalance = new BigNumber(mirrorAccount.balance.balance);
// we should sync again when new tokens are added or blacklist changes
const syncHash = await getSyncHash(currency.id, blacklistedTokenIds);
const shouldSyncFromScratch = !initialAccount || syncHash !== initialAccount?.syncHash;
const pendingOperations = shouldSyncFromScratch ? [] : (initialAccount?.pendingOperations ?? []);
const oldOperations = shouldSyncFromScratch ? [] : (initialAccount?.operations ?? []);
const latestOperation = oldOperations[0];
// grab latest operation timestamps for incremental sync
let latestOperationTimestamp: string | null = null;
if (!shouldSyncFromScratch && latestOperation) {
const timestamp = Math.floor(latestOperation.date.getTime() / 1000);
latestOperationTimestamp = new BigNumber(timestamp).toFixed(9);
}
const latestAccountOperations = await listOperationsV2({
currencyId: currency.id,
address,
evmAddress,
mirrorTokens,
erc20Tokens,
...(latestOperationTimestamp && { cursor: latestOperationTimestamp }),
fetchAllPages: true,
skipFeesForTokenOperations: false,
useEncodedHash: true,
useSyntheticBlocks: false,
});
const newOperations = await prepareOperations(
latestAccountOperations.coinOperations,
latestAccountOperations.tokenOperations,
);
const enrichedNewOperations = applyPendingExtras(newOperations, pendingOperations);
const operations = shouldSyncFromScratch
? enrichedNewOperations
: mergeOps(oldOperations, enrichedNewOperations);
const delegation =
typeof mirrorAccount.staked_node_id === "number"
? {
nodeId: mirrorAccount.staked_node_id,
delegated: accountBalance,
pendingReward: new BigNumber(mirrorAccount.pending_reward),
}
: null;
const newSubAccounts = await getSubAccounts({
ledgerAccountId: liveAccountId,
latestTokenOperations: latestAccountOperations.tokenOperations,
mirrorTokens,
erc20Tokens,
});
const subAccounts = shouldSyncFromScratch
? newSubAccounts
: mergeSubAccounts(initialAccount, newSubAccounts);
return {
id: liveAccountId,
freshAddress: address,
syncHash,
lastSyncDate: new Date(),
balance: accountBalance,
spendableBalance: accountBalance,
operations: operations,
operationsCount: operations.length,
// NOTE: there are no "blocks" in hedera
// set a value just so that operations are considered confirmed according to isConfirmedOperation
blockHeight: HARDCODED_BLOCK_HEIGHT,
subAccounts,
hederaResources: {
maxAutomaticTokenAssociations: mirrorAccount.max_automatic_token_associations,
isAutoTokenAssociationEnabled: mirrorAccount.max_automatic_token_associations === -1,
delegation,
},
};
};
export const buildIterateResult: IterateResultBuilder = async ({ result: rootResult }) => {
return async ({ currency, derivationMode, index }) => {
const mirrorAccounts = await apiClient.getAccountsForPublicKey({
configOrCurrencyId: currency.id,
publicKey: rootResult.publicKey,
});
const addresses = mirrorAccounts.map(a => a.account);
const derivationScheme = getDerivationScheme({ derivationMode, currency });
const freshAddressPath = runDerivationScheme(derivationScheme, currency, { account: index });
return addresses[index]
? ({
address: addresses[index],
publicKey: addresses[index],
path: freshAddressPath,
} satisfies GetAddressResult)
: null;
};
};