@boostxyz/sdk 5.2.1 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +10 -0
  2. package/dist/Actions/Action.cjs +1 -1
  3. package/dist/Actions/Action.js +1 -1
  4. package/dist/Actions/EventAction.cjs +1 -1
  5. package/dist/Actions/EventAction.cjs.map +1 -1
  6. package/dist/Actions/EventAction.d.ts +82 -38
  7. package/dist/Actions/EventAction.d.ts.map +1 -1
  8. package/dist/Actions/EventAction.js +437 -297
  9. package/dist/Actions/EventAction.js.map +1 -1
  10. package/dist/AllowLists/AllowList.cjs +1 -1
  11. package/dist/AllowLists/AllowList.js +2 -2
  12. package/dist/AllowLists/SimpleAllowList.cjs +1 -1
  13. package/dist/AllowLists/SimpleAllowList.js +2 -2
  14. package/dist/AllowLists/SimpleDenyList.cjs +1 -1
  15. package/dist/AllowLists/SimpleDenyList.js +3 -3
  16. package/dist/Auth/PassthroughAuth.cjs +1 -1
  17. package/dist/Auth/PassthroughAuth.js +1 -1
  18. package/dist/BoostCore.cjs +2 -2
  19. package/dist/BoostCore.cjs.map +1 -1
  20. package/dist/BoostCore.d.ts +42 -1
  21. package/dist/BoostCore.d.ts.map +1 -1
  22. package/dist/BoostCore.js +360 -318
  23. package/dist/BoostCore.js.map +1 -1
  24. package/dist/BoostRegistry.cjs +1 -1
  25. package/dist/BoostRegistry.js +2 -2
  26. package/dist/{Budget-N0YEfSt2.cjs → Budget-AoNx7uFd.cjs} +2 -2
  27. package/dist/{Budget-N0YEfSt2.cjs.map → Budget-AoNx7uFd.cjs.map} +1 -1
  28. package/dist/{Budget-C0SMvfEl.js → Budget-DYIV9iNK.js} +3 -3
  29. package/dist/{Budget-C0SMvfEl.js.map → Budget-DYIV9iNK.js.map} +1 -1
  30. package/dist/Budgets/Budget.cjs +1 -1
  31. package/dist/Budgets/Budget.js +2 -2
  32. package/dist/Budgets/ManagedBudget.cjs +1 -1
  33. package/dist/Budgets/ManagedBudget.js +2 -2
  34. package/dist/Deployable/DeployableTarget.cjs +1 -1
  35. package/dist/Deployable/DeployableTarget.js +1 -1
  36. package/dist/Deployable/DeployableTargetWithRBAC.cjs +1 -1
  37. package/dist/Deployable/DeployableTargetWithRBAC.js +2 -2
  38. package/dist/Incentive-BbkfwGOb.cjs +2 -0
  39. package/dist/Incentive-BbkfwGOb.cjs.map +1 -0
  40. package/dist/{Incentive-DBZHQ9Np.js → Incentive-qlnv5kQB.js} +77 -50
  41. package/dist/Incentive-qlnv5kQB.js.map +1 -0
  42. package/dist/Incentives/AllowListIncentive.cjs +1 -1
  43. package/dist/Incentives/AllowListIncentive.js +3 -3
  44. package/dist/Incentives/CGDAIncentive.cjs +1 -1
  45. package/dist/Incentives/CGDAIncentive.js +2 -2
  46. package/dist/Incentives/ERC20Incentive.cjs +1 -1
  47. package/dist/Incentives/ERC20Incentive.js +6 -6
  48. package/dist/Incentives/ERC20PeggedIncentive.d.ts +10 -1
  49. package/dist/Incentives/ERC20PeggedIncentive.d.ts.map +1 -1
  50. package/dist/Incentives/ERC20VariableCriteriaIncentive.cjs +1 -1
  51. package/dist/Incentives/ERC20VariableCriteriaIncentive.js +2 -2
  52. package/dist/Incentives/ERC20VariableIncentive.cjs +1 -1
  53. package/dist/Incentives/ERC20VariableIncentive.js +2 -2
  54. package/dist/Incentives/Incentive.cjs +1 -1
  55. package/dist/Incentives/Incentive.js +2 -2
  56. package/dist/Incentives/PointsIncentive.cjs +1 -1
  57. package/dist/Incentives/PointsIncentive.js +2 -2
  58. package/dist/{SimpleDenyList-B8QeJthf.js → SimpleDenyList-ByAr4X1r.js} +3 -3
  59. package/dist/{SimpleDenyList-B8QeJthf.js.map → SimpleDenyList-ByAr4X1r.js.map} +1 -1
  60. package/dist/{SimpleDenyList-DIb4xX3j.cjs → SimpleDenyList-CsRXJPwm.cjs} +2 -2
  61. package/dist/{SimpleDenyList-DIb4xX3j.cjs.map → SimpleDenyList-CsRXJPwm.cjs.map} +1 -1
  62. package/dist/Validators/LimitedSignerValidator.cjs +1 -1
  63. package/dist/Validators/LimitedSignerValidator.js +2 -2
  64. package/dist/Validators/SignerValidator.cjs +1 -1
  65. package/dist/Validators/SignerValidator.js +2 -2
  66. package/dist/Validators/Validator.cjs +1 -1
  67. package/dist/Validators/Validator.js +1 -1
  68. package/dist/{deployments-COxshLqt.js → deployments-D0fs26TV.js} +16 -16
  69. package/dist/{deployments-COxshLqt.js.map → deployments-D0fs26TV.js.map} +1 -1
  70. package/dist/{deployments-BGpr4ppG.cjs → deployments-DoIOqxco.cjs} +2 -2
  71. package/dist/deployments-DoIOqxco.cjs.map +1 -0
  72. package/dist/deployments.json +3 -3
  73. package/dist/errors.cjs +1 -1
  74. package/dist/errors.cjs.map +1 -1
  75. package/dist/errors.d.ts +40 -0
  76. package/dist/errors.d.ts.map +1 -1
  77. package/dist/errors.js +42 -16
  78. package/dist/errors.js.map +1 -1
  79. package/dist/{generated-ClbO_ULI.js → generated-Cyvr_Tjx.js} +446 -438
  80. package/dist/generated-Cyvr_Tjx.js.map +1 -0
  81. package/dist/{generated-CRD9XfOT.cjs → generated-DtYPHhtX.cjs} +2 -2
  82. package/dist/generated-DtYPHhtX.cjs.map +1 -0
  83. package/dist/index.cjs +1 -1
  84. package/dist/index.js +160 -155
  85. package/package.json +1 -1
  86. package/src/Actions/EventAction.test.ts +285 -3
  87. package/src/Actions/EventAction.ts +402 -124
  88. package/src/BoostCore.test.ts +51 -3
  89. package/src/BoostCore.ts +73 -0
  90. package/src/Incentives/ERC20PeggedIncentive.ts +33 -4
  91. package/src/errors.ts +50 -0
  92. package/dist/Incentive-BpZePiOD.cjs +0 -2
  93. package/dist/Incentive-BpZePiOD.cjs.map +0 -1
  94. package/dist/Incentive-DBZHQ9Np.js.map +0 -1
  95. package/dist/deployments-BGpr4ppG.cjs.map +0 -1
  96. package/dist/generated-CRD9XfOT.cjs.map +0 -1
  97. package/dist/generated-ClbO_ULI.js.map +0 -1
