@gear-js/api
Version:
A JavaScript library that provides functionality to connect GEAR Component APIs.
244 lines (241 loc) • 9.65 kB
JavaScript
import { randomAsHex } from '@polkadot/util-crypto';
import '@polkadot/util';
import { SubmitProgramError, ProgramDoesNotExistError, ProgramHasNoMetahash } from '../errors/program.errors.js';
import { RpcMethodNotSupportedError } from '../errors/rpc.js';
import '@polkadot/api';
import { encodePayload } from '../utils/create-payload.js';
import { generateCodeHash, generateProgramId } from '../utils/generate.js';
import { getExtrinsic } from '../utils/getExtrinsic.js';
import { getIdsFromKeys } from '../utils/prefixes.js';
import '../utils/reply-code.js';
import { validateValue, validateGasLimit, validateProgramId } from '../utils/validate.js';
import { GearGas } from './Gas.js';
import { GearTransaction } from './Transaction.js';
const PROGRAM_STATE_CHANGES_SUB = 'gear_subscribeProgramStateChanges';
class GearProgram extends GearTransaction {
_api;
calculateGas;
// public resumeSession: GearResumeSession;
constructor(_api) {
super(_api);
this._api = _api;
this.calculateGas = new GearGas(_api);
// this.resumeSession = new GearResumeSession(_api);
}
upload(args, metaOrHexRegistry, typeIndexOrTypeName) {
validateValue(args.value, this._api);
validateGasLimit(args.gasLimit, this._api);
const salt = args.salt || randomAsHex(20);
const code = typeof args.code === 'string' ? args.code : this._api.createType('Bytes', Array.from(args.code));
const payload = encodePayload(args.initPayload, metaOrHexRegistry, 'init', typeIndexOrTypeName);
const codeId = generateCodeHash(code);
const programId = generateProgramId(codeId, salt);
try {
const txArgs = [code, salt, payload, args.gasLimit, args.value || 0, args.keepAlive];
this.extrinsic = getExtrinsic(this._api, 'gear', 'uploadProgram', txArgs);
return { programId, codeId, salt, extrinsic: this.extrinsic };
}
catch (error) {
console.log(error);
throw new SubmitProgramError();
}
}
create({ codeId, initPayload, value, gasLimit, ...args }, metaOrHexRegistry, typeIndexOrTypeName) {
validateValue(value, this._api);
validateGasLimit(gasLimit, this._api);
const payload = encodePayload(initPayload, metaOrHexRegistry, 'init', typeIndexOrTypeName);
const salt = args.salt || randomAsHex(20);
const programId = generateProgramId(codeId, salt);
try {
const txArgs = [codeId, salt, payload, gasLimit, value || 0, args.keepAlive];
this.extrinsic = getExtrinsic(this._api, 'gear', 'createProgram', txArgs);
return { programId, salt, extrinsic: this.extrinsic };
}
catch (_) {
throw new SubmitProgramError();
}
}
/**
* ### Pay program rent
* @param programId
* @param blockCount
* @returns
* @example
* ```javascript
* const tx = await api.program.payRent('0x...', 100_000);
* tx.signAndSend(account, (events) => {
* events.forEach(({event}) => console.log(event.toHuman()))
* })
* ```
*/
async payRent(programId, blockCount) {
await validateProgramId(programId, this._api);
return this._api.tx.gear.payProgramRent(programId, blockCount);
}
/**
* ### Calculate the cost of rent for a certain number of blocks
* @param blockCount
* @returns u128 number
*/
calcualtePayRent(blockCount) {
return this.costPerBlock.muln(blockCount);
}
/**
* Get ids of all uploaded programs
* @returns Array of program ids
*/
async allUploadedPrograms(count) {
const prefix = this._api.query.gearProgram.programStorage.keyPrefix();
const programIds = [];
if (count) {
const keys = await this._api.rpc.state.getKeysPaged(prefix, count);
programIds.push(...getIdsFromKeys(keys, prefix));
}
else {
count = 1000;
const keys = await this._api.rpc.state.getKeysPaged(prefix, count);
programIds.push(...getIdsFromKeys(keys, prefix));
let keysLength = keys.length;
let lastKey = keys.at(-1);
while (keysLength === count) {
const keys = await this._api.rpc.state.getKeysPaged(prefix, count, lastKey);
programIds.push(...getIdsFromKeys(keys, prefix));
lastKey = keys.at(-1);
keysLength = keys.length;
}
}
return programIds;
}
/**
*
* @param id A program id
* @returns `true` if address belongs to program, and `false` otherwise
*/
async exists(id) {
const program = (await this._api.query.gearProgram.programStorage(id));
return program.isSome;
}
/**
* @deprecated use `api.program.codeId` instead
*/
async codeHash(id) {
return this.codeId(id);
}
/**
* Get code's id of the program uploaded on chain
* @param programId
* @returns codeId of the program code
*/
async codeId(id) {
const program = await this._api.programStorage.getProgram(id);
return program.codeId.toHex();
}
/**
* ### Get hash of program metadata
* @param programId
* @param at (optional) block hash
* @returns
*/
async metaHash(programId, at) {
try {
const metaHash = (await this._api.rpc.gear.readMetahash(programId, at || null));
return metaHash.toHex();
}
catch (error) {
if (error.code === 8000) {
if (error.data.includes('Program not found')) {
throw new ProgramDoesNotExistError(programId);
}
if (error.data.includes('unreachable')) {
throw new ProgramHasNoMetahash(programId);
}
}
throw error;
}
}
get costPerBlock() {
return this._api.consts.gear.programRentCostPerBlock;
}
get rentMinimalResumePeriod() {
return this._api.consts.gear.programRentMinimalResumePeriod;
}
get rentFreePeriod() {
return this._api.consts.gear.programRentFreePeriod;
}
/**
* ## Subscribe to Program State Changes
*
* Subscribe to real-time notifications when program state changes on the blockchain.
* This method uses server-side filtering to efficiently track specific programs without polling.
*
* The subscription notifies whenever one or more programs undergo state modifications, such as:
* - Program code updates
* - Program creation or termination
* - Program status changes
*
* @param programIds - List of program IDs to monitor, or null to listen to all programs.
* When null, the callback will be triggered for any program state change on the blockchain.
* Pass a specific array of HexStrings to filter notifications to only those programs.
*
* @param callback - Async or sync function called when monitored program state changes.
* Parameters:
* - blockHash (HexString): Hash of the block where the state change occurred
* - programIds (HexString[]): Array of program IDs that changed state in this block
*
* The callback can perform async operations (e.g., fetching updated program state).
*
* @returns Unsubscribe function. Call this to stop receiving state change notifications.
*
* @throws RpcMethodNotSupportedError - If the connected node does not support the subscription method.
*
* @example
* ```typescript
* // Monitor all program state changes
* const unsubscribe = await api.program.subscribeToStateChanges(
* null, // listen to all programs
* (blockHash, programIds) => {
* console.log(`Block: ${blockHash}`);
* console.log(`Updated programs: ${programIds.join(', ')}`);
* }
* );
*
* // Later, stop listening
* unsubscribe();
* ```
*
* @example
* ```typescript
* // Monitor specific programs
* const myPrograms = ['0x...', '0x...'];
*
* const unsubscribe = await api.program.subscribeToStateChanges(
* myPrograms,
* async (blockHash, changedProgramIds) => {
* console.log(`Programs changed in block: ${blockHash}`);
*
* // Perform async operations on updated programs
* for (const programId of changedProgramIds) {
* // Check if program still exists
* const exists = await api.program.exists(programId);
* // Get updated code ID
* const codeId = await api.program.codeId(programId);
* console.log(`Program ${programId} - Exists: ${exists}, Code: ${codeId}`);
* }
* }
* );
* ```
*
* @see https://github.com/gear-tech/gear/pull/4895 for implementation details
*/
subscribeToStateChanges(programIds, callback) {
if (!this._api.rpcMethods.includes(PROGRAM_STATE_CHANGES_SUB)) {
throw new RpcMethodNotSupportedError(PROGRAM_STATE_CHANGES_SUB);
}
return this._api.rpc.gear.subscribeProgramStateChanges(programIds, (result) => {
if (result.isEmpty || !result.block_hash || result.block_hash.isEmpty)
return;
callback(result.block_hash.toHex(), result.program_ids.toArray().map((item) => item.toHex()));
});
}
}
export { GearProgram };