@boostxyz/sdk 5.2.1 → 5.3.1

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 +84 -41
  7. package/dist/Actions/EventAction.d.ts.map +1 -1
  8. package/dist/Actions/EventAction.js +1866 -378
  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 +294 -3
  87. package/src/Actions/EventAction.ts +417 -154
  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
@@ -6,11 +6,13 @@ import {
6
6
  writeEventActionExecute,
7
7
  } from '@boostxyz/evm';
8
8
  import { bytecode } from '@boostxyz/evm/artifacts/contracts/actions/EventAction.sol/EventAction.json';
9
+ import { abi } from '@boostxyz/signatures/events';
9
10
  import { getTransaction, getTransactionReceipt } from '@wagmi/core';
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
  *
@@ -569,16 +585,15 @@ export class EventAction extends DeployableTarget<
569
585
  ) {
570
586
  return undefined;
571
587
  }
572
- const decodedLogs = receipt.logs
573
- .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
- });
588
+
589
+ let decodedLogs: EventLogs;
590
+ if (signature === TRANSFER_SIGNATURE) {
591
+ ({ decodedLogs } = await this.decodeTransferLogs(receipt));
592
+ } else {
593
+ decodedLogs = receipt.logs
594
+ .filter((log) => log.topics[0] === toEventSelector(event))
595
+ .map((log) => decodeAndReorderLogArgs(event, log));
596
+ }
582
597
 
583
598
  for (let log of decodedLogs) {
584
599
  if (!isAddressEqual(log.address, claimant.targetContract)) continue;
@@ -703,7 +718,7 @@ export class EventAction extends DeployableTarget<
703
718
 
704
719
  // Use the provided logs, no need to fetch receipt
705
720
  if ('logs' in params) {
706
- return this.isActionEventValid(actionStep, params.logs);
721
+ return this.isActionEventValid(actionStep, params.logs, event);
707
722
  }
708
723
 
709
724
  const receipt = await getTransactionReceipt(this._config, {
@@ -719,22 +734,15 @@ export class EventAction extends DeployableTarget<
719
734
 
720
735
  // Special handling for Transfer events
721
736
  if (actionStep.signature === TRANSFER_SIGNATURE) {
722
- return this.decodeTransferLogs(receipt, actionStep);
737
+ const { decodedLogs, event } = await this.decodeTransferLogs(receipt);
738
+ return this.isActionEventValid(actionStep, decodedLogs, event);
723
739
  }
724
740
 
725
741
  const decodedLogs = receipt.logs
726
742
  .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
- });
733
-
734
- return { ...log, eventName, args };
735
- });
743
+ .map((log) => decodeAndReorderLogArgs(event, log));
736
744
 
737
- return this.isActionEventValid(actionStep, decodedLogs);
745
+ return this.isActionEventValid(actionStep, decodedLogs, event);
738
746
  }
