UNPKG

@substrate/api-sidecar

Version:

REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.

306 lines 15.2 kB
"use strict"; // Copyright 2017-2025 Parity Technologies (UK) Ltd. // This file is part of Substrate API Sidecar. // // Substrate API Sidecar is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. Object.defineProperty(exports, "__esModule", { value: true }); exports.AccountsBalanceInfoService = void 0; const util_1 = require("@polkadot/util"); const http_errors_1 = require("http-errors"); const AbstractService_1 = require("../AbstractService"); class AccountsBalanceInfoService extends AbstractService_1.AbstractService { /** * Fetch balance information for an account at a given block. * N.B. assumes all non native tokens are from ORML tokens pallet. * * @param hash `BlockHash` to make call at. * @param address Address of the account to get the balance info of. * @param token Token to get the balance info of. */ async fetchAccountBalanceInfo(hash, historicApi, address, token, denominate) { var _a; const { api } = this; if (denominate && historicApi.registry.chainDecimals.length === 0) { throw new http_errors_1.BadRequest("Invalid use of the query parameter `denominated`. This chain doesn't have a valid chain decimal to denominate a value."); } const capitalizeTokens = historicApi.registry.chainTokens.map((token) => token.toUpperCase()); const tokenIdx = capitalizeTokens.indexOf(token); const decimal = historicApi.registry.chainDecimals[tokenIdx]; /** * Check two different cases where a historicApi is needed in order * to have the correct runtime methods. * * a) Does the block use the oldest api where the free balance is found * using `historicApi.query.balances.freeBalance`. * * b) Does the block use an older api where the free balance is within the * AccountInfo type, but the storage does not yet have the `.at` method. * * NOTE: This PR also checks to see if `frozen` exists or `miscFrozen` and `feeFrozen`. * There was a breaking change in https://github.com/paritytech/substrate/pull/12951 which * changed the format of the Account Data returned by the chain. */ if (historicApi.query.balances.freeBalance) { const [header, free, locks, reserved, nonce] = await Promise.all([ api.rpc.chain.getHeader(hash), historicApi.query.balances.freeBalance(address), historicApi.query.balances.locks(address), historicApi.query.balances.reservedBalance(address), historicApi.query.system.accountNonce(address), ]).catch((err) => { throw this.createHttpErrorForAddr(address, err); }); const at = { hash, height: header.number.toNumber().toString(10), }; if (free) { return { at, nonce, tokenSymbol: token, free: this.inDenominationBal(denominate, free, decimal), reserved: this.inDenominationBal(denominate, reserved, decimal), miscFrozen: 'miscFrozen does not exist for this runtime', feeFrozen: 'feeFrozen does not exist for this runtime', frozen: 'frozen does not exist for this runtime', transferable: 'transferable formula not supported for this runtime', locks: this.inDenominationLocks(denominate, locks, decimal), }; } else { throw new http_errors_1.BadRequest('Account not found'); } } else if (!historicApi.query.system.account.at) { const [header, accountInfo, locks] = await Promise.all([ api.rpc.chain.getHeader(hash), historicApi.query.system.account(address), historicApi.query.balances.locks(address), ]).catch((err) => { throw this.createHttpErrorForAddr(address, err); }); const { data, nonce } = accountInfo; let free, reserved, feeFrozen, miscFrozen, frozen, transferable; if ((_a = accountInfo.data) === null || _a === void 0 ? void 0 : _a.frozen) { const maybeTransferable = await this.calculateTransferable(data, hash, address); free = data.free; reserved = data.reserved; frozen = data.frozen; transferable = maybeTransferable ? maybeTransferable : 'transferable formula not supported for this runtime'; miscFrozen = 'miscFrozen does not exist for this runtime'; feeFrozen = 'feeFrozen does not exist for this runtime'; } else { const tmpData = data; free = tmpData.free; reserved = tmpData.reserved; feeFrozen = tmpData.feeFrozen; miscFrozen = tmpData.miscFrozen; transferable = 'transferable formula not supported for this runtime'; frozen = 'frozen does not exist for this runtime'; } const at = { hash, height: header.number.toNumber().toString(10), }; if (accountInfo && locks) { return { at, nonce, tokenSymbol: token, free: this.inDenominationBal(denominate, free, decimal), reserved: this.inDenominationBal(denominate, reserved, decimal), miscFrozen: typeof miscFrozen === 'string' ? miscFrozen : this.inDenominationBal(denominate, miscFrozen, decimal), feeFrozen: typeof feeFrozen === 'string' ? feeFrozen : this.inDenominationBal(denominate, feeFrozen, decimal), frozen: typeof frozen === 'string' ? frozen : this.inDenominationBal(denominate, frozen, decimal), transferable: typeof transferable === 'string' ? transferable : this.inDenominationBal(denominate, transferable, decimal), locks: this.inDenominationLocks(denominate, locks, decimal), }; } else { throw new http_errors_1.BadRequest('Account not found'); } } let locks, header, accountInfo, accountData; // We assume the first token is the native token if (token.toUpperCase() === historicApi.registry.chainTokens[0].toUpperCase()) { [header, locks, accountInfo] = await Promise.all([ api.rpc.chain.getHeader(hash), historicApi.query.balances.locks(address), historicApi.query.system.account(address), ]).catch((err) => { throw this.createHttpErrorForAddr(address, err); }); accountData = accountInfo.data != null ? accountInfo.data : await historicApi.query.balances.account(address); } else { // Assume we are using ORML token pallet let locksAny, accountDataAny; try { [header, locksAny, accountDataAny, accountInfo] = await Promise.all([ api.rpc.chain.getHeader(hash), historicApi.query.tokens.locks(address, { Token: token }), historicApi.query.tokens.accounts(address, { Token: token, }), historicApi.query.system.account(address), ]); } catch { throw new http_errors_1.BadRequest('An error occured while attempting to query for a non-native token; ' + 'the token specified is likely invalid.'); } // Coerce the ORML query results from polkadot-js generic Codec to exact type locks = locksAny; accountData = accountDataAny; } const at = { hash, height: header.number.toNumber().toString(10), }; if (accountData && locks && accountInfo) { const { nonce } = accountInfo; let free, reserved, feeFrozen, miscFrozen, frozen, transferable; if (accountData.miscFrozen) { const tmpData = accountData; free = tmpData.free; reserved = tmpData.reserved; feeFrozen = tmpData.feeFrozen; miscFrozen = tmpData.miscFrozen; frozen = 'frozen does not exist for this runtime'; transferable = 'transferable formula not supported for this runtime'; } else { const tmpData = accountData; const maybeTransferable = await this.calculateTransferable(tmpData, hash, address); free = tmpData.free; reserved = tmpData.reserved; frozen = tmpData.frozen; transferable = maybeTransferable ? maybeTransferable : 'transferable formula not supported for this runtime'; feeFrozen = 'feeFrozen does not exist for this runtime'; miscFrozen = 'miscFrozen does not exist for this runtime'; } return { at, nonce, tokenSymbol: token, free: this.inDenominationBal(denominate, free, decimal), reserved: this.inDenominationBal(denominate, reserved, decimal), miscFrozen: typeof miscFrozen === 'string' ? miscFrozen : this.inDenominationBal(denominate, miscFrozen, decimal), feeFrozen: typeof feeFrozen === 'string' ? feeFrozen : this.inDenominationBal(denominate, feeFrozen, decimal), frozen: typeof frozen === 'string' ? frozen : this.inDenominationBal(denominate, frozen, decimal), transferable: typeof transferable === 'string' ? transferable : this.inDenominationBal(denominate, transferable, decimal), locks: this.inDenominationLocks(denominate, locks, decimal), }; } else { throw new http_errors_1.BadRequest('Account not found'); } } async calculateTransferable(data, hash, addr) { const apiAt = await this.api.at(hash); const allLocked = await this.isAllLocked(addr, hash); let transferable; if (data === null || data === void 0 ? void 0 : data.frozen) { const { frozen, free, reserved } = data; const noFrozenReserved = frozen.isZero() && reserved.isZero(); const ED = apiAt.consts.balances.existentialDeposit; const maybeED = noFrozenReserved ? new util_1.BN(0) : ED; const frozenReserveDif = frozen.sub(reserved); transferable = apiAt.registry.createType('Balance', allLocked ? 0 : (0, util_1.bnMax)(new util_1.BN(0), free.sub((0, util_1.bnMax)(maybeED, frozenReserveDif)))); } return transferable; } async isAllLocked(addr, hash) { const apiAt = await this.api.at(hash); try { // Get current block header (contains block number) and locks in parallel const [header, locks] = await Promise.all([ this.api.rpc.chain.getHeader(), // Current best block apiAt.query.balances.locks(addr), ]); const bestNumber = header.number.unwrap(); if (!Array.isArray(locks) || locks.length === 0) { return false; } // Filter locks that haven't expired yet const validLocks = locks.filter(({ until }) => !until || until.gt(bestNumber)); // Check if any valid lock has maximum amount (all funds locked) return validLocks.some((lock) => lock.amount.isMax()); } catch (error) { console.error('Error checking if all funds are locked:', error); return false; } } /** * Apply a denomination to a balance depending on the chains decimal value. * * @param balance free balance available encoded as Balance. This will be * represented as an atomic value. * @param dec The chains given decimal token value. It must be > 0, and it * is applied to the given atomic value given by the `balance`. */ applyDenominationBalance(balance, dec) { const strBalance = balance.toString(); // We dont want to denominate a zero balance or zero decimal if (strBalance === '0' || dec === 0) { return strBalance; } // If the denominated value will be less then zero, pad it correctly if (strBalance.length <= dec) { return '.'.padEnd(dec - strBalance.length + 1, '0').concat(strBalance); } const lenDiff = strBalance.length - dec; return strBalance.substring(0, lenDiff) + '.' + strBalance.substring(lenDiff, strBalance.length); } /** * Parse and denominate the `amount` key in each BalanceLock * * @param locks A vector containing BalanceLock objects * @param dec The chains given decimal value */ applyDenominationLocks(locks, dec) { return locks.map((lock) => { return { id: lock.id, amount: this.applyDenominationBalance(lock.amount, dec), reasons: lock.reasons, }; }); } /** * Either denominate a value, or return the original Balance as an atomic value. * * @param denominate Boolean to determine whether or not we denominate a balance * @param bal Inputted Balance * @param dec Decimal value used to denominate a Balance */ inDenominationBal(denominate, bal, dec) { return denominate ? this.applyDenominationBalance(bal, dec) : bal; } /** * Either denominate the Balance's within Locks or return the original Locks. * * @param denominate Boolean to determine whether or not we denominate a balance * @param locks Inputted Vec<BalanceLock>, only the amount key will be denominated * @param dec Decimal value used to denominate a Balance */ inDenominationLocks(denominate, locks, dec) { return denominate ? this.applyDenominationLocks(locks, dec) : locks; } } exports.AccountsBalanceInfoService = AccountsBalanceInfoService; //# sourceMappingURL=AccountsBalanceInfoService.js.map