@chorus-one/substrate
Version:
All-in-one toolkit for building staking dApps on Substrate Network SDK blockchains(Polkadot, Kusama, etc.)
293 lines (292 loc) • 13 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubstrateStaker = void 0;
const util_crypto_1 = require("@polkadot/util-crypto");
const tx_1 = require("./tx");
const api_1 = require("@polkadot/api");
const util_crypto_2 = require("@polkadot/util-crypto");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
/**
* This class provides the functionality to stake, nominate, unbond, and withdraw funds for a Substrate-based blockchains.
*
* It also provides the ability to retrieve staking information and rewards for a delegator.
*/
class SubstrateStaker {
networkConfig;
api;
/**
* This **static** method is used to derive an address from a public key.
*
* It can be used for signer initialization, e.g. `FireblocksSigner` or `LocalSigner`.
*
* @returns Returns an array containing the derived address.
*/
static getAddressDerivationFn = () => async (publicKey, _derivationPath) => {
return [(0, util_crypto_1.encodeAddress)(publicKey)];
};
/**
* This creates a new SubstrateStaker instance.
*
* @param params - Initialization parameters
* @param params.rpcUrl - RPC URL (e.g. wss://rpc.polkadot.io)
* @param params.rewardDestination - Reward destination (e.g., RewardDestination.STASH or RewardDestination.CONTROLLER)
* @param params.denomMultiplier - (Optional) Multiplier to convert the base coin unit to its smallest subunit (e.g., `1000000000000` for 1 DOT = 1000000000000 Planck)
* @param params.fee - (Optional) Transaction fee (e.g. '0.001' for 0.001 DOT)
* @param params.indexer - (Optional) Indexer instance to supplement missing node RPC features
*
* @returns An instance of SusbstrateStaker.
*/
constructor(params) {
const { ...networkConfig } = params;
this.networkConfig = networkConfig;
}
/**
* Initializes the SubstrateStaker instance and connects to the blockchain.
*
* @returns A promise which resolves once the Staker instance has been initialized.
*/
async init() {
const provider = new api_1.WsProvider(this.networkConfig.rpcUrl);
this.api = await api_1.ApiPromise.create({ provider, noInitWarn: true });
if (this.networkConfig.denomMultiplier === undefined) {
const decimals = this.api.registry.chainDecimals;
if (decimals.length !== 1) {
throw new Error(`expected one chain decimial information, got ${decimals.length}`);
}
this.networkConfig.denomMultiplier = (10 ** this.api.registry.chainDecimals[0]).toString();
}
}
/**
* Closes the SubstrateStaker instance and disconnects from the blockchain.
*
* @returns A promise which resolves once the Staker instance has been closed.
*/
async close() {
const api = this.getApi();
await api.disconnect();
}
/**
* Builds a staking (delegation) transaction.
*
* @param params - Parameters for building the transaction
* @param params.amount - The amount to stake, specified in base units of the native token (e.g. `DOT` for Polkadot)
*
* @returns Returns a promise that resolves to a Polkadot staking transaction.
*/
async buildStakeTx(params) {
const { amount } = params;
return this.buildUnsignedTx({ section: 'staking', method: 'bond' }, [
(0, tx_1.macroToDenomAmount)(amount, (0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier)),
this.networkConfig.rewardDestination
]);
}
/**
* Builds a nomination transaction - allows the user to pick trusted validators to delegate to.
*
* @param params - Parameters for building the transaction
* @param params.validatorAddresses - The list of validator addresses to nominate to
*
* @returns Returns a promise that resolves to a Substrate nomination transaction.
*/
async buildNominateTx(params) {
const { validatorAddresses } = params;
return this.buildUnsignedTx({ section: 'staking', method: 'nominate' }, validatorAddresses);
}
/**
* Builds an unstaking (undelegation) transaction.
*
* @param params - Parameters for building the transaction
* @param params.amount - The amount to unstake, specified in base units of the native token (e.g. `DOT` for Polkadot)
*
* @returns Returns a promise that resolves to a Substrate unstaking transaction.
*/
async buildUnstakeTx(params) {
const { amount } = params;
return this.buildUnsignedTx({ section: 'staking', method: 'unbond' }, [
(0, tx_1.macroToDenomAmount)(amount, (0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier))
]);
}
/**
* Builds a transaction to withdraw all unstaked funds from the validator contract.
*
* @returns Returns a promise that resolves to a Substrate withdraw transaction.
*/
async buildWithdrawTx() {
return this.buildUnsignedTx({ section: 'staking', method: 'withdrawUnbonded' }, [null /* slashing spans */]);
}
/**
* Builds a transaction to delegate more tokens to a validator.
*
* @param params - Parameters for building the transaction
* @param params.amount - The amount to stake, specified in base units of the native token (e.g. `DOT` for Polkadot)
*
* @returns Returns a promise that resolves to a Substrate bond extra transaction.
*/
async buildBondExtraTx(params) {
const { amount } = params;
return this.buildUnsignedTx({ section: 'staking', method: 'bondExtra' }, [
(0, tx_1.macroToDenomAmount)(amount, (0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier))
]);
}
/**
* Retrieves the staking information for a specified delegator.
*
* @param params - Parameters for the request
* @param params.delegatorAddress - The delegator (wallet) address
* @param params.validatorAddress - (Optional) The validator address to assert the delegator is staking with
* @param params.status - (Optional) The status of nomination (default: 'active')
*
* @returns Returns a promise that resolves to the staking information for the specified delegator.
*/
async getStake(params) {
const api = this.getApi();
const { delegatorAddress, validatorAddress, status } = params;
const r = await api.query.staking.ledger(delegatorAddress);
if (r.isEmpty) {
return { balance: '0' };
}
const v = r.toJSON();
if (validatorAddress !== undefined) {
const validators = (await api.query.staking.nominators(delegatorAddress));
const found = validators.toJSON()['targets'].filter((v) => v.toString() === validatorAddress);
if (found.length === 0) {
throw new Error('validator not found in nominators');
}
}
if (status === 'total') {
const total = v?.['total'];
if (typeof total !== 'string') {
throw new Error('JSON value is malformed');
}
return { balance: (0, tx_1.denomToMacroAmount)(total, (0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier)) };
}
const active = v?.['active'];
if (typeof active !== 'string') {
throw new Error('JSON value is malformed');
}
return { balance: (0, tx_1.denomToMacroAmount)(active, (0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier)) };
}
/**
* Signs a transaction using the provided signer.
*
* @param params - Parameters for the signing process
* @param params.signer - Signer instance
* @param params.signerAddress - The address of the signer
* @param params.tx - The transaction to sign
* @param params.blocks - (Optional) The number of blocks until the transaction expires
*
* @returns A promise that resolves to an object containing the signed transaction.
*/
async sign(params) {
const api = this.getApi();
const { signer, signerAddress, tx: unsignedTx, blocks } = params;
const options = {};
if (blocks === 0) {
// forever living extrinsic
options.era = 0;
}
else if (blocks !== undefined) {
const signedBlock = await api.rpc.chain.getBlock();
options.blockHash = signedBlock.block.header.hash;
options.era = api.createType('ExtrinsicEra', {
current: signedBlock.block.header.number,
period: blocks
});
}
let tip = '0';
if (this.networkConfig.fee !== undefined) {
if (this.networkConfig.fee.tip !== undefined) {
const tipBig = (0, bignumber_js_1.default)(this.networkConfig.fee.tip);
if (tipBig.isNaN()) {
throw new Error('tip is not a number');
}
tip = tipBig.multipliedBy((0, tx_1.getDenomMultiplier)(this.networkConfig.denomMultiplier)).toString(10);
}
}
const signingInfo = await api.derive.tx.signingInfo(signerAddress, undefined, options.era);
const payload = api.createType('SignerPayload', {
address: signerAddress,
blockNumber: signingInfo.header ? signingInfo.header.number : 0,
method: unsignedTx.method,
nonce: signingInfo.nonce,
era: options.era,
genesisHash: api.genesisHash,
blockHash: signingInfo.header ? signingInfo.header.hash : api.genesisHash,
runtimeVersion: api.runtimeVersion,
signedExtensions: api.registry.signedExtensions,
version: api.extrinsicVersion,
tip
});
const signature = await this.signRaw(signer, payload);
return {
signedTx: unsignedTx.addSignature(signerAddress, signature, payload.toPayload())
};
}
/**
* This method is used to broadcast a signed transaction to the Substrate network.
*
* @param params - Parameters for the broadcast
* @param params.signedTx - The signed transaction to be broadcasted
*
* @returns Returns a promise that resolves to the response of the transaction that was broadcast to the network.
*/
async broadcast(params) {
const api = this.getApi();
const { signedTx } = params;
// NOTE: Alternative approach to sign and send (useful for troubleshooting)
// const options: Partial<SignerOptions> = { signer: new SubstrateSigner(...) }
// const submittableExtrinsic = await unsignedTx.signAsync(account, optionss)
const status = await api.rpc.author.submitAndWatchExtrinsic(signedTx);
if (status === undefined) {
throw new Error('broadcast failed with empty response');
}
return {
txHash: signedTx.hash.toHex(),
status
};
}
/**
* Retrieves the status of a transaction using the transaction hash.
*
* @param params - Parameters for the transaction status request
* @param params.txHash - The transaction hash to query
*
* @returns A promise that resolves to an object containing the transaction status.
*/
async getTxStatus(params) {
const { txHash } = params;
if (this.networkConfig.indexer == undefined) {
throw new Error('unable to find indexer instance');
}
const indexer = this.networkConfig.indexer;
return await indexer.getTxStatus(txHash);
}
getApi() {
if (this.api === undefined) {
throw new Error('SubstrateStaker instance is not initialized. Did you forget to call init()?');
}
return this.api;
}
async buildUnsignedTx(txCall, params) {
const api = this.getApi();
if (!(txCall.section in api.tx && txCall.method in api.tx[txCall.section])) {
throw new Error(`unable to find method ${txCall.section}.${txCall.method}`);
}
if (params.length === 1) {
return { tx: api.tx[txCall.section][txCall.method](params) };
}
return { tx: api.tx[txCall.section][txCall.method](...params) };
}
async signRaw(signer, payload) {
const { data, address } = payload.toRaw();
const msg = data.length > (256 + 1) * 2 ? (0, util_crypto_2.blake2AsHex)(data) : data;
const message = msg.substring(2);
const signerData = { payload };
const { sig } = await signer.sign(address, { message, data: signerData }, { note: JSON.stringify(payload.toPayload(), null, 2) });
return '0x00' + sig.fullSig;
}
}
exports.SubstrateStaker = SubstrateStaker;