@boostxyz/sdk 5.2.0 → 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 (105) 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-BwRH0A6j.js → Incentive-qlnv5kQB.js} +203 -124
  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/ERC20PeggedVariableCriteriaIncentive.d.ts +16 -4
  51. package/dist/Incentives/ERC20PeggedVariableCriteriaIncentive.d.ts.map +1 -1
  52. package/dist/Incentives/ERC20VariableCriteriaIncentive.cjs +1 -1
  53. package/dist/Incentives/ERC20VariableCriteriaIncentive.js +2 -2
  54. package/dist/Incentives/ERC20VariableIncentive.cjs +1 -1
  55. package/dist/Incentives/ERC20VariableIncentive.js +2 -2
  56. package/dist/Incentives/Incentive.cjs +1 -1
  57. package/dist/Incentives/Incentive.js +2 -2
  58. package/dist/Incentives/PointsIncentive.cjs +1 -1
  59. package/dist/Incentives/PointsIncentive.js +2 -2
  60. package/dist/{SimpleDenyList-B8QeJthf.js → SimpleDenyList-ByAr4X1r.js} +3 -3
  61. package/dist/{SimpleDenyList-B8QeJthf.js.map → SimpleDenyList-ByAr4X1r.js.map} +1 -1
  62. package/dist/{SimpleDenyList-DIb4xX3j.cjs → SimpleDenyList-CsRXJPwm.cjs} +2 -2
  63. package/dist/{SimpleDenyList-DIb4xX3j.cjs.map → SimpleDenyList-CsRXJPwm.cjs.map} +1 -1
  64. package/dist/Validators/LimitedSignerValidator.cjs +1 -1
  65. package/dist/Validators/LimitedSignerValidator.js +2 -2
  66. package/dist/Validators/SignerValidator.cjs +1 -1
  67. package/dist/Validators/SignerValidator.js +2 -2
  68. package/dist/Validators/Validator.cjs +1 -1
  69. package/dist/Validators/Validator.js +1 -1
  70. package/dist/{deployments-COxshLqt.js → deployments-D0fs26TV.js} +16 -16
  71. package/dist/{deployments-COxshLqt.js.map → deployments-D0fs26TV.js.map} +1 -1
  72. package/dist/{deployments-BGpr4ppG.cjs → deployments-DoIOqxco.cjs} +2 -2
  73. package/dist/deployments-DoIOqxco.cjs.map +1 -0
  74. package/dist/deployments.json +3 -3
  75. package/dist/errors.cjs +1 -1
  76. package/dist/errors.cjs.map +1 -1
  77. package/dist/errors.d.ts +40 -0
  78. package/dist/errors.d.ts.map +1 -1
  79. package/dist/errors.js +42 -16
  80. package/dist/errors.js.map +1 -1
  81. package/dist/{generated-ClbO_ULI.js → generated-Cyvr_Tjx.js} +446 -438
  82. package/dist/generated-Cyvr_Tjx.js.map +1 -0
  83. package/dist/{generated-CRD9XfOT.cjs → generated-DtYPHhtX.cjs} +2 -2
  84. package/dist/generated-DtYPHhtX.cjs.map +1 -0
  85. package/dist/index.cjs +1 -1
  86. package/dist/index.js +160 -155
  87. package/dist/utils.cjs +1 -1
  88. package/dist/utils.cjs.map +1 -1
  89. package/dist/utils.js +367 -19
  90. package/dist/utils.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/Actions/EventAction.test.ts +285 -3
  93. package/src/Actions/EventAction.ts +402 -124
  94. package/src/BoostCore.test.ts +51 -3
  95. package/src/BoostCore.ts +73 -0
  96. package/src/Incentives/ERC20PeggedIncentive.ts +33 -4
  97. package/src/Incentives/ERC20PeggedVariableCriteriaIncentive.test.ts +67 -25
  98. package/src/Incentives/ERC20PeggedVariableCriteriaIncentive.ts +89 -7
  99. package/src/errors.ts +50 -0
  100. package/dist/Incentive-BwRH0A6j.js.map +0 -1
  101. package/dist/Incentive-CZw_hu64.cjs +0 -2
  102. package/dist/Incentive-CZw_hu64.cjs.map +0 -1
  103. package/dist/deployments-BGpr4ppG.cjs.map +0 -1
  104. package/dist/generated-CRD9XfOT.cjs.map +0 -1
  105. package/dist/generated-ClbO_ULI.js.map +0 -1
