UNPKG

@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
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 };