@@ -7,10 +7,12 @@ import {
7
7
  } from '@boostxyz/evm';
8
8
  import { bytecode } from '@boostxyz/evm/artifacts/contracts/actions/EventAction.sol/EventAction.json';
9
9
  import { getTransaction, getTransactionReceipt } from '@wagmi/core';
10
+ import type { AbiEventParameter } from 'abitype';
10
11
  import { match } from 'ts-pattern';
11
12
  import {
12
13
  type AbiEvent,
13
14
  type AbiFunction,
15
+ type AbiParameter,
14
16
  type Address,
15
17
  type GetLogsReturnType,
16
18
  type GetTransactionParameters,
@@ -41,6 +43,8 @@ import {
41
43
  FieldValueUndefinedError,
42
44
  FunctionDataDecodeError,
43
45
  InvalidNumericalCriteriaError,
46
+ InvalidTupleDecodingError,
47
+ InvalidTupleEncodingError,
44
48
  NoEventActionStepsProvidedError,
45
49
  TooManyEventActionStepsProvidedError,
46
50
  UnparseableAbiParamError,
@@ -88,6 +92,8 @@ export enum PrimitiveType {
88
92
  ADDRESS = 1,
89
93
  BYTES = 2,
90
94
  STRING = 3,
95
+ // Note: TUPLE remains in the enum but is no longer handled directly by `validateFieldAgainstCriteria`.
96
+ TUPLE = 4,
91
97
  }
92
98
 
93
99
  /**
@@ -113,6 +119,9 @@ export interface Criteria {
113
119
  /**
114
120
  * The index in the logs argument array where the field is located.
115
121
  *
122
+ * If `fieldType` is TUPLE, this value is **bitpacked** with up to 5 sub-indexes,
123
+ * with the maximum 6-bit value used as a "terminator" to indicate no further indexes.
124
+ *
116
125
  * @type {number}
117
126
  */
118
127
  fieldIndex: number;
@@ -323,6 +332,13 @@ export interface EventActionPayloadRaw {
323
332
  */
324
333
  export type EventLogs = GetLogsReturnType<AbiEvent, AbiEvent[], true>;
325
334
 
335
+ /**
336
+ * Single event log
337
+ * @export
338
+ * @typedef {EventLog}
339
+ */
340
+ export type EventLog = EventLogs[0] & { args: unknown[] };
341
+
326
342
  /**
327
343
  * A generic event action
328
344
  *
@@ -571,14 +587,7 @@ export class EventAction extends DeployableTarget<
571
587
  }
572
588
  const decodedLogs = receipt.logs
573
589
  .filter((log) => log.topics[0] === toEventSelector(event))
574
- .map((log) => {
575
- const { eventName, args } = decodeEventLog({
576
- abi: [event],
577
- data: log.data,
578
- topics: log.topics,
579
- });
580
- return { ...log, eventName, args };
581
- });
590
+ .map((log) => decodeAndReorderLogArgs(event, log));
582
591
 
583
592
  for (let log of decodedLogs) {
584
593
  if (!isAddressEqual(log.address, claimant.targetContract)) continue;
@@ -703,7 +712,7 @@ export class EventAction extends DeployableTarget<
703
712
 
704
713
  // Use the provided logs, no need to fetch receipt
705
714
  if ('logs' in params) {
706
- return this.isActionEventValid(actionStep, params.logs);
715
+ return this.isActionEventValid(actionStep, params.logs, event);
707
716
  }
708
717
 
709
718
  const receipt = await getTransactionReceipt(this._config, {
@@ -724,17 +733,9 @@ export class EventAction extends DeployableTarget<
724
733
 
725
734
  const decodedLogs = receipt.logs
726
735
  .filter((log) => log.topics[0] === toEventSelector(event))
727
- .map((log) => {
728
- const { eventName, args } = decodeEventLog({
729
- abi: [event],
730
- data: log.data,
731
- topics: log.topics,
732
- });
736
+ .map((log) => decodeAndReorderLogArgs(event, log));
733
737
 
734
- return { ...log, eventName, args };
735
- });
736
-
737
- return this.isActionEventValid(actionStep, decodedLogs);
738
+ return this.isActionEventValid(actionStep, decodedLogs, event);
738
739
  }
739
740
  if (actionStep.signatureType === SignatureType.FUNC) {
740
741
  if ('hash' in params) {
@@ -756,20 +757,44 @@ export class EventAction extends DeployableTarget<
756
757
 
757
758
  /**
758
759
  * Validates a single action event with a given criteria against logs.
759
- * If logs are provided in the optional `params` argument, then those logs will be used instead of being fetched with the configured client.
760
760
  *
761
761
  * @public
762
- * @async
763
762
  * @param {ActionStep} actionStep - The action step containing the event to validate.
764
763
  * @param {EventLogs} logs - Event logs to validate the given step against
765
- * @returns {Promise<boolean>} Resolves to true if the action event is valid, throws if input is invalid, otherwise false.
764
+ * @param {AbiEvent} eventAbi - The ABI definition of the event
765
+ * @returns {boolean} Resolves to true if the action event is valid, throws if input is invalid, otherwise false.
766
766
  */
767
- public isActionEventValid(actionStep: ActionStep, logs: EventLogs) {
767
+ public isActionEventValid(
768
+ actionStep: ActionStep,
769
+ logs: EventLogs,
770
+ eventAbi: AbiEvent,
771
+ ): boolean {
768
772
  const criteria = actionStep.actionParameter;
769
773
  if (!logs.length) return false;
774
+
775
+ // Check each log
770
776
  for (let log of logs) {
771
- if (this.validateLogAgainstCriteria(criteria, log)) {
772
- return true;
777
+ // parse out final (scalar) field from the log args
778
+ try {
779
+ if (!Array.isArray(log.args)) {
780
+ throw new DecodedArgsMalformedError({
781
+ log,
782
+ criteria,
783
+ fieldValue: undefined,
784
+ });
785
+ }
786
+ const { value, type } = this.parseFieldFromAbi(
787
+ log.args,
788
+ criteria.fieldIndex,
789
+ eventAbi.inputs || [],
790
+ criteria.fieldType,
791
+ );
792
+ criteria.fieldType = type;
793
+ if (this.validateFieldAgainstCriteria(criteria, value, { log })) {
794
+ return true;
795
+ }
796
+ } catch {
797
+ // If there's an error on this log, keep trying with the next one
773
798
  }
774
799
  }
775
800
  return false;
@@ -821,7 +846,15 @@ export class EventAction extends DeployableTarget<
821
846
  return { ...log, eventName, args };
822
847
  });
823
848
 
824
- return this.isActionEventValid(actionStep, decodedLogs);
849
+ return this.isActionEventValid(actionStep, decodedLogs, {
850
+ name: 'Transfer',
851
+ type: 'event',
852
+ inputs: [
853
+ { type: 'address', indexed: true },
854
+ { type: 'address', indexed: true },
855
+ { type: 'uint256', indexed: true },
856
+ ],
857
+ });
825
858
  } catch {
826
859
  // ERC20
827
860
  try {
@@ -844,13 +877,91 @@ export class EventAction extends DeployableTarget<
844
877
  return { ...log, eventName, args };
845
878
  });
846
879
 
847
- return this.isActionEventValid(actionStep, decodedLogs);
880
+ return this.isActionEventValid(actionStep, decodedLogs, {
881
+ name: 'Transfer',
882
+ type: 'event',
883
+ inputs: [
884
+ { type: 'address', indexed: true },
885
+ { type: 'address', indexed: true },
886
+ { type: 'uint256' },
887
+ ],
888
+ });
848
889
  } catch {
849
890
  throw new DecodedArgsError('Failed to decode transfer logs');
850
891
  }
851
892
  }
852
893
  }
