UNPKG

@layerzerolabs/hardhat-deploy

Version:

Hardhat Plugin For Replicable Deployments And Tests

441 lines (422 loc) 18.2 kB
import { ExternalProvider, JsonRpcFetchFunc, JsonRpcSigner, Networkish, TransactionReceipt, TransactionRequest, TransactionResponse, Web3Provider, } from '@ethersproject/providers'; import {HttpNetworkConfig} from 'hardhat/types'; import {TronSigner} from './signer'; import {BigNumber, Wallet} from 'ethers'; import { Time, TronTransactionFailedError, TronWebError, ensure0x, strip0x, } from './utils'; import { Deferrable, HDNode, isAddress, parseTransaction, } from 'ethers/lib/utils'; import TronWeb from 'tronweb'; import {Transaction, TronWebError1} from 'tronweb/interfaces'; /** * A provider for interacting with the TRON blockchain, extending the Web3Provider. * * `TronWeb3Provider` is designed to integrate TRON's blockchain functionalities with the Web3 interface. * It extends the `Web3Provider` class, adapting it to work with the TRON network. * This class manages a collection of `TronSigner` instances for transaction signing * and provides methods for interacting with the TRON blockchain, such as sending transactions, * estimating gas, and retrieving transaction details. * * Key Features: * - Signer Management: Maintains a collection of `TronSigner` instances for different addresses. * - Transaction Handling: Provides methods for sending transactions, estimating gas, and more. * - TronWeb Integration: Utilizes TronWeb for direct interactions with the TRON network. * - Configurable: Can be configured with custom network settings and HTTP headers. * * @extends Web3Provider * * @constructor * @param {ExternalProvider | JsonRpcFetchFunc} provider - The underlying JSON-RPC provider. * @param {HttpNetworkConfig} config - Configuration for the network, including HTTP headers and URL. * @param {Networkish | undefined} [network] - The network configuration. */ export class TronWeb3Provider extends Web3Provider { protected signers = new Map<string, TronSigner>(); public ro_tronweb: TronWeb; public gasPrice: {time: number; value?: BigNumber} = {time: Time.NOW}; public maxFeeLimit?: number; public FALLBACK_MAX_FEE_LIMIT = 15e9; // 15,000 TRX; private readonly fullHost: string; private readonly headers: Record<string, string>; constructor( provider: ExternalProvider | JsonRpcFetchFunc, config: HttpNetworkConfig, network?: Networkish | undefined ) { super(provider, network); const {httpHeaders: headers, url, accounts} = config; let fullHost = url; // the address of the tron node has the jsonrpc path chopped off fullHost = fullHost.replace(/\/jsonrpc\/?$/, ''); this.fullHost = fullHost; this.headers = headers; this.ro_tronweb = new TronWeb({fullHost, headers}); // instantiate Tron Signer if (Array.isArray(accounts)) { for (const pk of accounts) { const addr = new Wallet(pk).address; this.signers.set(addr, new TronSigner(fullHost, headers, pk, this)); } } else if (typeof accounts !== 'string' && 'mnemonic' in accounts) { const hdNode = HDNode.fromMnemonic( accounts.mnemonic, accounts.passphrase ); const derivedNode = hdNode.derivePath( `${accounts.path}/${accounts.initialIndex}` ); this.signers.set( derivedNode.address, new TronSigner(fullHost, headers, derivedNode.privateKey, this) ); } else { throw new Error( 'unable to instantiate Tron Signer, unrecognized private key' ); } } /** * Adds a new signer to the signer collection. * * This method creates and adds a new `TronSigner` instance to the signer collection using the provided private key. * If a signer already exists for the derived address, it returns the existing signer. * Otherwise, it creates a new `TronSigner`, adds it to the collection, and returns it. * * @param pk - The private key to create a new signer. * @returns The newly added or existing `TronSigner` instance. */ addSigner(pk: string): TronSigner { const addr = new Wallet(pk).address; if (this.signers.has(addr)) return this.signers.get(addr)!; const signer = new TronSigner(this.fullHost, this.headers, pk, this); this.signers.set(addr, signer); return signer; } /** * Retrieves the transaction count for an account. * * This method overrides the `getTransactionCount` method. Since the Tron protocol does not support * the concept of nonces as in Ethereum, this method returns a dummy value. * * @returns A promise that resolves to the dummy transaction count. */ override async getTransactionCount(): Promise<number> { console.log( 'getTransactionCount is not available in the Tron protocol, returning dummy value 1 ...' ); return 1; } /** * Retrieves a signer instance for a given address. * * This method overrides the `getSigner` method to return a signer instance * associated with the provided address. If no signer is found for the given address, it throws an error. * * @template T - The type of signer to be returned, either `TronSigner` or `JsonRpcSigner`. * @param address - The address to retrieve the signer for. * @returns The signer instance corresponding to the given address. * @throws Throws an error if no signer exists for the provided address. */ override getSigner<T extends TronSigner | JsonRpcSigner = JsonRpcSigner>( address: string ): T { const signer = this.signers.get(address); if (!signer) { throw new Error(`No Tron signer exists for this address ${address}`); } return signer as T; } /** * Retrieves the current gas price with caching. * * This method overrides the `getGasPrice` method to include a caching mechanism with a 15-second TTL. * If the cached value is recent (within 15 seconds), it returns the cached value. Otherwise, it fetches * the current gas price from the network. If fetching fails, it defaults to a predefined energy price. * * @returns A promise that resolves to the current gas price as a BigNumber. */ override async getGasPrice(): Promise<BigNumber> { const DEFAULT_ENERGY_PRICE = BigNumber.from('1000'); const {time, value} = this.gasPrice; if (time > Time.NOW - 15 * Time.SECOND && value) return value; const gasPrice = (await super.getGasPrice()) ?? DEFAULT_ENERGY_PRICE; this.gasPrice = {time: Time.NOW, value: gasPrice}; return gasPrice; } /** * Sends a signed transaction to the network. * * This method first checks if the signed transaction is a simple TRX transfer (send TRX transaction). * If so, it handles the transaction through the `sendTrx` method. * * @param signedTransaction - The signed transaction or a promise that resolves to it. * @returns A promise that resolves to the transaction response. */ override async sendTransaction( signedTransaction: string | Promise<string> ): Promise<TransactionResponse> { signedTransaction = await signedTransaction; const deser = parseTransaction(signedTransaction); const {to, data, from, value} = deser; // 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' ); } // otherwise don't alter behavior return super.sendTransaction(signedTransaction); } /** * Sends TRX from one account to another. * * This method handles the sending of TRX tokens by creating, signing, and sending a transaction. * It accounts for the difference in decimal places between TRX (6 decimals) and ETH (18 decimals). * If the value is extremely large (more than 1000 TRX), it scales down the value to prevent errors. * After sending the transaction, it waits briefly for the transaction to be processed. * * @param from - The address to send TRX from. * @param to - The address to send TRX to. * @param value - The amount of TRX to send, as a BigNumber. * @returns A promise that resolves to the transaction response. * @throws Throws an error if the transaction fails. */ async sendTrx( from: string, to: string, value: BigNumber ): Promise<TransactionResponse> { if (value.gt(10 ** 9)) value = value.div(10 ** 12); const unsignedTx = await this.ro_tronweb.transactionBuilder.sendTrx( this.ro_tronweb.address.toHex(to), Math.floor(value.toNumber()), this.ro_tronweb.address.toHex(from) ); const signedTx = await this.getSigner<TronSigner>(from).sign(unsignedTx); 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<TronSigner>(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<TronSigner>(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<TransactionResponse> { 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; } /** * Creates a function that waits for a specified number of confirmations of a transaction. * * This method generates a function that, when called, will continuously check for the number of confirmations * of a given transaction until it reaches the specified target. It checks the transaction status every second. * If the transaction is found to have failed (status 0), a `TronTransactionFailedError` is thrown. * * @param initialConfirmations - The initial number of confirmations at the time of this method call. * @param hash - The hash of the transaction to wait for. * @returns A function that takes `targetConfirmations` and returns a promise that resolves to the transaction receipt. */ private _buildWait(initialConfirmations: number, hash: string) { return async ( targetConfirmations?: number ): Promise<TransactionReceipt> => { let curr_conf = initialConfirmations; while (targetConfirmations && curr_conf < targetConfirmations) { await Time.sleep(Time.SECOND); // sleep 1 sec const {confirmations: latest_conf} = await this.getTransactionWithRetry( hash, 3 ); curr_conf = latest_conf; } const receipt = await this.getTransactionReceipt(ensure0x(hash)); const {status} = receipt; if (status === 0) { throw new TronTransactionFailedError(receipt); } return receipt; }; } /** * Attempts to retrieve a transaction response from the jsonrpc node using the hash, with a retry mechanism. * * This method tries to get a transaction by its hash. If the initial attempt fails, it retries * the operation, up to a specified number of times. Between each retry, the method waits for * a period that increases linearly, with an additional random jitter to avoid simultaneous * retry spikes. This approach is useful for handling transient network issues, or the sync delay that can happen between * a Tron fullNode and its rpc node * * @param hash The hash of the transaction to retrieve. * @param retries The maximum number of attempts to retrieve the transaction. Defaults to 10. * @returns A promise that resolves to the transaction response. */ public async getTransactionWithRetry( hash: string, retries = 10 ): Promise<TransactionResponse> { for (let i = 1; i < retries; i++) { try { const response = await this.getTransaction(ensure0x(hash)); // can return null! if (response == null) throw ''; return response; } catch (error) {} // Linear backoff with jitter const jitter = Math.floor(Math.random() * 300); await Time.sleep(Time.SECOND + jitter); } return await this.getTransaction(ensure0x(hash)); } /** * Estimates the gas required for a transaction on the TRON network. * * This method overrides the `estimateGas` method to accommodate TRON's [specific requirements](https://developers.tron.network/reference/eth_estimategas). * TRON does not support EIP-1559 transactions and nonces, so related fields are removed from the transaction object. * It then calls the superclass's `estimateGas` method for the actual estimation. * * @param transaction - The transaction object to estimate gas for. * @returns A promise that resolves to the estimated gas as a BigNumber. */ override async estimateGas( transaction: Deferrable<TransactionRequest> ): Promise<BigNumber> { const toDel = ['type', 'maxFeePerGas', 'maxPriorityFeePerGas', 'nonce']; for (const field of toDel) { delete (transaction as {[key: string]: any})[field]; } 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<boolean> { 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'; } /** * Asynchronously retrieves and caches the maximum fee limit from the chain parameters. * If the parameter is not found, a predefined fallback value is used. * The value is cached for future calls to this method. * * @returns {Promise<number>} A promise that resolves with the cached or newly retrieved maximum fee limit. */ async getMaxFeeLimit(): Promise<number> { if (this.maxFeeLimit == undefined) { const params = await this.ro_tronweb.trx.getChainParameters(); const param = params.find(({key}) => key === 'getMaxFeeLimit'); this.maxFeeLimit = param?.value ?? this.FALLBACK_MAX_FEE_LIMIT; } return this.maxFeeLimit; } }