@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
297 lines • 11.5 kB
JavaScript
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