UNPKG

@ledgerhq/coin-algorand

Version:
297 lines 11.5 kB
import { emptyHistoryCache, encodeAccountId } from "@ledgerhq/coin-framework/account"; import { inferSubOperations } from "@ledgerhq/coin-framework/serialization"; import { makeSync, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets/index"; import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { promiseAllBatched } from "@ledgerhq/live-promise"; import { BigNumber } from "bignumber.js"; import algorandAPI from "./api"; import { AlgoTransactionType } from "./api"; import { computeAlgoMaxSpendable } from "./logic"; import { addPrefixToken, extractTokenId } from "./tokens"; const getASAOperationAmount = (transaction, accountAddress) => { let assetAmount = new BigNumber(0); if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) { const details = transaction.details; const assetSender = details.assetSenderAddress ? details.assetSenderAddress : transaction.senderAddress; // Account is either sender or recipient (if both the balance is unchanged) if ((assetSender === accountAddress) !== (details.assetRecipientAddress === accountAddress)) { assetAmount = assetAmount.plus(details.assetAmount); } // Account is either sender or close-to, but not both if ((assetSender === accountAddress) !== (details.assetCloseToAddress && details.assetCloseToAddress === accountAddress)) { if (details.assetCloseAmount) { assetAmount = assetAmount.plus(details.assetCloseAmount); } } } return assetAmount; }; const getOperationAmounts = (transaction, accountAddress) => { let amount = new BigNumber(0); let rewards = new BigNumber(0); if (transaction.senderAddress === accountAddress) { const senderRewards = transaction.senderRewards; amount = amount.minus(senderRewards).plus(transaction.fee); rewards = rewards.plus(senderRewards); } if (transaction.type === AlgoTransactionType.PAYMENT) { const details = transaction.details; if (transaction.senderAddress === details.recipientAddress) { return { amount, rewards, }; } if (transaction.senderAddress === accountAddress) { amount = amount.plus(details.amount); } if (details.recipientAddress === accountAddress) { const recipientRewards = transaction.recipientRewards; amount = amount.plus(details.amount).plus(recipientRewards); rewards = rewards.plus(recipientRewards); } if (transaction.closeRewards && details.closeAmount && details.closeToAddress === accountAddress) { const closeRewards = transaction.closeRewards; amount = amount.plus(details.closeAmount).plus(closeRewards); rewards = rewards.plus(closeRewards); } } return { amount, rewards, }; }; const getASAOperationType = (transaction, accountAddress) => { return transaction.senderAddress === accountAddress ? "OUT" : "IN"; }; const getOperationType = (transaction, accountAddress) => { if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) { const details = transaction.details; if (details.assetAmount.isZero() && transaction.senderAddress === details.assetRecipientAddress) { return "OPT_IN"; } else if (details.assetCloseToAddress && transaction.senderAddress === accountAddress) { return "OPT_OUT"; } else { return "FEES"; } } return transaction.senderAddress === accountAddress ? "OUT" : "IN"; }; const getOperationSenders = (transaction) => { return [transaction.senderAddress]; }; const getOperationRecipients = (transaction) => { const recipients = []; if (transaction.type === AlgoTransactionType.PAYMENT) { const details = transaction.details; recipients.push(details.recipientAddress); if (details.closeToAddress) { recipients.push(details.closeToAddress); } } else if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) { const details = transaction.details; recipients.push(details.assetRecipientAddress); if (details.assetCloseToAddress) { recipients.push(details.assetCloseToAddress); } } return recipients; }; const getOperationAssetId = (transaction) => { if (transaction.type === AlgoTransactionType.ASSET_TRANSFER) { const details = transaction.details; return details.assetId; } }; const mapTransactionToOperation = (tx, accountId, accountAddress, subAccounts) => { const hash = tx.id; const blockHeight = tx.round; const date = new Date(parseInt(tx.timestamp) * 1000); const fee = tx.fee; const memo = tx.note; const senders = getOperationSenders(tx); const recipients = getOperationRecipients(tx); const { amount, rewards } = getOperationAmounts(tx, accountAddress); const type = getOperationType(tx, accountAddress); const assetId = getOperationAssetId(tx); const subOperations = subAccounts ? inferSubOperations(tx.id, subAccounts) : []; return { id: encodeOperationId(accountId, hash, type), hash, date, type, value: amount, fee, senders, recipients, blockHeight, blockHash: null, accountId, subOperations, extra: { rewards, memo, assetId, }, }; }; const mapTransactionToASAOperation = (tx, accountId, accountAddress) => { const hash = tx.id; const blockHeight = tx.round; const date = new Date(parseInt(tx.timestamp) * 1000); const fee = tx.fee; const senders = getOperationSenders(tx); const recipients = getOperationRecipients(tx); const type = getASAOperationType(tx, accountAddress); const amount = getASAOperationAmount(tx, accountAddress); return { id: encodeOperationId(accountId, hash, type), hash, date, type, value: amount, fee, senders, recipients, blockHeight, blockHash: null, accountId, extra: {}, }; }; export const getAccountShape = async (info, syncConfig) => { const { address, initialAccount, currency, derivationMode } = info; const oldOperations = initialAccount?.operations || []; const startAt = oldOperations.length ? (oldOperations[0].blockHeight || 0) + 1 : 0; const accountId = encodeAccountId({ type: "js", version: "2", currencyId: currency.id, xpubOrAddress: address, derivationMode, }); const { round, balance, pendingRewards, assets } = await algorandAPI.getAccount(address); const nbAssets = assets.length; // NOTE Actual spendable amount depends on the transaction const spendableBalance = computeAlgoMaxSpendable({ accountBalance: balance, nbAccountAssets: nbAssets, mode: "send", }); const newTransactions = await algorandAPI.getAccountTransactions(address, startAt); const subAccounts = await buildSubAccounts({ currency, accountId, initialAccount, initialAccountAddress: address, assets, newTransactions, syncConfig, }); const newOperations = newTransactions.map(tx => mapTransactionToOperation(tx, accountId, address, subAccounts)); const operations = mergeOps(oldOperations, newOperations); return { id: accountId, xpub: address, blockHeight: round, balance, spendableBalance, operations, operationsCount: operations.length, subAccounts: subAccounts || [], algorandResources: { rewards: pendingRewards, nbAssets, }, }; }; async function buildSubAccount({ parentAccountId, parentAccountAddress, token, initialTokenAccount, newTransactions, balance, }) { const extractedId = extractTokenId(token.id); const tokenAccountId = parentAccountId + "+" + extractedId; const oldOperations = initialTokenAccount?.operations || []; const newOperations = newTransactions .filter(tx => tx.type === AlgoTransactionType.ASSET_TRANSFER) .filter(tx => { const details = tx.details; return Number(details.assetId) === Number(extractedId); }) .filter(tx => getOperationType(tx, parentAccountAddress) !== "OPT_IN") .map(tx => mapTransactionToASAOperation(tx, tokenAccountId, parentAccountAddress)); const operations = mergeOps(oldOperations, newOperations); const tokenAccount = { type: "TokenAccount", id: tokenAccountId, parentId: parentAccountId, token, operationsCount: operations.length, operations, pendingOperations: [], balance, spendableBalance: balance, swapHistory: [], creationDate: operations.length > 0 ? operations[operations.length - 1].date : new Date(), balanceHistoryCache: emptyHistoryCache, }; return tokenAccount; } async function buildSubAccounts({ currency, accountId, initialAccount, initialAccountAddress, assets, newTransactions, syncConfig, }) { const { blacklistedTokenIds = [] } = syncConfig; if (listTokensForCryptoCurrency(currency).length === 0) return undefined; const tokenAccounts = []; const existingAccountByTicker = {}; // used for fast lookup const existingAccountTickers = []; // used to keep track of ordering if (initialAccount && initialAccount.subAccounts) { for (const existingSubAccount of initialAccount.subAccounts) { if (existingSubAccount.type === "TokenAccount") { const { ticker, id } = existingSubAccount.token; if (!blacklistedTokenIds.includes(id)) { existingAccountTickers.push(ticker); existingAccountByTicker[ticker] = existingSubAccount; } } } } // filter by token existence await promiseAllBatched(3, assets, async (asset) => { const token = findTokenById(addPrefixToken(asset.assetId)); if (token && !blacklistedTokenIds.includes(token.id)) { const initialTokenAccount = existingAccountByTicker[token.ticker]; const tokenAccount = await buildSubAccount({ parentAccountId: accountId, parentAccountAddress: initialAccountAddress, initialTokenAccount, token, newTransactions, balance: asset.balance, }); if (tokenAccount) tokenAccounts.push(tokenAccount); } }); // Preserve order of tokenAccounts from the existing token accounts tokenAccounts.sort((a, b) => { const i = existingAccountTickers.indexOf(a.token.ticker); const j = existingAccountTickers.indexOf(b.token.ticker); if (i === j) return 0; if (i < 0) return 1; if (j < 0) return -1; return i - j; }); return tokenAccounts; } export const sync = makeSync({ getAccountShape }); //# sourceMappingURL=synchronization.js.map