853
894
 
895
+ /**
896
+ * Parses the final (scalar) field from a set of decoded arguments, given an ABI definition.
897
+ * If the fieldType is TUPLE, we decode `fieldIndex` as a bitpacked array of indexes to drill down
898
+ * into nested tuples. Otherwise, we parse the single `fieldIndex` as normal.
899
+ *
900
+ * @public
901
+ * @param {readonly unknown[]} allArgs - The decoded arguments array from an event log or function call.
902
+ * @param {number} criteriaIndex - The field index (bitpacked if TUPLE).
903
+ * @param {AbiParameter[]} abiInputs - The ABI inputs describing each decoded argument.
904
+ * @param {PrimitiveType} declaredType - Either TUPLE or a standard scalar type
905
+ * @returns {{ value: string | bigint | Hex; type: Exclude<PrimitiveType, PrimitiveType.TUPLE> }}
906
+ */
907
+ public parseFieldFromAbi(
908
+ allArgs: readonly unknown[],
909
+ criteriaIndex: number,
910
+ abiInputs: readonly AbiParameter[],
911
+ declaredType: PrimitiveType,
912
+ ): {
913
+ value: string | bigint | Hex;
914
+ type: Exclude<PrimitiveType, PrimitiveType.TUPLE>;
915
+ } {
916
+ // If ANY_ACTION_PARAM, return a dummy "any" value so we can do special-case checks
917
+ if (criteriaIndex === CheatCodes.ANY_ACTION_PARAM) {
918
+ return { value: zeroHash, type: PrimitiveType.BYTES };
919
+ }
920
+
921
+ // If it's not TUPLE, parse as a single index (existing logic)
922
+ if (declaredType !== PrimitiveType.TUPLE) {
923
+ if (!Array.isArray(allArgs) || criteriaIndex >= allArgs.length) {
924
+ throw new FieldValueUndefinedError({
925
+ fieldValue: allArgs,
926
+ criteria: {
927
+ filterType: FilterType.EQUAL,
928
+ fieldType: declaredType,
929
+ fieldIndex: criteriaIndex,
930
+ filterData: zeroHash,
931
+ },
932
+ });
933
+ }
934
+ const abiParam = abiInputs[criteriaIndex];
935
+ if (!abiParam || !abiParam.type) {
936
+ throw new UnparseableAbiParamError(criteriaIndex, abiParam as AbiEvent);
937
+ }
938
+ const rawValue = allArgs[criteriaIndex];
939
+
940
+ const finalType = abiTypeToPrimitiveType(abiParam.type);
941
+
942
+ if (
943
+ finalType === PrimitiveType.ADDRESS &&
944
+ (typeof rawValue !== 'string' || !isAddress(rawValue))
945
+ ) {
946
+ throw new FieldValueUndefinedError({
947
+ fieldValue: rawValue,
948
+ criteria: {
949
+ fieldIndex: criteriaIndex,
950
+ filterType: FilterType.EQUAL,
951
+ fieldType: finalType,
952
+ filterData: zeroHash,
953
+ },
954
+ });
955
+ }
956
+
957
+ return { value: rawValue as string | bigint | Hex, type: finalType };
958
+ }
959
+
960
+ // Otherwise, declaredType === TUPLE => decode bitpacked indexes
961
+ const indexes = unpackFieldIndexes(criteriaIndex);
962
+ return parseNestedTupleValue(allArgs as unknown[], indexes, abiInputs);
963
+ }
964
+
854
965
  /**
855
966
  * Validates a single action function with a given criteria against the transaction input.
856
967
  *
@@ -870,10 +981,10 @@ export class EventAction extends DeployableTarget<
870
981
  params: Pick<ValidateActionStepParams, 'abiItem' | 'knownSignatures'>,
871
982
  ) {
872
983
  const criteria = actionStep.actionParameter;
873
- let signature = actionStep.signature;
984
+ const signature = actionStep.signature;
874
985
 
875
986
  let func: AbiFunction;
876
- if (params.abiItem) func = params?.abiItem as AbiFunction;
987
+ if (params.abiItem) func = params.abiItem as AbiFunction;
877
988
  else {
878
989
  const sigPool = params.knownSignatures as Record<Hex, AbiFunction>;
879
990
  func = sigPool[signature] as AbiFunction;
@@ -892,31 +1003,37 @@ export class EventAction extends DeployableTarget<
892
1003
  throw new FunctionDataDecodeError([func], e as Error);
893
1004
  }
894
1005
 
895
- // Validate the criteria against decoded arguments using fieldIndex
896
- const decodedArgs = decodedData.args;
897
-
898
- if (!decodedArgs || !decodedData) return false;
899
-
900
- if (
901
- !this.validateFunctionAgainstCriteria(
902
- criteria,
903
- decodedArgs as (string | bigint)[],
904
- )
905
- ) {
1006
+ if (!decodedData?.args) {
906
1007
  return false;
907
1008
  }
908
1009
 
909
- return true;
1010
+ try {
1011
+ const { value, type } = this.parseFieldFromAbi(
1012
+ decodedData.args as unknown[],
1013
+ criteria.fieldIndex,
1014
+ func.inputs || [],
1015
+ criteria.fieldType,
1016
+ );
1017
+ criteria.fieldType = type;
1018
+ return this.validateFieldAgainstCriteria(criteria, value, {
1019
+ decodedArgs: decodedData.args as readonly (string | bigint)[],
1020
+ });
1021
+ } catch {
1022
+ return false;
1023
+ }
910
1024
  }
1025
+
911
1026
  /**
912
- * Validates a field against a given criteria.
1027
+ * Validates a field against a given criteria. The field is assumed to be a non-tuple scalar,
1028
+ * along with its final resolved `PrimitiveType`. (Any TUPLE logic has been extracted elsewhere.)
913
1029
  *
914
1030
  * @param {Criteria} criteria - The criteria to validate against.
915
1031
  * @param {string | bigint | Hex} fieldValue - The field value to validate.
1032
+ * @param {Exclude<PrimitiveType, PrimitiveType.TUPLE>} fieldType - The final resolved primitive type.
916
1033
  * @param {Object} input - Additional context for validation.
917
1034
  * @param {EventLogs[0]} [input.log] - The event log, if validating an event.
918
1035
  * @param {readonly (string | bigint)[]} [input.decodedArgs] - The decoded function arguments, if validating a function call.
919
- * @returns {Promise<boolean>} - Returns true if the field passes the criteria, false otherwise.
1036
+ * @returns {boolean} - Returns true if the field passes the criteria, false otherwise.
920
1037
  */
