UNPKG

contract-helper

Version:

A contract helper for tron and eth network

522 lines (502 loc) 12.7 kB
import { TronWeb } from "tronweb"; import { AggregateCall, AggregateContractResponse, CONTRACT_SUCCESS, ContractCallArgs, ContractSendArgs, FastTransactionResult, MultiCallArgs, SendTransaction, SimpleTransactionResult, TronProvider, TronFormatValue, TronTransactionRequest, SetTronFee, } from "./types"; import { buildAggregateCall, buildUpAggregateResponse, formatBase58Address, transformContractCallArgs, } from "./contract-utils"; import { ContractParamter, SignedTransaction } from "tronweb/lib/esm/types"; import { ContractHelperBase } from "./contract-helper-base"; import wait from "wait"; import { retry } from "./helper"; import BigNumber from "bignumber.js"; import { FunctionFragment } from "ethers"; import { BroadcastTronTransactionError, TransactionReceiptError, } from "./errors"; const ABI = [ { inputs: [ { components: [ { internalType: "address", name: "target", type: "address", }, { internalType: "bytes", name: "callData", type: "bytes", }, ], internalType: "struct TronMulticall.Call[]", name: "calls", type: "tuple[]", }, ], name: "aggregate", outputs: [ { internalType: "uint256", name: "blockNumber", type: "uint256", }, { internalType: "bytes[]", name: "returnData", type: "bytes[]", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getBasefee", outputs: [ { internalType: "uint256", name: "basefee", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "uint256", name: "blockNumber", type: "uint256", }, ], name: "getBlockHash", outputs: [ { internalType: "bytes32", name: "blockHash", type: "bytes32", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getBlockNumber", outputs: [ { internalType: "uint256", name: "blockNumber", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getChainId", outputs: [ { internalType: "uint256", name: "chainid", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getCurrentBlockCoinbase", outputs: [ { internalType: "address", name: "coinbase", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getCurrentBlockDifficulty", outputs: [ { internalType: "uint256", name: "difficulty", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getCurrentBlockTimestamp", outputs: [ { internalType: "uint256", name: "timestamp", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "addr", type: "address", }, ], name: "getEthBalance", outputs: [ { internalType: "uint256", name: "balance", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getLastBlockHash", outputs: [ { internalType: "bytes32", name: "blockHash", type: "bytes32", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "accountAddress", type: "address", }, { internalType: "trcToken", name: "id", type: "trcToken", }, ], name: "getTokenBalance", outputs: [ { internalType: "uint256", name: "balance", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "addr", type: "address", }, ], name: "isContract", outputs: [ { internalType: "bool", name: "result", type: "bool", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "bytes[]", name: "data", type: "bytes[]", }, ], name: "multicall", outputs: [ { internalType: "bytes[]", name: "results", type: "bytes[]", }, ], stateMutability: "view", type: "function", }, ]; export class TronContractHelper extends ContractHelperBase<"tron"> { private provider: TronProvider; private formatValueType: TronFormatValue; private feeCalculation?: SetTronFee; constructor( multicallContractAddress: string, provider: TronProvider, formatValue: TronFormatValue, feeCalculation?: SetTronFee ) { super(multicallContractAddress); this.provider = provider; this.formatValueType = formatValue; this.feeCalculation = feeCalculation; } private formatToEthAddress(address: string) { if (TronWeb.isAddress(address)) { return ( "0x" + TronWeb.address.toChecksumAddress(address).slice(2).toLowerCase() ); } throw new Error(`${address} is invalid address.`); } /** * Map call contract to match contract format * @param calls The calls context */ private mapCallContextToMatchContractFormat(calls: AggregateCall[]) { return calls.map((call) => [ this.formatToEthAddress(call.target), call.encodedData, ]); } private buildAggregateCall(multiCallArgs: MultiCallArgs[]) { return buildAggregateCall( multiCallArgs, (fragment, values) => { const funcABI = JSON.parse(fragment.format("json")); const params = this.provider.utils.abi.encodeParamsV2ByABI( funcABI, values ); const selector = fragment.selector; const encodedData = `${selector}${params.slice(2)}`; return encodedData; }, "tron" ); } private buildUpAggregateResponse<T>( multiCallArgs: MultiCallArgs[], response: AggregateContractResponse ) { return buildUpAggregateResponse<T>( multiCallArgs, response, (fragment, data) => { const funcABI: FunctionFragment = JSON.parse(fragment.format("json")); return this.provider.utils.abi.decodeParamsV2ByABI(funcABI, data); }, (value, fragment) => { return this.handleContractValue(value, fragment); }, "tron" ); } private formatValue(value: any, type: string) { switch (true) { case type.endsWith("[]"): const itemType = type.slice(0, -2); return value.map((el: any) => this.formatValue(el, itemType)); case type.startsWith("uint"): case type.startsWith("int"): return this.formatValueType?.uint === "bigint" ? BigInt(value.toString()) : new BigNumber(value.toString()); case type === "address": return this.formatValueType?.address === "checksum" ? TronWeb.address.toChecksumAddress(value) : this.formatValueType?.address === "hex" ? TronWeb.address.toHex(value) : formatBase58Address(value); default: return value; } } private handleContractValue<T>( value: any, functionFragment: FunctionFragment ) { const outputs = functionFragment.outputs; if (outputs.length === 1 && !outputs[0].name) { return this.formatValue(value, outputs[0].type); } const result: Array<any> = []; for (let [index, output] of outputs.entries()) { result[index] = this.formatValue(value[index], output.type); if (output.name) { result[output.name] = this.formatValue(value[output.name], output.type); } } return result; } /** * Execute the multicall contract call * @param calls The calls */ public async multicall<T>(calls: MultiCallArgs[]) { const provider = this.provider; const address = this.multicallAddress; const contract = provider.contract(ABI, address); const paramters = this.mapCallContextToMatchContractFormat( this.buildAggregateCall(calls) ); const contractResponse = await contract.aggregate(paramters).call(); return this.buildUpAggregateResponse<T>(calls, contractResponse); } public async call<T>(contractCallArgs: ContractCallArgs) { const { address, abi, method, args = [], } = transformContractCallArgs(contractCallArgs, "tron"); const contract = this.provider.contract(abi as any, address); const rawResult = await contract[method.name](...args).call(); const result = this.handleContractValue(rawResult, method.fragment); return result as T; } static async broadcastTransaction( provider: TronProvider, signedTransaction: SignedTransaction<ContractParamter> ) { const broadcast = await provider.trx.sendRawTransaction(signedTransaction); if (broadcast.code) { const err = new BroadcastTronTransactionError(broadcast.message); err.code = broadcast.code; if (broadcast.message) { err.message = provider.toUtf8(broadcast.message); } const error = new BroadcastTronTransactionError(err.message); error.code = broadcast.code; throw error; } return broadcast.transaction.txID; } private async getFeeParams(provider: TronProvider) { const feeCalculation = this.feeCalculation; if (feeCalculation) { return await feeCalculation({ provider }); } return {}; } async send( from: string, sendTransaction: SendTransaction<"tron">, contractOption: ContractSendArgs<"tron"> ) { const { address, method, options, args = [], } = transformContractCallArgs<"tron">(contractOption, "tron"); const functionFragment = method.fragment; const provider = this.provider; const fee = await this.getFeeParams(provider); const feeParams = fee.feeLimit ? { feeLimit: Number(fee.feeLimit.toString()), } : {}; const transaction = await provider.transactionBuilder.triggerSmartContract( address, functionFragment.format("sighash"), { ...feeParams, ...(options ? options : {}) }, functionFragment.inputs.map((el, i) => ({ type: el.type, value: args[i], })), from ); let txId = await sendTransaction( transaction.transaction, this.provider, "tron" ); return txId; } async fastCheckTransactionResult(txId: string) { return retry( async () => { const transaction = (await this.provider.trx.getTransaction( txId )) as any as FastTransactionResult; if (!transaction.ret?.length) { await wait(1000); return this.fastCheckTransactionResult(txId); } if ( !transaction.ret.every( (result) => result.contractRet === CONTRACT_SUCCESS ) ) { throw new TransactionReceiptError( transaction.ret .filter((el) => el.contractRet !== CONTRACT_SUCCESS) .map((el) => el.contractRet) .join(","), { txId: transaction.txID } ); } return { txId: transaction.txID }; }, 10, 1000 ); } async finalCheckTransactionResult( txId: string ): Promise<SimpleTransactionResult> { const output = await this.provider.trx.getTransactionInfo(txId); if (!Object.keys(output).length) { await wait(3000); return this.finalCheckTransactionResult(txId); } const transactionInfo = { blockNumber: BigInt(output.blockNumber), txId: output.id, }; if (output.result && output.result === "FAILED") { const errMsg = this.provider.toUtf8(output.resMessage); throw new TransactionReceiptError(errMsg, transactionInfo); } if (!Object.prototype.hasOwnProperty.call(output, "contractResult")) { const errMsg = "Failed to execute: " + JSON.stringify(output, null, 2); throw new TransactionReceiptError(errMsg, transactionInfo); } return transactionInfo; } }