Skip to content

Commit

Permalink
feat(sdk): add additional helpers into EventAction for function valid…
Browse files Browse the repository at this point in the history
…ation
  • Loading branch information
topocount committed Sep 23, 2024
1 parent 1cb65a0 commit 0778732
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 34 deletions.
171 changes: 156 additions & 15 deletions packages/sdk/src/Actions/EventAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -253,6 +257,36 @@ export class EventAction extends DeployableTarget<
* @returns {Promise<boolean>}
*/
public async isActionStepValid(
actionStep: ActionStep,
params?: GetLogsParams<Abi, ContractEventName<Abi>> & {
knownEvents?: Record<Hex, AbiEvent>;
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<Abi, ContractEventName<Abi>> & {
* knownEvents?: Record<Hex, AbiEvent>;
* logs?: Log[];
* }} [params]
* @returns {Promise<boolean>}
*/
public async isActionEventValid(
actionStep: ActionStep,
params?: GetLogsParams<Abi, ContractEventName<Abi>> & {
knownEvents?: Record<Hex, AbiEvent>;
Expand Down Expand Up @@ -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<Abi, ContractEventName<Abi>> & {
* knownEvents?: Record<Hex, AbiEvent>;
* txHash?: Hex;
* }} [params]
* @returns {Promise<boolean>}
*/
public async isActionFunctionValid(
actionStep: ActionStep,
params?: GetLogsParams<Abi, ContractEventName<Abi>> & {
knownEvents?: Record<Hex, AbiEvent>;
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<Hex, AbiFunction>)[
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<boolean>} - 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;
Expand All @@ -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<boolean>} - 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<boolean>} - 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,
});
}

/**
Expand Down
Loading

0 comments on commit 0778732

Please sign in to comment.