739
747
  if (actionStep.signatureType === SignatureType.FUNC) {
740
748
  if ('hash' in params) {
@@ -756,27 +764,51 @@ export class EventAction extends DeployableTarget<
756
764
 
757
765
  /**
758
766
  * 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
767
  *
761
768
  * @public
762
- * @async
763
769
  * @param {ActionStep} actionStep - The action step containing the event to validate.
764
770
  * @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.
771
+ * @param {AbiEvent} eventAbi - The ABI definition of the event
772
+ * @returns {boolean} Resolves to true if the action event is valid, throws if input is invalid, otherwise false.
766
773
  */
767
- public isActionEventValid(actionStep: ActionStep, logs: EventLogs) {
774
+ public isActionEventValid(
775
+ actionStep: ActionStep,
776
+ logs: EventLogs,
777
+ eventAbi: AbiEvent,
778
+ ): boolean {
768
779
  const criteria = actionStep.actionParameter;
769
780
  if (!logs.length) return false;
781
+
782
+ // Check each log
770
783
  for (let log of logs) {
771
- if (this.validateLogAgainstCriteria(criteria, log)) {
772
- return true;
784
+ // parse out final (scalar) field from the log args
785
+ try {
786
+ if (!Array.isArray(log.args)) {
787
+ throw new DecodedArgsMalformedError({
788
+ log,
789
+ criteria,
790
+ fieldValue: undefined,
791
+ });
792
+ }
793
+ const { value, type } = this.parseFieldFromAbi(
794
+ log.args,
795
+ criteria.fieldIndex,
796
+ eventAbi.inputs || [],
797
+ criteria.fieldType,
798
+ );
799
+ criteria.fieldType = type;
800
+ if (this.validateFieldAgainstCriteria(criteria, value, { log })) {
801
+ return true;
802
+ }
803
+ } catch {
804
+ // If there's an error on this log, keep trying with the next one
773
805
  }
774
806
  }
775
807
  return false;
776
808
  }
777
809
 
778
810
  /**
779
- * Decodes transfer logs specifically for ERC721 and ERC20 Transfer events.
811
+ * Decodes logs specifically for ERC721 and ERC20 Transfer events.
780
812
  *
781
813
  * This special handling is required because both ERC20 and ERC721 Transfer events:
782
814
  * 1. Share the same event signature (0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef)
@@ -788,69 +820,127 @@ export class EventAction extends DeployableTarget<
788
820
  * try decoding both ways to determine which type of Transfer event we're dealing with.
789
821
  *
790
822
  * @param {GetTransactionReceiptReturnType} receipt - The transaction receipt containing the logs
791
- * @param {ActionStep} actionStep - The action step being validated
792
- * @returns {Promise<boolean>} - Returns true if the transfer logs are valid for either ERC20 or ERC721
823
+ * @returns {Promise<{ decodedLogs: EventLogs; event: AbiEvent }>} - Returns the decoded logs and the transfer event ABI used for decoding
793
824
  * @throws {DecodedArgsError} - Throws if neither ERC20 nor ERC721 decoding succeeds
794
825
  */
795
826
  private async decodeTransferLogs(
796
827
  receipt: GetTransactionReceiptReturnType,
797
- actionStep: ActionStep,
798
- ) {
828
+ ): Promise<{ decodedLogs: EventLogs; event: AbiEvent }> {
799
829
  const filteredLogs = receipt.logs.filter(
800
830
  (log) => log.topics[0] === TRANSFER_SIGNATURE,
801
831
  );
832
+ const event = abi[
833
+ 'Transfer(address indexed,address indexed,uint256 indexed)'
834
+ ] as AbiEvent;
802
835
 
803
836
  // ERC721
804
837
  try {
805
838
  const decodedLogs = filteredLogs.map((log) => {
806
839
  const { eventName, args } = decodeEventLog({
807
- abi: [
808
- {
809
- name: 'Transfer',
810
- type: 'event',
811
- inputs: [
812
- { type: 'address', indexed: true },
813
- { type: 'address', indexed: true },
814
- { type: 'uint256', indexed: true },
815
- ],
816
- },
817
- ],
840
+ abi: [event],
818
841
  data: log.data,
819
842
  topics: log.topics,
820
843
  });
821
844
  return { ...log, eventName, args };
822
845
  });
823
846
 
824
- return this.isActionEventValid(actionStep, decodedLogs);
847
+ return {
848
+ decodedLogs,
849
+ event,
850
+ };
825
851
  } catch {
826
852
  // ERC20
827
853
  try {
854
+ event.inputs[2]!.indexed = false;
828
855
  const decodedLogs = filteredLogs.map((log) => {
829
856
  const { eventName, args } = decodeEventLog({
830
- abi: [
831
- {
832
- name: 'Transfer',
833
- type: 'event',
834
- inputs: [
835
- { type: 'address', indexed: true },
836
- { type: 'address', indexed: true },
837
- { type: 'uint256' },
838
- ],
839
- },
840
- ],
857
+ abi: [event],
841
858
  data: log.data,
842
859
  topics: log.topics,
843
860
  });
844
861
  return { ...log, eventName, args };
845
862
  });
846
863
 
847
- return this.isActionEventValid(actionStep, decodedLogs);
864
+ return {
865
+ decodedLogs,
866
+ event,
867
+ };
848
868
  } catch {
849
869
  throw new DecodedArgsError('Failed to decode transfer logs');
850
870
  }
851
871
  }
852
872
  }
853
873
 
874
+ /**
875
+ * Parses the final (scalar) field from a set of decoded arguments, given an ABI definition.
876
+ * If the fieldType is TUPLE, we decode `fieldIndex` as a bitpacked array of indexes to drill down
877
+ * into nested tuples. Otherwise, we parse the single `fieldIndex` as normal.
878
+ *
879
+ * @public
880
+ * @param {readonly unknown[]} allArgs - The decoded arguments array from an event log or function call.
881
+ * @param {number} criteriaIndex - The field index (bitpacked if TUPLE).
882
+ * @param {AbiParameter[]} abiInputs - The ABI inputs describing each decoded argument.
883
+ * @param {PrimitiveType} declaredType - Either TUPLE or a standard scalar type
884
+ * @returns {{ value: string | bigint | Hex; type: Exclude<PrimitiveType, PrimitiveType.TUPLE> }}
885
+ */
886
+ public parseFieldFromAbi(
887
+ allArgs: readonly unknown[],
888
+ criteriaIndex: number,
889
+ abiInputs: readonly AbiParameter[],
890
+ declaredType: PrimitiveType,
891
+ ): {
892
+ value: string | bigint | Hex;
893
+ type: Exclude<PrimitiveType, PrimitiveType.TUPLE>;
894
+ } {
895
+ // If ANY_ACTION_PARAM, return a dummy "any" value so we can do special-case checks
896
+ if (criteriaIndex === CheatCodes.ANY_ACTION_PARAM) {
897
+ return { value: zeroHash, type: PrimitiveType.BYTES };
898
+ }
899
+
900
+ // If it's not TUPLE, parse as a single index (existing logic)
901
+ if (declaredType !== PrimitiveType.TUPLE) {
902
+ if (!Array.isArray(allArgs) || criteriaIndex >= allArgs.length) {
903
+ throw new FieldValueUndefinedError({
904
+ fieldValue: allArgs,
905
+ criteria: {
906
+ filterType: FilterType.EQUAL,
907
+ fieldType: declaredType,
908
+ fieldIndex: criteriaIndex,
909
+ filterData: zeroHash,
910
+ },
911
+ });
912
+ }
913
+ const abiParam = abiInputs[criteriaIndex];
914
+ if (!abiParam || !abiParam.type) {
915
+ throw new UnparseableAbiParamError(criteriaIndex, abiParam as AbiEvent);
916
+ }
917
+ const rawValue = allArgs[criteriaIndex];
918
+
919
+ const finalType = abiTypeToPrimitiveType(abiParam.type);
920
+
921
+ if (
922
+ finalType === PrimitiveType.ADDRESS &&
923
+ (typeof rawValue !== 'string' || !isAddress(rawValue))
924
+ ) {
925
+ throw new FieldValueUndefinedError({
926
+ fieldValue: rawValue,
927
+ criteria: {
928
+ fieldIndex: criteriaIndex,
929
+ filterType: FilterType.EQUAL,
930
+ fieldType: finalType,
931
+ filterData: zeroHash,
932
+ },
933
+ });
934
+ }
935
+
936
+ return { value: rawValue as string | bigint | Hex, type: finalType };
937
+ }
938
+
939
+ // Otherwise, declaredType === TUPLE => decode bitpacked indexes
940
+ const indexes = unpackFieldIndexes(criteriaIndex);
941
+ return parseNestedTupleValue(allArgs as unknown[], indexes, abiInputs);
942
+ }
943
+
854
944
  /**
855
945
  * Validates a single action function with a given criteria against the transaction input.
856
946
  *
@@ -870,10 +960,10 @@ export class EventAction extends DeployableTarget<
870
960
  params: Pick<ValidateActionStepParams, 'abiItem' | 'knownSignatures'>,
871
961
  ) {
872
962
  const criteria = actionStep.actionParameter;
873
- let signature = actionStep.signature;
963
+ const signature = actionStep.signature;
874
964
 
875
965
  let func: AbiFunction;
876
- if (params.abiItem) func = params?.abiItem as AbiFunction;
966
+ if (params.abiItem) func = params.abiItem as AbiFunction;
877
967
  else {
878
968
  const sigPool = params.knownSignatures as Record<Hex, AbiFunction>;
879
969
  func = sigPool[signature] as AbiFunction;
@@ -892,31 +982,37 @@ export class EventAction extends DeployableTarget<
892
982
  throw new FunctionDataDecodeError([func], e as Error);
893
983
  }
894
984
 
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
- ) {
985
+ if (!decodedData?.args) {
906
986
  return false;
907
987
  }
908
988
 
909
- return true;
989
+ try {
990
+ const { value, type } = this.parseFieldFromAbi(
991
+ decodedData.args as unknown[],
992
+ criteria.fieldIndex,
993
+ func.inputs || [],
994
+ criteria.fieldType,
995
+ );
996
+ criteria.fieldType = type;
997
+ return this.validateFieldAgainstCriteria(criteria, value, {
998
+ decodedArgs: decodedData.args as readonly (string | bigint)[],
999
+ });
1000
+ } catch {
1001
+ return false;
1002
+ }
910
1003
  }
1004
+
911
1005
  /**
912
- * Validates a field against a given criteria.
1006
+ * Validates a field against a given criteria. The field is assumed to be a non-tuple scalar,
1007
+ * along with its final resolved `PrimitiveType`. (Any TUPLE logic has been extracted elsewhere.)
913
1008
  *
914
1009
  * @param {Criteria} criteria - The criteria to validate against.
915
1010
  * @param {string | bigint | Hex} fieldValue - The field value to validate.
1011
+ * @param {Exclude<PrimitiveType, PrimitiveType.TUPLE>} fieldType - The final resolved primitive type.
916
1012
  * @param {Object} input - Additional context for validation.
917
1013
  * @param {EventLogs[0]} [input.log] - The event log, if validating an event.
918
1014
  * @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.
1015
+ * @returns {boolean} - Returns true if the field passes the criteria, false otherwise.
920
1016
  */
921
1017
  public validateFieldAgainstCriteria(
922
1018
  criteria: Criteria,
@@ -925,6 +1021,10 @@ export class EventAction extends DeployableTarget<
925
1021
  | { log: EventLogs[0] }
926
1022
  | { decodedArgs: readonly (string | bigint)[] },
927
1023
  ): boolean {
1024
+ /*
1025
+ * Special-case: ANY_ACTION_PARAM. If we have filterType=EQUAL, fieldType=BYTES, fieldIndex=255,
1026
+ * we consider that a wildcard match. Return true immediately.
1027
+ */
928
1028
  if (
929
1029
  criteria.filterType === FilterType.EQUAL &&
930
1030
  criteria.fieldType === PrimitiveType.BYTES &&
@@ -932,11 +1032,17 @@ export class EventAction extends DeployableTarget<
932
1032
  ) {
933
1033
  return true;
934
1034
  }
1035
+ if (criteria.fieldType === PrimitiveType.TUPLE) {
1036
+ throw new InvalidTupleDecodingError(
1037
+ 'Tuples should not be passed into validateFieldAgainstCriteria',
1038
+ );
1039
+ }
1040
+ const fieldType = criteria.fieldType;
935
1041
 
936
- // Type narrow based on criteria.filterType
1042
+ // Evaluate filter based on the final fieldType
937
1043
  switch (criteria.filterType) {
938
1044
  case FilterType.EQUAL:
939
- return match(criteria.fieldType)
1045
+ return match(fieldType)
940
1046
  .with(PrimitiveType.ADDRESS, () =>
941
1047
  isAddressEqual(criteria.filterData, fieldValue as Address),
942
1048
  )
@@ -951,7 +1057,7 @@ export class EventAction extends DeployableTarget<
951
1057
  .otherwise(() => fieldValue === criteria.filterData);
952
1058
 
953
1059
  case FilterType.NOT_EQUAL:
954
- return match(criteria.fieldType)
1060
+ return match(fieldType)
955
1061
  .with(
956
1062
  PrimitiveType.ADDRESS,
957
1063
  () => !isAddressEqual(criteria.filterData, fieldValue as Address),
@@ -967,7 +1073,7 @@ export class EventAction extends DeployableTarget<
967
1073
  .otherwise(() => fieldValue !== criteria.filterData);
968
1074
 
969
1075
  case FilterType.GREATER_THAN:
970
- if (criteria.fieldType === PrimitiveType.UINT) {
1076
+ if (fieldType === PrimitiveType.UINT) {
971
1077
  return BigInt(fieldValue) > BigInt(criteria.filterData);
972
1078
  }
973
1079
  throw new InvalidNumericalCriteriaError({
@@ -975,8 +1081,9 @@ export class EventAction extends DeployableTarget<
975
1081
  criteria,
976
1082
  fieldValue,
977
1083
  });
1084
+
978
1085
  case FilterType.GREATER_THAN_OR_EQUAL:
979
- if (criteria.fieldType === PrimitiveType.UINT) {
1086
+ if (fieldType === PrimitiveType.UINT) {
980
1087
  return BigInt(fieldValue) >= BigInt(criteria.filterData);
981
1088
  }
982
1089
  throw new InvalidNumericalCriteriaError({
@@ -986,7 +1093,7 @@ export class EventAction extends DeployableTarget<
986
1093
  });
987
1094
 
988
1095
  case FilterType.LESS_THAN:
989
- if (criteria.fieldType === PrimitiveType.UINT) {
1096
+ if (fieldType === PrimitiveType.UINT) {
990
1097
  return BigInt(fieldValue) < BigInt(criteria.filterData);
991
1098
  }
992
1099
  throw new InvalidNumericalCriteriaError({
@@ -994,8 +1101,9 @@ export class EventAction extends DeployableTarget<
994
1101
  criteria,
995
1102
  fieldValue,
996
1103
  });
1104
+
997
1105
  case FilterType.LESS_THAN_OR_EQUAL:
998
- if (criteria.fieldType === PrimitiveType.UINT) {
1106
+ if (fieldType === PrimitiveType.UINT) {
999
1107
  return BigInt(fieldValue) <= BigInt(criteria.filterData);
1000
1108
  }
1001
1109
  throw new InvalidNumericalCriteriaError({
@@ -1006,11 +1114,11 @@ export class EventAction extends DeployableTarget<
1006
1114
 
1007
1115
  case FilterType.CONTAINS:
1008
1116
  if (
1009
- criteria.fieldType === PrimitiveType.BYTES ||
1010
- criteria.fieldType === PrimitiveType.STRING
1117
+ fieldType === PrimitiveType.BYTES ||
1118
+ fieldType === PrimitiveType.STRING
1011
1119
  ) {
1012
1120
  let substring;
1013
- if (criteria.fieldType === PrimitiveType.STRING) {
1121
+ if (fieldType === PrimitiveType.STRING) {
1014
1122
  substring = fromHex(criteria.filterData, 'string');
1015
1123
  } else {
1016
1124
  // truncate the `0x` prefix
@@ -1032,12 +1140,11 @@ export class EventAction extends DeployableTarget<
1032
1140
  fieldValue,
1033
1141
  });
1034
1142
  }
1035
-
1036
- if (criteria.fieldType === PrimitiveType.STRING) {
1037
- // fieldValue is decoded by the ABI
1143
+ if (fieldType === PrimitiveType.STRING) {
1038
1144
  const regexString = fromHex(criteria.filterData, 'string');
1039
1145
  return new RegExp(regexString).test(fieldValue);
1040
1146
  }
1147
+ // Otherwise unrecognized or not applicable
1041
1148
 
1042
1149
  default:
1043
1150
  throw new UnrecognizedFilterTypeError({
@@ -1048,68 +1155,6 @@ export class EventAction extends DeployableTarget<
1048
1155
  }
1049
1156
  }
1050
1157
 
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
1158
  /**
1114
1159
  * @inheritdoc
1115
1160
  *
@@ -1159,6 +1204,15 @@ export class EventAction extends DeployableTarget<
1159
1204
  };
1160
1205
  }
1161
1206
 
1207
+ /**
1208
+ * Determines whether a string or bytes field is indexed in the event definition.
1209
+ * If the user tries to filter on an indexed string/bytes, we throw an error.
1210
+ *
1211
+ * @public
1212
+ * @param {ActionStep} step
1213
+ * @param {AbiEvent} event
1214
+ * @returns {boolean}
1215
+ */
1162
1216
  public isArraylikeIndexed(step: ActionStep, event: AbiEvent) {
1163
1217
  if (
1164
1218
  (step.actionParameter.fieldType === PrimitiveType.STRING ||
@@ -1171,9 +1225,114 @@ export class EventAction extends DeployableTarget<
1171
1225
  }
1172
1226
  }
1173
1227
 
1228
+ /**
1229
+ * Checks if a particular ABI parameter is the "tuple" variant that can have `components`.
1230
+ *
1231
+ * @param {AbiParameter} param
1232
+ * @returns {boolean}
1233
+ */
1234
+ function isTupleAbiParameter(
1235
+ param: AbiParameter,
1236
+ ): param is Extract<AbiParameter, { components: readonly AbiParameter[] }> {
1237
+ return param.type === 'tuple' || param.type.startsWith('tuple[');
1238
+ }
1239
+
1240
+ /**
1241
+ * Recursively parses nested tuples by following an array of sub-indexes (unpacked from bitpacked `fieldIndex`).
1242
+ * Each entry in `indexes` is used to pick which sub-component in the current tuple's `components`.
1243
+ * If we encounter the "terminator" or run out of indexes, we stop.
1244
+ *
1245
+ * @param {unknown[]} rawArgs - The top-level arguments array.
1246
+ * @param {number[]} indexes - The array of indexes from `unpackFieldIndexes(...)`.
1247
+ * @param {readonly AbiParameter[]} abiInputs - The top-level ABI inputs for the entire arguments array.
1248
+ * @returns {{ value: string | bigint | Hex, type: Exclude<PrimitiveType, PrimitiveType.TUPLE> }}
1249
+ */
1250
+ function parseNestedTupleValue(
1251
+ rawArgs: unknown[],
1252
+ indexes: number[],
1253
+ abiInputs: readonly AbiParameter[],
1254
+ ): {
1255
+ value: string | bigint | Hex;
1256
+ type: Exclude<PrimitiveType, PrimitiveType.TUPLE>;
1257
+ } {
1258
+ if (!indexes.length) {
1259
+ throw new InvalidTupleDecodingError(
1260
+ `No indexes found; cannot parse TUPLE field`,
1261
+ );
1262
+ }
1263
+
1264
+ // The first index picks which top-level ABI param to look at
1265
+ const idx = indexes[0] ?? abiInputs.length + 1;
1266
+ // If idx is out of range or is a "terminator," fail fast
1267
+ if (idx >= abiInputs.length) {
1268
+ throw new InvalidTupleDecodingError(undefined, idx);
1269
+ }
1270
+
1271
+ const param = abiInputs[idx];
1272
+ const rawValue = rawArgs[idx];
1273
+
1274
+ // If param isn't a tuple, we are at a leaf
1275
+ if (!isTupleAbiParameter(param!)) {
1276
+ const finalType = abiTypeToPrimitiveType(param!.type);
1277
+ return { value: rawValue as string | bigint | Hex, type: finalType };
1278
+ }
1279
+
1280
+ // Otherwise param is a tuple => rawValue must be an array of subfields
1281
+ if (!Array.isArray(rawValue)) {
1282
+ throw new InvalidTupleDecodingError(
1283
+ `rawValue is not an array, but param.type is tuple`,
1284
+ );
1285
+ }
1286
+
1287
+ // Move to the next sub-index
1288
+ const remaining = indexes.slice(1);
1289
+ if (!remaining.length) {
1290
+ // If there are no more indexes, we can't pick a sub-component
1291
+ // Typically you'd want at least one more index to say "which subfield in the tuple we want"
1292
+ throw new InvalidTupleDecodingError(undefined, -1);
1293
+ }
1294
+
1295
+ // Check the next index for param.components
1296
+ const subIdx = remaining[0] ?? param.components.length + 1;
1297
+ if (subIdx >= param.components.length) {
1298
+ throw new InvalidTupleDecodingError(undefined, subIdx);
1299
+ }
1300
+
1301
+ // Recurse deeper using param.components as the "new top-level" ABI param list
1302
+ return parseNestedTupleValue(rawValue, remaining, param.components);
1303
+ }
1304
+
1305
+ /**
1306
+ * Maps an ABI type string (e.g. 'uint256', 'address', 'bytes', 'string', etc.) to a `PrimitiveType`.
1307
+ *
1308
+ * @param {string} abiType
1309
+ * @returns {Exclude<PrimitiveType, PrimitiveType.TUPLE>}
1310
+ */
1311
+ function abiTypeToPrimitiveType(
1312
+ abiType: string,
1313
+ ): Exclude<PrimitiveType, PrimitiveType.TUPLE> {
1314
+ const lower = abiType.toLowerCase();
1315
+
1316
+ if (lower.startsWith('uint') || lower.startsWith('int')) {
1317
+ return PrimitiveType.UINT;
1318
+ }
1319
+ if (lower === 'address') {
1320
+ return PrimitiveType.ADDRESS;
1321
+ }
1322
+ if (lower === 'bytes' || lower.startsWith('bytes')) {
1323
+ return PrimitiveType.BYTES;
1324
+ }
1325
+ if (lower === 'string') {
1326
+ return PrimitiveType.STRING;
1327
+ }
1328
+
1329
+ // If it doesn't match any known scalar, throw. We expect parseNestedTupleValue() to handle nested tuple logic separately.
1330
+ throw new DecodedArgsError(`Unrecognized ABI type: ${abiType}`);
1331
+ }
1332
+
1174
1333
  function _dedupeActionSteps(_steps: ActionStep[]): ActionStep[] {
1175
- const steps: ActionStep[] = [],
1176
- signatures: Record<string, boolean> = {};
1334
+ const steps: ActionStep[] = [];
1335
+ const signatures: Record<string, boolean> = {};
1177
1336
  for (let step of _steps) {
1178
1337
  const signature = JSON.stringify(step);
1179
1338
  if (signatures[signature]) continue;
@@ -1182,6 +1341,7 @@ function _dedupeActionSteps(_steps: ActionStep[]): ActionStep[] {
1182
1341
  }
1183
1342
  return steps;
1184
1343
  }
1344
+
1185
1345
  type RawActionStep = Overwrite<ActionStep, { chainid: bigint }>;
1186
1346
  type RawActionClaimant = Overwrite<ActionClaimant, { chainid: bigint }>;
1187
1347
 
@@ -1268,7 +1428,7 @@ export function prepareEventActionPayload({
1268
1428
  components: [
1269
1429
  { type: 'uint8', name: 'filterType' },
1270
1430
  { type: 'uint8', name: 'fieldType' },
1271
- { type: 'uint8', name: 'fieldIndex' },
1431
+ { type: 'uint32', name: 'fieldIndex' },
1272
1432
  { type: 'bytes', name: 'filterData' },
1273
1433
  ],
1274
1434
  },
@@ -1289,7 +1449,7 @@ export function prepareEventActionPayload({
1289
1449
  components: [
1290
1450
  { type: 'uint8', name: 'filterType' },
1291
1451
  { type: 'uint8', name: 'fieldType' },
1292
- { type: 'uint8', name: 'fieldIndex' },
1452
+ { type: 'uint32', name: 'fieldIndex' },
1293
1453
  { type: 'bytes', name: 'filterData' },
1294
1454
  ],
1295
1455
  },
@@ -1310,7 +1470,7 @@ export function prepareEventActionPayload({
1310
1470
  components: [
1311
1471
  { type: 'uint8', name: 'filterType' },
1312
1472
  { type: 'uint8', name: 'fieldType' },
1313
- { type: 'uint8', name: 'fieldIndex' },
1473
+ { type: 'uint32', name: 'fieldIndex' },
1314
1474
  { type: 'bytes', name: 'filterData' },
1315
1475
  ],
1316
1476
  },
@@ -1331,7 +1491,7 @@ export function prepareEventActionPayload({
1331
1491
  components: [
1332
1492
  { type: 'uint8', name: 'filterType' },
1333
1493
  { type: 'uint8', name: 'fieldType' },
1334
- { type: 'uint8', name: 'fieldIndex' },
1494
+ { type: 'uint32', name: 'fieldIndex' },
1335
1495
  { type: 'bytes', name: 'filterData' },
1336
1496
  ],
1337
1497
  },
@@ -1426,3 +1586,106 @@ export function transactionSenderClaimant(chainId: number): ActionClaimant {
1426
1586
  chainid: chainId,
1427
1587
  };
1428
1588
  }
1589
+
1590
+ // Helper functions to bit-pack and decode fieldIndex values
1591
+ const MAX_FIELD_INDEX = 0b111111; // Maximum value for 6 bits (63)
1592
+
1593
+ /**
1594
+ * Packs up to five indexes into a single uint32 value.
1595
+ *
1596
+ * @param {number[]} indexes - Array of up to five indexes to pack.
1597
+ * @returns {number} - Packed uint32 value.
1598
+ * @throws {Error} - If more than five indexes are provided or an index exceeds the maximum value.
1599
+ */
1600
+ export function packFieldIndexes(indexes: number[]): number {
1601
+ if (indexes.length > 5) {
1602
+ throw new InvalidTupleEncodingError('Can only pack up to 5 indexes.');
1603
+ }
1604
+
1605
+ let packed = 0;
1606
+ indexes.forEach((index, i) => {
1607
+ if (index > MAX_FIELD_INDEX) {
1608
+ throw new InvalidTupleEncodingError(
1609
+ `Index ${index} exceeds the maximum allowed value (${MAX_FIELD_INDEX}).`,
1610
+ );
1611
+ }
1612
+ packed |= (index & MAX_FIELD_INDEX) << (i * 6); // Each index occupies 6 bits
1613
+ });
1614
+ if (indexes.length < 5) {
1615
+ packed |= MAX_FIELD_INDEX << (indexes.length * 6); // Terminator
1616
+ }
1617
+
1618
+ return packed;
1619
+ }
1620
+
1621
+ /**
1622
+ * Unpacks a uint32 fieldIndex value into an array of up to five indexes.
1623
+ *
1624
+ * @param {number} packed - Packed uint32 value.
1625
+ * @returns {number[]} - Array of unpacked indexes.
1626
+ */
1627
+ export function unpackFieldIndexes(packed: number): number[] {
1628
+ const indexes: number[] = [];
1629
+ for (let i = 0; i < 5; i++) {
1630
+ const index = (packed >> (i * 6)) & MAX_FIELD_INDEX;
1631
+ if (index === MAX_FIELD_INDEX) break; // Terminator value
1632
+ indexes.push(index);
1633
+ }
1634
+ return indexes;
1635
+ }
1636
+
1637
+ /**
1638
+ * Decodes an event log and reorders the arguments to match the original ABI order.
1639
+ * This is necessary because viem's decodeEventLog reorders indexed parameters to the front.
1640
+ *
1641
+ * @param event - The event ABI definition
1642
+ * @param log - The log to decode
1643
+ * @returns {EventLog} The decoded log with arguments in the original ABI order
1644
+ */
1645
+ export function decodeAndReorderLogArgs(event: AbiEvent, log: Log) {
1646
+ const decodedLog = decodeEventLog({
1647
+ abi: [event],
1648
+ data: log.data,
1649
+ topics: log.topics,
1650
+ });
1651
+
1652
+ const argsArray = Array.isArray(decodedLog.args)
1653
+ ? decodedLog.args
1654
+ : Object.values(decodedLog.args);
1655
+
1656
+ if (!event.inputs.some((input) => input.indexed)) {
1657
+ return {
1658
+ ...log,
1659
+ ...decodedLog,
1660
+ } as EventLog;
1661
+ }
1662
+
1663
+ const indexedIndices: number[] = [];
1664
+ const nonIndexedIndices: number[] = [];
1665
+ for (let i = 0; i < event.inputs.length; i++) {
1666
+ if (event.inputs[i]!.indexed) {
1667
+ indexedIndices.push(i);
1668
+ } else {
1669
+ nonIndexedIndices.push(i);
1670
+ }
1671
+ }
1672
+
1673
+ const reorderedArgs = Array.from({ length: event.inputs.length });
1674
+ let currentIndex = 0;
1675
+
1676
+ // Place the indexed arguments in their original positions
1677
+ for (let i = 0; i < indexedIndices.length; i++) {
1678
+ reorderedArgs[indexedIndices[i]!] = argsArray[currentIndex++];
1679
+ }
1680
+
1681
+ // Place the non-indexed arguments in their original positions
1682
+ for (let i = 0; i < nonIndexedIndices.length; i++) {
1683
+ reorderedArgs[nonIndexedIndices[i]!] = argsArray[currentIndex++];
1684
+ }
1685
+
1686
+ return {
1687
+ ...log,
1688
+ eventName: decodedLog.eventName,
1689
+ args: reorderedArgs,
1690
+ } as EventLog;
1691
+ }