Skip to content

Commit

Permalink
refactor(ContractCallResult): Added errorMessage, plain value a…
Browse files Browse the repository at this point in the history
…nd `success` flag (#1440)

* refactor: first commit

* refactor: fixed some tests

* refactor: fixed some tests

* feat: fixed tests

* refactor: fixed some tests

* refactor: fixed docs tests

* feat: refactor

---------

Co-authored-by: Fabio Rigamonti <[email protected]>
  • Loading branch information
freemanzMrojo and fabiorigam authored Oct 30, 2024
1 parent 56340a2 commit 2993e32
Show file tree
Hide file tree
Showing 13 changed files with 465 additions and 147 deletions.
32 changes: 28 additions & 4 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,34 @@ const multipleClausesResult =
contract.clause.decimals()
]);

expect(multipleClausesResult[0]).toEqual([expectedBalance]);
expect(multipleClausesResult[1]).toEqual(['SampleToken']);
expect(multipleClausesResult[2]).toEqual(['ST']);
expect(multipleClausesResult[3]).toEqual([18]);
expect(multipleClausesResult[0]).toEqual({
success: true,
result: {
plain: expectedBalance,
array: [expectedBalance]
}
});
expect(multipleClausesResult[1]).toEqual({
success: true,
result: {
plain: 'SampleToken',
array: ['SampleToken']
}
});
expect(multipleClausesResult[2]).toEqual({
success: true,
result: {
plain: 'ST',
array: ['ST']
}
});
expect(multipleClausesResult[3]).toEqual({
success: true,
result: {
plain: 18,
array: [18]
}
});
```

> ⚠️ **Warning:**
Expand Down
32 changes: 28 additions & 4 deletions docs/examples/contracts/contract-create-ERC20-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,33 @@ const multipleClausesResult =
contract.clause.decimals()
]);

expect(multipleClausesResult[0]).toEqual([expectedBalance]);
expect(multipleClausesResult[1]).toEqual(['SampleToken']);
expect(multipleClausesResult[2]).toEqual(['ST']);
expect(multipleClausesResult[3]).toEqual([18]);
expect(multipleClausesResult[0]).toEqual({
success: true,
result: {
plain: expectedBalance,
array: [expectedBalance]
}
});
expect(multipleClausesResult[1]).toEqual({
success: true,
result: {
plain: 'SampleToken',
array: ['SampleToken']
}
});
expect(multipleClausesResult[2]).toEqual({
success: true,
result: {
plain: 'ST',
array: ['ST']
}
});
expect(multipleClausesResult[3]).toEqual({
success: true,
result: {
plain: 18,
array: [18]
}
});

// END_SNIPPET: ERC20MultiClausesReadSnippet
8 changes: 7 additions & 1 deletion docs/examples/evm-extension/evm-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,4 +791,10 @@ const totalSupply = await thorSoloClient.contracts.executeCall(
// END_SNIPPET: EVMExtensionSnippet

// Check the result
expect(totalSupply).toStrictEqual([10000000000000000000000000000n]);
expect(totalSupply).toStrictEqual({
result: {
array: [10000000000000000000000000000n],
plain: 10000000000000000000000000000n
},
success: true
});
10 changes: 9 additions & 1 deletion packages/errors/src/available-errors/contract/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,12 @@ import { type ObjectErrorData } from '../types';
*/
class ContractDeploymentFailed extends VechainSDKError<ObjectErrorData> {}

export { ContractDeploymentFailed };
/**
* Error when calling a read function on a contract.
*
* WHEN TO USE:
* * Error will be thrown when a read (call) operation fails.
*/
class ContractCallError extends VechainSDKError<ObjectErrorData> {}

export { ContractCallError, ContractDeploymentFailed };
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import { type ThorClient } from '../../../../../thor-client';
* @returns The current gas price in Wei unit considering that 1 VTHO equals 1e18 Wei.
*/
const ethGasPrice = async (thorClient: ThorClient): Promise<string> => {
const result = BigInt(
(await Promise.resolve(
thorClient.contracts.getBaseGasPrice()
)) as string
);
const {
result: { plain }
} = await thorClient.contracts.getBaseGasPrice();

return '0x' + result.toString(16);
return '0x' + BigInt(plain as bigint).toString(16);
};

export { ethGasPrice };
75 changes: 53 additions & 22 deletions packages/network/src/thor-client/contracts/contracts-module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
ABIContract,
Address,
Clause,
dataUtils,
Hex,
Address,
VET,
Units,
Clause,
VET,
type ABIFunction
} from '@vechain/sdk-core';
import { type Abi } from 'abitype';
Expand Down Expand Up @@ -68,6 +68,41 @@ class ContractsModule {
return new Contract<Tabi>(address, abi, this.thor, signer);
}

/**
* Extracts the decoded contract call result from the response of a simulated transaction.
* @param {string} encodedData Data returned from the simulated transaction.
* @param {ABIFunction} functionAbi Function ABI of the contract function.
* @param {boolean} reverted Whether the transaction was reverted.
* @returns {ContractCallResult} An object containing the decoded contract call result.
*/
private getContractCallResult(
encodedData: string,
functionAbi: ABIFunction,
reverted: boolean
): ContractCallResult {
if (reverted) {
const errorMessage = decodeRevertReason(encodedData) ?? '';
return {
success: false,
result: {
errorMessage
}
};
}

// Returning the decoded result both as plain and array.
const encodedResult = Hex.of(encodedData);
const plain = functionAbi.decodeResult(encodedResult);
const array = functionAbi.decodeOutputAsArray(encodedResult);
return {
success: true,
result: {
plain,
array
}
};
}

/**
* Executes a read-only call to a smart contract function, simulating the transaction to obtain the result.
*
Expand All @@ -84,7 +119,7 @@ class ContractsModule {
functionAbi: ABIFunction,
functionData: unknown[],
contractCallOptions?: ContractCallOptions
): Promise<ContractCallResult | string> {
): Promise<ContractCallResult> {
// Simulate the transaction to get the result of the contract call
const response = await this.thor.transactions.simulateTransaction(
[
Expand All @@ -97,39 +132,35 @@ class ContractsModule {
contractCallOptions
);

if (response[0].reverted) {
/**
* The decoded revert reason of the transaction.
* Solidity may revert with Error(string) or Panic(uint256).
*
* @link see [Error handling: Assert, Require, Revert and Exceptions](https://docs.soliditylang.org/en/latest/control-structures.html#error-handling-assert-require-revert-and-exceptions)
*/
return decodeRevertReason(response[0].data) ?? '';
} else {
// Returning an array of values.
// The viem format is a single value/JSON object (ABIFunction#decodeResult)
return functionAbi.decodeOutputAsArray(Hex.of(response[0].data));
}
return this.getContractCallResult(
response[0].data,
functionAbi,
response[0].reverted
);
}

/**
* Executes a read-only call to multiple smart contract functions, simulating the transaction to obtain the results.
* @param clauses - An array of contract clauses to interact with the contract functions.
* @param options - (Optional) Additional options for the contract call, such as the sender's address, gas limit, and gas price, which can affect the simulation's context.
* @returns A promise that resolves to an array of decoded outputs of the smart contract function calls, the format of which depends on the functions' return types.
*/
public async executeMultipleClausesCall(
clauses: ContractClause[],
options?: SimulateTransactionOptions
): Promise<Array<ContractCallResult | string>> {
): Promise<ContractCallResult[]> {
// Simulate the transaction to get the result of the contract call
const response = await this.thor.transactions.simulateTransaction(
clauses.map((clause) => clause.clause),
options
);
// Returning an array of values.
// The viem format is a single value/JSON object (ABIFunction#decodeResult)
// Returning the decoded results both as plain and array.
return response.map((res, index) =>
clauses[index].functionAbi.decodeOutputAsArray(Hex.of(res.data))
this.getContractCallResult(
res.data,
clauses[index].functionAbi,
res.reverted
)
);
}

Expand Down Expand Up @@ -226,7 +257,7 @@ class ContractsModule {
*
* @returns The base gas price in wei.
*/
public async getBaseGasPrice(): Promise<unknown> {
public async getBaseGasPrice(): Promise<ContractCallResult> {
return await this.executeCall(
BUILT_IN_CONTRACTS.PARAMS_ADDRESS,
ABIContract.ofAbi(BUILT_IN_CONTRACTS.PARAMS_ABI).getFunction('get'),
Expand Down
53 changes: 35 additions & 18 deletions packages/network/src/thor-client/contracts/model/contract-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {
Units,
VET
} from '@vechain/sdk-core';
import { InvalidTransactionField } from '@vechain/sdk-errors';
import {
ContractCallError,
InvalidTransactionField
} from '@vechain/sdk-errors';
import type {
Abi,
AbiEvent,
Expand All @@ -17,7 +20,7 @@ import type {
import { type VeChainSigner } from '../../../signer';
import { type FilterCriteria } from '../../logs';
import { type SendTransactionResult } from '../../transactions/types';
import { type ContractCallResult, type ContractClause } from '../types';
import { type ContractClause } from '../types';
import { type Contract } from './contract';
import { ContractFilter } from './contract-filter';
import {
Expand Down Expand Up @@ -48,7 +51,7 @@ function getReadProxy<TAbi extends Abi>(
ExtractAbiFunction<TAbi, 'balanceOf'>['inputs'],
'inputs'
>
): Promise<ContractCallResult> => {
): Promise<unknown[]> => {
// check if the clause comment is provided as an argument

const extractOptionsResult = extractAndRemoveAdditionalOptions(
Expand All @@ -61,21 +64,35 @@ function getReadProxy<TAbi extends Abi>(
const revisionValue =
extractOptionsResult.clauseAdditionalOptions?.revision;

return (await contract.thor.contracts.executeCall(
contract.address,
contract.getFunctionAbi(prop),
extractOptionsResult.args,
{
caller:
contract.getSigner() !== undefined
? await contract.getSigner()?.getAddress()
: undefined,
...contract.getContractReadOptions(),
comment: clauseComment,
revision: revisionValue,
includeABI: true
}
)) as ContractCallResult;
const functionAbi = contract.getFunctionAbi(prop);

const executeCallResult =
await contract.thor.contracts.executeCall(
contract.address,
functionAbi,
extractOptionsResult.args,
{
caller:
contract.getSigner() !== undefined
? await contract.getSigner()?.getAddress()
: undefined,
...contract.getContractReadOptions(),
comment: clauseComment,
revision: revisionValue,
includeABI: true
}
);

if (!executeCallResult.success) {
throw new ContractCallError(
functionAbi.stringSignature,
executeCallResult.result.errorMessage as string,
{
contractAddress: contract.address
}
);
}
return executeCallResult.result.array as unknown[];
};
}
});
Expand Down
9 changes: 8 additions & 1 deletion packages/network/src/thor-client/contracts/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ type ContractCallOptions = SimulateTransactionOptions & ClauseOptions;
/**
* Represents the result of a contract call operation, encapsulating the output of the call.
*/
type ContractCallResult = unknown[];
interface ContractCallResult {
success: boolean;
result: {
plain?: unknown; // Success result as a plain value (might be literal or object).
array?: unknown[]; // Success result as an array (values are the same as in plain).
errorMessage?: string;
};
}

/**
* Represents a contract clause, which includes the clause and the corresponding function ABI.
Expand Down
12 changes: 8 additions & 4 deletions packages/network/src/utils/vns/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,16 @@ const resolveNames = async (
const resolveUtilsAddress = NetworkContracts[genesisBlock.id].resolveUtils;

// use the resolveUtils to lookup names
const [addresses] = (await thorClient.contracts.executeCall(
const callGetAddresses = await thorClient.contracts.executeCall(
resolveUtilsAddress,
ABIItem.ofSignature(
ABIFunction,
'function getAddresses(string[] names) returns (address[] addresses)'
),
[names]
)) as string[][];
);

const [addresses] = callGetAddresses.result.array as string[][];

return addresses.map((address) => {
// zero addresses are missing configuration entries
Expand Down Expand Up @@ -101,14 +103,16 @@ const lookupAddresses = async (
const resolveUtilsAddress = NetworkContracts[genesisBlock.id].resolveUtils;

// use the resolveUtils to lookup names
const [names] = (await thorClient.contracts.executeCall(
const callGetNames = await thorClient.contracts.executeCall(
resolveUtilsAddress,
ABIItem.ofSignature(
ABIFunction,
'function getNames(address[] addresses) returns (string[] names)'
),
[addresses]
)) as string[][];
);

const [names] = callGetNames.result.array as string[][];

return names.map((name) => {
// empty strings indicate a missing entry
Expand Down
Loading

1 comment on commit 2993e32

@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.06% (4350/4391) 97.71% (1413/1446) 99.11% (896/904)
Title Tests Skipped Failures Errors Time
core 808 0 💤 0 ❌ 0 🔥 2m 15s ⏱️
network 734 0 💤 0 ❌ 0 🔥 5m 9s ⏱️
errors 42 0 💤 0 ❌ 0 🔥 16.551s ⏱️

Please sign in to comment.