sails-js
Version:
Typescript library for working with Sails programs
294 lines (291 loc) • 10.6 kB
JavaScript
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 };