@@ -17,6 +17,8 @@ import { BoostNotFoundError, IncentiveNotCloneableError } from "./errors";
17
17
  import type { ERC20Incentive } from "./Incentives/ERC20Incentive";
18
18
  import { bytes4 } from "./utils";
19
19
  import { BoostValidatorEOA } from "./Validators/Validator";
20
+ import { AssetType } from "./transfers";
21
+ import { waitForTransactionReceipt } from "@wagmi/core";
20
22
 
21
23
  let fixtures: Fixtures, budgets: BudgetFixtures;
22
24
 
@@ -32,7 +34,7 @@ describe("BoostCore", () => {
32
34
  const { core } = fixtures;
33
35
 
34
36
  const { budget, erc20 } = budgets;
35
- await core.createBoost({
37
+ const payload = {
36
38
  protocolFee: 0n,
37
39
  maxParticipants: 5n,
38
40
  budget: budget,
@@ -59,8 +61,11 @@ describe("BoostCore", () => {
59
61
  manager: budget.assertValidAddress(),
60
62
  }),
61
63
  ],
62
- });
63
- expect(await core.getBoostCount()).toBe(1n);
64
+ }
65
+ const { hash } = await core.createBoostRaw(payload)
66
+ await waitForTransactionReceipt(defaultOptions.config, { hash })
67
+ await core.createBoost(payload);
68
+ expect(await core.getBoostCount()).toBe(2n);
64
69
  });
65
70
 
66
71
  test("throws a typed error if no boost exists", async () => {
@@ -904,4 +909,47 @@ describe("BoostCore", () => {
904
909
  )
905
910
  expect(totalFee).toBe(100000000000000000n)
906
911
  });
912
+
913
+ test("can get incentive fees information", async () => {
914
+ const { core } = fixtures;
915
+ const { budget, erc20 } = budgets;
916
+
917
+ // Create a new boost
918
+ const boost = await core.createBoost({
919
+ protocolFee: 0n,
920
+ maxParticipants: 10n,
921
+ budget: budget,
922
+ action: core.EventAction(
923
+ makeMockEventActionPayload(
924
+ core.assertValidAddress(),
925
+ erc20.assertValidAddress(),
926
+ ),
927
+ ),
928
+ validator: core.SignerValidator({
929
+ signers: [defaultOptions.account.address],
930
+ validatorCaller: defaultOptions.account.address,
931
+ }),
932
+ allowList: core.SimpleAllowList({
933
+ owner: defaultOptions.account.address,
934
+ allowed: [defaultOptions.account.address],
935
+ }),
936
+ incentives: [
937
+ core.ERC20Incentive({
938
+ asset: erc20.assertValidAddress(),
939
+ reward: parseEther("1"),
940
+ limit: 10n,
941
+ strategy: StrategyType.POOL,
942
+ manager: budget.assertValidAddress(),
943
+ }),
944
+ ],
945
+ });
946
+
947
+ const feesInfo = await core.getIncentiveFeesInfo(boost.id, 0n);
948
+
949
+ expect(feesInfo).toBeDefined();
950
+ expect(feesInfo.protocolFee).toBe(1000n);
951
+ expect(feesInfo.protocolFeesRemaining).toBe(parseEther("1"));
952
+ expect(feesInfo.assetType).toBe(AssetType.ERC20);
953
+ expect(feesInfo.asset.toLowerCase()).toBe(erc20.assertValidAddress());
954
+ });
907
955
  });
package/src/BoostCore.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  readBoostCoreCreateBoostAuth,
4
4
  readBoostCoreGetBoost,
5
5
  readBoostCoreGetBoostCount,
6
+ readBoostCoreGetIncentiveFeesInfo,
6
7
  readBoostCoreProtocolFee,
7
8
  readBoostCoreProtocolFeeReceiver,
8
9
  readIAuthIsAuthorized,
