/* eslint-disable no-control-regex */ import utils from '../../utils/index.js'; import { encodeParamsV2ByABI, decodeParamsV2ByABI } from '../../utils/abi.js'; import { TronWeb } from '../../tronweb.js'; import { Contract } from './index.js'; import { sha3 } from '../../utils/crypto.js'; export interface CallOptions { feeLimit?: number; callValue?: number; callTokenValue?: number; callTokenId?: number; userFeePercentage?: number; shouldPollResponse?: boolean; from?: string | false; rawParameter?: string; _isConstant?: true; } export interface SendOptions { from?: string | false; feeLimit?: number; callValue?: number; rawParameter?: string; userFeePercentage?: number; shouldPollResponse?: boolean; pollTimes?: number; rawResponse?: boolean; keepTxID?: boolean; } import type { FragmentTypes, StateMutabilityTypes, FunctionFragment, FallbackFragment, ReceiveFragment, EventFragment, AbiInputsType, AbiOutputsType, } from '../../types/ABI.js'; export type AbiFragmentNoErrConstructor = FunctionFragment | EventFragment | FallbackFragment | ReceiveFragment; const getFunctionSelector = (abi: AbiFragmentNoErrConstructor) => { if ('stateMutability' in abi) { (abi.stateMutability as StateMutabilityTypes) = abi.stateMutability ? abi.stateMutability.toLowerCase() : 'nonpayable'; } (abi.type as FragmentTypes) = abi.type ? abi.type.toLowerCase() : ''; if (abi.type === 'fallback' || abi.type === 'receive') return '0x'; const iface = new utils.ethersUtils.Interface([abi]); let obj; if (abi.type === 'event') { obj = iface.getEvent(abi.name); } else { obj = iface.getFunction(abi.name); } if (obj) { return obj.format('sighash'); } throw new Error('unknown function'); }; const decodeOutput = (abi: AbiFragmentNoErrConstructor, output: string) => { return decodeParamsV2ByABI(abi, output); }; export class Method { tronWeb: TronWeb; contract: Contract; abi: AbiFragmentNoErrConstructor; name: string; inputs: AbiInputsType; outputs: AbiOutputsType; functionSelector: string | null; signature: string; defaultOptions: { feeLimit: number; callValue: number; userFeePercentage: number; shouldPollResponse: boolean; }; constructor(contract: Contract, abi: AbiFragmentNoErrConstructor) { this.tronWeb = contract.tronWeb; this.contract = contract; this.abi = abi; this.name = abi.name || abi.type; this.inputs = abi.inputs || []; this.outputs = []; if ('outputs' in abi && abi.outputs) { this.outputs = abi.outputs; } this.functionSelector = getFunctionSelector(abi); this.signature = sha3(this.functionSelector, false).slice(0, 8); this.defaultOptions = { feeLimit: this.tronWeb.feeLimit, callValue: 0, userFeePercentage: 100, shouldPollResponse: false, // Only used for sign() }; } decodeInput(data: string) { const abi = JSON.parse(JSON.stringify(this.abi)); abi.outputs = abi.inputs; return decodeOutput(abi, '0x' + data); } onMethod(...args: any[]) { let rawParameter = ''; if (this.abi && !/event/i.test(this.abi.type)) { rawParameter = encodeParamsV2ByABI(this.abi, args); } return { call: async (options: CallOptions = {}) => { options = { ...options, rawParameter, }; return await this._call([], [], options); }, send: async (options: SendOptions = {}, privateKey = this.tronWeb.defaultPrivateKey) => { options = { ...options, rawParameter, }; return await this._send([], [], options, privateKey); }, }; } async _call(types: [], args: [], options: CallOptions = {}) { if (types.length !== args.length) { throw new Error('Invalid argument count provided'); } if (!this.contract.address) { throw new Error('Smart contract is missing address'); } if (!this.contract.deployed) { throw new Error('Calling smart contracts requires you to load the contract first'); } if ('stateMutability' in this.abi) { const { stateMutability } = this.abi; if (stateMutability && !['pure', 'view'].includes(stateMutability.toLowerCase())) { throw new Error(`Methods with state mutability "${stateMutability}" must use send()`); } } options = { ...this.defaultOptions, from: this.tronWeb.defaultAddress.hex, ...options, _isConstant: true, }; const parameters = args.map((value, index) => ({ type: types[index], value, })); const transaction = await this.tronWeb.transactionBuilder.triggerSmartContract( this.contract.address, this.functionSelector!, options, parameters, options.from ? this.tronWeb.address.toHex(options.from) : undefined ); if (!utils.hasProperty(transaction, 'constant_result')) { throw new Error('Failed to execute'); } const len = transaction.constant_result![0].length; if (len === 0 || len % 64 === 8) { let msg = 'The call has been reverted or has thrown an error.'; if (len !== 0) { msg += ' Error message: '; let msg2 = ''; const chunk = transaction.constant_result![0].substring(8); for (let i = 0; i < len - 8; i += 64) { msg2 += this.tronWeb.toUtf8(chunk.substring(i, i + 64)); } msg += msg2 .replace(/(\u0000|\u000b|\f)+/g, ' ') .replace(/ +/g, ' ') .replace(/\s+$/g, ''); } throw new Error(msg); } let output = decodeOutput(this.abi, '0x' + transaction.constant_result![0]); if (output.length === 1 && Object.keys(output).length === 1) { output = output[0]; } return output; } async _send(types: [], args: [], options: SendOptions = {}, privateKey = this.tronWeb.defaultPrivateKey) { if (types.length !== args.length) { throw new Error('Invalid argument count provided'); } if (!this.contract.address) { throw new Error('Smart contract is missing address'); } if (!this.contract.deployed) { throw new Error('Calling smart contracts requires you to load the contract first'); } const { stateMutability } = this.abi as { stateMutability: StateMutabilityTypes }; if (['pure', 'view'].includes(stateMutability.toLowerCase())) { throw new Error(`Methods with state mutability "${stateMutability}" must use call()`); } // If a function isn't payable, dont provide a callValue. if (!['payable'].includes(stateMutability.toLowerCase())) { options.callValue = 0; } options = { ...this.defaultOptions, from: this.tronWeb.defaultAddress.hex, ...options, }; const parameters = args.map((value, index) => ({ type: types[index], value, })); const address = privateKey ? this.tronWeb.address.fromPrivateKey(privateKey) : this.tronWeb.defaultAddress.base58; const transaction = await this.tronWeb.transactionBuilder.triggerSmartContract( this.contract.address, this.functionSelector!, options, parameters, this.tronWeb.address.toHex(address as string) ); if (!transaction.result || !transaction.result.result) { throw new Error('Unknown error: ' + JSON.stringify(transaction, null, 2)); } // If privateKey is false, this won't be signed here. We assume sign functionality will be replaced. const signedTransaction = await this.tronWeb.trx.sign(transaction.transaction, privateKey); if (!signedTransaction.signature) { if (!privateKey) { throw new Error('Transaction was not signed properly'); } throw new Error('Invalid private key provided'); } const broadcast = await this.tronWeb.trx.sendRawTransaction(signedTransaction); if (broadcast.code) { const err = { error: broadcast.code, message: broadcast.code as unknown as string, }; if (broadcast.message) err.message = this.tronWeb.toUtf8(broadcast.message); const error = new Error(err.message); (error as any).error = broadcast.code; throw error; } if (!options.shouldPollResponse) { return signedTransaction.txID; } const checkResult: (index: number) => any = async (index) => { if (index === (options.pollTimes || 20)) { const error: any = new Error('Cannot find result in solidity node'); error.error = 'Cannot find result in solidity node'; error.transaction = signedTransaction; throw error; } const output = await this.tronWeb.trx.getTransactionInfo(signedTransaction.txID); if (!Object.keys(output).length) { await new Promise((r) => setTimeout(r, 3000)); return checkResult(index + 1); } if (output.result && output.result === 'FAILED') { const error: any = new Error(this.tronWeb.toUtf8(output.resMessage)); error.error = this.tronWeb.toUtf8(output.resMessage); error.transaction = signedTransaction; error.output = output; throw error; } if (!utils.hasProperty(output, 'contractResult')) { const error: any = new Error('Failed to execute: ' + JSON.stringify(output, null, 2)); error.error = 'Failed to execute: ' + JSON.stringify(output, null, 2); error.transaction = signedTransaction; error.output = output; throw error; } if (options.rawResponse) { return output; } let decoded = decodeOutput(this.abi, '0x' + output.contractResult[0]); if (decoded.length === 1 && Object.keys(decoded).length === 1) { decoded = decoded[0]; } if (options.keepTxID) { return [signedTransaction.txID, decoded]; } return decoded; }; return checkResult(0); } }