@ledgerhq/coin-ton
Version:
196 lines • 9.56 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.sync = exports.getAccountShape = void 0;
const index_1 = require("@ledgerhq/coin-framework/account/index");
const jsHelpers_1 = require("@ledgerhq/coin-framework/bridge/jsHelpers");
const operation_1 = require("@ledgerhq/coin-framework/operation");
const logs_1 = require("@ledgerhq/logs");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const flatMap_1 = __importDefault(require("lodash/flatMap"));
const api_1 = require("./bridge/bridgeHelpers/api");
const txn_1 = require("./bridge/bridgeHelpers/txn");
const logic_1 = require("./logic");
const ton_1 = require("@ton/ton");
const jettonTxMessageHashesMap = new Map();
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 = ton_1.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 (0, api_1.fetchLastBlockNumber)();
const accountId = (0, index_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: publicKey,
derivationMode,
});
(0, logs_1.log)("debug", `Generation account shape for ${address}`);
const syncHash = (0, logic_1.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 (0, api_1.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([
(0, txn_1.getTransactions)(address),
(0, txn_1.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([
(0, txn_1.getTransactions)(address, oldOps[0].extra.lt),
(0, txn_1.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 (0, api_1.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 = (0, flatMap_1.default)(newTxs.transactions, (0, txn_1.mapTxToOps)(accountId, address, newTxs.address_book));
const newJettonOps = (0, flatMap_1.default)(newJettonTxs, (0, txn_1.mapJettonTxToOps)(accountId, address, newTxs.address_book, jettonTxMessageHashesMap));
const operations = shouldSyncFromScratch ? newOps : (0, jsHelpers_1.mergeOps)(oldOps, newOps);
const subAccounts = await getSubAccounts(info, accountId, newJettonOps, blacklistedTokenIds, shouldSyncFromScratch);
const toReturn = {
id: accountId,
balance: new bignumber_js_1.default(balance),
spendableBalance: new bignumber_js_1.default(balance),
operations,
operationsCount: operations.length,
subAccounts,
blockHeight,
xpub: publicKey,
lastSyncDate: new Date(),
};
return toReturn;
};
exports.getAccountShape = getAccountShape;
const getSubAccountShape = async (info, parentId, token, ops, shouldSyncFromScratch) => {
const walletsInfo = await (0, api_1.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 = (0, index_1.encodeTokenAccountId)(parentId, token);
const oldOps = info.initialAccount?.subAccounts?.find(a => a.id === tokenAccountId)?.operations;
const operations = !oldOps || shouldSyncFromScratch ? ops : (0, jsHelpers_1.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_js_1.default(balance),
spendableBalance: new bignumber_js_1.default(balance),
operations,
operationsCount: operations.length,
pendingOperations: maybeExistingSubAccount ? maybeExistingSubAccount.pendingOperations : [],
creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(),
balanceHistoryCache: index_1.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 } = (0, operation_1.decodeOperationId)(op.id);
const { token } = (0, index_1.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 } = (0, index_1.decodeAccountId)(initialAccount.id);
if (xpubOrAddress.length === 64)
return xpubOrAddress;
}
throw Error("[ton] pubkey was not properly restored");
}
exports.sync = (0, jsHelpers_1.makeSync)({ getAccountShape: exports.getAccountShape, postSync });
//# sourceMappingURL=synchronisation.js.map