diff --git a/docs/contracts.md b/docs/contracts.md index 98ba620be..4887b4fda 100644 --- a/docs/contracts.md +++ b/docs/contracts.md @@ -159,16 +159,21 @@ const _soloUrl = 'http://localhost:8669/'; const soloNetwork = new HttpClient(_soloUrl); const thorSoloClient = new ThorClient(soloNetwork); -// Deploying the ERC20 contract using the Thor client and the deployer's private key -const transaction = await thorSoloClient.contracts.deployContract( - privateKeyDeployer, - erc20ContractBytecode +// Creating the contract factory +const contractFactory = thorSoloClient.contracts.createContractFactory( + VIP180_ABI, + erc20ContractBytecode, + privateKeyDeployer ); +// Deploying the contract +await contractFactory.startDeployment(); + +// Awaiting the contract deployment +const contract = await contractFactory.waitForDeployment(); + // Awaiting the transaction receipt to confirm successful contract deployment -const receipt = await thorSoloClient.transactions.waitForTransaction( - transaction.id -); +const receipt = contract.deployTransactionReceipt; // Asserting that the contract deployment didn't revert, indicating a successful deployment expect(receipt.reverted).toEqual(false); @@ -217,18 +222,19 @@ const thorSoloClient = new ThorClient(soloNetwork); // Defining a function for deploying the ERC20 contract const setupERC20Contract = async (): Promise => { - // Deploying the ERC20 contract using the Thor client and the deployer's private key - const transaction = await thorSoloClient.contracts.deployContract( - privateKeyDeployer, - erc20ContractBytecode + const contractFactory = thorSoloClient.contracts.createContractFactory( + VIP180_ABI, + erc20ContractBytecode, + privateKeyDeployer ); - // Awaiting the transaction receipt to confirm successful contract deployment - const receipt = await thorSoloClient.transactions.waitForTransaction( - transaction.id - ); + // Deploying the contract + await contractFactory.startDeployment(); + + // Waiting for the contract to be deployed + const contract = await contractFactory.waitForDeployment(); - return receipt.outputs[0].contractAddress; + return contract.address; }; // Setting up the ERC20 contract and getting its address diff --git a/docs/examples/contracts/contract-create-ERC20-token.ts b/docs/examples/contracts/contract-create-ERC20-token.ts index 4be8a76cb..7a9ee4020 100644 --- a/docs/examples/contracts/contract-create-ERC20-token.ts +++ b/docs/examples/contracts/contract-create-ERC20-token.ts @@ -18,16 +18,21 @@ const _soloUrl = 'http://localhost:8669/'; const soloNetwork = new HttpClient(_soloUrl); const thorSoloClient = new ThorClient(soloNetwork); -// Deploying the ERC20 contract using the Thor client and the deployer's private key -const transaction = await thorSoloClient.contracts.deployContract( - privateKeyDeployer, - erc20ContractBytecode +// Creating the contract factory +const contractFactory = thorSoloClient.contracts.createContractFactory( + VIP180_ABI, + erc20ContractBytecode, + privateKeyDeployer ); +// Deploying the contract +await contractFactory.startDeployment(); + +// Awaiting the contract deployment +const contract = await contractFactory.waitForDeployment(); + // Awaiting the transaction receipt to confirm successful contract deployment -const receipt = await thorSoloClient.transactions.waitForTransaction( - transaction.id -); +const receipt = contract.deployTransactionReceipt; // Asserting that the contract deployment didn't revert, indicating a successful deployment expect(receipt.reverted).toEqual(false); diff --git a/docs/examples/contracts/contract-transfer-ERC20-token.ts b/docs/examples/contracts/contract-transfer-ERC20-token.ts index ffc8e342e..051db5781 100644 --- a/docs/examples/contracts/contract-transfer-ERC20-token.ts +++ b/docs/examples/contracts/contract-transfer-ERC20-token.ts @@ -20,18 +20,19 @@ const thorSoloClient = new ThorClient(soloNetwork); // Defining a function for deploying the ERC20 contract const setupERC20Contract = async (): Promise => { - // Deploying the ERC20 contract using the Thor client and the deployer's private key - const transaction = await thorSoloClient.contracts.deployContract( - privateKeyDeployer, - erc20ContractBytecode + const contractFactory = thorSoloClient.contracts.createContractFactory( + VIP180_ABI, + erc20ContractBytecode, + privateKeyDeployer ); - // Awaiting the transaction receipt to confirm successful contract deployment - const receipt = await thorSoloClient.transactions.waitForTransaction( - transaction.id - ); + // Deploying the contract + await contractFactory.startDeployment(); + + // Waiting for the contract to be deployed + const contract = await contractFactory.waitForDeployment(); - return receipt.outputs[0].contractAddress; + return contract.address; }; // Setting up the ERC20 contract and getting its address diff --git a/docs/examples/thor-client/contract.ts b/docs/examples/thor-client/contract.ts index 7a1b9642a..69dbbedab 100644 --- a/docs/examples/thor-client/contract.ts +++ b/docs/examples/thor-client/contract.ts @@ -1,5 +1,6 @@ import { HttpClient, ThorClient } from '@vechain/vechain-sdk-network'; import { expect } from 'expect'; +import type { DeployParams, InterfaceAbi } from '@vechain/vechain-sdk-core'; // 1 - Create thor client for solo network @@ -13,14 +14,43 @@ const privateKeyDeployer = '706e6acd567fdc22db54aead12cb39db01c4832f149f95299aa8dd8bef7d28ff'; // Private key of a test account with VTHO (energy) to pay for the deployment const contractBytecode: string = - '0x608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806360fe47b11461003b5780636d4ce63c14610057575b600080fd5b610055600480360381019061005091906100c3565b610075565b005b61005f61007f565b60405161006c91906100ff565b60405180910390f35b8060008190555050565b60008054905090565b600080fd5b6000819050919050565b6100a08161008d565b81146100ab57600080fd5b50565b6000813590506100bd81610097565b92915050565b6000602082840312156100d9576100d8610088565b5b60006100e7848285016100ae565b91505092915050565b6100f98161008d565b82525050565b600060208201905061011460008301846100f0565b9291505056fea26469706673582212206fd02e4453276839e38700c049f3c14a66636c948f1c2466388da61fb7574f3164736f6c63430008170033'; - -const result = await thorSoloClient.contracts.deployContract( - privateKeyDeployer, - contractBytecode + '0x608060405234801561001057600080fd5b506040516102063803806102068339818101604052810190610032919061007a565b80600081905550506100a7565b600080fd5b6000819050919050565b61005781610044565b811461006257600080fd5b50565b6000815190506100748161004e565b92915050565b6000602082840312156100905761008f61003f565b5b600061009e84828501610065565b91505092915050565b610150806100b66000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806360fe47b11461003b5780636d4ce63c14610057575b600080fd5b610055600480360381019061005091906100c3565b610075565b005b61005f61007f565b60405161006c91906100ff565b60405180910390f35b8060008190555050565b60008054905090565b600080fd5b6000819050919050565b6100a08161008d565b81146100ab57600080fd5b50565b6000813590506100bd81610097565b92915050565b6000602082840312156100d9576100d8610088565b5b60006100e7848285016100ae565b91505092915050565b6100f98161008d565b82525050565b600060208201905061011460008301846100f0565b9291505056fea2646970667358221220785262acbf50fa50a7b4dc8d8087ca8904c7e6b847a13674503fdcbac903b67e64736f6c63430008170033'; + +const deployedContractAbi: InterfaceAbi = [ + { + inputs: [], + name: 'get', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [{ internalType: 'uint256', name: 'x', type: 'uint256' }], + name: 'set', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + } +]; + +// Creating the contract factory +let contractFactory = thorSoloClient.contracts.createContractFactory( + deployedContractAbi, + contractBytecode, + privateKeyDeployer ); -const receipt = await thorSoloClient.transactions.waitForTransaction(result.id); +// Deploy parameters to be used for the contract creation +const deployParams: DeployParams = { types: ['uint'], values: ['100'] }; + +// Deploying the contract +contractFactory = await contractFactory.startDeployment(deployParams); + +// Awaiting the contract deployment +const contract = await contractFactory.waitForDeployment(); + +// Awaiting the transaction receipt to confirm successful contract deployment +const receipt = contract.deployTransactionReceipt; expect(receipt.reverted).toEqual(false); expect(receipt.outputs[0].contractAddress).toBeDefined(); diff --git a/packages/core/src/utils/const/data.ts b/packages/core/src/utils/const/data.ts index eef956f05..97fcff76e 100644 --- a/packages/core/src/utils/const/data.ts +++ b/packages/core/src/utils/const/data.ts @@ -32,7 +32,7 @@ const DECIMAL_INTEGER_REGEX = /^\d+$/; * Allows optional "-" prefix and validates both integer and floating point numbers. * Also allows for numbers with no leading digits (i.e. ".123", which is equivalent to "0.123"). */ -const NUMERIC_REGEX = /^-?\d*(\.\d+)?$/; +const NUMERIC_REGEX = /(^-?\d+(\.\d+)?)$|(^-?\.\d+)$/; /** * Default length of thor id hex string. diff --git a/packages/core/src/utils/data/data.ts b/packages/core/src/utils/data/data.ts index 6d73ded53..84fcfc3c3 100644 --- a/packages/core/src/utils/data/data.ts +++ b/packages/core/src/utils/data/data.ts @@ -115,9 +115,6 @@ const removePrefix = (hex: string): string => { * @returns - A boolean indicating whether the input is a valid numeric string. */ const isNumeric = (value: string): boolean => { - if (value === '') { - return false; - } return NUMERIC_REGEX.test(value); }; diff --git a/packages/errors/src/model/core/contract.ts b/packages/errors/src/model/core/contract.ts new file mode 100644 index 000000000..0fa745741 --- /dev/null +++ b/packages/errors/src/model/core/contract.ts @@ -0,0 +1,19 @@ +import { type DefaultErrorData } from '../../types'; +import { ErrorBase } from '../base'; + +/** + * Error to be thrown when the contract deployment failed. + */ +class ContractDeploymentFailedError extends ErrorBase< + CONTRACT.CONTRACT_DEPLOYMENT_FAILED, + DefaultErrorData +> {} + +/** + * Errors enum. + */ +enum CONTRACT { + CONTRACT_DEPLOYMENT_FAILED = 'CONTRACT_DEPLOYMENT_FAILED' +} + +export { ContractDeploymentFailedError, CONTRACT }; diff --git a/packages/errors/src/model/core/index.ts b/packages/errors/src/model/core/index.ts index 9583ca72a..bd1f8b3d7 100644 --- a/packages/errors/src/model/core/index.ts +++ b/packages/errors/src/model/core/index.ts @@ -2,6 +2,7 @@ export * from './abi'; export * from './address'; export * from './bloom'; export * from './certificate'; +export * from './contract'; export * from './data'; export * from './hdnode'; export * from './keystore'; diff --git a/packages/errors/src/types/errorTypes.ts b/packages/errors/src/types/errorTypes.ts index cddf3d244..f2e566a63 100644 --- a/packages/errors/src/types/errorTypes.ts +++ b/packages/errors/src/types/errorTypes.ts @@ -8,11 +8,18 @@ import { CertificateNotSignedError, ContractInterfaceError, DATA, + EIP1193, + EIP1193ChainDisconnected, + EIP1193Disconnected, + type EIP1193ProviderRpcErrorData, + EIP1193Unauthorized, + EIP1193UnsupportedMethod, + EIP1193UserRejectedRequest, type ErrorBase, + FUNCTION, HDNODE, HTTP_CLIENT, HTTPClientError, - POLL_ERROR, type HTTPClientErrorData, InvalidAbiDataToDecodeError, InvalidAbiDataToEncodeError, @@ -32,39 +39,36 @@ import { InvalidKeystoreError, InvalidKeystorePasswordError, InvalidRLPError, - PollExecutionError, type InvalidRLPErrorData, - type PollErrorData, InvalidSecp256k1MessageHashError, InvalidSecp256k1PrivateKeyError, InvalidSecp256k1SignatureError, InvalidSecp256k1SignatureRecoveryError, + JSONRPC, + JSONRPCDefaultError, + type JSONRPCErrorData, + JSONRPCInternalError, + JSONRPCInvalidParams, + JSONRPCInvalidRequest, + JSONRPCMethodNotFound, + JSONRPCParseError, KEYSTORE, + NotImplementedError, + POLL_ERROR, + type PollErrorData, + PollExecutionError, RLP, SECP256K1, TRANSACTION, TransactionAlreadySignedError, TransactionBodyError, TransactionDelegationError, - TransactionNotSignedError, - FUNCTION, - NotImplementedError, - EIP1193, - type EIP1193ProviderRpcErrorData, - EIP1193UserRejectedRequest, - EIP1193Unauthorized, - EIP1193UnsupportedMethod, - EIP1193Disconnected, - EIP1193ChainDisconnected, - JSONRPC, - type JSONRPCErrorData, - JSONRPCParseError, - JSONRPCInvalidRequest, - JSONRPCMethodNotFound, - JSONRPCInvalidParams, - JSONRPCInternalError, - JSONRPCDefaultError + TransactionNotSignedError } from '../model'; +import { + CONTRACT, + ContractDeploymentFailedError +} from '../model/core/contract'; /** * @note: REGISTER YOUR NEW FANCY ERRORS BELOW! @@ -95,7 +99,8 @@ type ErrorCode = | POLL_ERROR | FUNCTION | EIP1193 - | JSONRPC; + | JSONRPC + | CONTRACT; /** * Conditional type to get the error data type from the error code. @@ -156,7 +161,8 @@ const ERROR_CODES = { POLL_ERROR, FUNCTION, EIP1193, - JSONRPC + JSONRPC, + CONTRACT }; /** @@ -271,7 +277,9 @@ type ErrorType = ? JSONRPCInternalError : ErrorCodeT extends JSONRPC.DEFAULT ? JSONRPCDefaultError - : never; + : ErrorCodeT extends CONTRACT.CONTRACT_DEPLOYMENT_FAILED + ? ContractDeploymentFailedError + : never; /** * Map to get the error class from the error code. @@ -362,7 +370,10 @@ const ErrorClassMap = new Map< [JSONRPC.METHOD_NOT_FOUND, JSONRPCMethodNotFound], [JSONRPC.INVALID_PARAMS, JSONRPCInvalidParams], [JSONRPC.INTERNAL_ERROR, JSONRPCInternalError], - [JSONRPC.DEFAULT, JSONRPCDefaultError] + [JSONRPC.DEFAULT, JSONRPCDefaultError], + + // CONTRACT + [CONTRACT.CONTRACT_DEPLOYMENT_FAILED, ContractDeploymentFailedError] ]); export { diff --git a/packages/network/src/thor-client/contracts/contracts-module.ts b/packages/network/src/thor-client/contracts/contracts-module.ts index 9656cc95e..0c796da6d 100644 --- a/packages/network/src/thor-client/contracts/contracts-module.ts +++ b/packages/network/src/thor-client/contracts/contracts-module.ts @@ -1,6 +1,5 @@ import { contract, - type DeployParams, type InterfaceAbi, PARAMS_ABI, PARAMS_ADDRESS, @@ -10,6 +9,7 @@ import { import type { ContractCallOptions, ContractTransactionOptions } from './types'; import { type SendTransactionResult } from '../transactions'; import { type ThorClient } from '../thor-client'; +import { ContractFactory } from './model'; /** * Represents a module for interacting with smart contracts on the blockchain. @@ -22,48 +22,20 @@ class ContractsModule { constructor(readonly thor: ThorClient) {} /** - * Deploys a smart contract to the blockchain. + * Creates a new instance of `ContractFactory` configured with the specified ABI, bytecode, and private key. + * This factory is used to deploy new smart contracts to the blockchain network managed by this instance. * - * @param privateKey - The private key of the account deploying the smart contract. - * @param contractBytecode - The bytecode of the smart contract to be deployed. - * @param deployParams - The parameters to pass to the smart contract constructor. - * @param options - (Optional) An object containing options for the transaction body. Includes all options of the `buildTransactionBody` method - * besides `isDelegated`. - * - * @returns A promise that resolves to a `TransactionSendResult` object representing the result of the deployment. + * @param abi - The Application Binary Interface (ABI) of the contract, which defines the contract's methods and events. + * @param bytecode - The compiled bytecode of the contract, representing the contract's executable code. + * @param privateKey - The private key used for signing transactions during contract deployment, ensuring the deployer's identity. + * @returns An instance of `ContractFactory` configured with the provided ABI, bytecode, and private key, ready for deploying contracts. */ - public async deployContract( - privateKey: string, - contractBytecode: string, - deployParams?: DeployParams, - options?: ContractTransactionOptions - ): Promise { - // Build a transaction for deploying the smart contract - const deployContractClause = contract.clauseBuilder.deployContract( - contractBytecode, - deployParams - ); - - // Estimate the gas cost of the transaction - const gasResult = await this.thor.gas.estimateGas( - [deployContractClause], - addressUtils.fromPrivateKey(Buffer.from(privateKey, 'hex')) - ); - - const txBody = await this.thor.transactions.buildTransactionBody( - [deployContractClause], - gasResult.totalGas, - options - ); - - // Sign the transaction with the provided private key - const signedTx = await this.thor.transactions.signTransaction( - txBody, - privateKey - ); - - // Send the signed transaction to the blockchain - return await this.thor.transactions.sendTransaction(signedTx); + public createContractFactory( + abi: InterfaceAbi, + bytecode: string, + privateKey: string + ): ContractFactory { + return new ContractFactory(abi, bytecode, privateKey, this.thor); } /** diff --git a/packages/network/src/thor-client/contracts/model/contract-factory.ts b/packages/network/src/thor-client/contracts/model/contract-factory.ts new file mode 100644 index 000000000..b217b728e --- /dev/null +++ b/packages/network/src/thor-client/contracts/model/contract-factory.ts @@ -0,0 +1,169 @@ +import { + addressUtils, + contract, + type DeployParams, + type InterfaceAbi +} from '@vechain/vechain-sdk-core'; +import type { ContractTransactionOptions } from '../types'; +import { type ThorClient } from '../../thor-client'; +import { Contract } from './contract'; +import { assert, buildError, ERROR_CODES } from '@vechain/vechain-sdk-errors'; +import { + type SendTransactionResult, + type TransactionReceipt +} from '../../transactions'; + +/** + * A factory class for deploying smart contracts to a blockchain using a ThorClient. + */ +class ContractFactory { + /** + * The ABI (Application Binary Interface) of the contract. + */ + private readonly abi: InterfaceAbi; + + /** + * The bytecode of the smart contract. + */ + private readonly bytecode: string; + + /** + * The private key used for signing transactions. + */ + private readonly privateKey: string; + + /** + * An instance of ThorClient to interact with the blockchain. + */ + private readonly thor: ThorClient; + + /** + * The result of the deploy transaction, undefined until a deployment is started. + */ + private deployTransaction: SendTransactionResult | undefined; + + /** + * Initializes a new instance of the `ContractFactory` class. + * @param abi The Application Binary Interface (ABI) of the contract, which defines the contract's methods and events. + * @param bytecode The compiled bytecode of the contract, representing the contract's executable code. + * @param privateKey The private key used for signing transactions during contract deployment, ensuring the deployer's identity. + * @param thor An instance of ThorClient to interact with the blockchain. + */ + constructor( + abi: InterfaceAbi, + bytecode: string, + privateKey: string, + thor: ThorClient + ) { + this.abi = abi; + this.bytecode = bytecode; + this.privateKey = privateKey; + this.thor = thor; + } + + /** + * Initiates the deployment of a smart contract. + * + * This method performs several steps to deploy a smart contract: + * 1. Builds a transaction clause for deploying the contract. + * 2. Estimates the gas cost required for the transaction. + * 3. Constructs the transaction body with the estimated gas cost. + * 4. Signs the transaction using the provided private key. + * 5. Sends the signed transaction to the blockchain. + * + * @param {DeployParams?} deployParams (Optional) parameters for contract deployment. + * @param {ContractTransactionOptions?} options (Optional) transaction options, such as gas limit. + * @returns {Promise} A promise that resolves to the instance of `ContractFactory`, + * allowing for fluent chaining of further actions or queries. + * @throws {Error} Throws an error if any step in the deployment process fails. + */ + public async startDeployment( + deployParams?: DeployParams, + options?: ContractTransactionOptions + ): Promise { + // Build a transaction for deploying the smart contract + const deployContractClause = contract.clauseBuilder.deployContract( + this.bytecode, + deployParams + ); + + // Estimate the gas cost of the transaction + const gasResult = await this.thor.gas.estimateGas( + [deployContractClause], + addressUtils.fromPrivateKey(Buffer.from(this.privateKey, 'hex')) + ); + + const txBody = await this.thor.transactions.buildTransactionBody( + [deployContractClause], + gasResult.totalGas, + options + ); + + // Sign the transaction with the provided private key + const signedTx = await this.thor.transactions.signTransaction( + txBody, + this.privateKey + ); + + // Send the signed transaction to the blockchain + this.deployTransaction = + await this.thor.transactions.sendTransaction(signedTx); + + return this; + } + + /** + * Waits for the completion of a contract deployment transaction. + * + * This method checks for the presence of a deploy transaction result and then + * waits for the transaction to be processed. Upon successful processing, it + * constructs and returns a new `Contract` instance based on the transaction receipt. + * + * @throws An error if the deploy transaction result is not found or if the + * contract deployment fails. + * @returns {Promise} A promise that resolves to a `Contract` instance + * once the deployment transaction is completed. + */ + public async waitForDeployment(): Promise { + // Check if the deploy transaction result is available + if (this.deployTransaction === undefined) { + throw buildError( + ERROR_CODES.CONTRACT.CONTRACT_DEPLOYMENT_FAILED, + 'Cannot find a contract deployment transaction.', + { deployTransaction: this.deployTransaction } + ); + } + + // Wait for the transaction to be processed + const transactionReceipt = + await this.thor.transactions.waitForTransaction( + this.deployTransaction.id + ); + + // Ensure that the transaction receipt is valid + assert( + transactionReceipt?.outputs[0]?.contractAddress !== null && + transactionReceipt?.outputs[0]?.contractAddress !== undefined, + ERROR_CODES.CONTRACT.CONTRACT_DEPLOYMENT_FAILED, + 'Contract deployment failed.', + { deployTransaction: this.deployTransaction } + ); + + // Construct and return a new Contract instance + return new Contract( + transactionReceipt?.outputs[0].contractAddress as string, + this.abi, + this.thor, + transactionReceipt as TransactionReceipt + ); + } + + /** + * Returns the deploy transaction result, if available. + */ + public getDeployTransaction(): SendTransactionResult | undefined { + return this.deployTransaction; + } +} + +export { ContractFactory }; diff --git a/packages/network/src/thor-client/contracts/model/contract.ts b/packages/network/src/thor-client/contracts/model/contract.ts new file mode 100644 index 000000000..cda0f33fc --- /dev/null +++ b/packages/network/src/thor-client/contracts/model/contract.ts @@ -0,0 +1,35 @@ +import { type InterfaceAbi } from '@vechain/vechain-sdk-core'; +import type { TransactionReceipt } from '../../transactions'; +import { type ThorClient } from '../../thor-client'; + +/** + * A class representing a smart contract deployed on the blockchain. + */ +class Contract { + private readonly thor: ThorClient; + public address: string; + public abi: InterfaceAbi; + + public deployTransactionReceipt: TransactionReceipt | undefined; + + /** + * Initializes a new instance of the `Contract` class. + * @param address The address of the contract. + * @param abi The Application Binary Interface (ABI) of the contract, which defines the contract's methods and events. + * @param thor An instance of ThorClient to interact with the blockchain. + * @param transactionReceipt (Optional) The transaction receipt of the contract deployment. + */ + constructor( + address: string, + abi: InterfaceAbi, + thor: ThorClient, + transactionReceipt?: TransactionReceipt + ) { + this.abi = abi; + this.thor = thor; + this.address = address; + this.deployTransactionReceipt = transactionReceipt; + } +} + +export { Contract }; diff --git a/packages/network/src/thor-client/contracts/model/index.ts b/packages/network/src/thor-client/contracts/model/index.ts new file mode 100644 index 000000000..2a3d5f0b8 --- /dev/null +++ b/packages/network/src/thor-client/contracts/model/index.ts @@ -0,0 +1,2 @@ +export { ContractFactory } from './contract-factory'; +export { Contract } from './contract'; diff --git a/packages/network/tests/thor-client/contracts/contract.erc721.solo.test.ts b/packages/network/tests/thor-client/contracts/contract.erc721.solo.test.ts index 78f3e19dd..b794de240 100644 --- a/packages/network/tests/thor-client/contracts/contract.erc721.solo.test.ts +++ b/packages/network/tests/thor-client/contracts/contract.erc721.solo.test.ts @@ -39,14 +39,20 @@ describe('ThorClient - ERC721 Contracts', () => { beforeAll(async () => { thorSoloClient = new ThorClient(soloNetwork); - // Deploy the ERC721 contract and set the contract address - const response = await thorSoloClient.contracts.deployContract( - TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey, - erc721ContractBytecode + // Create the ERC721 contract factory + + const factory = thorSoloClient.contracts.createContractFactory( + ERC721_ABI, + erc721ContractBytecode, + TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey ); + await factory.startDeployment(); + + const contract = await factory.waitForDeployment(); + const transactionReceiptDeployContract = - await thorSoloClient.transactions.waitForTransaction(response.id); + contract.deployTransactionReceipt; expect(transactionReceiptDeployContract).toBeDefined(); expect( diff --git a/packages/network/tests/thor-client/contracts/contract.solo.test.ts b/packages/network/tests/thor-client/contracts/contract.solo.test.ts index 5fe0da0a0..44acf979a 100644 --- a/packages/network/tests/thor-client/contracts/contract.solo.test.ts +++ b/packages/network/tests/thor-client/contracts/contract.solo.test.ts @@ -14,11 +14,9 @@ import { testingContractTestCases } from './fixture'; import { addressUtils, type DeployParams } from '@vechain/vechain-sdk-core'; -import { - ThorClient, - type TransactionSendResult, - type TransactionReceipt -} from '../../../src'; +import { ThorClient, type TransactionReceipt } from '../../../src'; +import { type ContractFactory } from '../../../src/thor-client/contracts/model'; +import { ContractDeploymentFailedError } from '@vechain/vechain-sdk-errors'; /** * Tests for the ThorClient class, specifically focusing on contract-related functionality. @@ -44,36 +42,34 @@ describe('ThorClient - Contracts', () => { * * @returns A promise that resolves to a `TransactionSendResult` object representing the result of the deployment. */ - async function deployExampleContract(): Promise { + async function createExampleContractFactory(): Promise { const deployParams: DeployParams = { types: ['uint'], values: ['100'] }; - // Deploy the contract using the deployContract method - return await thorSoloClient.contracts.deployContract( - TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.privateKey, + const contractFactory = thorSoloClient.contracts.createContractFactory( + deployedContractAbi, contractBytecode, - deployParams + TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.privateKey ); + + // Start the deployment of the contract + return await contractFactory.startDeployment(deployParams); } /** - * Test case for deploying a smart contract using the deployContract method. + * Test case for deploying a smart contract using the contract factory. */ - test('deployContract', async () => { + test('deploy a contract', async () => { // Deploy an example contract and get the transaction response - const response = await deployExampleContract(); + const response = await createExampleContractFactory(); // Poll until the transaction receipt is available - const transactionReceipt = - (await thorSoloClient.transactions.waitForTransaction( - response.id - )) as TransactionReceipt; - + const contract = await response.waitForDeployment(); // Extract the contract address from the transaction receipt - const contractAddress = transactionReceipt.outputs[0].contractAddress; + const contractAddress = contract.address; // Call the get function of the deployed contract to verify that the stored value is 100 const result = await thorSoloClient.contracts.executeContractCall( - contractAddress as string, + contractAddress, deployedContractAbi, 'get', [] @@ -82,57 +78,65 @@ describe('ThorClient - Contracts', () => { expect(result).toEqual([100n]); // Assertions - expect(transactionReceipt.reverted).toBe(false); - expect(transactionReceipt.outputs).toHaveLength(1); + expect(contract.deployTransactionReceipt?.reverted).toBe(false); + expect(contract.deployTransactionReceipt?.outputs).toHaveLength(1); expect(contractAddress).not.toBeNull(); - expect(addressUtils.isAddress(contractAddress as string)).toBe(true); + expect(addressUtils.isAddress(contractAddress)).toBe(true); }, 10000); /** - * Test case for deploying an ERC20 smart contract. - * It utilizes the deployErc20Contract method to deploy the contract, - * then verifies the deployment by checking the contract's balance. - * - * The test involves the following steps: - * 1. Deploy the ERC20 contract. - * 2. Poll for the transaction receipt of the deployment. - * 3. Extract the deployed contract's address from the transaction receipt. - * 4. Execute a contract call to check the balance of a specific account. - * 5. Assert that the balance is as expected. - * - * Test timeout is set to 10000 ms. + * Test case for a failed smart contract using the contract factory. */ - test('deployErc20Contract', async () => { - // Deploy the ERC20 contract and receive a response - const response = await thorSoloClient.contracts.deployContract( - TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey, - erc20ContractBytecode + test('failed contract deployment', async () => { + // Create a contract factory + let contractFactory = thorSoloClient.contracts.createContractFactory( + deployedContractAbi, + contractBytecode, + TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.privateKey ); - // Poll until the transaction receipt is available - // This receipt includes details of the deployment transaction - const transactionReceiptDeployContract = - (await thorSoloClient.transactions.waitForTransaction( - response.id - )) as TransactionReceipt; + // Start the deployment of the contract + contractFactory = await contractFactory.startDeployment(); - // Extract the contract address from the transaction receipt - // The contract address is needed for further interactions with the contract - const contractAddress = - transactionReceiptDeployContract.outputs[0].contractAddress; + // Wait for the deployment to complete and obtain the contract instance + await expect(contractFactory.waitForDeployment()).rejects.toThrow( + ContractDeploymentFailedError + ); + }, 10000); - // Execute a contract call to get the balance of the contract manager's account - // This checks if the deployment was successful and the contract is operational - const result = await thorSoloClient.contracts.executeContractCall( - contractAddress as string, + /** + * Test case for waiting for a contract deployment not started. + */ + test('wait for a contract deployment not started', async () => { + // Create a contract factory + const contractFactory = thorSoloClient.contracts.createContractFactory( + deployedContractAbi, + contractBytecode, + TEST_ACCOUNTS.TRANSACTION.TRANSACTION_SENDER.privateKey + ); + + // Waiting for a deployment that has not started + await expect(contractFactory.waitForDeployment()).rejects.toThrow( + ContractDeploymentFailedError + ); + }, 10000); + + test('deployErc20Contract with Contract Factory', async () => { + // Deploy the ERC20 contract and receive a response + let factory = thorSoloClient.contracts.createContractFactory( deployedERC20Abi, - 'balanceOf', - [TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.address] + erc20ContractBytecode, + TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey ); - // Assert that the balance matches the expected value - // The expected value is a predefined number representing the contract's initial balance - expect(result).toEqual([BigInt(1000000000000000000000000n)]); + factory = await factory.startDeployment(); + + expect(factory.getDeployTransaction()).not.toBe(undefined); + + const contract = await factory.waitForDeployment(); + + expect(contract.address).not.toBe(null); + expect(addressUtils.isAddress(contract.address)).toBe(true); }, 10000); /** @@ -146,30 +150,23 @@ describe('ThorClient - Contracts', () => { * */ test('Execute ERC20 contract operations', async () => { - // Deploy an ERC20 contract and store the response which includes the transaction ID - const response = await thorSoloClient.contracts.deployContract( - TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey, - erc20ContractBytecode + // Deploy the ERC20 contract + let factory = thorSoloClient.contracts.createContractFactory( + deployedERC20Abi, + erc20ContractBytecode, + TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey ); - // Wait for the transaction to complete and obtain its receipt, - // which contains details such as the contract address - const transactionReceiptDeployContract = - (await thorSoloClient.transactions.waitForTransaction( - response.id - )) as TransactionReceipt; + factory = await factory.startDeployment(); - // Retrieve the contract address from the transaction receipt, - // as it is necessary for further interactions with the contract - const contractAddress = - transactionReceiptDeployContract.outputs[0].contractAddress; + const contract = await factory.waitForDeployment(); // Execute a 'transfer' transaction on the deployed contract, // transferring a specified amount of tokens const transferResult = await thorSoloClient.contracts.executeContractTransaction( TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey, - contractAddress as string, + contract.address, deployedERC20Abi, 'transfer', [TEST_ACCOUNTS.TRANSACTION.TRANSACTION_RECEIVER.address, 1000] @@ -187,7 +184,7 @@ describe('ThorClient - Contracts', () => { // Execute a 'balanceOf' call on the contract to check the balance of the receiver const balanceOfResult = await thorSoloClient.contracts.executeContractCall( - contractAddress as string, + contract.address, deployedERC20Abi, 'balanceOf', [TEST_ACCOUNTS.TRANSACTION.TRANSACTION_RECEIVER.address], @@ -205,48 +202,34 @@ describe('ThorClient - Contracts', () => { * Test case for retrieving the bytecode of a deployed smart contract. */ test('get Contract Bytecode', async () => { - // Deploy an example contract and get the transaction response - const response = await deployExampleContract(); + // Create a contract factory that is already deploying the example contract + const factory = await createExampleContractFactory(); - // Poll until the transaction receipt is available - const transactionReceipt = - (await thorSoloClient.transactions.waitForTransaction( - response.id - )) as TransactionReceipt; - - // Extract the contract address from the transaction receipt - const contractAddress = transactionReceipt.outputs[0].contractAddress; + // Wait for the deployment to complete and obtain the contract instance + const contract = await factory.waitForDeployment(); // Retrieve the bytecode of the deployed contract const contractBytecodeResponse = - await thorSoloClient.accounts.getBytecode( - contractAddress as string - ); + await thorSoloClient.accounts.getBytecode(contract.address); // Assertion: Compare with the expected deployed contract bytecode expect(contractBytecodeResponse).toBe(deployedContractBytecode); }, 10000); /** - * Test case for deploying a smart contract using the deployContract method. + * Test case for deploying a smart contract using the contract factory. */ test('call a contract function', async () => { - // Deploy an example contract and get the transaction response - const response = await deployExampleContract(); - - // Poll until the transaction receipt is available - const transactionReceiptDeployContract = - (await thorSoloClient.transactions.waitForTransaction( - response.id - )) as TransactionReceipt; + // Create a contract factory that is already deploying the example contract + const factory = await createExampleContractFactory(); - const contractAddress = transactionReceiptDeployContract.outputs[0] - .contractAddress as string; + // Wait for the deployment to complete and obtain the contract instance + const contract = await factory.waitForDeployment(); const callFunctionSetResponse = await thorSoloClient.contracts.executeContractTransaction( TEST_ACCOUNTS.TRANSACTION.CONTRACT_MANAGER.privateKey, - contractAddress, + contract.address, deployedContractAbi, 'set', [123] @@ -261,7 +244,7 @@ describe('ThorClient - Contracts', () => { const callFunctionGetResult = await thorSoloClient.contracts.executeContractCall( - contractAddress, + contract.address, deployedContractAbi, 'get', []