From 0778732a7d6a71129c55f76c1913b393b999567e Mon Sep 17 00:00:00 2001 From: Kevin Siegler <17910833+topocount@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:28:59 -0500 Subject: [PATCH] feat(sdk): add additional helpers into EventAction for function validation --- packages/sdk/src/Actions/EventAction.ts | 171 +++++++++++++++++++++--- packages/sdk/src/errors.ts | 85 +++++++++--- 2 files changed, 222 insertions(+), 34 deletions(-) diff --git a/packages/sdk/src/Actions/EventAction.ts b/packages/sdk/src/Actions/EventAction.ts index aee39b40..213b81e5 100644 --- a/packages/sdk/src/Actions/EventAction.ts +++ b/packages/sdk/src/Actions/EventAction.ts @@ -7,13 +7,17 @@ import { } from '@boostxyz/evm'; import { bytecode } from '@boostxyz/evm/artifacts/contracts/actions/EventAction.sol/EventAction.json'; import events from '@boostxyz/signatures/events'; +import functions from '@boostxyz/signatures/functions'; import { type Abi, type AbiEvent, + type AbiFunction, type Address, type ContractEventName, type Hex, type Log, + type PublicClient, + decodeFunctionData, isAddressEqual, } from 'viem'; import { getLogs } from 'viem/actions'; @@ -44,13 +48,13 @@ import { type RawActionStep, type ReadParams, RegistryType, + SignatureType, type WriteParams, dedupeActionSteps, fromRawActionStep, isEventActionPayloadSimple, prepareEventActionPayload, } from '../utils'; -import type { SignatureType } from './../utils'; export type { EventActionPayload, @@ -253,6 +257,36 @@ export class EventAction extends DeployableTarget< * @returns {Promise} */ public async isActionStepValid( + actionStep: ActionStep, + params?: GetLogsParams> & { + knownEvents?: Record; + logs?: Log[]; + txHash?: Hex; + }, + ) { + if (actionStep.signatureType === SignatureType.EVENT) { + return await this.isActionEventValid(actionStep, params); + } + if (actionStep.signatureType === SignatureType.FUNC) { + return await this.isActionFunctionValid(actionStep, params); + } + return false; + } + + /** + * Validates a single action event with a given criteria against logs. + * If logs are provided in the optional `params` argument, then those logs will be used instead of fetched with the configured client. + * + * @public + * @async + * @param {ActionStep} actionStep + * @param {?GetLogsParams> & { + * knownEvents?: Record; + * logs?: Log[]; + * }} [params] + * @returns {Promise} + */ + public async isActionEventValid( actionStep: ActionStep, params?: GetLogsParams> & { knownEvents?: Record; @@ -289,26 +323,82 @@ export class EventAction extends DeployableTarget< } return true; } + /** + * Validates a single action function with a given criteria against logs. + * If logs are provided in the optional `params` argument, then those logs will be used instead of fetched with the configured client. + * + * @public + * @async + * @param {ActionStep} actionStep + * @param {?GetLogsParams> & { + * knownEvents?: Record; + * txHash?: Hex; + * }} [params] + * @returns {Promise} + */ + public async isActionFunctionValid( + actionStep: ActionStep, + params?: GetLogsParams> & { + knownEvents?: Record; + txHash?: Hex; + }, + ) { + const criteria = actionStep.actionParameter; + const signature = actionStep.signature; + if (!params?.txHash) { + // Should we return false in this case? + throw new Error('txHash is required for function validation'); + } + const client = this._config.getClient({ + chainId: params?.chainId, + }) as PublicClient; + // Fetch the transaction receipt and decode the function input using `viem` utilities + const transaction = await client.getTransaction({ hash: params.txHash }); + const func = (functions.abi as Record)[ + signature + ] as AbiFunction; + + const decodedData = decodeFunctionData({ + abi: [func], + data: transaction.input, + }); + // Validate the criteria against decoded arguments using fieldIndex + const decodedArgs = decodedData.args; + + if (!decodedArgs) return false; + + if ( + !this.validateFunctionAgainstCriteria( + criteria, + decodedArgs as (string | bigint)[], + ) + ) { + return false; + } + + return true; + } /** - * Validates a {@link Log} against a given criteria. + * Validates a field against a given criteria. * * @param {Criteria} criteria - The criteria to validate against. - * @param {Log} log - The Viem event log. - * @returns {boolean} - Returns true if the log passes the criteria, false otherwise. + * @param {string | bigint} fieldValue - The field value to validate. + * @param {Log} log - Optional, for error handling context. + * @returns {Promise} - Returns true if the field passes the criteria, false otherwise. */ - public validateLogAgainstCriteria(criteria: Criteria, log: Log) { - const fieldValue = log.topics.at(criteria.fieldIndex); - if (fieldValue === undefined) { - throw new FieldValueUndefinedError({ log, criteria, fieldValue }); - } + public validateFieldAgainstCriteria( + criteria: Criteria, + fieldValue: string | bigint, + input: { log: Log } | { decodedArgs: readonly (string | bigint)[] }, + ): boolean { // Type narrow based on criteria.filterType switch (criteria.filterType) { case FilterType.EQUAL: if (criteria.fieldType === PrimitiveType.ADDRESS) { return isAddressEqual( criteria.filterData, - `0x${fieldValue.slice(-40)}`, + `0x${(fieldValue as string).slice(-40)}`, ); } return fieldValue === criteria.filterData; @@ -320,26 +410,77 @@ export class EventAction extends DeployableTarget< if (criteria.fieldType === PrimitiveType.UINT) { return BigInt(fieldValue) > BigInt(criteria.filterData); } - throw new InvalidNumericalCriteriaError({ log, criteria, fieldValue }); + throw new InvalidNumericalCriteriaError({ + ...input, + criteria, + fieldValue, + }); case FilterType.LESS_THAN: if (criteria.fieldType === PrimitiveType.UINT) { return BigInt(fieldValue) < BigInt(criteria.filterData); } - throw new InvalidNumericalCriteriaError({ log, criteria, fieldValue }); + throw new InvalidNumericalCriteriaError({ + ...input, + criteria, + fieldValue, + }); case FilterType.CONTAINS: if ( criteria.fieldType === PrimitiveType.BYTES || criteria.fieldType === PrimitiveType.STRING ) { - return fieldValue.includes(criteria.filterData); + return (fieldValue as string).includes(criteria.filterData); } - throw new FieldValueNotComparableError({ log, criteria, fieldValue }); + throw new FieldValueNotComparableError({ + ...input, + criteria, + fieldValue, + }); default: - throw new UnrecognizedFilterTypeError({ log, criteria, fieldValue }); + throw new UnrecognizedFilterTypeError({ + ...input, + criteria, + fieldValue, + }); + } + } + + /** + * Validates a {@link Log} against a given criteria. + * + * @param {Criteria} criteria - The criteria to validate against. + * @param {Log} log - The Viem event log. + * @returns {Promise} - Returns true if the log passes the criteria, false otherwise. + */ + public validateLogAgainstCriteria(criteria: Criteria, log: Log): boolean { + const fieldValue = log.topics.at(criteria.fieldIndex); + if (fieldValue === undefined) { + throw new FieldValueUndefinedError({ log, criteria, fieldValue }); } + return this.validateFieldAgainstCriteria(criteria, fieldValue, { log }); + } + + /** + * Validates a function's decoded arguments against a given criteria. + * + * @param {Criteria} criteria - The criteria to validate against. + * @param {unknown[]} decodedArgs - The decoded arguments of the function call. + * @returns {Promise} - Returns true if the decoded argument passes the criteria, false otherwise. + */ + public validateFunctionAgainstCriteria( + criteria: Criteria, + decodedArgs: readonly (string | bigint)[], + ): boolean { + const fieldValue = decodedArgs[criteria.fieldIndex]; + if (fieldValue === undefined) { + throw new FieldValueUndefinedError({ decodedArgs, criteria, fieldValue }); + } + return this.validateFieldAgainstCriteria(criteria, fieldValue, { + decodedArgs, + }); } /** diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index e26e64af..e80d022e 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -380,6 +380,28 @@ export class TooManyEventActionStepsProvidedError extends Error { } } +/** + * Function action validation context to help debug other validation errors + * + * @interface FunctionActionValidationMeta + * @typedef {FunctionActionValidationMeta} + */ +interface FunctionActionValidationMeta { + decodedArgs: readonly (string | bigint)[]; + /** + * The value pulled off the log being validated against + * + * @type {*} + * biome-ignore lint/suspicious/noExplicitAny: this can be a few different types based on what the log emits + */ + fieldValue: any; + /** + * The criteria being used to compare during validation + * + * @type {Criteria} + */ + criteria: Criteria; +} /** * Event action validation context to help debug other validation errors * @@ -413,17 +435,23 @@ interface EventActionValidationMeta { * Instantiated with relevent context data for more in depth debugging. * * @export - * @class EventActionValidationError - * @typedef {EventActionValidationError} + * @class FieldActionValidationError + * @typedef {FieldActionValidationError} * @extends {Error} */ -export class EventActionValidationError extends Error { +export class FieldActionValidationError extends Error { + /** + * The function input arguments being validated against + * + * @type {decodedArgs} + */ + decodedArgs?: readonly (string | bigint)[]; /** * The viem log being validated against * * @type {Log} */ - log: Log; + log?: Log; /** * The value pulled off the log being validated against * @@ -438,7 +466,7 @@ export class EventActionValidationError extends Error { */ criteria: Criteria; /** - * Creates an instance of EventActionValidationError. + * Creates an instance of FieldActionValidationError. * * @constructor * @param {string} message @@ -449,12 +477,23 @@ export class EventActionValidationError extends Error { */ constructor( message: string, - { fieldValue, criteria, log }: EventActionValidationMeta, + { + fieldValue, + criteria, + ...args + }: EventActionValidationMeta | FunctionActionValidationMeta, ) { super(message); this.fieldValue = fieldValue; this.criteria = criteria; - this.log = log; + + switch (true) { + case 'log' in args: + this.log = args.log; + break; + case 'decodedArgs' in args: + this.decodedArgs = args.decodedArgs; + } } } @@ -464,16 +503,18 @@ export class EventActionValidationError extends Error { * @export * @class FieldValueUndefinedError * @typedef {FieldValueUndefinedError} - * @extends {EventActionValidationError} + * @extends {FieldActionValidationError} */ -export class FieldValueUndefinedError extends EventActionValidationError { +export class FieldValueUndefinedError extends FieldActionValidationError { /** * Creates an instance of FieldValueUndefinedError. * * @constructor * @param {EventActionValidationMeta} metadata */ - constructor(metadata: EventActionValidationMeta) { + constructor( + metadata: EventActionValidationMeta | FunctionActionValidationMeta, + ) { super('Field value is undefined', metadata); } } @@ -484,16 +525,18 @@ export class FieldValueUndefinedError extends EventActionValidationError { * @export * @class InvalidNumericalCriteriaError * @typedef {InvalidNumericalCriteriaError} - * @extends {EventActionValidationError} + * @extends {FieldActionValidationError} */ -export class InvalidNumericalCriteriaError extends EventActionValidationError { +export class InvalidNumericalCriteriaError extends FieldActionValidationError { /** * Creates an instance of InvalidNumericalCriteria. * * @constructor * @param {EventActionValidationMeta} metadata */ - constructor(metadata: EventActionValidationMeta) { + constructor( + metadata: EventActionValidationMeta | FunctionActionValidationMeta, + ) { super( 'Numerical comparisons cannot be used with non-numerical criteria', metadata, @@ -507,16 +550,18 @@ export class InvalidNumericalCriteriaError extends EventActionValidationError { * @export * @class FieldValueNotComparableError * @typedef {FieldValueNotComparableError} - * @extends {EventActionValidationError} + * @extends {FieldActionValidationError} */ -export class FieldValueNotComparableError extends EventActionValidationError { +export class FieldValueNotComparableError extends FieldActionValidationError { /** * Creates an instance of FieldValueNotComparableError. * * @constructor * @param {EventActionValidationMeta} metadata */ - constructor(metadata: EventActionValidationMeta) { + constructor( + metadata: EventActionValidationMeta | FunctionActionValidationMeta, + ) { super('Filter can only be used with bytes or string field type', metadata); } } @@ -527,16 +572,18 @@ export class FieldValueNotComparableError extends EventActionValidationError { * @export * @class UnrecognizedFilterTypeError * @typedef {UnrecognizedFilterTypeError} - * @extends {EventActionValidationError} + * @extends {FieldActionValidationError} */ -export class UnrecognizedFilterTypeError extends EventActionValidationError { +export class UnrecognizedFilterTypeError extends FieldActionValidationError { /** * Creates an instance of UnrecognizedFilterTypeError. * * @constructor * @param {EventActionValidationMeta} metadata */ - constructor(metadata: EventActionValidationMeta) { + constructor( + metadata: EventActionValidationMeta | FunctionActionValidationMeta, + ) { super('Invalid FilterType provided', metadata); } }