@ledgerhq/coin-ton
Version:
189 lines • 9.09 kB
JavaScript
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