921
1038
  public validateFieldAgainstCriteria(
922
1039
  criteria: Criteria,
@@ -925,6 +1042,10 @@ export class EventAction extends DeployableTarget<
925
1042
  | { log: EventLogs[0] }
926
1043
  | { decodedArgs: readonly (string | bigint)[] },
927
1044
  ): boolean {
1045
+ /*
1046
+ * Special-case: ANY_ACTION_PARAM. If we have filterType=EQUAL, fieldType=BYTES, fieldIndex=255,
1047
+ * we consider that a wildcard match. Return true immediately.
1048
+ */
928
1049
  if (
929
1050
  criteria.filterType === FilterType.EQUAL &&
930
1051
  criteria.fieldType === PrimitiveType.BYTES &&
@@ -932,11 +1053,17 @@ export class EventAction extends DeployableTarget<
932
1053
  ) {
933
1054
  return true;
934
1055
  }
1056
+ if (criteria.fieldType === PrimitiveType.TUPLE) {
1057
+ throw new InvalidTupleDecodingError(
1058
+ 'Tuples should not be passed into validateFieldAgainstCriteria',
1059
+ );
1060
+ }
1061
+ const fieldType = criteria.fieldType;
935
1062
 
936
- // Type narrow based on criteria.filterType
1063
+ // Evaluate filter based on the final fieldType
937
1064
  switch (criteria.filterType) {
938
1065
  case FilterType.EQUAL:
939
- return match(criteria.fieldType)
1066
+ return match(fieldType)
940
1067
  .with(PrimitiveType.ADDRESS, () =>
941
1068
  isAddressEqual(criteria.filterData, fieldValue as Address),
942
1069
  )
@@ -951,7 +1078,7 @@ export class EventAction extends DeployableTarget<
951
1078
  .otherwise(() => fieldValue === criteria.filterData);
952
1079
 
953
1080
  case FilterType.NOT_EQUAL:
954
- return match(criteria.fieldType)
1081
+ return match(fieldType)
955
1082
  .with(
956
1083
  PrimitiveType.ADDRESS,
957
1084
  () => !isAddressEqual(criteria.filterData, fieldValue as Address),
@@ -967,7 +1094,7 @@ export class EventAction extends DeployableTarget<
967
1094
  .otherwise(() => fieldValue !== criteria.filterData);
968
1095
 
969
1096
  case FilterType.GREATER_THAN:
970
- if (criteria.fieldType === PrimitiveType.UINT) {
1097
+ if (fieldType === PrimitiveType.UINT) {
971
1098
  return BigInt(fieldValue) > BigInt(criteria.filterData);
972
1099
  }
973
1100
  throw new InvalidNumericalCriteriaError({
@@ -975,8 +1102,9 @@ export class EventAction extends DeployableTarget<
975
1102
  criteria,
976
1103
  fieldValue,
977
1104
  });
1105
+
978
1106
  case FilterType.GREATER_THAN_OR_EQUAL:
979
- if (criteria.fieldType === PrimitiveType.UINT) {
1107
+ if (fieldType === PrimitiveType.UINT) {
980
1108
  return BigInt(fieldValue) >= BigInt(criteria.filterData);
981
1109
  }
982
1110
  throw new InvalidNumericalCriteriaError({
@@ -986,7 +1114,7 @@ export class EventAction extends DeployableTarget<
986
1114
  });
987
1115
 
988
1116
  case FilterType.LESS_THAN:
989
- if (criteria.fieldType === PrimitiveType.UINT) {
1117
+ if (fieldType === PrimitiveType.UINT) {
990
1118
  return BigInt(fieldValue) < BigInt(criteria.filterData);
991
1119
  }
992
1120
  throw new InvalidNumericalCriteriaError({
@@ -994,8 +1122,9 @@ export class EventAction extends DeployableTarget<
994
1122
  criteria,
995
1123
  fieldValue,
996
1124
  });
1125
+
997
1126
  case FilterType.LESS_THAN_OR_EQUAL:
998
- if (criteria.fieldType === PrimitiveType.UINT) {
1127
+ if (fieldType === PrimitiveType.UINT) {
999
1128
  return BigInt(fieldValue) <= BigInt(criteria.filterData);
1000
1129
  }
1001
1130
  throw new InvalidNumericalCriteriaError({
@@ -1006,11 +1135,11 @@ export class EventAction extends DeployableTarget<
1006
1135
 
1007
1136
  case FilterType.CONTAINS:
1008
1137
  if (
1009
- criteria.fieldType === PrimitiveType.BYTES ||
1010
- criteria.fieldType === PrimitiveType.STRING
1138
+ fieldType === PrimitiveType.BYTES ||
1139
+ fieldType === PrimitiveType.STRING
1011
1140
  ) {
1012
1141
  let substring;
1013
- if (criteria.fieldType === PrimitiveType.STRING) {
1142
+ if (fieldType === PrimitiveType.STRING) {
1014
1143
  substring = fromHex(criteria.filterData, 'string');
1015
1144
  } else {
1016
1145
  // truncate the `0x` prefix
@@ -1032,12 +1161,11 @@ export class EventAction extends DeployableTarget<
1032
1161
  fieldValue,
1033
1162
  });
1034
1163
  }
1035
-
1036
- if (criteria.fieldType === PrimitiveType.STRING) {
1037
- // fieldValue is decoded by the ABI
1164
+ if (fieldType === PrimitiveType.STRING) {
1038
1165
  const regexString = fromHex(criteria.filterData, 'string');
1039
1166
  return new RegExp(regexString).test(fieldValue);
1040
1167
  }
1168
+ // Otherwise unrecognized or not applicable
1041
1169
 
1042
1170
  default:
1043
1171
  throw new UnrecognizedFilterTypeError({
@@ -1048,68 +1176,6 @@ export class EventAction extends DeployableTarget<
1048
1176
  }
1049
1177
  }
1050
1178
 
1051
- /**
1052
- * Validates a {@link Log} against a given criteria.
1053
- * If the criteria's fieldIndex is 255 (using CheatCodes enum), it is reserved for anyValidation
1054
- *
1055
- * @param {Criteria} criteria - The criteria to validate against.
1056
- * @param {Log} log - The Viem event log.
1057
- * @returns {boolean} - Returns true if the log passes the criteria, false otherwise.
1058
- */
1059
- public validateLogAgainstCriteria(
1060
- criteria: Criteria,
1061
- log: EventLogs[0],
1062
- ): boolean {
1063
- if (
1064
- !Array.isArray(log.args) ||
1065
- (log.args.length <= criteria.fieldIndex &&
1066
- criteria.fieldIndex !== CheatCodes.ANY_ACTION_PARAM)
1067
- ) {
1068
- throw new DecodedArgsMalformedError({
1069
- log,
1070
- criteria,
1071
- fieldValue: undefined,
1072
- });
1073
- }
1074
-
1075
- const fieldValue =
1076
- criteria.fieldIndex === CheatCodes.ANY_ACTION_PARAM
1077
- ? zeroHash
1078
- : log.args.at(criteria.fieldIndex);
1079
-
1080
- if (fieldValue === undefined) {
1081
- throw new FieldValueUndefinedError({ log, criteria, fieldValue });
1082
- }
1083
- return this.validateFieldAgainstCriteria(criteria, fieldValue, { log });
1084
- }
1085
-
1086
- /**
1087
- * Validates a function's decoded arguments against a given criteria.
1088
- * If the criteria's fieldIndex is 255 (using CheatCodes enum), it is reserved for anyValidation
1089
- *
1090
- * @param {Criteria} criteria - The criteria to validate against.
1091
- * @param {unknown[]} decodedArgs - The decoded arguments of the function call.
1092
- * @returns {Promise<boolean>} - Returns true if the decoded argument passes the criteria, false otherwise.
1093
- */
1094
- public validateFunctionAgainstCriteria(
1095
- criteria: Criteria,
1096
- decodedArgs: readonly (string | bigint)[],
1097
- ): boolean {
1098
- const fieldValue =
1099
- criteria.fieldIndex === CheatCodes.ANY_ACTION_PARAM
1100
- ? zeroHash
1101
- : decodedArgs[criteria.fieldIndex];
1102
- if (fieldValue === undefined) {
1103
- throw new FieldValueUndefinedError({
1104
- criteria,
1105
- fieldValue,
1106
- });
1107
- }
1108
- return this.validateFieldAgainstCriteria(criteria, fieldValue, {
1109
- decodedArgs,
1110
- });
1111
- }
1112
-
1113
1179
  /**
1114
1180
  * @inheritdoc
1115
1181
  *
@@ -1159,6 +1225,15 @@ export class EventAction extends DeployableTarget<
1159
1225
  };
1160
1226
  }
1161
1227
 
1228
+ /**
1229
+ * Determines whether a string or bytes field is indexed in the event definition.
1230
+ * If the user tries to filter on an indexed string/bytes, we throw an error.
1231
+ *
1232
+ * @public
1233
+ * @param {ActionStep} step
1234
+ * @param {AbiEvent} event
1235
+ * @returns {boolean}
1236
+ */
1162
1237
  public isArraylikeIndexed(step: ActionStep, event: AbiEvent) {
1163
1238
  if (
1164
1239
  (step.actionParameter.fieldType === PrimitiveType.STRING ||
@@ -1171,9 +1246,114 @@ export class EventAction extends DeployableTarget<
1171
1246
  }
1172
1247
  }
1173
1248
 
1249
+ /**
1250
+ * Checks if a particular ABI parameter is the "tuple" variant that can have `components`.
1251
+ *
1252
+ * @param {AbiParameter} param
1253
+ * @returns {boolean}
1254
+ */
1255
+ function isTupleAbiParameter(
1256
+ param: AbiParameter,
1257
+ ): param is Extract<AbiParameter, { components: readonly AbiParameter[] }> {
1258
+ return param.type === 'tuple' || param.type.startsWith('tuple[');
1259
+ }
1260
+
1261
+ /**
1262
+ * Recursively parses nested tuples by following an array of sub-indexes (unpacked from bitpacked `fieldIndex`).
1263
+ * Each entry in `indexes` is used to pick which sub-component in the current tuple's `components`.
1264
+ * If we encounter the "terminator" or run out of indexes, we stop.
1265
+ *
1266
+ * @param {unknown[]} rawArgs - The top-level arguments array.
1267
+ * @param {number[]} indexes - The array of indexes from `unpackFieldIndexes(...)`.
1268
+ * @param {readonly AbiParameter[]} abiInputs - The top-level ABI inputs for the entire arguments array.
1269
+ * @returns {{ value: string | bigint | Hex, type: Exclude<PrimitiveType, PrimitiveType.TUPLE> }}
1270
+ */
1271
+ function parseNestedTupleValue(
1272
+ rawArgs: unknown[],
1273
+ indexes: number[],
1274
+ abiInputs: readonly AbiParameter[],
1275
+ ): {
1276
+ value: string | bigint | Hex;
1277
+ type: Exclude<PrimitiveType, PrimitiveType.TUPLE>;
1278
+ } {
1279
+ if (!indexes.length) {
1280
+ throw new InvalidTupleDecodingError(
1281
+ `No indexes found; cannot parse TUPLE field`,
1282
+ );
1283
+ }
1284
+
1285
+ // The first index picks which top-level ABI param to look at
1286
+ const idx = indexes[0] ?? abiInputs.length + 1;
1287
+ // If idx is out of range or is a "terminator," fail fast
1288
+ if (idx >= abiInputs.length) {
1289
+ throw new InvalidTupleDecodingError(undefined, idx);
1290
+ }
1291
+
1292
+ const param = abiInputs[idx];
1293
+ const rawValue = rawArgs[idx];
1294
+
1295
+ // If param isn't a tuple, we are at a leaf
1296
+ if (!isTupleAbiParameter(param!)) {
1297
+ const finalType = abiTypeToPrimitiveType(param!.type);
1298
+ return { value: rawValue as string | bigint | Hex, type: finalType };
1299
+ }
1300
+
1301
+ // Otherwise param is a tuple => rawValue must be an array of subfields
1302
+ if (!Array.isArray(rawValue)) {
1303
+ throw new InvalidTupleDecodingError(
1304
+ `rawValue is not an array, but param.type is tuple`,
1305
+ );
1306
+ }
1307
+
1308
+ // Move to the next sub-index
1309
+ const remaining = indexes.slice(1);
1310
+ if (!remaining.length) {
1311
+ // If there are no more indexes, we can't pick a sub-component
1312
+ // Typically you'd want at least one more index to say "which subfield in the tuple we want"
1313
+ throw new InvalidTupleDecodingError(undefined, -1);
1314
+ }
1315
+
1316
+ // Check the next index for param.components
1317
+ const subIdx = remaining[0] ?? param.components.length + 1;
1318
+ if (subIdx >= param.components.length) {
1319
+ throw new InvalidTupleDecodingError(undefined, subIdx);
1320
+ }
1321
+
1322
+ // Recurse deeper using param.components as the "new top-level" ABI param list
1323
+ return parseNestedTupleValue(rawValue, remaining, param.components);
1324
+ }
1325
+
1326
+ /**
1327
+ * Maps an ABI type string (e.g. 'uint256', 'address', 'bytes', 'string', etc.) to a `PrimitiveType`.
1328
+ *
1329
+ * @param {string} abiType
1330
+ * @returns {Exclude<PrimitiveType, PrimitiveType.TUPLE>}
1331
+ */
1332
+ function abiTypeToPrimitiveType(
1333
+ abiType: string,
1334
+ ): Exclude<PrimitiveType, PrimitiveType.TUPLE> {
1335
+ const lower = abiType.toLowerCase();
1336
+
1337
+ if (lower.startsWith('uint') || lower.startsWith('int')) {
1338
+ return PrimitiveType.UINT;
1339
+ }
1340
+ if (lower === 'address') {
1341
+ return PrimitiveType.ADDRESS;
1342
+ }
1343
+ if (lower === 'bytes' || lower.startsWith('bytes')) {
1344
+ return PrimitiveType.BYTES;
1345
+ }
1346
+ if (lower === 'string') {
1347
+ return PrimitiveType.STRING;
1348
+ }
1349
+
1350
+ // If it doesn't match any known scalar, throw. We expect parseNestedTupleValue() to handle nested tuple logic separately.
1351
+ throw new DecodedArgsError(`Unrecognized ABI type: ${abiType}`);
1352
+ }
1353
+
1174
1354
  function _dedupeActionSteps(_steps: ActionStep[]): ActionStep[] {
1175
- const steps: ActionStep[] = [],
1176
- signatures: Record<string, boolean> = {};
1355
+ const steps: ActionStep[] = [];
1356
+ const signatures: Record<string, boolean> = {};
1177
1357
  for (let step of _steps) {
1178
1358
  const signature = JSON.stringify(step);
1179
1359
  if (signatures[signature]) continue;
@@ -1182,6 +1362,7 @@ function _dedupeActionSteps(_steps: ActionStep[]): ActionStep[] {
1182
1362
  }
1183
1363
  return steps;
1184
1364
  }
1365
+
1185
1366
  type RawActionStep = Overwrite<ActionStep, { chainid: bigint }>;
1186
1367
  type RawActionClaimant = Overwrite<ActionClaimant, { chainid: bigint }>;
1187
1368
 
@@ -1268,7 +1449,7 @@ export function prepareEventActionPayload({
1268
1449
  components: [
1269
1450
  { type: 'uint8', name: 'filterType' },
1270
1451
  { type: 'uint8', name: 'fieldType' },
1271
- { type: 'uint8', name: 'fieldIndex' },
1452
+ { type: 'uint32', name: 'fieldIndex' },
1272
1453
  { type: 'bytes', name: 'filterData' },
1273
1454
  ],
1274
1455
  },
@@ -1289,7 +1470,7 @@ export function prepareEventActionPayload({
1289
1470
  components: [
1290
1471
  { type: 'uint8', name: 'filterType' },
1291
1472
  { type: 'uint8', name: 'fieldType' },
1292
- { type: 'uint8', name: 'fieldIndex' },
1473
+ { type: 'uint32', name: 'fieldIndex' },
1293
1474
  { type: 'bytes', name: 'filterData' },
1294
1475
  ],
1295
1476
  },
@@ -1310,7 +1491,7 @@ export function prepareEventActionPayload({
1310
1491
  components: [
1311
1492
  { type: 'uint8', name: 'filterType' },
1312
1493
  { type: 'uint8', name: 'fieldType' },
1313
- { type: 'uint8', name: 'fieldIndex' },
1494
+ { type: 'uint32', name: 'fieldIndex' },
1314
1495
  { type: 'bytes', name: 'filterData' },
1315
1496
  ],
1316
1497
  },
@@ -1331,7 +1512,7 @@ export function prepareEventActionPayload({
1331
1512
  components: [
1332
1513
  { type: 'uint8', name: 'filterType' },
1333
1514
  { type: 'uint8', name: 'fieldType' },
1334
- { type: 'uint8', name: 'fieldIndex' },
1515
+ { type: 'uint32', name: 'fieldIndex' },
1335
1516
  { type: 'bytes', name: 'filterData' },
1336
1517
  ],
1337
1518
  },
@@ -1426,3 +1607,100 @@ export function transactionSenderClaimant(chainId: number): ActionClaimant {
1426
1607
  chainid: chainId,
1427
1608
  };
1428
1609
  }
1610
+
1611
+ // Helper functions to bit-pack and decode fieldIndex values
1612
+ const MAX_FIELD_INDEX = 0b111111; // Maximum value for 6 bits (63)
1613
+
1614
+ /**
1615
+ * Packs up to five indexes into a single uint32 value.
1616
+ *
1617
+ * @param {number[]} indexes - Array of up to five indexes to pack.
1618
+ * @returns {number} - Packed uint32 value.
1619
+ * @throws {Error} - If more than five indexes are provided or an index exceeds the maximum value.
1620
+ */
1621
+ export function packFieldIndexes(indexes: number[]): number {
1622
+ if (indexes.length > 5) {
1623
+ throw new InvalidTupleEncodingError('Can only pack up to 5 indexes.');
1624
+ }
1625
+
1626
+ let packed = 0;
1627
+ indexes.forEach((index, i) => {
1628
+ if (index > MAX_FIELD_INDEX) {
1629
+ throw new InvalidTupleEncodingError(
1630
+ `Index ${index} exceeds the maximum allowed value (${MAX_FIELD_INDEX}).`,
1631
+ );
1632
+ }
1633
+ packed |= (index & MAX_FIELD_INDEX) << (i * 6); // Each index occupies 6 bits
1634
+ });
1635
+
1636
+ return packed;
1637
+ }
1638
+
1639
+ /**
1640
+ * Unpacks a uint32 fieldIndex value into an array of up to five indexes.
1641
+ *
1642
+ * @param {number} packed - Packed uint32 value.
1643
+ * @returns {number[]} - Array of unpacked indexes.
1644
+ */
1645
+ export function unpackFieldIndexes(packed: number): number[] {
1646
+ const indexes: number[] = [];
1647
+ for (let i = 0; i < 5; i++) {
1648
+ const index = (packed >> (i * 6)) & MAX_FIELD_INDEX;
1649
+ if (index === MAX_FIELD_INDEX) break; // Terminator value
1650
+ indexes.push(index);
1651
+ }
1652
+ return indexes;
1653
+ }
1654
+
1655
+ /**
1656
+ * Decodes an event log and reorders the arguments to match the original ABI order.
1657
+ * This is necessary because viem's decodeEventLog reorders indexed parameters to the front.
1658
+ *
1659
+ * @param event - The event ABI definition
1660
+ * @param log - The log to decode
1661
+ * @returns {EventLog} The decoded log with arguments in the original ABI order
1662
+ */
1663
+ export function decodeAndReorderLogArgs(event: AbiEvent, log: Log) {
1664
+ const decodedLog = decodeEventLog({
1665
+ abi: [event],
1666
+ data: log.data,
1667
+ topics: log.topics,
1668
+ });
1669
+
1670
+ const argsArray = Array.isArray(decodedLog.args)
1671
+ ? decodedLog.args
1672
+ : Object.values(decodedLog.args);
1673
+
1674
+ if (!event.inputs.some((input) => input.indexed)) {
1675
+ return decodedLog as EventLog;
1676
+ }
1677
+
1678
+ const indexedIndices: number[] = [];
1679
+ const nonIndexedIndices: number[] = [];
1680
+ for (let i = 0; i < event.inputs.length; i++) {
1681
+ if (event.inputs[i]!.indexed) {
1682
+ indexedIndices.push(i);
1683
+ } else {
1684
+ nonIndexedIndices.push(i);
1685
+ }
1686
+ }
1687
+
1688
+ const reorderedArgs = new Array(event.inputs.length);
1689
+ let currentIndex = 0;
1690
+
1691
+ // Place the indexed arguments in their original positions
1692
+ for (let i = 0; i < indexedIndices.length; i++) {
1693
+ reorderedArgs[indexedIndices[i]!] = argsArray[currentIndex++];
1694
+ }
1695
+
1696
+ // Place the non-indexed arguments in their original positions
1697
+ for (let i = 0; i < nonIndexedIndices.length; i++) {
1698
+ reorderedArgs[nonIndexedIndices[i]!] = argsArray[currentIndex++];
1699
+ }
1700
+
1701
+ return {
1702
+ ...log,
1703
+ eventName: decodedLog.eventName,
1704
+ args: reorderedArgs,
1705
+ } as EventLog;
1706
+ }