UNPKG

@gear-js/api

Version:

A JavaScript library that provides functionality to connect GEAR Component APIs.

244 lines (241 loc) 9.65 kB
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 };