@@ -13,6 +14,7 @@ import {
13
14
  simulateBoostCoreSetProtocolFeeReceiver,
14
15
  writeBoostCoreClaimIncentive,
15
16
  writeBoostCoreClaimIncentiveFor,
17
+ writeBoostCoreCreateBoost,
16
18
  writeBoostCoreSetCreateBoostAuth,
17
19
  writeBoostCoreSetProtocolFeeReceiver,
18
20
  } from '@boostxyz/evm';
@@ -29,6 +31,8 @@ import {
29
31
  type Address,
30
32
  type ContractEventName,
31
33
  type Hex,
34
+ encodePacked,
35
+ keccak256,
32
36
  parseEventLogs,
33
37
  zeroAddress,
34
38
  zeroHash,
@@ -126,8 +130,10 @@ import {
126
130
  InvalidProtocolChainIdError,
127
131
  MustInitializeBudgetError,
128
132
  } from './errors';
133
+ import type { AssetType } from './transfers';
129
134
  import {
130
135
  type GenericLog,
136
+ type HashAndSimulatedResult,
131
137
  type ReadParams,
132
138
  type WriteParams,
133
139
  assertValidAddressByChainId,
@@ -275,6 +281,20 @@ export type CreateBoostPayload = {
275
281
  owner?: Address;
276
282
  };
277
283
 
284
+ /**
285
+ * Represents the information about the disbursal of an incentive.
286
+ *
287
+ * @export
288
+ * @typedef {IncentiveDisbursalInfo}
289
+ */
290
+ export type IncentiveDisbursalInfo = {
291
+ assetType: AssetType;
292
+ asset: Address;
293
+ protocolFeesRemaining: bigint;
294
+ protocolFee: bigint;
295
+ tokenId: bigint;
296
+ };
297
+
278
298
  /**
279
299
  * The core contract for the Boost protocol. Used to create and retrieve deployed Boosts.
280
300
  *
@@ -407,6 +427,27 @@ export class BoostCore extends Deployable<
407
427
  });
408
428
  }
409
429
 
430
+ /**
431
+ * Create a new Boost.
432
+ *
433
+ * @public
434
+ * @async
435
+ * @param {CreateBoostPayload} _boostPayload
436
+ * @param {?WriteParams} [params]
437
+ * @returns {Promise<HashAndSimulatedResult>}
438
+ */
439
+ public async createBoostRaw(
440
+ _boostPayload: CreateBoostPayload,
441
+ _params?: WriteParams,
442
+ ): Promise<HashAndSimulatedResult> {
443
+ const { request, result } = await this.simulateCreateBoost(
444
+ _boostPayload,
445
+ _params,
446
+ );
447
+ const hash = await writeBoostCoreCreateBoost(this._config, request);
448
+ return { hash, result };
449
+ }
450
+
410
451
  /**
411
452
  * Returns a simulated Boost creation.
412
453
  *
@@ -1046,6 +1087,38 @@ export class BoostCore extends Deployable<
1046
1087
  return { hash, result };
1047
1088
  }
1048
1089
 
1090
+ /**
1091
+ * Get the incentives fees information for a given Boost ID and Incentive ID.
1092
+ *
1093
+ * @public
1094
+ * @async
1095
+ * @param {bigint} boostId - The ID of the Boost
1096
+ * @param {bigint} incentiveId - The ID of the Incentive
1097
+ * @param {?ReadParams} [params]
1098
+ * @returns {Promise<IncentiveDisbursalInfo>}
1099
+ */
1100
+ public async getIncentiveFeesInfo(
1101
+ boostId: bigint,
1102
+ incentiveId: bigint,
1103
+ params?: ReadParams,
1104
+ ) {
1105
+ const key = keccak256(
1106
+ encodePacked(['uint256', 'uint256'], [boostId, incentiveId]),
1107
+ );
1108
+
1109
+ return await readBoostCoreGetIncentiveFeesInfo(this._config, {
1110
+ ...assertValidAddressByChainId(
1111
+ this._config,
1112
+ this.addresses,
1113
+ params?.chainId,
1114
+ ),
1115
+ args: [key],
1116
+ ...this.optionallyAttachAccount(),
1117
+ // biome-ignore lint/suspicious/noExplicitAny: Accept any shape of valid wagmi/viem parameters, wagmi does the same thing internally
1118
+ ...(params as any),
1119
+ });
1120
+ }
1121
+
1049
1122
  /**
1050
1123
  * Retrieves the claim information from a transaction receipt.
1051
1124
  *
@@ -21,9 +21,9 @@ import {
21
21
  type Address,
22
22
  type ContractEventName,
23
23
  type Hex,
24
+ decodeAbiParameters,
24
25
  encodeAbiParameters,
25
26
  zeroAddress,
26
- zeroHash,
27
27
  } from 'viem';
28
28
  import { ERC20PeggedIncentive as ERC20PeggedIncentiveBases } from '../../dist/deployments.json';
29
29
  import type {
@@ -422,11 +422,11 @@ export class ERC20PeggedIncentive extends DeployableTarget<
422
422
  * @returns {Promise<bigint>} - True if total claims is less than limit
423
423
  */
424
424
  public async getRemainingClaimPotential(params?: ReadParams) {
425
- const [claims, limit] = await Promise.all([
426
- this.claims(params),
425
+ const [totalClaimed, limit] = await Promise.all([
426
+ this.totalClaimed(params),
427
427
  this.limit(params),
428
428
  ]);
429
- return limit - claims;
429
+ return limit - totalClaimed;
430
430
  }
431
431
 
432
432
  /**
@@ -478,6 +478,35 @@ export class ERC20PeggedIncentive extends DeployableTarget<
478
478
  [rewardAmount],
479
479
  );
480
480
  }
481
+
482
+ /**
483
+ * Decodes claim data for the ERC20PeggedIncentive, returning the claim amount.
484
+ * Useful when deriving amount claimed from logs.
485
+ *
486
+ * @public
487
+ * @param {Hex} claimData
488
+ * @returns {BigInt} Returns the reward amount from a claim data payload
489
+ */
490
+ public decodeClaimData(claimData: Hex) {
491
+ const boostClaimData = decodeAbiParameters(
492
+ [
493
+ {
494
+ type: 'tuple',
495
+ name: 'BoostClaimData',
496
+ components: [
497
+ { type: 'bytes', name: 'validatorData' },
498
+ { type: 'bytes', name: 'incentiveData' },
499
+ ],
500
+ },
501
+ ],
502
+ claimData,
503
+ );
504
+ const signedAmount = decodeAbiParameters(
505
+ [{ type: 'uint256' }],
506
+ boostClaimData[0].incentiveData,
507
+ )[0];
508
+ return signedAmount;
509
+ }
481
510
  }
482
511
 
483
512
  /**
@@ -33,6 +33,7 @@ import {
33
33
  gasRebateIncentiveCriteria,
34
34
  } from "./ERC20VariableCriteriaIncentive";
35
35
  import { allKnownSignatures } from "@boostxyz/test/allKnownSignatures";
36
+ import { readMockErc20BalanceOf } from "@boostxyz/evm";
36
37
 
37
38
  /**
38
39
  * A basic ERC721 mint scalar criteria for testing
@@ -73,11 +74,11 @@ export function basicErc721MintScalarCriteria(
73
74
  let fixtures: Fixtures,
74
75
  erc20: MockERC20,
75
76
  erc721: MockERC721,
76
- erc20Incentive: ERC20PeggedVariableCriteriaIncentive,
77
+ erc20PeggedVariableCriteriaIncentive: ERC20PeggedVariableCriteriaIncentive,
77
78
  budgets: BudgetFixtures,
78
79
  boost: Boost;
79
80
 
80
- describe("ERC20VariableCriteriaIncentive", () => {
81
+ describe("ERC20PeggedVariableCriteriaIncentive", () => {
81
82
  beforeAll(async () => {
82
83
  fixtures = await loadFixture(deployFixtures(defaultOptions));
83
84
  });
@@ -86,7 +87,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
86
87
  budgets = await loadFixture(fundBudget(defaultOptions, fixtures));
87
88
  erc20 = await loadFixture(fundErc20(defaultOptions));
88
89
  erc721 = await loadFixture(fundErc721(defaultOptions));
89
- erc20Incentive = fixtures.core.ERC20PeggedVariableCriteriaIncentive({
90
+ erc20PeggedVariableCriteriaIncentive = fixtures.core.ERC20PeggedVariableCriteriaIncentive({
90
91
  asset: budgets.erc20.assertValidAddress(),
91
92
  reward: parseEther("1"),
92
93
  limit: parseEther("10"),
@@ -97,48 +98,48 @@ describe("ERC20VariableCriteriaIncentive", () => {
97
98
 
98
99
  boost = await freshBoost(fixtures, {
99
100
  budget: budgets.budget,
100
- incentives: [erc20Incentive],
101
+ incentives: [erc20PeggedVariableCriteriaIncentive],
101
102
  });
102
103
  expect(isAddress(boost.incentives[0]!.assertValidAddress())).toBe(true);
103
104
  });
104
105
 
105
106
  describe("Basic Parameters", () => {
106
107
  test("should return correct asset address", async () => {
107
- const asset = await erc20Incentive.asset();
108
+ const asset = await erc20PeggedVariableCriteriaIncentive.asset();
108
109
  expect(isAddressEqual(asset, budgets.erc20.assertValidAddress())).toBe(true);
109
110
  });
110
111
 
111
112
  test("should return correct peg token address", async () => {
112
- const peg = await erc20Incentive.peg();
113
+ const peg = await erc20PeggedVariableCriteriaIncentive.peg();
113
114
  expect(isAddressEqual(peg, erc20.assertValidAddress())).toBe(true);
114
115
  });
115
116
 
116
117
  test("should return correct reward amount", async () => {
117
- const reward = await erc20Incentive.reward();
118
+ const reward = await erc20PeggedVariableCriteriaIncentive.reward();
118
119
  expect(reward).toBe(parseEther("1"));
119
120
  });
120
121
 
121
122
  test("should return correct limit", async () => {
122
- const limit = await erc20Incentive.limit();
123
+ const limit = await erc20PeggedVariableCriteriaIncentive.limit();
123
124
  expect(limit).toBe(parseEther("10"));
124
125
  });
125
126
 
126
127
  test("should return correct max reward", async () => {
127
- const maxReward = await erc20Incentive.getMaxReward();
128
+ const maxReward = await erc20PeggedVariableCriteriaIncentive.getMaxReward();
128
129
  expect(maxReward).toBe(parseEther("20"));
129
130
  });
130
131
  });
131
132
 
132
133
  describe("Remaining Claims", () => {
133
134
  test("should calculate remaining claim potential correctly", async () => {
134
- const remaining = await erc20Incentive.getRemainingClaimPotential();
135
- const limit = await erc20Incentive.limit();
136
- const totalClaimed = await erc20Incentive.totalClaimed();
135
+ const remaining = await erc20PeggedVariableCriteriaIncentive.getRemainingClaimPotential();
136
+ const limit = await erc20PeggedVariableCriteriaIncentive.limit();
137
+ const totalClaimed = await erc20PeggedVariableCriteriaIncentive.totalClaimed();
137
138
  expect(remaining).toBe(limit - totalClaimed);
138
139
  });
139
140
 
140
141
  test("should return true for canBeClaimed when claims remain", async () => {
141
- const canBeClaimed = await erc20Incentive.canBeClaimed();
142
+ const canBeClaimed = await erc20PeggedVariableCriteriaIncentive.canBeClaimed();
142
143
  expect(canBeClaimed).toBe(true);
143
144
  });
144
145
  });
@@ -159,7 +160,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
159
160
  describe("Claim Data", () => {
160
161
  test("should properly encode claim data", () => {
161
162
  const rewardAmount = parseEther("1");
162
- const encodedData = erc20Incentive.buildClaimData(rewardAmount);
163
+ const encodedData = erc20PeggedVariableCriteriaIncentive.buildClaimData(rewardAmount);
163
164
  expect(encodedData).toBe(
164
165
  "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
165
166
  );
@@ -168,14 +169,14 @@ describe("ERC20VariableCriteriaIncentive", () => {
168
169
 
169
170
  describe("Owner", () => {
170
171
  test("should return correct owner", async () => {
171
- const owner = await erc20Incentive.owner();
172
+ const owner = await erc20PeggedVariableCriteriaIncentive.owner();
172
173
  expect(isAddressEqual(owner, fixtures.core.assertValidAddress())).toBe(true);
173
174
  });
174
175
  });
175
176
 
176
177
  describe("Current Reward", () => {
177
178
  test("should return valid current reward", async () => {
178
- const currentReward = await erc20Incentive.currentReward();
179
+ const currentReward = await erc20PeggedVariableCriteriaIncentive.currentReward();
179
180
  expect(currentReward).toBeDefined();
180
181
  expect(currentReward).toBeTypeOf("bigint");
181
182
  });
@@ -190,7 +191,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
190
191
  recipient,
191
192
  1n,
192
193
  );
193
- const scalar = await erc20Incentive.getIncentiveScalar({
194
+ const scalar = await erc20PeggedVariableCriteriaIncentive.getIncentiveScalar({
194
195
  hash,
195
196
  chainId: 31337,
196
197
  knownSignatures: allKnownSignatures,
@@ -202,7 +203,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
202
203
  test("should return a valid scalar for event-based criteria", async () => {
203
204
  boost = await freshBoost(fixtures, {
204
205
  budget: budgets.budget,
205
- incentives: [erc20Incentive],
206
+ incentives: [erc20PeggedVariableCriteriaIncentive],
206
207
  });
207
208
  const recipient = accounts[1].account;
208
209
  const { hash } = await erc721.transferFromRaw(
@@ -210,7 +211,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
210
211
  recipient,
211
212
  1n,
212
213
  );
213
- const scalar = await erc20Incentive.getIncentiveScalar({
214
+ const scalar = await erc20PeggedVariableCriteriaIncentive.getIncentiveScalar({
214
215
  hash,
215
216
  chainId: 31337,
216
217
  knownSignatures: allKnownSignatures,
@@ -223,7 +224,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
223
224
  // Ensure that the gasRebateIncentiveCriteria returns the correct structure
224
225
  const gasRebateCriteria = gasRebateIncentiveCriteria();
225
226
 
226
- erc20Incentive = fixtures.core.ERC20PeggedVariableCriteriaIncentive({
227
+ erc20PeggedVariableCriteriaIncentive = fixtures.core.ERC20PeggedVariableCriteriaIncentive({
227
228
  asset: budgets.erc20.assertValidAddress(),
228
229
  reward: 1n,
229
230
  limit: 1n,
@@ -242,7 +243,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
242
243
 
243
244
  boost = await freshBoost(fixtures, {
244
245
  budget: budgets.budget,
245
- incentives: [erc20Incentive],
246
+ incentives: [erc20PeggedVariableCriteriaIncentive],
246
247
  });
247
248
 
248
249
  // Validate that the deployed incentive has the correct criteria set up
@@ -261,7 +262,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
261
262
  const { hash } = await erc20.mintRaw(recipient, parseEther("100"));
262
263
 
263
264
  try {
264
- await erc20Incentive.getIncentiveScalar({
265
+ await erc20PeggedVariableCriteriaIncentive.getIncentiveScalar({
265
266
  hash,
266
267
  chainId: 31337,
267
268
  knownSignatures: allKnownSignatures,
@@ -276,7 +277,7 @@ describe("ERC20VariableCriteriaIncentive", () => {
276
277
  const { hash } = await erc20.mintRaw(recipient, parseEther("100"));
277
278
 
278
279
  try {
279
- await erc20Incentive.getIncentiveScalar({
280
+ await erc20PeggedVariableCriteriaIncentive.getIncentiveScalar({
280
281
  hash,
281
282
  chainId: 31337,
282
283
  knownSignatures: allKnownSignatures,
@@ -299,11 +300,11 @@ describe("ERC20VariableCriteriaIncentive", () => {
299
300
  // rebase this
300
301
  const boost = await freshBoost(fixtures, {
301
302
  budget: budgets.budget,
302
- incentives: [erc20Incentive],
303
+ incentives: [erc20PeggedVariableCriteriaIncentive],
303
304
  });
304
305
  const [amount, address] = await budgets.budget.clawbackFromTarget(
305
306
  fixtures.core.assertValidAddress(),
306
- erc20Incentive.buildClawbackData(1n),
307
+ erc20PeggedVariableCriteriaIncentive.buildClawbackData(1n),
307
308
  boost.id,
308
309
  0,
309
310
  );
@@ -312,4 +313,45 @@ describe("ERC20VariableCriteriaIncentive", () => {
312
313
  true,
313
314
  );
314
315
  });
316
+
317
+ test('can claim', async () => {
318
+ const referrer = accounts.at(1)?.account!
319
+ const trustedSigner = accounts.at(0)!
320
+ const claimant = trustedSigner.account!;
321
+ const asset = await erc20PeggedVariableCriteriaIncentive.asset()
322
+ const signedAmount = 2n
323
+ const incentiveData = erc20PeggedVariableCriteriaIncentive.buildClaimData(signedAmount)
324
+ const claimDataPayload = await boost.validator.encodeClaimData({
325
+ signer: trustedSigner,
326
+ incentiveData,
327
+ chainId: defaultOptions.config.chains[0].id,
328
+ incentiveQuantity: boost.incentives.length,
329
+ claimant,
330
+ boostId: boost.id
331
+ })
332
+
333
+ // verify reward amount
334
+ const incentiveClaimAmount = await erc20PeggedVariableCriteriaIncentive.decodeClaimData(claimDataPayload)
335
+ expect(incentiveClaimAmount).toBe(signedAmount)
336
+
337
+ const balanceBeforeClaim = await readMockErc20BalanceOf(defaultOptions.config, {
338
+ address: asset,
339
+ args: [claimant],
340
+ })
341
+
342
+ await fixtures.core.claimIncentiveFor(
343
+ boost.id,
344
+ 0n,
345
+ referrer,
346
+ claimDataPayload,
347
+ claimant,
348
+ );
349
+
350
+ const balanceAfterClaim = await readMockErc20BalanceOf(defaultOptions.config, {
351
+ address: asset,
352
+ args: [claimant],
353
+ })
354
+
355
+ expect(balanceAfterClaim - balanceBeforeClaim).toBe(signedAmount);
356
+ })
315
357
  });
@@ -29,6 +29,7 @@ import {
29
29
  decodeAbiParameters,
30
30
  decodeFunctionData,
31
31
  encodeAbiParameters,
32
+ parseEther,
32
33
  parseEventLogs,
33
34
  zeroAddress,
34
35
  } from 'viem';
@@ -651,27 +652,108 @@ export class ERC20PeggedVariableCriteriaIncentive extends DeployableTarget<
651
652
  * Builds the claim data for the ERC20PeggedVariableCriteriaIncentivePayload.
652
653
  *
653
654
  * @public
654
- * @param {bigint} rewardAmount
655
+ * @param {bigint} signedAmount
655
656
  * @returns {Hash} Returns the encoded claim data
656
657
  * @description This function returns the encoded claim data for the ERC20PeggedVariableCriteriaIncentivePayload.
657
658
  */
658
- public buildClaimData(rewardAmount: bigint) {
659
+ public buildClaimData(signedAmount: bigint) {
659
660
  return encodeAbiParameters(
660
- [{ type: 'uint256', name: 'rewardAmount' }],
661
- [rewardAmount],
661
+ [{ type: 'uint256', name: 'signedAmount' }],
662
+ [signedAmount],
662
663
  );
663
664
  }
664
665
 
665
666
  /**
666
- * Decodes claim data for the ERC20PeggedVariableCriteriaIncentive, returning the encoded claim amount.
667
+ * Decodes claim data for the ERC20PeggedVariableCriteriaIncentive, returning the claim amount.
667
668
  * Useful when deriving amount claimed from logs.
668
669
  *
669
670
  * @public
670
671
  * @param {Hex} claimData
671
672
  * @returns {BigInt} Returns the reward amount from a claim data payload
672
673
  */
673
- public decodeClaimData(data: Hex) {
674
- return BigInt(decodeAbiParameters([{ type: 'uint256' }], data)[0]);
674
+ public async decodeClaimData(claimData: Hex) {
675
+ const boostClaimData = decodeAbiParameters(
676
+ [
677
+ {
678
+ type: 'tuple',
679
+ name: 'BoostClaimData',
680
+ components: [
681
+ { type: 'bytes', name: 'validatorData' },
682
+ { type: 'bytes', name: 'incentiveData' },
683
+ ],
684
+ },
685
+ ],
686
+ claimData,
687
+ );
688
+ const signedAmount = decodeAbiParameters(
689
+ [{ type: 'uint256' }],
690
+ boostClaimData[0].incentiveData,
691
+ )[0];
692
+ let claimAmount = signedAmount;
693
+ const [reward, maxReward] = await Promise.all([
694
+ this.reward(),
695
+ this.getMaxReward(),
696
+ ]);
697
+
698
+ if (reward === 0n) {
699
+ return claimAmount;
700
+ } else {
701
+ claimAmount = (reward * signedAmount) / parseEther('1');
702
+ }
703
+
704
+ if (maxReward !== 0n && claimAmount > maxReward) {
705
+ claimAmount = maxReward;
706
+ }
707
+
708
+ return claimAmount;
709
+ }
710
+
711
+ /**
712
+ * Decodes claim data for the ERC20PeggedVariableCriteriaIncentive, returning the claim amount.
713
+ * Useful when deriving amount claimed from logs.
714
+ * Use this function instead of `decodeClaimData` if you have reward details.
715
+ *
716
+ * @public
717
+ * @param {Hex} claimData
718
+ * @param {bigint} [reward]
719
+ * @param {bigint} [maxReward]
720
+ * @returns {BigInt} Returns the reward amount from a claim data payload
721
+ */
722
+ public decodeClaimDataWithRewardDetails(
723
+ claimData: Hex,
724
+ reward: bigint,
725
+ maxReward: bigint,
726
+ ) {
727
+ const boostClaimData = decodeAbiParameters(
728
+ [
729
+ {
730
+ type: 'tuple',
731
+ name: 'BoostClaimData',
732
+ components: [
733
+ { type: 'bytes', name: 'validatorData' },
734
+ { type: 'bytes', name: 'incentiveData' },
735
+ ],
736
+ },
737
+ ],
738
+ claimData,
739
+ );
740
+ const signedAmount = decodeAbiParameters(
741
+ [{ type: 'uint256' }],
742
+ boostClaimData[0].incentiveData,
743
+ )[0];
744
+ let claimAmount = signedAmount;
745
+
746
+ if (reward === 0n) {
747
+ return claimAmount;
748
+ } else {
749
+ claimAmount = (reward * signedAmount) / parseEther('1');
750
+ }
751
+
752
+ if (maxReward !== 0n && claimAmount > maxReward) {
753
+ claimAmount = maxReward;
754
+ }
755
+
756
+ return claimAmount;
675
757
  }
676
758
  }
677
759
 
package/src/errors.ts CHANGED
@@ -865,3 +865,53 @@ export class BoostNotFoundError extends Error {
865
865
  this.id = id;
866
866
  }
867
867
  }
868
+
869
+ /**
870
+ * Thrown when a tuple decoding operation encounters unexpected structure or data.
871
+ *
872
+ * @export
873
+ * @class InvalidTupleDecodingError
874
+ * @typedef {InvalidTupleDecodingError}
875
+ * @extends {Error}
876
+ */
877
+ export class InvalidTupleDecodingError extends Error {
878
+ /**
879
+ * The index of the tuple that caused the error.
880
+ *
881
+ * @type {number}
882
+ */
883
+ public readonly tupleIndex: number;
884
+
885
+ /**
886
+ * Creates an instance of InvalidTupleDecodingError.
887
+ *
888
+ * @constructor
889
+ * @param {string} tupleName - The name or identifier of the tuple that failed to decode.
890
+ * @param {string} [message] - Optional custom message for more context.
891
+ */
892
+ constructor(message?: string, tupleIndex?: number) {
893
+ super(message || `Failed to decode tuple: ${tupleIndex}`);
894
+ this.tupleIndex = tupleIndex ?? -1;
895
+ }
896
+ }
897
+
898
+ export class InvalidTupleEncodingError extends Error {
899
+ /**
900
+ * The index of the tuple that caused the error.
901
+ *
902
+ * @type {number}
903
+ */
904
+ public readonly tupleIndex: number;
905
+
906
+ /**
907
+ * Creates an instance of InvalidTupleEncodingError.
908
+ *
909
+ * @constructor
910
+ * @param {string} tupleName - The name or identifier of the tuple that failed to encode.
911
+ * @param {string} [message] - Optional custom message for more context.
912
+ */
913
+ constructor(message?: string, tupleIndex?: number) {
914
+ super(message || `Failed to encode tuple: ${tupleIndex}`);
915
+ this.tupleIndex = tupleIndex ?? -1;
916
+ }
917
+ }