UNPKG

contract-helper

Version:

A contract helper for tron and eth network

727 lines (676 loc) 22.8 kB
import { expect } from "chai"; import sinon from "sinon"; import { TronContractHelper } from "../src/tron"; import { TronWeb } from "tronweb"; import BigNumber from "bignumber.js"; import { ABIFunctionNotProvidedError, BroadcastTronTransactionError, ContractAddressNotProvidedError, ContractMethodNotProvidedError, TransactionReceiptError, } from "../src/errors"; import { config } from "dotenv"; config(); const PRIVATE_KEY = process.env.PRIVATE_KEY || ""; const FULL_NODE = "https://nile.trongrid.io"; const SOLIDITY_NODE = "https://nile.trongrid.io"; const EVENT_SERVER = "https://nile.trongrid.io"; const MULTICALL_V2 = "TZHL5DTcqr6r3uugk2fgtZKHwe4Yp2bsQi"; const MULTI_TYPES = "TUqZGqv18iusqsC84jsHkFD71VWTobe3k8"; const ERC20 = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; const ABI = [ { inputs: [], name: "getUser", outputs: [ { internalType: "address", name: "owner", type: "address" }, { internalType: "uint256", name: "amount", type: "uint256" }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "getOwner", outputs: [{ internalType: "address", name: "", type: "address" }], stateMutability: "view", type: "function", }, { inputs: [], name: "getList", outputs: [{ internalType: "uint256[]", name: "", type: "uint256[]" }], stateMutability: "pure", type: "function", }, { inputs: [], name: "getBytes", outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], stateMutability: "pure", type: "function", }, ]; const ERC20_ABI = [ { inputs: [ { internalType: "address", name: "owner", type: "address" }, { internalType: "address", name: "spender", type: "address" }, ], name: "allowance", outputs: [{ internalType: "uint256", name: "", type: "uint256" }], stateMutability: "view", type: "function", }, ]; describe("tron createTransaction", () => { it("creates transaction without feeCalculation", async () => { const triggerStub = sinon.stub().resolves({ transaction: { raw_data: { contract: [] } }, }); const provider = { transactionBuilder: { triggerSmartContract: triggerStub }, } as any; const helper = new TronContractHelper( MULTICALL_V2, provider, {}, undefined ); const from = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; await helper.createTransaction(from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], options: { feeLimit: 888 }, }); expect(triggerStub.calledOnce).to.equal(true); const callArgs = triggerStub.firstCall.args; expect(callArgs[2]).to.include({ feeLimit: 888 }); }); it("applies feeCalculation when provided", async () => { const triggerStub = sinon.stub().resolves({ transaction: { raw_data: { contract: [] } }, }); const feeCalculation = sinon.stub().resolves({ feeLimit: 123n }); const provider = { transactionBuilder: { triggerSmartContract: triggerStub }, } as any; const helper = new TronContractHelper( MULTICALL_V2, provider, {}, feeCalculation ); const from = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; await helper.createTransaction(from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }); expect(feeCalculation.calledOnce).to.equal(true); const callArgs = triggerStub.firstCall.args; expect(callArgs[2]).to.include({ feeLimit: 123 }); }); it("passes estimateFee to feeCalculation", async () => { const triggerStub = sinon.stub().resolves({ transaction: { raw_data: { contract: [] } }, }); const feeCalculation = sinon.stub().resolves({ feeLimit: 55n }); const provider = { transactionBuilder: { triggerSmartContract: triggerStub }, } as any; const helper = new TronContractHelper( MULTICALL_V2, provider, {}, feeCalculation ); const from = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; await helper.createTransaction( from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }, { estimateFee: false } ); expect( feeCalculation.calledWithMatch({ provider, options: { estimateFee: false }, }) ).to.equal(true); }); }); (PRIVATE_KEY ? describe : describe.skip)( "tron createTransaction (real provider)", () => { const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); it("creates approve transaction with tronweb provider", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const tx = await helper.createTransaction(from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }); expect(tx).to.have.property("raw_data"); // @ts-ignore expect(tx.raw_data.contract?.length).to.be.greaterThan(0); }); } ); (PRIVATE_KEY ? describe : describe.skip)( "tron multicall (real provider)", () => { const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); // multicall it("supports multicall with multiple address arguments", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const owner = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const result = await helper.multicall<{ allowance: BigNumber; owner: string; list: BigNumber[]; }>([ { key: "allowance", address: ERC20, abi: ERC20_ABI, method: "allowance", args: [owner, MULTICALL_V2], }, { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner", }, { key: "list", address: MULTI_TYPES, abi: ABI, method: "getList", }, ]); expect(result.allowance).to.be.instanceOf(BigNumber); expect(result.allowance.toString()).to.match(/^\d+$/); expect(result.owner).to.equal(MULTICALL_V2); expect(result.list[0]).to.be.instanceOf(BigNumber); }); it("calls getUser via multicall and returns expected values", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const result = await helper.multicall<{ owner: string; user: Array<any> & { owner: string; amount: BigNumber }; }>([ { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner", }, { key: "user", address: MULTI_TYPES, abi: ABI, method: "getUser", }, ]); expect(result.user.owner).to.equal(result.owner); expect(result.user.amount).to.be.instanceOf(BigNumber); expect(result.user.amount.toString()).to.equal("42"); }); it("throws when required params are missing", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); try { await helper.multicall([ { key: "user", address: "" as any, abi: ABI, method: "getUser", }, ]); expect.fail("should have thrown"); } catch (err) { expect(err).to.be.instanceOf(ContractAddressNotProvidedError); } try { await helper.multicall([ { key: "user", address: MULTI_TYPES, abi: ABI, method: "" as any, }, ]); expect.fail("should have thrown"); } catch (err) { expect(err).to.be.instanceOf(ContractMethodNotProvidedError); } }); it("formats address and uint values with different formatValue settings", async () => { const defaultHelper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const defaultResult = await defaultHelper.multicall<{ owner: string; list: BigNumber[]; }>([ { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner" }, { key: "list", address: MULTI_TYPES, abi: ABI, method: "getList" }, ]); expect(defaultResult.owner).to.equal(MULTICALL_V2); expect(defaultResult.list[0]).to.be.instanceOf(BigNumber); expect(defaultResult.list[0].toNumber()).to.equal(1); const bigintHelper = new TronContractHelper(MULTICALL_V2, tronWeb, { uint: "bigint", }); const bigintResult = await bigintHelper.multicall<{ list: bigint[] }>([ { key: "list", address: MULTI_TYPES, abi: ABI, method: "getList" }, ]); expect(typeof bigintResult.list[0]).to.equal("bigint"); const hexHelper = new TronContractHelper(MULTICALL_V2, tronWeb, { address: "hex", }); const hexResult = await hexHelper.multicall<{ owner: string }>([ { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner" }, ]); expect(hexResult.owner).to.match(/^41[0-9a-f]{40}$/); const checksumHelper = new TronContractHelper(MULTICALL_V2, tronWeb, { address: "checksum", }); const checksumResult = await checksumHelper.multicall<{ owner: string }>([ { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner" }, ]); expect(checksumResult.owner).to.equal( TronWeb.address.toChecksumAddress(checksumResult.owner) ); const base58Helper = new TronContractHelper(MULTICALL_V2, tronWeb, { address: "base58", }); const base58Result = await base58Helper.multicall<{ owner: string }>([ { key: "owner", address: MULTI_TYPES, abi: ABI, method: "getOwner" }, ]); expect(base58Result.owner).to.equal( TronWeb.address.fromHex(checksumResult.owner) ); }); it("throws when method is unrelated", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); try { await helper.multicall([ { key: "bad", address: MULTI_TYPES, abi: ABI, method: "notExist", }, ]); expect.fail("should have thrown"); } catch (err) { expect(err).to.be.instanceOf(ABIFunctionNotProvidedError); } }); it("throws when address is unrelated", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); try { await helper.multicall([ { key: "user", address: "T0000000000000000000000000000000000", abi: ABI, method: "getUser", }, ]); expect.fail("should have thrown"); } catch (err) { expect(err).to.be.instanceOf(Error); } }); it("throws when args are invalid", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); try { await helper.multicall([ { key: "user", address: MULTI_TYPES, abi: ABI, method: "getUser", args: [1], }, ]); expect.fail("should have thrown"); } catch (err) { expect(err).to.be.instanceOf(Error); } }); // call it("calls getUser and exposes named outputs", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const owner = await helper.call<string>({ address: MULTI_TYPES, abi: ABI, method: "getOwner", }); const user = await helper.call< Array<any> & { owner: string; amount: BigNumber } >({ address: MULTI_TYPES, abi: ABI, method: "getUser", }); expect(user.owner).to.equal(owner); expect(user.amount).to.be.instanceOf(BigNumber); expect(user.amount.toString()).to.equal("42"); }); it("calls getUser with full signature without abi", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const owner = await helper.call<string>({ address: MULTI_TYPES, abi: ABI, method: "getOwner", }); const user = await helper.call<[string, BigNumber]>({ address: MULTI_TYPES, method: "function getUser() view returns (address,uint256)", }); expect(user[0]).to.equal(owner); expect(user[1]).to.be.instanceOf(BigNumber); expect(user[1].toString()).to.equal("42"); }); it("calls getBytes and returns bytes32 string", async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const bytes = await helper.call<string>({ address: MULTI_TYPES, abi: ABI, method: "getBytes", }); expect(bytes).to.be.a("string"); expect(bytes).to.match(/^0x[0-9a-fA-F]{64}$/); }); } ); (PRIVATE_KEY ? describe : describe.skip)("tron send (real provider)", () => { const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); const sendTx = async (tx: any, provider: any) => { const signed = await tronWeb.trx.sign(tx, PRIVATE_KEY); const response = await provider.trx.sendRawTransaction(signed); return response.transaction.txID as string; }; it("sendTransaction broadcasts and returns tx id", async function () { this.timeout(120000); const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const tx = await helper.createTransaction(from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }); const txId = await helper.sendTransaction(tx as any, sendTx as any); expect(txId).to.be.a("string"); expect(txId.length).to.be.greaterThan(0); }); it("send creates and broadcasts transaction", async function () { this.timeout(120000); const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const txId = await helper.send( from, sendTx as any, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }, {} ); expect(txId).to.be.a("string"); expect(txId.length).to.be.greaterThan(0); }); }); (PRIVATE_KEY ? describe : describe.skip)( "tron checkTransactionResult (real provider)", () => { const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); const sendTx = async (tx: any, provider: any) => { const signed = await tronWeb.trx.sign(tx, PRIVATE_KEY); const response = await provider.trx.sendRawTransaction(signed); return response.transaction.txID as string; }; const buildApproveTx = async () => { const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; return helper.createTransaction(from, { address: ERC20, method: "function approve(address,uint256)", args: [from, "1"], }); }; it("fastCheckTransactionResult times out on real provider", async function () { this.timeout(180000); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const tx = await buildApproveTx(); const txId = await helper.sendTransaction(tx as any, sendTx as any); const handled = helper .fastCheckTransactionResult(txId, 1) .catch((err) => err); const err = await handled; expect(err).to.be.instanceOf(TransactionReceiptError); expect((err as Error).message).to.include("timeout"); }); it("finalCheckTransactionResult times out on real provider", async function () { this.timeout(180000); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const tx = await buildApproveTx(); const txId = await helper.sendTransaction(tx as any, sendTx as any); const handled = helper .finalCheckTransactionResult(txId, 1) .catch((err) => err); const err = await handled; expect(err).to.be.instanceOf(TransactionReceiptError); expect((err as Error).message).to.include("timeout"); }); it("fastCheckTransactionResult returns txId", async function () { this.timeout(180000); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const tx = await buildApproveTx(); const txId = await helper.sendTransaction(tx as any, sendTx as any); const result = await helper.fastCheckTransactionResult(txId); expect(result.txId).to.equal(txId); }); it("finalCheckTransactionResult returns txId and blockNumber", async function () { this.timeout(180000); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const helper = new TronContractHelper(MULTICALL_V2, tronWeb, {}); const tx = await buildApproveTx(); const txId = await helper.sendTransaction(tx as any, sendTx as any); const result = await helper.finalCheckTransactionResult(txId); expect(result.txId).to.equal(txId); expect(result.blockNumber).to.be.a("bigint"); }); it("finalCheckTransactionResult times out", async () => { const clock = sinon.useFakeTimers(); const provider = { trx: { getTransactionInfo: sinon.stub().resolves({}), }, } as any; const helper = new TronContractHelper(MULTICALL_V2, provider, {}); const handled = helper .finalCheckTransactionResult("missing", 10) .catch((err) => err); await clock.tickAsync(3010); const err = await handled; expect(err).to.be.instanceOf(TransactionReceiptError); expect((err as Error).message).to.include("timeout"); clock.restore(); }); it("fastCheckTransactionResult times out", async () => { const clock = sinon.useFakeTimers(); const provider = { trx: { getTransaction: sinon.stub().resolves({}), }, } as any; const helper = new TronContractHelper(MULTICALL_V2, provider, {}); const handled = helper .fastCheckTransactionResult("missing", 10) .catch((err) => err); await clock.tickAsync(1010); const err = await handled; expect(err).to.be.instanceOf(TransactionReceiptError); expect((err as Error).message).to.include("timeout"); clock.restore(); }); } ); describe("tron broadcastTransaction", () => { it("broadcasts transaction and returns tx id", async () => { const provider = { trx: { sendRawTransaction: sinon.stub().resolves({ transaction: { txID: "txid" }, }), }, } as any; const result = await TronContractHelper.broadcastTransaction( provider, {} as any ); expect(result).to.equal("txid"); }); it("throws broadcast error with code and decoded message", async () => { const provider = { trx: { sendRawTransaction: sinon.stub().resolves({ code: 1, message: "0x01", }), }, toUtf8: sinon.stub().returns("decoded"), } as any; try { await TronContractHelper.broadcastTransaction(provider, {} as any); expect.fail("should have thrown"); } catch (err: any) { expect(err).to.have.property("code", 1); expect(err.message).to.equal("decoded"); } }); it("broadcasts a real approve transaction", async function () { this.timeout(120000); if (!PRIVATE_KEY) { this.skip(); } const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const txResponse = await tronWeb.transactionBuilder.triggerSmartContract( ERC20, "approve(address,uint256)", {}, [ { type: "address", value: from }, { type: "uint256", value: "1" }, ], from ); const tx = txResponse.transaction; const signed = await tronWeb.trx.sign(tx, PRIVATE_KEY); const txId = await TronContractHelper.broadcastTransaction( tronWeb as any, signed as any ); expect(txId).to.equal(signed.txID); }); it("fails to broadcast with invalid signature", async function () { this.timeout(120000); if (!PRIVATE_KEY) { this.skip(); } const tronWeb = new TronWeb({ fullNode: FULL_NODE, solidityNode: SOLIDITY_NODE, eventServer: EVENT_SERVER, privateKey: PRIVATE_KEY, }); const from = tronWeb.address.fromPrivateKey(PRIVATE_KEY) as string; const balance = await tronWeb.trx.getBalance(from); if (balance < 1) { this.skip(); } const txResponse = await tronWeb.transactionBuilder.triggerSmartContract( ERC20, "approve(address,uint256)", {}, [ { type: "address", value: from }, { type: "uint256", value: "1" }, ], from ); const tx = txResponse.transaction; const signed = await tronWeb.trx.sign(tx, PRIVATE_KEY); signed.signature = ["00"]; try { await TronContractHelper.broadcastTransaction( tronWeb as any, signed as any ); expect.fail("should have thrown"); } catch (err: any) { expect(err).to.be.instanceOf(BroadcastTronTransactionError); expect(err).to.have.property("code"); } }); });