diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 90ef2e10..fd12d768 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,5 +1,7 @@ name: Publish Package to npmjs on: + push: + branches: [ci-testing] release: types: [published] jobs: @@ -16,6 +18,8 @@ jobs: with: version: 8 - run: pnpm install + - run: pnpm test - run: npm publish --access=restricted + if: ${{ github.event_name == 'release' && github.event.action == 'published' }} env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..6005412a --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,7 @@ +{ + "require": "ts-node/register/files", + "exit": true, + "full-trace": false, + "timeout": 30000, + "parallel": false +} diff --git a/package.json b/package.json index 6e68cc98..03ced139 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Hardhat Plugin For Replicable Deployments And Tests", "repository": { "type": "git", - "url": "git+https://github.com/wighawag/hardhat-deploy.git" + "url": "git+https://github.com/LayerZero-Labs/hardhat-deploy.git" }, "author": "wighawag", "license": "MIT", @@ -95,7 +95,7 @@ "lint": "eslint \"**/*.{js,ts}\" && solhint src/**/*.sol", "lint:fix": "eslint --fix \"**/*.{js,ts}\" && solhint --fix src/**/*.sol", "format": "prettier --write \"**/*.{ts,js,sol}\"", - "test": "mocha --timeout 20000 --exit", + "test": "mocha './test/*.test.ts'", "build": "rm -rf ./dist && tsc", "watch": "tsc -w", "publish:next": "npm publish --tag next", diff --git a/src/DeploymentsManager.ts b/src/DeploymentsManager.ts index 704934da..7525df2c 100644 --- a/src/DeploymentsManager.ts +++ b/src/DeploymentsManager.ts @@ -75,6 +75,7 @@ export class DeploymentsManager { public impersonateUnknownAccounts: boolean; public impersonatedAccounts: string[]; public addressesToProtocol: {[address: string]: string} = {}; + public readonly isTronNetworkWithTronSolc: boolean; private network: Network; @@ -136,6 +137,8 @@ export class DeploymentsManager { runAsNode: false, }; this.env = env; + this.isTronNetworkWithTronSolc = + network.tron && (this.env.config as any)?.tronSolc?.enable; this.deploymentsPath = env.config.paths.deployments; // TODO @@ -213,6 +216,8 @@ export class DeploymentsManager { contractName: string ): Promise => { if (this.db.onlyArtifacts) { + // For the Tron network there is already a mechanism in this file to ensure onlyArtifacts can only point to artifacts folder ending in -tron + // We assume those artifacts folder ending in -tron are compatible with Tron and have been compiled with tron-solc const artifactFromFolder = await getExtendedArtifactFromFolders( contractName, this.db.onlyArtifacts @@ -1052,7 +1057,7 @@ export class DeploymentsManager { if (externalContracts.deploy) { // make sure we're not deploying on Tron contracts that are not meant to be deployed there if ( - this.env.config.paths.artifacts.endsWith('-tron') && // are we using tron-solc compiler? + this.isTronNetworkWithTronSolc && // are we using tron-solc compiler and is the network a Tron network? externalContracts.artifacts.some((str) => !str.endsWith('-tron')) // are some of the artifacts folder not ending in -tron? ) { continue; @@ -1419,8 +1424,8 @@ export class DeploymentsManager { } private getImportPaths() { - // this check is true when the hardhat-tron-solc plugin is used - if (this.env.config.paths.artifacts.endsWith('-tron')) { + // this check is true when the hardhat-tron-solc plugin is used and the network is Tron + if (this.isTronNetworkWithTronSolc) { return this.getTronImportPaths(); } const importPaths = [this.env.config.paths.imports]; diff --git a/src/helpers.ts b/src/helpers.ts index 2b8ba1b3..3c63b07c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -559,7 +559,7 @@ export function addHelpers( if (options.deterministicDeployment) { // feature not ready for Tron yet if (network.tron) { - throw new Error('deterministic deployment not yet supported on Tron'); + throw new Error('deterministic deployment not supported on Tron'); } if (typeof unsignedTx.data === 'string') { const create2DeployerAddress = await ensureCreate2DeployerReady( @@ -2712,10 +2712,28 @@ data: ${data} } } - tx = await handleSpecificErrors( - ethersContract.functions[methodName](...ethersArgs) - ); - + if (network.tron) { + const method = ethersContract.interface.getFunction(methodName); + const methodParams = method.inputs.map((input) => input.type); + const funcSig = `${methodName}(${methodParams.join(',')})`; + const tronArgs = args.map((a, i) => ({ + type: methodParams[i], + value: a, + })); + tx = await handleSpecificErrors( + (ethersSigner.provider as TronWeb3Provider).triggerSmartContract( + from, + deployment.address, + funcSig, + tronArgs, + overrides + ) + ); + } else { + tx = await handleSpecificErrors( + ethersContract.functions[methodName](...ethersArgs) + ); + } tx = await onPendingTx(tx); if (options.log || hardwareWallet) { @@ -2832,16 +2850,16 @@ data: ${data} const extension: DeploymentsExtension = { ...partialExtension, fetchIfDifferent, - deploy, + deploy, // tron compatible diamond: { deploy: diamond, }, catchUnknownSigner, - execute, + execute, // tron compatible rawTx, - read, - deterministic, - getSigner, + read, // tron compatible + deterministic, // won't support tron (contracts addresses are dependent on timestamps) + getSigner, // tron compatible }; const utils = { diff --git a/src/tron/provider.ts b/src/tron/provider.ts index 453786cb..376b923d 100644 --- a/src/tron/provider.ts +++ b/src/tron/provider.ts @@ -16,10 +16,16 @@ import { TronTransactionFailedError, TronWebError, ensure0x, + strip0x, } from './utils'; -import {Deferrable, HDNode, parseTransaction} from 'ethers/lib/utils'; +import { + Deferrable, + HDNode, + isAddress, + parseTransaction, +} from 'ethers/lib/utils'; import TronWeb from 'tronweb'; -import {TronWebError1} from 'tronweb/interfaces'; +import {Transaction, TronWebError1} from 'tronweb/interfaces'; /** * A provider for interacting with the TRON blockchain, extending the Web3Provider. @@ -44,7 +50,7 @@ import {TronWebError1} from 'tronweb/interfaces'; * @param {Networkish | undefined} [network] - The network configuration. */ export class TronWeb3Provider extends Web3Provider { - protected signer = new Map(); + protected signers = new Map(); public ro_tronweb: TronWeb; public gasPrice: {time: number; value?: BigNumber} = {time: Time.NOW}; private readonly fullHost: string; @@ -67,7 +73,7 @@ export class TronWeb3Provider extends Web3Provider { if (Array.isArray(accounts)) { for (const pk of accounts) { const addr = new Wallet(pk).address; - this.signer.set(addr, new TronSigner(fullHost, headers, pk, this)); + this.signers.set(addr, new TronSigner(fullHost, headers, pk, this)); } } else if (typeof accounts !== 'string' && 'mnemonic' in accounts) { const hdNode = HDNode.fromMnemonic( @@ -77,7 +83,7 @@ export class TronWeb3Provider extends Web3Provider { const derivedNode = hdNode.derivePath( `${accounts.path}/${accounts.initialIndex}` ); - this.signer.set( + this.signers.set( derivedNode.address, new TronSigner(fullHost, headers, derivedNode.privateKey, this) ); @@ -100,9 +106,9 @@ export class TronWeb3Provider extends Web3Provider { */ addSigner(pk: string): TronSigner { const addr = new Wallet(pk).address; - if (this.signer.has(addr)) return this.signer.get(addr)!; + if (this.signers.has(addr)) return this.signers.get(addr)!; const signer = new TronSigner(this.fullHost, this.headers, pk, this); - this.signer.set(addr, signer); + this.signers.set(addr, signer); return signer; } @@ -135,7 +141,7 @@ export class TronWeb3Provider extends Web3Provider { override getSigner( address: string ): T { - const signer = this.signer.get(address); + const signer = this.signers.get(address); if (!signer) { throw new Error(`No Tron signer exists for this address ${address}`); } @@ -175,11 +181,17 @@ export class TronWeb3Provider extends Web3Provider { signedTransaction = await signedTransaction; const deser = parseTransaction(signedTransaction); const {to, data, from, value} = deser; - // is this a send eth transaction? - if (to && from && (!data || data == '0x')) { - return this.sendTrx(from, to, value); + + // is this a send trx transaction? + if (this.isSendTRX(to, from, data)) { + return this.sendTrx(from!, to!, value); + } + // is this a smart contract transaction? + if (await this.isSmartContractCall(to, from, data)) { + throw new Error( + 'direct smart contract call not yet implemented for Tron' + ); } - // TODO, smart contract calls, etc // otherwise don't alter behavior return super.sendTransaction(signedTransaction); @@ -211,10 +223,72 @@ export class TronWeb3Provider extends Web3Provider { this.ro_tronweb.address.toHex(from) ); const signedTx = await this.getSigner(from).sign(unsignedTx); - const response = await this.ro_tronweb.trx.sendRawTransaction(signedTx); + return this.sendRawTransaction(signedTx); + } + + /** + * Triggers a function call on a specified smart contract in the Tron network. + * + * This method constructs a transaction to call a function of a smart contract. It requires + * the sender's address, the contract address, the function signature, parameters for the function, + * and an options object which may include a gas limit and an optional value to send with the transaction. + * The fee limit for the transaction is determined using the sender's signer. The transaction + * is then signed and sent to the Tron network. + * + * @param from - The address of the sender initiating the contract call. + * @param contract - The address of the smart contract to interact with. + * @param funcSig - The function signature to call in the smart contract. + * @param params - An array of parameters for the function call, each with a type and value. + * @param options - An object containing optional parameters. + * @returns A promise that resolves to a `TransactionResponse` object representing the result of the transaction. + */ + async triggerSmartContract( + from: string, + contract: string, + funcSig: string, + params: {type: string; value: string | number}[], + options: { + gasLimit?: string | number | BigNumber; + value?: string | BigNumber; + } + ) { + const feeLimit = await this.getSigner(from).getFeeLimit( + {to: contract}, + options + ); + const {transaction} = + await this.ro_tronweb.transactionBuilder.triggerSmartContract( + this.ro_tronweb.address.toHex(contract), + funcSig, + {feeLimit, callValue: options.value?.toString() ?? 0}, + params, + this.ro_tronweb.address.toHex(from) + ); + const signedTx = await this.getSigner(from).sign(transaction); + return this.sendRawTransaction(signedTx); + } + + /** + * Sends a raw transaction to the Tron network and returns the transaction response. + * + * This method accepts a raw transaction object, sends it to the Tron network, and waits + * for the transaction to be acknowledged. After the transaction is acknowledged, it retrieves + * and returns the transaction response. If the transaction fails at any stage, the method + * throws an error. + * + * @param transaction - The raw transaction object to be sent to the network. + * @returns A promise that resolves to a `TransactionResponse` object, which includes details of the processed transaction. + * @throws `TronWebError` - If the transaction fails to be sent or acknowledged by the network. + * + */ + async sendRawTransaction( + transaction: Transaction + ): Promise { + const response = await this.ro_tronweb.trx.sendRawTransaction(transaction); if (!('result' in response) || !response.result) { throw new TronWebError(response as TronWebError1); } + console.log('\nTron transaction broadcast, waiting for response...'); const txRes = await this.getTransactionWithRetry(response.txid); txRes.wait = this._buildWait(txRes.confirmations, response.txid); return txRes; @@ -302,4 +376,47 @@ export class TronWeb3Provider extends Web3Provider { } return super.estimateGas(transaction); } + + /** + * Checks if a given transaction is a smart contract call. + * + * This method examines the `to`, `from`, and `data` fields of a transaction + * to determine if it is likely a call to a smart contract. It considers a transaction + * as a smart contract call if all fields are defined, the addresses are valid, + * the data field has a significant length, and there is associated contract code. + * + * @param to - The recipient address of the transaction. + * @param from - The sender address of the transaction. + * @param data - The data payload of the transaction. + * @returns A promise that resolves to `true` if the transaction is a smart contract call, otherwise `false`. + */ + async isSmartContractCall( + to?: string, + from?: string, + data?: string + ): Promise { + if ([to, from, data].some((f) => f == undefined)) return false; + if ([to, from].some((f) => isAddress(f!) == false)) return false; + if (data!.length <= 2) return false; + const contractCode = await this.getCode(to!); + return contractCode != undefined && strip0x(contractCode).length > 0; + } + + /** + * Determines if a transaction is a TRX (transfer) operation. + * + * This method checks if the provided `to`, `from`, and `data` fields + * of a transaction suggest a TRX operation. It considers a transaction as + * a TRX operation if the `to` and `from` fields are defined and the `data` field + * is either not present or equals '0x'. + * + * @param to - The recipient address of the transaction. + * @param from - The sender address of the transaction. + * @param data - The data payload of the transaction. + * @returns `true` if the transaction is likely a TRX operation, otherwise `false`. + */ + isSendTRX(to?: string, from?: string, data?: string): boolean { + if ([to, from].some((f) => f == undefined)) return false; + return !data || data == '0x'; + } } diff --git a/src/tron/signer.ts b/src/tron/signer.ts index f46aff97..02ba19a1 100644 --- a/src/tron/signer.ts +++ b/src/tron/signer.ts @@ -7,7 +7,7 @@ import {BigNumber, Wallet} from 'ethers'; import {Deferrable} from 'ethers/lib/utils'; import TronWeb from 'tronweb'; import {TronWeb3Provider} from './provider'; -import {Time, TronWebGetTransactionError, ensure0x, strip0x} from './utils'; +import {Time, TronWebGetTransactionError, strip0x} from './utils'; import {CreateSmartContract, MethodSymbol, TronTxMethods} from './types'; import {TronWebError} from './utils'; import { @@ -135,16 +135,7 @@ export class TronSigner extends Wallet { ); const signedTx = await this.sign(unsignedTx); - - const response = await this.tronweb.trx.sendRawTransaction(signedTx); - if (!('result' in response) || !response.result) { - throw new TronWebError(response as TronWebError1); // in this case tronweb returs an error-like object with a message and a code - } - console.log('\nTransaction broadcast, waiting for response...'); - const provider = this.provider as TronWeb3Provider; - const txRes = await provider.getTransactionWithRetry(response.txid); - txRes.wait = provider._buildWait(txRes.confirmations, response.txid); - return txRes; + return (this.provider as TronWeb3Provider).sendRawTransaction(signedTx); } /** diff --git a/src/tron/tronweb.d.ts b/src/tron/tronweb.d.ts index 0e21bd14..9bec0b02 100644 --- a/src/tron/tronweb.d.ts +++ b/src/tron/tronweb.d.ts @@ -251,7 +251,7 @@ declare module 'tronweb' { options: Record, parameter: any[], issuerAddress: string - ): Promise>; + ): Promise; undelegateResource( amount: number, receiverAddress: string, diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index ff8d86b2..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,4 +0,0 @@ ---require dotenv/config ---require ts-node/register ---require source-map-support/register ---recursive test/**/*.test.ts