Skip to content

Commit

Permalink
feat: call contract functions from the contract object (#565)
Browse files Browse the repository at this point in the history
* feat: call contract functions from the contract object

* docs: updating contract call examples

* refactor: contract getters and setters

---------

Co-authored-by: Fabio Rigamonti <[email protected]>
Co-authored-by: Rodolfo Pietro Calabrò <[email protected]>
  • Loading branch information
3 people authored Feb 16, 2024
1 parent ba86d4a commit f17bece
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 175 deletions.
36 changes: 12 additions & 24 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,8 @@ const receipt = contract.deployTransactionReceipt;
// Asserting that the contract deployment didn't revert, indicating a successful deployment
expect(receipt.reverted).toEqual(false);

// Executing a contract call to get the balance of the account that deployed the contract
const balance = await thorSoloClient.contracts.executeContractCall(
receipt.outputs[0].contractAddress,
VIP180_ABI,
'balanceOf',
[addressUtils.fromPrivateKey(Buffer.from(privateKeyDeployer, 'hex'))]
const balance = await contract.read.balanceOf(
addressUtils.fromPrivateKey(Buffer.from(privateKeyDeployer, 'hex'))
);

// Asserting that the initial balance of the deployer is the expected amount (1e24)
Expand All @@ -199,6 +195,7 @@ Once the contract is deployed, we can transfer tokens to another address using t
```typescript { name=contract-transfer-erc20-token, category=example }
import { VIP180_ABI } from '@vechain/vechain-sdk-core';
import {
Contract,
HttpClient,
ThorClient,
type TransactionReceipt
Expand All @@ -218,7 +215,7 @@ const soloNetwork = new HttpClient(_soloUrl);
const thorSoloClient = new ThorClient(soloNetwork);

// Defining a function for deploying the ERC20 contract
const setupERC20Contract = async (): Promise<string> => {
const setupERC20Contract = async (): Promise<Contract> => {
const contractFactory = thorSoloClient.contracts.createContractFactory(
VIP180_ABI,
erc20ContractBytecode,
Expand All @@ -229,29 +226,20 @@ const setupERC20Contract = async (): Promise<string> => {
await contractFactory.startDeployment();

// Waiting for the contract to be deployed
const contract = await contractFactory.waitForDeployment();

return contract.address;
return await contractFactory.waitForDeployment();
};

// Setting up the ERC20 contract and getting its address
const contractAddress = await setupERC20Contract();

// Executing a 'transfer' transaction on the ERC20 contract
const transferResult =
await thorSoloClient.contracts.executeContractTransaction(
privateKeyDeployer, // Using deployer's private key to authorize the transaction
contractAddress, // Contract address to which the transaction is sent
VIP180_ABI, // ABI of the ERC20 contract
'transfer', // Name of the function to be executed in the contract
['0x9e7911de289c3c856ce7f421034f66b6cde49c39', 10000] // Arguments for the 'transfer' function: recipient address and amount
);
const contract = await setupERC20Contract();

const transferResult = await contract.transact.transfer(
'0x9e7911de289c3c856ce7f421034f66b6cde49c39',
10000
);

// Wait for the transfer transaction to complete and obtain its receipt
const transactionReceiptTransfer =
(await thorSoloClient.transactions.waitForTransaction(
transferResult.id // Transaction ID of the executed transfer
)) as TransactionReceipt;
(await transferResult.wait()) as TransactionReceipt;

// Asserting that the transaction has not been reverted
expect(transactionReceiptTransfer.reverted).toEqual(false);
Expand Down
8 changes: 2 additions & 6 deletions docs/examples/contracts/contract-create-ERC20-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ const receipt = contract.deployTransactionReceipt;
// Asserting that the contract deployment didn't revert, indicating a successful deployment
expect(receipt.reverted).toEqual(false);

// Executing a contract call to get the balance of the account that deployed the contract
const balance = await thorSoloClient.contracts.executeContractCall(
receipt.outputs[0].contractAddress,
VIP180_ABI,
'balanceOf',
[addressUtils.fromPrivateKey(Buffer.from(privateKeyDeployer, 'hex'))]
const balance = await contract.read.balanceOf(
addressUtils.fromPrivateKey(Buffer.from(privateKeyDeployer, 'hex'))
);

// Asserting that the initial balance of the deployer is the expected amount (1e24)
Expand Down
28 changes: 10 additions & 18 deletions docs/examples/contracts/contract-transfer-ERC20-token.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VIP180_ABI } from '@vechain/vechain-sdk-core';
import {
Contract,
HttpClient,
ThorClient,
type TransactionReceipt
Expand All @@ -19,7 +20,7 @@ const soloNetwork = new HttpClient(_soloUrl);
const thorSoloClient = new ThorClient(soloNetwork);

// Defining a function for deploying the ERC20 contract
const setupERC20Contract = async (): Promise<string> => {
const setupERC20Contract = async (): Promise<Contract> => {
const contractFactory = thorSoloClient.contracts.createContractFactory(
VIP180_ABI,
erc20ContractBytecode,
Expand All @@ -30,29 +31,20 @@ const setupERC20Contract = async (): Promise<string> => {
await contractFactory.startDeployment();

// Waiting for the contract to be deployed
const contract = await contractFactory.waitForDeployment();

return contract.address;
return await contractFactory.waitForDeployment();
};

// Setting up the ERC20 contract and getting its address
const contractAddress = await setupERC20Contract();

// Executing a 'transfer' transaction on the ERC20 contract
const transferResult =
await thorSoloClient.contracts.executeContractTransaction(
privateKeyDeployer, // Using deployer's private key to authorize the transaction
contractAddress, // Contract address to which the transaction is sent
VIP180_ABI, // ABI of the ERC20 contract
'transfer', // Name of the function to be executed in the contract
['0x9e7911de289c3c856ce7f421034f66b6cde49c39', 10000] // Arguments for the 'transfer' function: recipient address and amount
);
const contract = await setupERC20Contract();

const transferResult = await contract.transact.transfer(
'0x9e7911de289c3c856ce7f421034f66b6cde49c39',
10000
);

// Wait for the transfer transaction to complete and obtain its receipt
const transactionReceiptTransfer =
(await thorSoloClient.transactions.waitForTransaction(
transferResult.id // Transaction ID of the executed transfer
)) as TransactionReceipt;
(await transferResult.wait()) as TransactionReceipt;

// Asserting that the transaction has not been reverted
expect(transactionReceiptTransfer.reverted).toEqual(false);
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,12 @@ class ContractsModule {
privateKey
);

// Send the signed transaction
return await this.thor.transactions.sendTransaction(signedTx);
const result = await this.thor.transactions.sendTransaction(signedTx);

result.wait = async () =>
await this.thor.transactions.waitForTransaction(result.id);

return result;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class ContractFactory {
private readonly thor: ThorClient;

/**
* The result of the deploy transaction, undefined until a deployment is started.
* The result of the deployment transaction, undefined until a deployment is started.
*/
private deployTransaction: SendTransactionResult | undefined;

Expand Down Expand Up @@ -154,6 +154,7 @@ class ContractFactory {
transactionReceipt?.outputs[0].contractAddress as string,
this.abi,
this.thor,
this.privateKey,
transactionReceipt as TransactionReceipt
);
}
Expand Down
147 changes: 141 additions & 6 deletions packages/network/src/thor-client/contracts/model/contract.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,169 @@
import { type InterfaceAbi } from '@vechain/vechain-sdk-core';
import type { TransactionReceipt } from '../../transactions';
import { addressUtils, type InterfaceAbi } from '@vechain/vechain-sdk-core';
import type {
SendTransactionResult,
TransactionReceipt
} from '../../transactions';
import { type ThorClient } from '../../thor-client';
import {
type ContractCallOptions,
type ContractTransactionOptions
} from '../types';

export type ContractFunction<T = unknown> = (...args: unknown[]) => Promise<T>;

type ContractFunctionRead = Record<string, ContractFunction>;

type ContractFunctionTransact = Record<
string,
ContractFunction<SendTransactionResult>
>;

/**
* A class representing a smart contract deployed on the blockchain.
*/
class Contract {
private readonly thor: ThorClient;
public address: string;
public abi: InterfaceAbi;
readonly thor: ThorClient;
readonly address: string;
readonly abi: InterfaceAbi;
callerPrivateKey: string;

readonly deployTransactionReceipt: TransactionReceipt | undefined;

public read: ContractFunctionRead = {};
public transact: ContractFunctionTransact = {};

public deployTransactionReceipt: TransactionReceipt | undefined;
private contractCallOptions: ContractCallOptions = {};
private contractTransactionOptions: ContractTransactionOptions = {};

/**
* 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 callerPrivateKey The private key used for signing transactions.
* @param transactionReceipt (Optional) The transaction receipt of the contract deployment.
*/
constructor(
address: string,
abi: InterfaceAbi,
thor: ThorClient,
callerPrivateKey: string,
transactionReceipt?: TransactionReceipt
) {
this.abi = abi;
this.thor = thor;
this.address = address;
this.deployTransactionReceipt = transactionReceipt;
this.callerPrivateKey = callerPrivateKey;
this.read = this.getReadProxy();
this.transact = this.getTransactProxy();
}

/**
* Sets the options for contract calls.
* @param options - The contract call options to set.
* @returns The updated contract call options.
*/
public setContractReadOptions(
options: ContractCallOptions
): ContractCallOptions {
this.contractCallOptions = options;
// initialize the proxy with the new options
this.read = this.getReadProxy();
return this.contractCallOptions;
}

/**
* Clears the current contract call options, resetting them to an empty object.
*/
public clearContractReadOptions(): void {
this.contractCallOptions = {};
this.read = this.getReadProxy();
}

/**
* Sets the options for contract transactions.
* @param options - The contract transaction options to set.
* @returns The updated contract transaction options.
*/
public setContractTransactOptions(
options: ContractTransactionOptions
): ContractTransactionOptions {
this.contractTransactionOptions = options;
// initialize the proxy with the new options
this.transact = this.getTransactProxy();
return this.contractTransactionOptions;
}

/**
* Clears the current contract transaction options, resetting them to an empty object.
*/
public clearContractTransactOptions(): void {
this.contractTransactionOptions = {};
this.transact = this.getTransactProxy();
}

/**
* Sets the private key of the caller for signing transactions.
* @param privateKey
*/
public setCallerPrivateKey(privateKey: string): string {
this.callerPrivateKey = privateKey;
this.transact = this.getTransactProxy();
this.read = this.getReadProxy();
return this.callerPrivateKey;
}

/**
* Creates a Proxy object for reading contract functions, allowing for the dynamic invocation of contract read operations.
* @returns A Proxy that intercepts calls to read contract functions, automatically handling the invocation with the configured options.
* @private
*/
private getReadProxy(): ContractFunctionRead {
return new Proxy(this.read, {
get: (_target, prop) => {
// Otherwise, assume that the function is a contract method
return async (...args: unknown[]) => {
return await this.thor.contracts.executeContractCall(
this.address,
this.abi,
prop.toString(),
args,
{
caller: addressUtils.fromPrivateKey(
Buffer.from(this.callerPrivateKey, 'hex')
),
...this.contractCallOptions
}
);
};
}
});
}

/**
* Creates a Proxy object for transacting with contract functions, allowing for the dynamic invocation of contract transaction operations.
* @returns A Proxy that intercepts calls to transaction contract functions, automatically handling the invocation with the configured options.
* @private
*/
private getTransactProxy(): ContractFunctionTransact {
return new Proxy(this.transact, {
get: (_target, prop) => {
// Otherwise, assume that the function is a contract method
return async (
...args: unknown[]
): Promise<SendTransactionResult> => {
return await this.thor.contracts.executeContractTransaction(
this.callerPrivateKey,
this.address,
this.abi,
prop.toString(),
args,
this.contractTransactionOptions
);
};
}
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
type GetTransactionInputOptions,
type TransactionDetail,
type GetTransactionReceiptInputOptions,
type TransactionSendResult,
type SimulateTransactionClause,
type SimulateTransactionOptions,
type TransactionSimulationResult,
Expand Down Expand Up @@ -113,7 +112,7 @@ class TransactionsModule {
*/
public async sendRawTransaction(
raw: string
): Promise<TransactionSendResult> {
): Promise<SendTransactionResult> {
// Validate raw transaction
assert(
dataUtils.isHexString(raw),
Expand Down Expand Up @@ -141,7 +140,7 @@ class TransactionsModule {
{
body: { raw }
}
)) as TransactionSendResult;
)) as SendTransactionResult;
}

/**
Expand Down
Loading

1 comment on commit f17bece

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Coverage

Summary

Lines Statements Branches Functions
Coverage: 99%
99.64% (2557/2566) 100% (527/527) 98.89% (536/542)
Title Tests Skipped Failures Errors Time
core 409 0 💤 0 ❌ 0 🔥 1m 35s ⏱️
network 237 0 💤 0 ❌ 0 🔥 3m 33s ⏱️
errors 43 0 💤 0 ❌ 0 🔥 13.166s ⏱️

Please sign in to comment.