@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
181 lines • 8.29 kB
JavaScript
;
// 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.AccountsForeignAssetsService = void 0;
const http_errors_1 = require("http-errors");
const AbstractService_1 = require("../AbstractService");
class AccountsForeignAssetsService extends AbstractService_1.AbstractService {
/**
* Fetch all the `ForeignAssetBalance`s for a given account address.
* If specific foreign assets are queried via multilocations, only those will be returned.
* Otherwise, all foreign asset balances for the account will be returned.
*
* @param hash `BlockHash` to make call at
* @param address `AccountId` associated with the balances
* @param foreignAssets An array of multilocation objects to be queried as JSON strings. If the length is zero
* all foreign assets associated to the account will be queried
*/
async fetchForeignAssetBalances(hash, address, foreignAssets) {
const { api } = this;
const [historicApi, { number }] = await Promise.all([api.at(hash), api.rpc.chain.getHeader(hash)]);
// Check if this runtime has the foreignAssets pallet
this.checkForeignAssetsError(historicApi);
let response;
if (foreignAssets.length === 0) {
/**
* This will query all foreign assets and then filter for the account's holdings
* We need to get all foreign assets first, then check which ones the account holds
*/
const foreignAssetEntries = await historicApi.query.foreignAssets.asset.entries();
const multiLocations = this.extractMultiLocationsFromAssetEntries(foreignAssetEntries);
response = await this.queryForeignAssets(historicApi, multiLocations, address);
}
else {
/**
* This will query all foreign assets by the requested multilocations
*/
const parsedMultiLocations = foreignAssets.map((ml) => {
try {
const parsed = JSON.parse(ml);
return historicApi.registry.createType('XcmVersionedLocation', parsed);
}
catch (err) {
throw new http_errors_1.BadRequest(`Invalid JSON format for multilocation: ${ml}`);
}
});
response = await this.queryForeignAssets(historicApi, parsedMultiLocations, address);
}
const at = {
hash,
height: number.unwrap().toString(10),
};
return {
at,
foreignAssets: response,
};
}
/**
* Takes in an array of multilocations and an `AccountId` and returns
* all balances tied to those foreign assets.
*
* Uses batch queries (.multi()) for better performance instead of individual queries.
*
* @param historicApi ApiPromise at a specific block
* @param multiLocations An Array of multilocation objects
* @param address An `AccountId` associated with the queried path
*/
async queryForeignAssets(historicApi, multiLocations, address) {
// Prepare all queries for batch call
const queries = multiLocations.map((multiLocation) => [multiLocation, address]);
// Single batched RPC call instead of N individual calls
const assetBalances = await historicApi.query.foreignAssets.account
.multi(queries)
.catch((err) => {
throw this.createHttpErrorForAddr(address, err);
});
// Process all results
const results = multiLocations.map((multiLocation, index) => {
const assetBalance = assetBalances[index];
/**
* The following checks for three different cases:
*/
// 1. Via runtime v9160 the updated storage introduces a `reason` field,
// and polkadot-js wraps the newly returned `PalletAssetsAssetAccount` in an `Option`.
if (assetBalance.isSome) {
const balanceProps = assetBalance.unwrap();
let isFrozen;
if ('isFrozen' in balanceProps) {
isFrozen = balanceProps.isFrozen;
}
else {
isFrozen = historicApi.registry.createType('bool', false);
}
return {
multiLocation,
balance: balanceProps.balance,
isFrozen: isFrozen,
isSufficient: balanceProps.reason.isSufficient,
};
}
// 2. `query.foreignAssets.account()` return `PalletAssetsAssetBalance` which excludes `reasons` but has
// `sufficient` as a key.
const balanceStruct = assetBalance;
if ('sufficient' in balanceStruct) {
const balanceProps = assetBalance;
return {
multiLocation,
balance: balanceProps.balance,
isFrozen: balanceProps.isFrozen,
isSufficient: balanceProps.sufficient,
};
}
// 3. The older legacy type of `PalletAssetsAssetBalance` has a key of `isSufficient` instead
// of `sufficient`.
if ('isSufficient' in balanceStruct) {
const balanceProps = assetBalance;
return {
multiLocation,
balance: balanceProps.balance,
isFrozen: balanceProps.isFrozen,
isSufficient: balanceProps.isSufficient,
};
}
/**
* This return value wont ever be reached as polkadot-js defaults the
* `balance` value to `0`, `isFrozen` to false, and `isSufficient` to false.
* This ensures that the typescript compiler is happy, but we also follow along
* with polkadot-js/substrate convention.
*/
return {
multiLocation,
balance: historicApi.registry.createType('u128', 0),
isFrozen: historicApi.registry.createType('bool', false),
isSufficient: historicApi.registry.createType('bool', false),
};
});
// Filter out assets with zero balance
// We filter here because querying all foreign assets will return many zeros
// for assets the account doesn't hold
return results.filter((asset) => asset.balance.isZero() === false);
}
/**
* Extract multilocations from foreign asset entries
* The storage key for foreignAssets.asset is just the multilocation
*
* @param entries Foreign asset storage entries
*/
extractMultiLocationsFromAssetEntries(entries) {
const multiLocations = [];
for (const [storageKey] of entries) {
multiLocations.push(storageKey.args[0]);
}
return multiLocations;
}
/**
* Checks if the historicApi has the foreignAssets pallet. If not
* it will throw a BadRequest error.
*
* @param historicApi Decorated historic api
*/
checkForeignAssetsError(historicApi) {
if (!historicApi.query.foreignAssets) {
throw new http_errors_1.BadRequest(`The runtime does not include the foreignAssets pallet at this block.`);
}
}
}
exports.AccountsForeignAssetsService = AccountsForeignAssetsService;
//# sourceMappingURL=AccountsForeignAssetsService.js.map