Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ContractCallResult): Added errorMessage, plain value and success flag #1440

Merged
merged 11 commits into from
Oct 30, 2024
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
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
Loading