UNPKG

@ledgerhq/coin-ton

Version:
189 lines 9.09 kB
import { decodeAccountId, decodeTokenAccountId, emptyHistoryCache, encodeAccountId, encodeTokenAccountId, } from "@ledgerhq/coin-framework/account/index"; import { makeSync, mergeOps, } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { decodeOperationId } from "@ledgerhq/coin-framework/operation"; import { log } from "@ledgerhq/logs"; import BigNumber from "bignumber.js"; import flatMap from "lodash/flatMap"; import { fetchAccountInfo, fetchAdjacentTransactions, fetchJettonWallets, fetchLastBlockNumber, } from "./bridge/bridgeHelpers/api"; import { getJettonTransfers, getTransactions, mapJettonTxToOps, mapTxToOps, } from "./bridge/bridgeHelpers/txn"; import { getSyncHash } from "./logic"; import { WalletContractV4 } from "@ton/ton"; const jettonTxMessageHashesMap = new Map(); export const getAccountShape = async (info, { blacklistedTokenIds }) => { let address = info.address; const { rest, currency, derivationMode, initialAccount } = info; const publicKey = reconciliatePubkey(rest?.publicKey, initialAccount); // handle when address is pubkey, can happen when accounts imported using accountID if (publicKey === address) { address = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from(publicKey, "hex"), }).address.toString({ bounceable: false, urlSafe: true }); // update the account info with the correct address info.address = address; } const blockHeight = await fetchLastBlockNumber(); const accountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: publicKey, derivationMode, }); log("debug", `Generation account shape for ${address}`); const syncHash = getSyncHash(currency, blacklistedTokenIds ?? []); const shouldSyncFromScratch = syncHash !== initialAccount?.syncHash; const newTxs = { transactions: [], address_book: {} }; const newJettonTxs = []; const oldOps = (initialAccount?.operations ?? []); const { last_transaction_lt, balance } = await fetchAccountInfo(address); // if last_transaction_lt is empty, then there are no transactions in account (as well in token accounts) if (last_transaction_lt) { if (oldOps.length === 0 || shouldSyncFromScratch) { const [tmpTxs, tmpJettonTxs] = await Promise.all([ getTransactions(address), getJettonTransfers(address), ]); newTxs.transactions.push(...tmpTxs.transactions); newTxs.address_book = { ...newTxs.address_book, ...tmpTxs.address_book }; newJettonTxs.push(...tmpJettonTxs); } else { // if they are the same, we have no new ops (including tokens) if (oldOps[0].extra.lt !== last_transaction_lt) { const [tmpTxs, tmpJettonTxs] = await Promise.all([ getTransactions(address, oldOps[0].extra.lt), getJettonTransfers(address, oldOps[0].extra.lt), ]); newTxs.transactions.push(...tmpTxs.transactions); newTxs.address_book = { ...newTxs.address_book, ...tmpTxs.address_book }; newJettonTxs.push(...tmpJettonTxs); } } } // Get origin hash_message for each jetton tranfer for (const tx of newJettonTxs) { const hash = tx.transaction_hash; try { if (!jettonTxMessageHashesMap.has(hash)) { const res = await fetchAdjacentTransactions(hash); const hash_message = res.transactions.at(0)?.in_msg?.hash; if (hash_message) { jettonTxMessageHashesMap.set(hash, hash_message); } } } catch (error) { console.error(`Error processing ton jetton hash ${hash}:`, error); } } const newOps = flatMap(newTxs.transactions, mapTxToOps(accountId, address, newTxs.address_book)); const newJettonOps = flatMap(newJettonTxs, mapJettonTxToOps(accountId, address, newTxs.address_book, jettonTxMessageHashesMap)); const operations = shouldSyncFromScratch ? newOps : mergeOps(oldOps, newOps); const subAccounts = await getSubAccounts(info, accountId, newJettonOps, blacklistedTokenIds, shouldSyncFromScratch); const toReturn = { id: accountId, balance: new BigNumber(balance), spendableBalance: new BigNumber(balance), operations, operationsCount: operations.length, subAccounts, blockHeight, xpub: publicKey, lastSyncDate: new Date(), }; return toReturn; }; const getSubAccountShape = async (info, parentId, token, ops, shouldSyncFromScratch) => { const walletsInfo = await fetchJettonWallets({ address: info.address, jettonMaster: token.contractAddress, }); if (walletsInfo.length !== 1) throw new Error("[ton] unexpected api response"); const { balance, address: jettonWallet } = walletsInfo[0]; const tokenAccountId = encodeTokenAccountId(parentId, token); const oldOps = info.initialAccount?.subAccounts?.find(a => a.id === tokenAccountId)?.operations; const operations = !oldOps || shouldSyncFromScratch ? ops : mergeOps(oldOps, ops); const maybeExistingSubAccount = info.initialAccount && info.initialAccount.subAccounts && info.initialAccount.subAccounts.find(a => a.id === tokenAccountId); return { type: "TokenAccount", id: tokenAccountId, parentId, token, balance: new BigNumber(balance), spendableBalance: new BigNumber(balance), operations, operationsCount: operations.length, pendingOperations: maybeExistingSubAccount ? maybeExistingSubAccount.pendingOperations : [], creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(), balanceHistoryCache: emptyHistoryCache, // calculated in the jsHelpers swapHistory: maybeExistingSubAccount ? maybeExistingSubAccount.swapHistory : [], jettonWallet, // Address of the jetton wallet contract that holds the token balance and handles transfers }; }; async function getSubAccounts(info, accountId, newOps, blacklistedTokenIds = [], shouldSyncFromScratch) { const opsPerToken = newOps.reduce((acc, op) => { const { accountId: tokenAccountId } = decodeOperationId(op.id); const { token } = decodeTokenAccountId(tokenAccountId); if (!token || blacklistedTokenIds.includes(token.id)) return acc; if (!acc.has(token)) acc.set(token, []); acc.get(token)?.push(op); return acc; }, new Map()); const subAccountsPromises = []; for (const [token, ops] of opsPerToken.entries()) { subAccountsPromises.push(getSubAccountShape(info, accountId, token, ops, shouldSyncFromScratch)); } return Promise.all(subAccountsPromises); } const postSync = (initial, synced) => { // Set of ids from the already existing subAccount from previous sync const initialSubAccountsIds = new Set(); for (const subAccount of initial.subAccounts || []) { initialSubAccountsIds.add(subAccount.id); } const initialPendingOperations = initial.pendingOperations || []; const { operations } = synced; const pendingOperations = initialPendingOperations.filter(op => !operations.some(o => o.id === op.id)); // Set of hashes from the pending operations of the main account const coinPendingOperationsHashes = new Set(); for (const op of pendingOperations) { coinPendingOperationsHashes.add(op.hash); } return { ...synced, pendingOperations, subAccounts: synced.subAccounts?.map(subAccount => { // If the subAccount is new, just return the freshly synced subAccount if (!initialSubAccountsIds.has(subAccount.id)) return subAccount; return { ...subAccount, pendingOperations: subAccount.pendingOperations.filter(tokenPendingOperation => // if the pending operation got removed from the main account, remove it as well coinPendingOperationsHashes.has(tokenPendingOperation.hash) && // if the transaction has been confirmed, remove it !subAccount.operations.some(op => op.id === tokenPendingOperation.id)), }; }), }; }; function reconciliatePubkey(publicKey, initialAccount) { if (publicKey?.length === 64) return publicKey; if (initialAccount) { if (initialAccount.xpub?.length === 64) return initialAccount.xpub; const { xpubOrAddress } = decodeAccountId(initialAccount.id); if (xpubOrAddress.length === 64) return xpubOrAddress; } throw Error("[ton] pubkey was not properly restored"); } export const sync = makeSync({ getAccountShape, postSync }); //# sourceMappingURL=synchronisation.js.map