UNPKG

sails-js

Version:

Typescript library for working with Sails programs

294 lines (291 loc) 10.6 kB
import { decodeAddress } from '@gear-js/api'; import { u8aConcat } from '@polkadot/util'; import { getPayloadMethod } from './util/lib/payload-method.js'; import { ZERO_ADDRESS } from './consts.js'; import { throwOnErrorReply } from './utils.js'; class TransactionBuilder { _api; _registry; _service; _method; _responseType; _onProgramCreated; _account; _signerOptions; _tx; _voucher; _gasInfo; programId; _prefixByteLength; _gasLimit; constructor(_api, _registry, extrinsic, _service, _method, payload, payloadType, _responseType, _programIdOrCodeOrCodeId, _onProgramCreated) { this._api = _api; this._registry = _registry; this._service = _service; this._method = _method; this._responseType = _responseType; this._onProgramCreated = _onProgramCreated; const encodedService = this._service ? this._registry.createType('String', this._service).toU8a() : new Uint8Array(); const encodedMethod = this._registry.createType('String', this._method).toU8a(); const data = payload === undefined ? new Uint8Array() : this._registry.createType(payloadType, payload).toU8a(); const _payload = u8aConcat(encodedService, encodedMethod, data); this._prefixByteLength = encodedMethod.byteLength; if (this._service) { this._prefixByteLength += encodedService.byteLength; } switch (extrinsic) { case 'send_message': { this.programId = _programIdOrCodeOrCodeId; this._tx = this._api.message.send({ destination: this.programId, gasLimit: 0, payload: _payload, value: 0, }); break; } case 'upload_program': { const { programId, extrinsic } = this._api.program.upload({ code: _programIdOrCodeOrCodeId, gasLimit: 0, initPayload: _payload, }); this.programId = programId; this._tx = extrinsic; break; } case 'create_program': { const { programId, extrinsic } = this._api.program.create({ codeId: _programIdOrCodeOrCodeId, gasLimit: 0, initPayload: _payload, }); this.programId = programId; this._tx = extrinsic; break; } } } _getGas(gas, increaseGas) { if (increaseGas === 0) return gas; if (increaseGas < 0 || increaseGas > 100) throw new Error('Invalid increaseGas value (0-100)'); return this._registry.createType('u64', gas.add(gas.muln(increaseGas / 100))); } _getValue(value) { return this._registry.createType('u128', value); } _setTxArg(index, value) { const args = this._tx.args.map((arg, i) => (i === index ? value : arg)); switch (this._tx.method.method) { case 'uploadProgram': { this._tx = this._api.tx.gear.uploadProgram(...args); break; } case 'createProgram': { this._tx = this._api.tx.gear.createProgram(...args); break; } case 'sendMessage': { this._tx = this._api.tx.gear.sendMessage(...args); break; } } } /** ## Get submittable extrinsic */ get extrinsic() { return this._tx; } /** ## Get payload of the transaction */ get payload() { return this._tx.args[0].toHex(); } /** * ## Calculate gas for transaction * @param allowOtherPanics Allow panics in other contracts to be triggered (default: false) * @param increaseGas Increase the gas limit by a percentage from 0 to 100 (default: 0) * @returns */ async calculateGas(allowOtherPanics = false, increaseGas = 0) { const source = this._account ? decodeAddress(typeof this._account === 'string' ? this._account : this._account.address) : ZERO_ADDRESS; let gas; let gasArgPosition; switch (this._tx.method.method) { case 'uploadProgram': { gas = await this._api.program.calculateGas.initUpload(source, this._tx.args[0].toHex(), this._tx.args[2].toHex(), this._tx.args[4], allowOtherPanics); break; } case 'createProgram': { gas = await this._api.program.calculateGas.initCreate(source, this._tx.args[0].toHex(), this._tx.args[2].toHex(), this._tx.args[4], allowOtherPanics); gasArgPosition = 3; break; } case 'sendMessage': { gas = await this._api.program.calculateGas.handle(source, this._tx.args[0].toHex(), this._tx.args[1].toHex(), this._tx.args[3], allowOtherPanics); gasArgPosition = 2; break; } default: { throw new Error('Unknown extrinsic'); } } this._gasInfo = gas; const finalGas = this._getGas(gas.min_limit, increaseGas); this._setTxArg(gasArgPosition, finalGas); this._gasLimit = finalGas.toBigInt(); return this; } /** * ## Set account for transaction * @param account * @param signerOptions */ withAccount(account, signerOptions) { this._account = account; if (signerOptions) { this._signerOptions = signerOptions; } return this; } /** * ## Set value for transaction * @param value */ withValue(value) { switch (this._tx.method.method) { case 'uploadProgram': case 'createProgram': { this._setTxArg(4, this._getValue(value)); break; } case 'sendMessage': { this._setTxArg(3, this._getValue(value)); break; } default: { throw new Error('Unknown extrinsic'); } } return this; } /** * ## Set gas for transaction * @param gas - bigint value or 'max'. If 'max', the gas limit will be set to the block gas limit. */ withGas(gas) { const _gas = gas === 'max' ? this._api.blockGasLimit : this._registry.createType('u64', gas); switch (this._tx.method.method) { case 'uploadProgram': case 'createProgram': { this._setTxArg(3, _gas); break; } case 'sendMessage': { this._setTxArg(2, _gas); break; } default: { throw new Error('Unknown extrinsic'); } } this._gasLimit = _gas.toBigInt(); return this; } /** * ## Use voucher for transaction * @param id Voucher id */ withVoucher(id) { if (this._tx.method.method !== 'sendMessage') { throw new Error('Voucher can be used only with sendMessage extrinsics'); } this._voucher = id; return this; } /** * ## Get transaction fee */ async transactionFee() { if (!this._account) { throw new Error('Account is required. Use withAccount() method to set account.'); } const info = await this._tx.paymentInfo(this._account, this._signerOptions); return info.partialFee.toBigInt(); } /** * ## Sign and send transaction */ async signAndSend() { if (!this._account) { throw new Error('Account is required. Use withAccount() method to set account.'); } if (!this._gasLimit) { await this.calculateGas(); } if (this._voucher) { const callParams = { SendMessage: this._tx }; this._tx = this._api.voucher.call(this._voucher, callParams); } let resolveFinalized; const isFinalized = new Promise((resolve) => { resolveFinalized = resolve; }); const { msgId, blockHash, programId } = await new Promise((resolve, reject) => this._tx .signAndSend(this._account, this._signerOptions, ({ events, status }) => { if (status.isInBlock) { let msgId; let programId; for (const { event } of events) { const { method, section, data } = event; if (section == 'gear') { if (method === 'MessageQueued') { msgId = data.id.toHex(); } else if (method == 'ProgramChanged') { programId = data.id.toHex(); } } else if (method === 'ExtrinsicSuccess') { resolve({ msgId, blockHash: status.asInBlock.toHex(), programId }); } else if (method === 'ExtrinsicFailed') { reject(this._api.getExtrinsicFailedError(event)); } } } else if (status.isFinalized) { resolveFinalized(true); } }) .catch((error) => { reject(error.message); })); if (this._onProgramCreated && programId) { await this._onProgramCreated(programId); } return { msgId, blockHash, txHash: this._tx.hash.toHex(), isFinalized, response: async (rawResult = false) => { const { data: { message: { payload, details }, }, } = await this._api.message.getReplyEvent(this.programId, msgId, blockHash); throwOnErrorReply(details.unwrap().code, payload, this._api.specVersion, this._registry); if (rawResult) { return payload.toHex(); } // prettier-ignore return this._registry.createType(this._responseType, payload.slice(this._prefixByteLength))[getPayloadMethod(this._responseType)](); }, }; } get gasInfo() { return this._gasInfo; } } export { TransactionBuilder };