@ledgerhq/coin-algorand
Version:
Ledger Algorand Coin integration
309 lines • 12.4 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 account_1 = require("@ledgerhq/coin-framework/account");
const serialization_1 = require("@ledgerhq/coin-framework/serialization");
const jsHelpers_1 = require("@ledgerhq/coin-framework/bridge/jsHelpers");
const state_1 = require("@ledgerhq/cryptoassets/state");
const operation_1 = require("@ledgerhq/coin-framework/operation");
const live_promise_1 = require("@ledgerhq/live-promise");
const bignumber_js_1 = require("bignumber.js");
const api_1 = __importDefault(require("./api"));
const api_2 = require("./api");
const logic_1 = require("./logic");
const tokens_1 = require("./tokens");
const SECONDS_TO_MILLISECONDS = 1000;
const getASAOperationAmount = (transaction, accountAddress) => {
let assetAmount = new bignumber_js_1.BigNumber(0);
if (transaction.type === api_2.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_js_1.BigNumber(0);
let rewards = new bignumber_js_1.BigNumber(0);
if (transaction.senderAddress === accountAddress) {
const senderRewards = transaction.senderRewards;
amount = amount.minus(senderRewards).plus(transaction.fee);
rewards = rewards.plus(senderRewards);
}
if (transaction.type === api_2.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 === api_2.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 === api_2.AlgoTransactionType.PAYMENT) {
const details = transaction.details;
recipients.push(details.recipientAddress);
if (details.closeToAddress) {
recipients.push(details.closeToAddress);
}
}
else if (transaction.type === api_2.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 === api_2.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(Number.parseInt(tx.timestamp) * SECONDS_TO_MILLISECONDS);
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 ? (0, serialization_1.inferSubOperations)(tx.id, subAccounts) : [];
return {
id: (0, operation_1.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(Number.parseInt(tx.timestamp) * SECONDS_TO_MILLISECONDS);
const fee = tx.fee;
const senders = getOperationSenders(tx);
const recipients = getOperationRecipients(tx);
const type = getASAOperationType(tx, accountAddress);
const amount = getASAOperationAmount(tx, accountAddress);
return {
id: (0, operation_1.encodeOperationId)(accountId, hash, type),
hash,
date,
type,
value: amount,
fee,
senders,
recipients,
blockHeight,
blockHash: null,
accountId,
extra: {},
};
};
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 = (0, account_1.encodeAccountId)({
type: "js",
version: "2",
currencyId: currency.id,
xpubOrAddress: address,
derivationMode,
});
const { round, balance, pendingRewards, assets } = await api_1.default.getAccount(address);
const nbAssets = assets.length;
// NOTE Actual spendable amount depends on the transaction
const spendableBalance = (0, logic_1.computeAlgoMaxSpendable)({
accountBalance: balance,
nbAccountAssets: nbAssets,
mode: "send",
});
const newTransactions = await api_1.default.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 = (0, jsHelpers_1.mergeOps)(oldOperations, newOperations);
return {
id: accountId,
xpub: address,
blockHeight: round,
balance,
spendableBalance,
operations,
operationsCount: operations.length,
subAccounts: subAccounts || [],
algorandResources: {
rewards: pendingRewards,
nbAssets,
},
};
};
exports.getAccountShape = getAccountShape;
async function buildSubAccount({ parentAccountId, parentAccountAddress, token, initialTokenAccount, newTransactions, balance, }) {
const extractedId = (0, tokens_1.extractTokenId)(token.id);
const tokenAccountId = parentAccountId + "+" + extractedId;
const oldOperations = initialTokenAccount?.operations || [];
const newOperations = newTransactions
.filter(tx => tx.type === api_2.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 = (0, jsHelpers_1.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: account_1.emptyHistoryCache,
};
return tokenAccount;
}
async function buildSubAccounts({ accountId, initialAccount, initialAccountAddress, assets, newTransactions, syncConfig, }) {
const { blacklistedTokenIds = [] } = syncConfig;
const tokenAccounts = [];
const existingAccountByTicker = {}; // used for fast lookup
const existingAccountTickers = []; // used to keep track of ordering
const assetsIds = assets.map(asset => asset.assetId);
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 (0, live_promise_1.promiseAllBatched)(3, assets, async (asset) => {
const token = await (0, state_1.getCryptoAssetsStore)().findTokenById((0, tokens_1.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, or use the
// order of input assets if no token accounts exist yet
tokenAccounts.sort((a, b) => {
const i = existingAccountTickers.length
? existingAccountTickers.indexOf(a.token.ticker)
: assetsIds.indexOf((0, tokens_1.extractTokenId)(a.token.id));
const j = existingAccountTickers.length
? existingAccountTickers.indexOf(b.token.ticker)
: assetsIds.indexOf((0, tokens_1.extractTokenId)(b.token.id));
if (i === j)
return 0;
if (i < 0)
return 1;
if (j < 0)
return -1;
return i - j;
});
return tokenAccounts;
}
exports.sync = (0, jsHelpers_1.makeSync)({ getAccountShape: exports.getAccountShape });
//# sourceMappingURL=synchronization.js.map