@substrate/api-sidecar
Version:
REST service that makes it easy to interact with blockchain nodes built using Substrate's FRAME framework.
291 lines • 13.7 kB
JavaScript
"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/>.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccountsVestingInfoService = void 0;
const bn_js_1 = __importDefault(require("bn.js"));
const http_errors_1 = require("http-errors");
const apiRegistry_1 = require("../../apiRegistry");
const chains_config_1 = require("../../chains-config");
const getRelayParentNumber_1 = require("../../util/relay/getRelayParentNumber");
const vestingCalculations_1 = require("../../util/vesting/vestingCalculations");
const AbstractService_1 = require("../AbstractService");
const consts_1 = require("../consts");
/**
* Asset Hub parachain ID on relay chains
*/
const ASSET_HUB_PARA_ID = 1000;
/**
* Vesting lock ID used in balances.locks
* This is "vesting " padded to 8 bytes (0x76657374696e6720)
*/
const VESTING_ID = '0x76657374696e6720';
class AccountsVestingInfoService extends AbstractService_1.AbstractService {
/**
* Fetch vesting information for an account at a given block.
*
* @param hash `BlockHash` to make call at
* @param address address of the account to get the vesting info of
* @param includeVested whether to calculate and include vested amounts (default: false)
* @param knownRelayBlockNumber optional relay block number to use for calculations (skips search when provided)
*/
async fetchAccountVestingInfo(hash, address, includeVested = false, knownRelayBlockNumber) {
const { api } = this;
const historicApi = await api.at(hash);
if (!historicApi.query.vesting) {
throw new http_errors_1.BadRequest(`Vesting pallet does not exist on the specified blocks runtime version`);
}
const [{ number }, vesting] = await Promise.all([
api.rpc.chain.getHeader(hash),
historicApi.query.vesting.vesting(address),
]).catch((err) => {
throw this.createHttpErrorForAddr(address, err);
});
const blockNumber = number.unwrap().toNumber();
const at = {
hash,
height: blockNumber.toString(10),
};
if (vesting.isNone) {
return {
at,
vesting: [],
};
}
const unwrapVesting = vesting.unwrap();
const vestingArray = Array.isArray(unwrapVesting) ? unwrapVesting : [unwrapVesting];
// If includeVested is not requested, return raw vesting data
if (!includeVested) {
return {
at,
vesting: vestingArray,
};
}
// Get the on-chain vesting lock amount from balances.locks
const vestingLocked = await this.getVestingLocked(historicApi, address);
// Calculate vested amounts based on chain type and migration state
const vestingResult = await this.calculateVestingAmounts(hash, blockNumber, vestingArray, vestingLocked, knownRelayBlockNumber);
if (vestingResult === null) {
// Unable to calculate (e.g., during migration or missing relay connection)
// Return raw vesting data without calculations
return {
at,
vesting: vestingArray,
};
}
return {
at,
vesting: vestingResult.schedules,
vestedBalance: vestingResult.vestedBalance,
vestingTotal: vestingResult.vestingTotal,
vestedClaimable: vestingResult.vestedClaimable,
blockNumberForCalculation: vestingResult.blockNumberForCalculation,
blockNumberSource: vestingResult.blockNumberSource,
};
}
/**
* Get the vesting lock amount from balances.locks.
* Returns 0 if no vesting lock exists.
*/
async getVestingLocked(historicApi, address) {
var _a;
if (!((_a = historicApi.query.balances) === null || _a === void 0 ? void 0 : _a.locks)) {
return new bn_js_1.default(0);
}
const locks = await historicApi.query.balances.locks(address);
for (const lock of locks) {
if (lock.id.toHex() === VESTING_ID) {
return new bn_js_1.default(lock.amount.toString());
}
}
return new bn_js_1.default(0);
}
/**
* Calculate vested amounts for vesting schedules.
* Returns null if calculation cannot be performed (e.g., during migration window).
*/
async calculateVestingAmounts(hash, blockNumber, vestingArray, vestingLocked, knownRelayBlockNumber) {
const specName = this.specName;
const isAssetHub = chains_config_1.assetHubSpecNames.has(specName);
if (isAssetHub) {
return this.calculateForAssetHub(hash, blockNumber, vestingArray, vestingLocked, knownRelayBlockNumber);
}
else {
return this.calculateForRelayChain(blockNumber, vestingArray, vestingLocked);
}
}
/**
* Calculate vested amounts for Asset Hub chains.
* Post-migration: uses relay chain inclusion block number for calculations.
* If knownRelayBlockNumber is provided, uses it directly instead of searching.
*/
async calculateForAssetHub(hash, blockNumber, vestingArray, vestingLocked, knownRelayBlockNumber) {
const specName = this.specName;
const boundaries = consts_1.MIGRATION_BOUNDARIES[specName];
if (!boundaries) {
// No migration boundaries defined for this Asset Hub - can't calculate
return null;
}
const migrationState = this.getAssetHubMigrationState(blockNumber, boundaries);
if (migrationState === 'during-migration') {
// During migration window, return 0 for claimable
return this.createZeroClaimableResult(vestingArray, vestingLocked, blockNumber.toString(), 'self');
}
if (migrationState === 'pre-migration') {
// Pre-migration: vesting didn't exist on Asset Hub
return null;
}
// Post-migration: need relay chain block number for calculations
// If knownRelayBlockNumber is provided (e.g., from useRcBlock), use it directly
if (knownRelayBlockNumber !== undefined) {
return this.performCalculation(vestingArray, vestingLocked, knownRelayBlockNumber, 'relay');
}
// Otherwise, search for the relay chain inclusion block number
const relayApis = apiRegistry_1.ApiPromiseRegistry.getApiByType('relay');
if (relayApis.length === 0) {
throw new http_errors_1.InternalServerError('Relay chain connection required for vesting calculations on Asset Hub post-migration. ' +
'Please configure MULTI_CHAIN_URL with a relay chain endpoint.');
}
const rcApi = relayApis[0].api;
const inclusionResult = await (0, getRelayParentNumber_1.getInclusionBlockNumber)(this.api, rcApi, hash, ASSET_HUB_PARA_ID);
if (!inclusionResult.found || inclusionResult.inclusionBlockNumber === null) {
// Inclusion not found within search depth
// Fall back to relay parent number for calculation
return this.performCalculation(vestingArray, vestingLocked, new bn_js_1.default(inclusionResult.relayParentNumber), 'relay');
}
return this.performCalculation(vestingArray, vestingLocked, new bn_js_1.default(inclusionResult.inclusionBlockNumber), 'relay');
}
/**
* Calculate vested amounts for relay chains.
* Pre-migration: uses the chain's own block number for calculations.
*/
calculateForRelayChain(blockNumber, vestingArray, vestingLocked) {
const specName = this.specName;
const assetHubSpec = consts_1.relayToSpecMapping.get(specName);
if (!assetHubSpec) {
// Not a known relay chain with migration boundaries
// Use single-chain calculation
return this.performCalculation(vestingArray, vestingLocked, new bn_js_1.default(blockNumber), 'self');
}
const boundaries = consts_1.MIGRATION_BOUNDARIES[assetHubSpec];
if (!boundaries) {
// No boundaries defined, use single-chain calculation
return this.performCalculation(vestingArray, vestingLocked, new bn_js_1.default(blockNumber), 'self');
}
const migrationState = this.getRelayChainMigrationState(blockNumber, boundaries);
if (migrationState === 'during-migration') {
// During migration window, return 0 for claimable
return this.createZeroClaimableResult(vestingArray, vestingLocked, blockNumber.toString(), 'self');
}
if (migrationState === 'post-migration') {
// Post-migration: vesting no longer exists on relay chain
// Return 0 for claimable since vesting has migrated
return this.createZeroClaimableResult(vestingArray, vestingLocked, blockNumber.toString(), 'self');
}
// Pre-migration: use relay chain's own block number
return this.performCalculation(vestingArray, vestingLocked, new bn_js_1.default(blockNumber), 'self');
}
/**
* Perform the actual vesting calculation for vesting schedules.
*/
performCalculation(vestingArray, vestingLocked, currentBlock, source) {
// Convert VestingInfo to calculation interface
const calcSchedules = vestingArray.map((v) => ({
locked: new bn_js_1.default(v.locked.toString()),
perBlock: new bn_js_1.default(v.perBlock.toString()),
startingBlock: new bn_js_1.default(v.startingBlock.toString()),
}));
// Calculate vested for each schedule
const schedules = vestingArray.map((v, idx) => ({
locked: v.locked.toString(),
perBlock: v.perBlock.toString(),
startingBlock: v.startingBlock.toString(),
vested: (0, vestingCalculations_1.calculateVested)(currentBlock, calcSchedules[idx]).toString(),
}));
// Calculate aggregate values
const vestedBalance = (0, vestingCalculations_1.calculateTotalVested)(currentBlock, calcSchedules);
const vestingTotal = (0, vestingCalculations_1.calculateVestingTotal)(calcSchedules);
const vestedClaimable = (0, vestingCalculations_1.calculateVestedClaimable)(vestingLocked, vestingTotal, vestedBalance);
return {
schedules,
vestedBalance: vestedBalance.toString(),
vestingTotal: vestingTotal.toString(),
vestedClaimable: vestedClaimable.toString(),
blockNumberForCalculation: currentBlock.toString(),
blockNumberSource: source,
};
}
/**
* Create a result with zero claimable for all schedules.
* Used during migration windows or post-migration on relay chain.
*/
createZeroClaimableResult(vestingArray, _vestingLocked, blockNumber, source) {
// Convert VestingInfo to calculation interface
const calcSchedules = vestingArray.map((v) => ({
locked: new bn_js_1.default(v.locked.toString()),
perBlock: new bn_js_1.default(v.perBlock.toString()),
startingBlock: new bn_js_1.default(v.startingBlock.toString()),
}));
const vestingTotal = (0, vestingCalculations_1.calculateVestingTotal)(calcSchedules);
const schedules = vestingArray.map((v) => ({
locked: v.locked.toString(),
perBlock: v.perBlock.toString(),
startingBlock: v.startingBlock.toString(),
vested: '0',
}));
return {
schedules,
vestedBalance: '0',
vestingTotal: vestingTotal.toString(),
vestedClaimable: '0',
blockNumberForCalculation: blockNumber,
blockNumberSource: source,
};
}
/**
* Determine migration state for an Asset Hub block.
*/
getAssetHubMigrationState(blockNumber, boundaries) {
if (blockNumber < boundaries.assetHubMigrationStartedAt) {
return 'pre-migration';
}
else if (blockNumber >= boundaries.assetHubMigrationEndedAt) {
return 'post-migration';
}
else {
return 'during-migration';
}
}
/**
* Determine migration state for a relay chain block.
*/
getRelayChainMigrationState(blockNumber, boundaries) {
if (blockNumber < boundaries.relayMigrationStartedAt) {
return 'pre-migration';
}
else if (blockNumber >= boundaries.relayMigrationEndedAt) {
return 'post-migration';
}
else {
return 'during-migration';
}
}
}
exports.AccountsVestingInfoService = AccountsVestingInfoService;
//# sourceMappingURL=AccountsVestingInfoService.js.map