@vechain/sdk-network
Version:
This module serves as the standard interface connecting decentralized applications (dApps) and users to the VeChainThor blockchain
415 lines (374 loc) • 15.3 kB
text/typescript
import {
Address,
Clause,
Revision,
type ContractClause,
type TransactionClause,
Units,
VET
} from '@vechain/sdk-core';
import {
ContractCallError,
InvalidTransactionField
} from '@vechain/sdk-errors';
import type {
Abi,
AbiParametersToPrimitiveTypes,
ExtractAbiEventNames,
ExtractAbiFunction,
ExtractAbiFunctionNames
} from 'abitype';
import { type VeChainSigner } from '../../../signer';
import { type FilterCriteria } from '../../logs';
import { type SendTransactionResult } from '../../transactions/types';
import { type Contract } from './contract';
import { ContractFilter } from './contract-filter';
import {
type ClauseAdditionalOptions,
type ClauseComment,
type ClauseRevision,
type ContractFunctionClause,
type ContractFunctionCriteria,
type ContractFunctionFilter,
type ContractFunctionRead,
type ContractFunctionTransact,
type TransactionValue
} from './types';
import type { ContractCallOptions } from '../types';
/**
* Creates a Proxy object for reading contract functions, allowing for the dynamic invocation of contract read operations.
* @param contract - The contract instance
* @returns A Proxy that intercepts calls to read contract functions, automatically handling the invocation with the configured options.
*/
function getReadProxy<TAbi extends Abi>(
contract: Contract<TAbi>
): ContractFunctionRead<TAbi, ExtractAbiFunctionNames<TAbi, 'pure' | 'view'>> {
return new Proxy(contract.read, {
get: (_target, prop) => {
// Otherwise, assume that the function is a contract method
return async (
...args: AbiParametersToPrimitiveTypes<
ExtractAbiFunction<TAbi, 'balanceOf'>['inputs'],
'inputs'
>
): Promise<unknown[]> => {
// check if the clause comment is provided as an argument
const extractOptionsResult = extractAndRemoveAdditionalOptions(
args as unknown[]
);
const clauseAdditionalOptions =
extractOptionsResult.clauseAdditionalOptions;
const functionAbi = contract.getFunctionAbi(prop);
const callOptions = {
caller:
contract.getSigner() !== undefined
? await contract.getSigner()?.getAddress()
: undefined,
...contract.getContractReadOptions(),
includeABI: true
} as ContractCallOptions & {
caller?: string;
includeABI: boolean;
};
if (clauseAdditionalOptions?.comment !== undefined) {
callOptions.comment = clauseAdditionalOptions.comment;
}
if (clauseAdditionalOptions?.revision !== undefined) {
callOptions.revision = Revision.of(
clauseAdditionalOptions.revision
);
}
const executeCallResult =
await contract.contractsModule.executeCall(
contract.address,
functionAbi,
extractOptionsResult.args,
callOptions
);
if (!executeCallResult.success) {
throw new ContractCallError(
functionAbi.stringSignature,
executeCallResult.result.errorMessage as string,
{
contractAddress: contract.address
}
);
}
// Return the properly typed result based on the function's outputs
return executeCallResult.result.array ?? [];
};
}
});
}
/**
* Creates a Proxy object for transacting with contract functions, allowing for the dynamic invocation of contract transaction operations.
* @param contract - The contract instance
* @returns A Proxy that intercepts calls to transaction contract functions, automatically handling the invocation with the configured options.
* @throws {InvalidTransactionField}
* @private
*/
function getTransactProxy<TAbi extends Abi>(
contract: Contract<TAbi>
): ContractFunctionTransact<
TAbi,
ExtractAbiFunctionNames<TAbi, 'nonpayable' | 'payable'>
> {
return new Proxy(contract.transact, {
get: (_target, prop) => {
// Otherwise, assume that the function is a contract method
return async (
...args: unknown[]
): Promise<SendTransactionResult> => {
if (contract.getSigner() === undefined) {
throw new InvalidTransactionField(
'getTransactProxy()',
'Caller signer is required to transact with the contract.',
{ fieldName: 'signer', prop }
);
}
// get the transaction options for the contract
const transactionOptions =
contract.getContractTransactOptions();
// check if the transaction value is provided as an argument
const extractAdditionalOptionsResult =
extractAndRemoveAdditionalOptions(args);
const transactionValue =
extractAdditionalOptionsResult.clauseAdditionalOptions
?.value;
const clauseComment =
extractAdditionalOptionsResult.clauseAdditionalOptions
?.comment;
args = extractAdditionalOptionsResult.args;
return await contract.contractsModule.executeTransaction(
contract.getSigner() as VeChainSigner,
contract.address,
contract.getFunctionAbi(prop),
args,
{
...transactionOptions,
value:
transactionOptions.value ??
transactionValue ??
'0x0',
comment: clauseComment,
includeABI: true
}
);
};
}
});
}
/**
* Creates a Proxy object for filtering contract events, allowing for the dynamic invocation of contract event filtering operations.
* @param contract - The contract instance to create the filter proxy for.
* @returns A Proxy that intercepts calls to filter contract events, automatically handling the invocation with the configured options.
*/
function getFilterProxy<TAbi extends Abi>(
contract: Contract<TAbi>
): ContractFunctionFilter<TAbi, ExtractAbiEventNames<TAbi>> {
return new Proxy(contract.filters, {
get: (_target, prop) => {
return (
// eslint-disable-next-line sonarjs/use-type-alias
args: Record<string, unknown> | unknown[] | undefined
): ContractFilter<TAbi> => {
const criteriaSet = buildCriteria(contract, prop, args);
return new ContractFilter<TAbi>(contract, [criteriaSet]);
};
}
});
}
/**
* Creates a Proxy object for interacting with contract functions, allowing for the dynamic invocation of contract functions.
* @param contract - The contract instance to create the clause proxy for.
* @returns A Proxy that intercepts calls to contract functions, automatically handling the invocation with the configured options.
*/
function getClauseProxy<TAbi extends Abi>(
contract: Contract<TAbi>
): ContractFunctionClause<TAbi, ExtractAbiFunctionNames<TAbi>> {
return new Proxy(contract.clause, {
get: (_target, prop) => {
return (...args: unknown[]): ContractClause => {
// get the transaction options for the contract
const transactionOptions =
contract.getContractTransactOptions();
// check if the transaction value is provided as an argument
const extractAdditionalOptionsResult =
extractAndRemoveAdditionalOptions(args);
const transactionValue =
extractAdditionalOptionsResult.clauseAdditionalOptions
?.value;
const clauseComment =
extractAdditionalOptionsResult.clauseAdditionalOptions
?.comment;
args = extractAdditionalOptionsResult.args;
// return the contract clause
return {
clause: Clause.callFunction(
Address.of(contract.address),
contract.getFunctionAbi(prop),
args,
VET.of(
transactionOptions.value ?? transactionValue ?? 0,
Units.wei
),
{
comment: clauseComment,
includeABI: true
}
) as TransactionClause,
functionAbi: contract.getFunctionAbi(prop)
};
};
}
});
}
/**
* Create a proxy object for building event criteria for the event filtering.
* @param contract - The contract instance to create the criteria proxy for.
* @returns A Proxy that intercepts calls to build event criteria, automatically handling the invocation with the configured options.
*/
function getCriteriaProxy<TAbi extends Abi>(
contract: Contract<TAbi>
): ContractFunctionCriteria<TAbi, ExtractAbiEventNames<TAbi>> {
return new Proxy(contract.criteria, {
get: (_target, prop) => {
return (
args: Record<string, unknown> | unknown[] | undefined
): FilterCriteria => {
return buildCriteria(contract, prop, args);
};
}
});
}
/**
* Builds the filter criteria for the contract filter.
* @param contract - The contract instance to create the criteria for.
* @param prop - The property name of the contract event.
* @param args - The arguments to filter the event.
* @returns The event criteria for the contract filter.
*/
function buildCriteria<TAbi extends Abi>(
contract: Contract<TAbi>,
prop: string | symbol,
args: Record<string, unknown> | unknown[] | undefined
): FilterCriteria {
// Create the VeChain sdk event ABI
const eventAbi = contract.getEventAbi(prop);
// Create a map of encoded filter topics for the event
const topics = new Map<number, string | undefined>(
eventAbi
.encodeFilterTopicsNoNull(args)
.map((topic, index) => [index, topic])
);
// Create the criteria set for the contract filter
return {
criteria: {
address: contract.address,
topic0: topics.get(0) as string, // the first topic is always defined since it's the event signature
topic1: topics.has(1) ? topics.get(1) : undefined,
topic2: topics.has(2) ? topics.get(2) : undefined,
topic3: topics.has(3) ? topics.get(3) : undefined,
topic4: topics.has(4) ? topics.get(4) : undefined
},
eventAbi
};
}
/**
* Extracts the transaction value and comment from the list of arguments, if present.
* @param args - The list of arguments to search for the transaction value.
* @returns The transaction value and comment object, if found in the arguments list. Also returns the list of arguments with the clause options removed.
*/
function extractAndRemoveAdditionalOptions(args: unknown[]): {
args: unknown[];
clauseAdditionalOptions: ClauseAdditionalOptions | undefined;
} {
// check if the transaction value is provided as an argument
const transactionValue = getTransactionValue(args);
const clauseComment = getClauseComment(args);
const clauseRevision = getRevision(args);
// if present remove the transaction value argument from the list of arguments
if (
transactionValue !== undefined ||
clauseComment !== undefined ||
clauseRevision !== undefined
) {
args = args.filter(
(arg) =>
!(
isTransactionValue(arg) ||
isTransactionComment(arg) ||
isRevision(arg)
)
);
}
return {
args,
clauseAdditionalOptions: {
value: transactionValue?.value,
comment: clauseComment?.comment,
revision: clauseRevision?.revision
}
};
}
/**
* Extracts the transaction value from the list of arguments, if present.
* @param args - The list of arguments to search for the transaction value.
* @returns The transaction value object, if found in the arguments list.
*/
function getTransactionValue(args: unknown[]): TransactionValue | undefined {
const found = args.find((arg) => isTransactionValue(arg));
if (!found) return undefined;
return {
value: (found.value as number | string | bigint).toString()
} as TransactionValue;
}
/**
* Extracts the clause comment from the list of arguments, if present.
* @param args - The list of arguments to search for the clause comment.
* @returns The clause comment object, if found in the arguments list.
*/
function getClauseComment(args: unknown[]): ClauseComment | undefined {
return args.find((arg) => isTransactionComment(arg)) as
| ClauseComment
| undefined;
}
/**
* Extracts the revision from the list of arguments, if present.
* @param args - The list of arguments to search for the revision.
* @returns The revision object, if found in the arguments list.
*/
function getRevision(args: unknown[]): ClauseRevision | undefined {
return args.find((arg) => isRevision(arg)) as ClauseRevision | undefined;
}
/**
* Type guard function to check if an object is a TransactionValue.
* @param obj - The object to check.
* @returns True if the object is a TransactionValue, false otherwise.
*/
function isTransactionValue(obj: unknown): obj is ClauseAdditionalOptions {
return (obj as ClauseAdditionalOptions).value !== undefined;
}
/**
* Type guard function to check if an object is a ClauseComment.
* @param obj - The object to check.
* @returns True if the object is a ClauseComment, false otherwise.
*/
function isTransactionComment(obj: unknown): obj is ClauseAdditionalOptions {
return (obj as ClauseAdditionalOptions).comment !== undefined;
}
/**
* Type guard function to check if an object is a revision.
* @param obj - The object to check.
* @returns True if the object is a revision, false otherwise.
*/
function isRevision(obj: unknown): obj is ClauseAdditionalOptions {
return (obj as ClauseAdditionalOptions).revision !== undefined;
}
export {
getClauseProxy,
getCriteriaProxy,
getFilterProxy,
getReadProxy,
getTransactProxy
};