@tokenops/sdk 2.0.0-alpha.2 → 2.0.0-alpha.3

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 (80) hide show
  1. package/CHANGELOG.md +39 -3
  2. package/README.md +11 -5
  3. package/dist/{chunk-PZZK3O3S.cjs → chunk-4AWQJQXY.cjs} +391 -43
  4. package/dist/{chunk-SPLNGUFF.js → chunk-4IB4IY5Y.js} +1 -1
  5. package/dist/{chunk-A4DCEE33.js → chunk-6HCT4QIX.js} +11 -10
  6. package/dist/{chunk-MM5BV5CS.cjs → chunk-6XOAYDC6.cjs} +4 -4
  7. package/dist/{chunk-FEDX7B6T.cjs → chunk-AHU2HMNE.cjs} +3 -3
  8. package/dist/{chunk-IGO5XSPS.js → chunk-BPIVVCSQ.js} +2 -2
  9. package/dist/{chunk-X7WBPCEU.js → chunk-BXX5TETM.js} +375 -27
  10. package/dist/{chunk-GADGBQJO.js → chunk-GCJFAY4X.js} +2 -2
  11. package/dist/{chunk-IUKKNJ2R.cjs → chunk-GKYZQJGP.cjs} +7 -7
  12. package/dist/{chunk-3YHBO2DL.cjs → chunk-IEKTGJ4J.cjs} +2 -2
  13. package/dist/{chunk-UHHMVBLU.cjs → chunk-JVNJSDQ4.cjs} +2 -2
  14. package/dist/{chunk-VW356KR3.js → chunk-LEYYG4RF.js} +126 -3
  15. package/dist/{chunk-PYYSB5ZN.js → chunk-NGU4JYIR.js} +1 -1
  16. package/dist/{chunk-FT5H2Q7Y.cjs → chunk-PG3WXT3K.cjs} +11 -10
  17. package/dist/{chunk-44YJPMQC.js → chunk-PK6Q6J5F.js} +1 -1
  18. package/dist/{chunk-EBLBPUPI.cjs → chunk-TND5NDV7.cjs} +18 -10
  19. package/dist/{chunk-EYDA6Q6C.js → chunk-U3TBQRT7.js} +10 -2
  20. package/dist/{chunk-WJMEBBU7.js → chunk-WRWRA4TN.js} +1 -1
  21. package/dist/{chunk-FHWSBGWE.cjs → chunk-YABHXJNH.cjs} +5 -5
  22. package/dist/{chunk-AREQJHKA.cjs → chunk-YTIYTV4K.cjs} +129 -2
  23. package/dist/core/addresses.d.ts +4 -4
  24. package/dist/core/errors.d.ts +1 -1
  25. package/dist/fhe-airdrop/abis/airdrop-base.d.ts +19 -0
  26. package/dist/fhe-airdrop/abis/ecdsa.d.ts +19 -0
  27. package/dist/fhe-airdrop/abis/factory.d.ts +113 -0
  28. package/dist/fhe-airdrop/abis/merkle.d.ts +19 -0
  29. package/dist/fhe-airdrop/advanced/index.cjs +4 -4
  30. package/dist/fhe-airdrop/advanced/index.js +1 -1
  31. package/dist/fhe-airdrop/advanced/react/index.cjs +110 -80
  32. package/dist/fhe-airdrop/advanced/react/index.d.cts +1 -0
  33. package/dist/fhe-airdrop/advanced/react/index.d.ts +1 -0
  34. package/dist/fhe-airdrop/advanced/react/index.js +34 -5
  35. package/dist/fhe-airdrop/advanced/react/useDisableCustomFee.d.ts +2 -1
  36. package/dist/fhe-airdrop/advanced/react/useFactoryRenounceRole.d.ts +4 -3
  37. package/dist/fhe-airdrop/advanced/react/useFactoryRevokeRole.d.ts +9 -7
  38. package/dist/fhe-airdrop/advanced/react/useFactoryRoleMembers.d.ts +6 -5
  39. package/dist/fhe-airdrop/advanced/react/useSetCustomFee.d.ts +2 -1
  40. package/dist/fhe-airdrop/advanced/react/useSetDefaultGasFee.d.ts +5 -3
  41. package/dist/fhe-airdrop/advanced/react/useSetMaxGasFee.d.ts +33 -0
  42. package/dist/fhe-airdrop/airdrop-base.d.ts +47 -8
  43. package/dist/fhe-airdrop/constants.d.ts +9 -1
  44. package/dist/fhe-airdrop/ecdsa.d.ts +13 -0
  45. package/dist/fhe-airdrop/errors.d.ts +59 -0
  46. package/dist/fhe-airdrop/factory.d.ts +62 -22
  47. package/dist/fhe-airdrop/guards.d.ts +87 -2
  48. package/dist/fhe-airdrop/index.cjs +67 -55
  49. package/dist/fhe-airdrop/index.d.cts +3 -3
  50. package/dist/fhe-airdrop/index.d.ts +3 -3
  51. package/dist/fhe-airdrop/index.js +4 -4
  52. package/dist/fhe-airdrop/react/index.cjs +237 -177
  53. package/dist/fhe-airdrop/react/index.d.cts +4 -1
  54. package/dist/fhe-airdrop/react/index.d.ts +4 -1
  55. package/dist/fhe-airdrop/react/index.js +72 -15
  56. package/dist/fhe-airdrop/react/keys.d.ts +8 -0
  57. package/dist/fhe-airdrop/react/useFactoryFees.d.ts +11 -4
  58. package/dist/fhe-airdrop/react/useIsAirdrop.d.ts +44 -0
  59. package/dist/fhe-airdrop/react/useRefreshComplianceBalance.d.ts +36 -0
  60. package/dist/fhe-airdrop/react/useResolveGasFee.d.ts +30 -0
  61. package/dist/fhe-airdrop/types.d.ts +13 -0
  62. package/dist/fhe-disperse/index.cjs +21 -21
  63. package/dist/fhe-disperse/index.js +2 -2
  64. package/dist/fhe-disperse/react/index.cjs +22 -22
  65. package/dist/fhe-disperse/react/index.js +3 -3
  66. package/dist/fhe-vesting/advanced/index.cjs +5 -5
  67. package/dist/fhe-vesting/advanced/index.js +3 -3
  68. package/dist/fhe-vesting/advanced/react/index.cjs +7 -7
  69. package/dist/fhe-vesting/advanced/react/index.js +4 -4
  70. package/dist/fhe-vesting/index.cjs +8 -8
  71. package/dist/fhe-vesting/index.js +2 -2
  72. package/dist/fhe-vesting/react/index.cjs +114 -114
  73. package/dist/fhe-vesting/react/index.js +3 -3
  74. package/dist/index.cjs +18 -18
  75. package/dist/index.js +1 -1
  76. package/dist/testnet-faucet/index.cjs +14 -14
  77. package/dist/testnet-faucet/index.js +2 -2
  78. package/dist/testnet-faucet/react/index.cjs +8 -8
  79. package/dist/testnet-faucet/react/index.js +3 -3
  80. package/package.json +1 -1
@@ -14,7 +14,8 @@ export interface DisableCustomFeeArgs extends WriteAccountOverride {
14
14
  * Requires `FEE_MANAGER_ROLE`.
15
15
  *
16
16
  * **Invalidates:** `useFactoryCustomFee({ creator })` for the TARGET creator -
17
- * `variables.creator`, never the sending admin.
17
+ * `variables.creator`, never the sending admin. Also every `useResolveGasFee`
18
+ * entry: dropping the override sends this creator back to the default.
18
19
  *
19
20
  * @example
20
21
  * const disableCustomFee = useDisableCustomFee();
@@ -16,9 +16,10 @@ export interface FactoryRenounceRoleArgs extends WriteAccountOverride {
16
16
  * resolved sending account. To remove a role from somebody else use
17
17
  * {@link useFactoryRevokeRole}.
18
18
  *
19
- * **No floor stops a sole admin renouncing here**, unlike an airdrop instance's
20
- * `LastAdmin` guard. A sole `DEFAULT_ADMIN_ROLE` holder renouncing freezes role
21
- * administration permanently; grant a successor first.
19
+ * **A sole `DEFAULT_ADMIN_ROLE` holder cannot renounce** (audit-14): the factory
20
+ * carries the same `LastAdmin` floor the instances do, so the call reverts
21
+ * instead of freezing role administration permanently. Grant the successor
22
+ * first, then renounce.
22
23
  *
23
24
  * **Invalidates:** every {@link useFactoryHasRole} entry for this factory
24
25
  * (prefix, all role/holder pairs), plus its {@link useFactoryRoleMembers} entry
@@ -14,13 +14,15 @@ export interface FactoryRevokeRoleArgs extends WriteAccountOverride {
14
14
  *
15
15
  * Requires `DEFAULT_ADMIN_ROLE`, which administers all five factory roles.
16
16
  *
17
- * **The factory declares no "never empty" floor.** Revoking the last
18
- * `DEFAULT_ADMIN_ROLE` holder succeeds and permanently freezes role
19
- * administration - no role can ever be granted or revoked again, including
20
- * putting an admin back. The four operational roles keep working for whoever
21
- * already holds them, with no way to rotate them. On the live deployment all
22
- * five roles sit on one key, so show {@link useFactoryRoleMembers}'s `count`
23
- * next to the button and grant the successor first.
17
+ * **`DEFAULT_ADMIN_ROLE` is floored at one live member** (audit-14). Revoking
18
+ * the last holder reverts `LastAdmin` rather than succeeding and freezing role
19
+ * administration, which is what it used to do. Granting the role to the zero
20
+ * address reverts `ZeroAdminGrant`, so the floor cannot be satisfied with a
21
+ * member nobody controls either.
22
+ *
23
+ * On the live deployment all five roles sit on one key, so a handover is two
24
+ * steps: grant the successor, then revoke. Show {@link useFactoryRoleMembers}'s
25
+ * `count` next to the button and disable it at `1n`.
24
26
  *
25
27
  * **Invalidates:** this factory's {@link useFactoryHasRole} entry for exactly
26
28
  * `(role, holder)` and its {@link useFactoryRoleMembers} entry for `role`.
@@ -26,10 +26,11 @@ export interface UseFactoryRoleMembersArgs extends AirdropHookOptions<FactoryRol
26
26
  * trusting a fixed string that describes one deployment at one moment.
27
27
  *
28
28
  * `count` is bundled with the list rather than split into its own hook because
29
- * it is what a UI needs in the same render to decide whether a revoke is safe -
30
- * the factory declares no "never empty" floor, so revoking the last
31
- * `DEFAULT_ADMIN_ROLE` holder succeeds and permanently freezes all role
32
- * administration.
29
+ * it is what a UI needs in the same render to decide whether a revoke will be
30
+ * accepted: since audit-14 the factory floors `DEFAULT_ADMIN_ROLE` at one live
31
+ * member, so revoking or renouncing the last holder reverts `LastAdmin`.
32
+ * Disable the button at `count === 1n` rather than letting the user burn gas
33
+ * discovering it.
33
34
  *
34
35
  * Unpaginated: factory role sets are operator-sized, not user-sized.
35
36
  *
@@ -40,7 +41,7 @@ export interface UseFactoryRoleMembersArgs extends AirdropHookOptions<FactoryRol
40
41
  *
41
42
  * @example
42
43
  * const { data } = useFactoryRoleMembers({ role: roles?.DEFAULT_ADMIN_ROLE });
43
- * const lastAdmin = data?.count === 1n;
44
+ * const lastAdmin = data?.count === 1n; // revoke/renounce would revert LastAdmin
44
45
  *
45
46
  * @alpha
46
47
  */
@@ -17,7 +17,8 @@ export interface SetCustomFeeArgs extends WriteAccountOverride {
17
17
  * **Invalidates:** `useFactoryCustomFee({ creator })` for the TARGET creator -
18
18
  * `variables.creator`, never the sending admin. `account` is the submitter and
19
19
  * has no cache entry of its own here, so keying the invalidation off it would
20
- * refresh the wrong row and leave the edited one stale.
20
+ * refresh the wrong row and leave the edited one stale. Also every
21
+ * `useResolveGasFee` entry, since an override is what that hook resolves.
21
22
  *
22
23
  * @example
23
24
  * const setCustomFee = useSetCustomFee();
@@ -15,9 +15,11 @@ export interface SetDefaultGasFeeArgs extends WriteAccountOverride {
15
15
  * instance's `gasFee()` is baked in at create time, so no deployed campaign's
16
16
  * fee changes.
17
17
  *
18
- * **Invalidates:** this factory's `useFactoryFees` entry (fn `"fees"`) only.
19
- * Deliberately NOT any instance's fn `"gasFee"` key - those are immutable
20
- * per instance and invalidating them would refetch unchanged values.
18
+ * **Invalidates:** this factory's `useFactoryFees` entry (fn `"fees"`) and every
19
+ * `useResolveGasFee` entry - the default is what resolves for every creator
20
+ * without an override, so all of them move at once. Deliberately NOT any
21
+ * instance's fn `"gasFee"` key: those are frozen at create and invalidating
22
+ * them would refetch unchanged values.
21
23
  *
22
24
  * @example
23
25
  * const setDefaultFee = useSetDefaultGasFee();
@@ -0,0 +1,33 @@
1
+ import { type UseMutationResult } from "@tanstack/react-query";
2
+ import type { Hex } from "viem";
3
+ import type { WriteAccountOverride } from "../../airdrop-base.js";
4
+ import { type AirdropClientOptions } from "../../react/_shared.js";
5
+ /** @alpha */
6
+ export interface SetMaxGasFeeArgs extends WriteAccountOverride {
7
+ /** The factory-wide ceiling in wei, `uint96` on-chain. `0n` admits only a zero fee. */
8
+ maxGasFee: bigint;
9
+ }
10
+ /**
11
+ * Set the factory's ceiling on every fee `setDefaultGasFee` / `setCustomFee` may
12
+ * configure, and on the fee resolved at create.
13
+ *
14
+ * Requires `DEFAULT_ADMIN_ROLE`, deliberately not `FEE_MANAGER_ROLE` (audit-09):
15
+ * the fee manager moves fees only within a bound the admin owns. This is the
16
+ * one fee control that is not a fee-manager operation.
17
+ *
18
+ * **Lowering it does not rewrite an already-configured fee.** The stale value
19
+ * stays in storage and reverts `GasFeeExceedsMaximum` at the next create until
20
+ * it is lowered too, so a reduction is two steps, not one.
21
+ *
22
+ * **Invalidates:** this factory's `useFactoryFees` entry (fn `"fees"`, which
23
+ * carries `maxGasFee`) and every `useResolveGasFee` entry, since the ceiling
24
+ * bounds what any creator's fee can resolve to. Deliberately NOT any instance's
25
+ * fn `"gasFee"` key - an instance's fee is frozen at create and cannot move.
26
+ *
27
+ * @example
28
+ * const setCeiling = useSetMaxGasFee();
29
+ * await setCeiling.mutateAsync({ maxGasFee: parseEther("0.5") });
30
+ *
31
+ * @alpha
32
+ */
33
+ export declare function useSetMaxGasFee(options?: AirdropClientOptions): UseMutationResult<Hex, Error, SetMaxGasFeeArgs>;
@@ -137,7 +137,7 @@ export interface WriteAccountOverride {
137
137
  * do not care about (an admin panel listing campaigns, say). The ECDSA and
138
138
  * Merkle clients extend it and add their own claim path.
139
139
  *
140
- * What it encapsulates beyond the raw ABI is the encrypted-view protocol: five
140
+ * What it encapsulates beyond the raw ABI is the encrypted-view protocol: six
141
141
  * of the contract's disclosure entrypoints look like getters and are not —
142
142
  * they mutate ACL state, so they are transactions, and their result must be
143
143
  * recovered from the receipt. See {@link extractGrantedHandle}.
@@ -217,8 +217,12 @@ export declare class AirdropBaseClient {
217
217
  * Sweep a plain ERC-20 that was sent to the instance by mistake. Requires
218
218
  * `RESCUER_ROLE`.
219
219
  *
220
- * The campaign's own token is ERC-7984, not ERC-20, so it cannot be reached
221
- * through this path — use {@link withdrawConfidential} for the pool.
220
+ * Passing the campaign's own token reverts `CannotRescueAirdropToken`
221
+ * (audit-05), matching {@link rescueOtherConfidentialToken}. ERC-7984 and
222
+ * ERC-20 are not disjoint in practice - a wrapper token answers both
223
+ * interfaces - so "the pool token is confidential, therefore out of reach
224
+ * here" was never a guarantee, only an assumption. Use
225
+ * {@link withdrawConfidential} for the pool.
222
226
  *
223
227
  * @returns The transaction hash.
224
228
  */
@@ -469,10 +473,38 @@ export declare class AirdropBaseClient {
469
473
  * @alpha
470
474
  */
471
475
  deploymentMode(): Promise<DeploymentMode>;
476
+ /**
477
+ * Re-grant the compliance-manager clone on the instance's current pool
478
+ * balance, and return the handle it was granted on.
479
+ *
480
+ * **Permissionless, and deliberately so** (audit-04). The token rotates the
481
+ * instance's balance handle on every incoming transfer, including one no
482
+ * airdrop code observes - a third party transferring in directly - and
483
+ * grants the fresh handle to the token and the holder only. Without this the
484
+ * compliance clone's grant can be stranded on a handle the token has already
485
+ * replaced, with no admin necessarily around to restore it. There is no
486
+ * argument to abuse: the only handle read is `address(this)`'s own balance,
487
+ * and the only grantee is the clone wired at initialization.
488
+ *
489
+ * This is therefore the normative way to obtain a readable live-balance
490
+ * handle - and it is a **snapshot, not a subscription**. Grants are
491
+ * append-only, so the handle this returns stays readable forever; but the
492
+ * next incoming transfer rotates the instance to a NEW handle the clone was
493
+ * never granted on. A compliance reader that refreshes once and stops is
494
+ * reading a stale balance, not a live one. Call it again after any transfer
495
+ * in, or before each read.
496
+ *
497
+ * @returns `{ handle, hash }` - the handle the compliance clone may now decrypt.
498
+ * @throws {@link ReceiptEventNotFoundError} when the transaction granted no ACL to the clone.
499
+ */
500
+ refreshComplianceBalance(args?: WriteAccountOverride): Promise<EncryptedViewResult>;
472
501
  /**
473
502
  * Read the instance's own remaining pool balance as a handle the **caller**
474
503
  * can decrypt. Requires `DISCLOSURE_ADMIN_ROLE`.
475
504
  *
505
+ * Also re-grants the compliance-manager clone on the same handle (audit-04),
506
+ * so an admin read doubles as a {@link refreshComplianceBalance}.
507
+ *
476
508
  * @returns `{ handle, hash }` — pass `handle` to the Zama relayer's `userDecrypt`.
477
509
  * @throws {@link ReceiptEventNotFoundError} when the transaction granted no ACL to the caller.
478
510
  */
@@ -515,11 +547,18 @@ export declare class AirdropBaseClient {
515
547
  /**
516
548
  * Grant `party` decrypt access to a handle you already hold.
517
549
  *
518
- * Two gates apply on-chain: the caller must itself be allowed on the handle
519
- * (bypassed by `DISCLOSURE_ADMIN_ROLE`), and the *instance* must be allowed
520
- * on it — so an arbitrary handle from another contract cannot be laundered
521
- * through this campaign. A failure of the first surfaces as
522
- * `FheHandleNotAllowedError`.
550
+ * Two gates apply on-chain. Gate #1: the handle must be **persistently**
551
+ * user-decryptable in the `(caller, instance)` context - BOTH legs, and a
552
+ * transient allowance does not satisfy either (audit-06). `DISCLOSURE_ADMIN_ROLE`
553
+ * bypasses gate #1 only. Gate #2: the instance must be allowed on the handle,
554
+ * so an arbitrary handle from another contract cannot be laundered through
555
+ * this campaign.
556
+ *
557
+ * **The transient case is the trap.** An `euint64` that just came back from
558
+ * an FHE op carries a transient allowance, which used to be enough here and
559
+ * no longer is. Handles worth disclosing come from the encrypted views on
560
+ * this client, whose grants are persistent. A gate #1 failure surfaces as
561
+ * `FheHandleNotAllowedError` carrying the offending handle.
523
562
  *
524
563
  * @param args.handle An existing `euint64` handle — a public pointer, not a value.
525
564
  * @returns The transaction hash. No new handle is produced.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @alpha
5
5
  */
6
- export declare const AIRDROP_CONTRACTS_COMMIT = "7175ac82f467f0f88c05aba1a4536892999b4251";
6
+ export declare const AIRDROP_CONTRACTS_COMMIT = "c5c12c2087f61bcd5bd1daa802563ed3cd68f002";
7
7
  /**
8
8
  * Deployment shell selected at create time. Mirrors the contract's `DeploymentMode` enum
9
9
  * (`IConfidentialAirdropTypes.sol`): `enum DeploymentMode { Clone, UUPS }`.
@@ -31,3 +31,11 @@ export declare const DEDUP_MODE: {
31
31
  };
32
32
  /** @alpha */
33
33
  export type DedupMode = keyof typeof DEDUP_MODE;
34
+ /**
35
+ * The largest value `CommonAirdropParams.maxAcceptedGasFee` can carry - the
36
+ * `uint96` ceiling. Passing it accepts whatever fee the factory resolves, which
37
+ * is the opt-out, not the default.
38
+ *
39
+ * @alpha
40
+ */
41
+ export declare const UINT96_MAX: bigint;
@@ -186,11 +186,24 @@ export declare class EcdsaAirdropClient extends AirdropBaseClient {
186
186
  * consuming either replay guard — the same input can still be claimed for
187
187
  * real afterwards. No fee.
188
188
  *
189
+ * **It is repeatable, not unconditional** (audit-07). While the claim is
190
+ * still available this may be called any number of times; once it is not,
191
+ * this reverts with the same error the consuming claim would raise, in the
192
+ * same order: the mode's dedup slot first (`AddressAlreadyClaimed` /
193
+ * `DedupIdAlreadyClaimed`), the EIP-712 digest second (`SignatureAlreadyUsed`).
194
+ * A rejected preview performs no FHE op, grants no ACL and emits nothing.
195
+ *
196
+ * So this is not a probe for "has this been claimed" - use
197
+ * {@link isSignatureValid}, which never reverts and answers the same
198
+ * question for free. Reach for this when you want the AMOUNT.
199
+ *
189
200
  * An encrypted view: the contract calls `FHE.allow(amount, msg.sender)`, so
190
201
  * the result is recovered from the receipt's ACL `Allowed` event, never
191
202
  * from a simulation. See {@link AirdropBaseClient}'s class TSDoc.
192
203
  *
193
204
  * @returns `{ handle, hash }` — pass `handle` to the Zama relayer's `userDecrypt`.
205
+ * @throws {@link AlreadyClaimedError} when this recipient's dedup slot is consumed.
206
+ * @throws {@link DedupIdConsumedError} when this `dedupId`'s slot is consumed.
194
207
  */
195
208
  getClaimAmount(args: GetClaimAmountArgs): Promise<EncryptedViewResult>;
196
209
  /**
@@ -315,4 +315,63 @@ export declare class ClaimWindowClosedError extends TokenOpsSdkError {
315
315
  cause?: unknown;
316
316
  });
317
317
  }
318
+ /**
319
+ * `create*` refused the fee the factory resolved for this creator: it landed
320
+ * above their `CommonAirdropParams.maxAcceptedGasFee`.
321
+ *
322
+ * The fee is frozen into the instance at initialization and is immutable
323
+ * afterwards, while the value that resolves is set by the factory's fee manager.
324
+ * The bound is how a creator declines a fee raised between planning and sending
325
+ * - so the fix is a deliberate one: re-send with a higher bound, or wait for the
326
+ * fee to come down. Raising the bound to {@link UINT96_MAX} accepts whatever the
327
+ * factory charges, now and at every later re-plan.
328
+ *
329
+ * @alpha
330
+ */
331
+ export declare class GasFeeNotAcceptedError extends TokenOpsSdkError {
332
+ readonly name = "GasFeeNotAcceptedError";
333
+ readonly context: {
334
+ method: string;
335
+ contractAddress: Address;
336
+ /** The fee the factory resolved for this creator, in wei. */
337
+ resolvedFee?: bigint;
338
+ /** The bound the creator declared, in wei. */
339
+ maxAcceptedGasFee?: bigint;
340
+ };
341
+ constructor(args: {
342
+ method: string;
343
+ contractAddress: Address;
344
+ resolvedFee?: bigint;
345
+ maxAcceptedGasFee?: bigint;
346
+ cause?: unknown;
347
+ });
348
+ }
349
+ /**
350
+ * The canonical factory's registry does not contain this address, so it is not
351
+ * an airdrop this SDK deployed.
352
+ *
353
+ * Instances are the wrong place to ask. An address with the same ABI, the same
354
+ * events and a pool the deployer controls is trivial to stand up, and nothing
355
+ * observable at the instance distinguishes it - `factory.isAirdrop` against a
356
+ * factory address taken from `src/core/addresses.ts` is the only check that
357
+ * means anything (audit-10).
358
+ *
359
+ * @alpha
360
+ */
361
+ export declare class UnrecognisedAirdropError extends TokenOpsSdkError {
362
+ readonly name = "UnrecognisedAirdropError";
363
+ readonly context: {
364
+ method: string;
365
+ contractAddress: Address;
366
+ factory: Address;
367
+ };
368
+ constructor(args: {
369
+ method: string;
370
+ /** The address that failed the check. */
371
+ contractAddress: Address;
372
+ /** The factory whose registry was consulted. */
373
+ factory: Address;
374
+ cause?: unknown;
375
+ });
376
+ }
318
377
  export declare const airdropProductMapper: RevertNameMapper;
@@ -1,19 +1,15 @@
1
+ import type { AbiParameterToPrimitiveType, ExtractAbiFunction } from "abitype";
1
2
  import type { Account, Address, Hex, PublicClient, WalletClient } from "viem";
2
3
  import { type SdkTelemetry } from "../core/telemetry.js";
3
4
  import type { EncryptedInput } from "../fhe/types.js";
5
+ import { airdropFactoryAbi as factoryAbi } from "./abis/factory.js";
4
6
  import type { WriteAccountOverride } from "./airdrop-base.js";
5
7
  import { type DeploymentMode } from "./constants.js";
6
8
  import { type EncryptorSource } from "./encryption.js";
7
9
  import type { CommonAirdropParams, CreateAirdropArgs, CreateAirdropResult, EcdsaAirdropParams, MerkleAirdropParams } from "./types.js";
8
- /** The on-chain `CommonAirdropParams` struct, field-for-field. */
9
- interface ContractCommonParams {
10
- token: Address;
11
- startTime: number;
12
- endTime: number;
13
- canExtendClaimWindow: boolean;
14
- unwrappable: boolean;
15
- complianceAdmin: Address;
16
- }
10
+ type ContractMerkleParams = AbiParameterToPrimitiveType<ExtractAbiFunction<typeof factoryAbi, "createMerkleConfidentialAirdrop">["inputs"][0]>;
11
+ /** The on-chain `CommonAirdropParams` struct, field-for-field, straight off the ABI. */
12
+ type ContractCommonParams = ContractMerkleParams["common"];
17
13
  /**
18
14
  * Convert the SDK's `"clone"` / `"uups"` name to the contract's
19
15
  * `DeploymentMode` ordinal.
@@ -362,11 +358,26 @@ export declare class ConfidentialAirdropFactoryClient {
362
358
  * @returns The page, possibly shorter than `limit`.
363
359
  */
364
360
  airdrops(offset: bigint, limit: bigint): Promise<readonly Address[]>;
361
+ /**
362
+ * Whether THIS factory created `candidate` - the genuineness check (audit-10).
363
+ *
364
+ * Nothing observable at an instance proves which factory, if any, deployed
365
+ * it: an attacker can deploy a contract with the same ABI, the same events
366
+ * and a pool it controls. The only trustworthy question is whether the
367
+ * canonical factory's own registry contains the address, which is why this
368
+ * read has to be addressed to a factory taken from
369
+ * `src/core/addresses.ts` rather than one the instance names.
370
+ *
371
+ * @param candidate The address to check.
372
+ * @returns True when this factory's registry contains `candidate`.
373
+ */
374
+ isAirdrop(candidate: Address): Promise<boolean>;
365
375
  /**
366
376
  * The compliance-manager clone bound to an instance.
367
377
  *
368
- * Doubles as a provenance check: the factory records this mapping only for
369
- * instances it deployed, so a zero address means the instance is not ours.
378
+ * Doubles as a provenance check, though a weaker one than {@link isAirdrop}:
379
+ * the factory records this mapping only for instances it deployed, so a zero
380
+ * address means the instance is not ours.
370
381
  *
371
382
  * @param airdrop Instance address.
372
383
  * @returns The clone address, or the zero address when unknown to this factory.
@@ -403,6 +414,37 @@ export declare class ConfidentialAirdropFactoryClient {
403
414
  feeCollector(): Promise<Address>;
404
415
  /** Per-claim gas fee new instances default to absent a {@link setCustomFee} override. */
405
416
  defaultGasFee(): Promise<bigint>;
417
+ /** The factory's own ceiling on every configurable fee and every create-time resolved fee. */
418
+ maxGasFee(): Promise<bigint>;
419
+ /**
420
+ * Set the factory's ceiling on every fee `setDefaultGasFee`/`setCustomFee`
421
+ * may configure, and on the fee resolved at create.
422
+ *
423
+ * Requires `DEFAULT_ADMIN_ROLE`, deliberately not `FEE_MANAGER_ROLE`: the fee
424
+ * manager moves fees only within a bound the admin controls. `0n` admits a
425
+ * zero fee and nothing else.
426
+ *
427
+ * Lowering it below an already-configured fee does not rewrite that fee - the
428
+ * stale value stays in storage and reverts `GasFeeExceedsMaximum` at the next
429
+ * create until it is lowered too.
430
+ *
431
+ * @throws {@link InvalidArgumentError} when `maxGasFee` is below a currently configured fee (`GasFeeExceedsMaximum`).
432
+ */
433
+ setMaxGasFee(maxGasFee: bigint, account?: Account | Address): Promise<Hex>;
434
+ /**
435
+ * The per-claim fee a `create*` from `creator` would freeze into the new
436
+ * instance right now: their {@link getCustomFee} override when enabled, else
437
+ * {@link defaultGasFee}.
438
+ *
439
+ * This is the number `CommonAirdropParams.maxAcceptedGasFee` is checked
440
+ * against, so it is also what to show a creator before they choose a bound.
441
+ * It is a snapshot, not a quote: `FEE_MANAGER_ROLE` can move it between this
442
+ * read and the create, which is exactly what the bound exists to catch.
443
+ *
444
+ * @param creator The account that will send `create*`.
445
+ * @returns The resolved fee in wei.
446
+ */
447
+ resolveGasFee(creator: Address): Promise<bigint>;
406
448
  /**
407
449
  * Point future `createEcdsaAirdrop*` calls at a new ECDSA implementation.
408
450
  * Requires `IMPL_MANAGER_ROLE`. Does not affect already-deployed instances —
@@ -571,17 +613,15 @@ export declare class ConfidentialAirdropFactoryClient {
571
613
  /**
572
614
  * Revoke `role` from `holder`. Requires the role's admin role.
573
615
  *
574
- * Unlike an airdrop instance, the factory declares no "never empty" floor:
575
- * revoking the last `DEFAULT_ADMIN_ROLE` member succeeds, and what it
576
- * freezes is **role administration**, permanently. `DEFAULT_ADMIN_ROLE`
577
- * administers all four operational roles and itself, so with the set empty
578
- * no role can ever be granted or revoked again - including putting an admin
579
- * back. The four operational roles keep working for whoever already holds
580
- * them: a `FEE_MANAGER_ROLE` holder can still call {@link setDefaultGasFee},
581
- * an `IMPL_MANAGER_ROLE` holder can still rotate an implementation. The
582
- * factory therefore keeps running with exactly the role holders it had at
583
- * that moment, with no way to rotate or remove them. Grant the successor
584
- * first.
616
+ * **The factory floors `DEFAULT_ADMIN_ROLE` at one live member** (audit-14),
617
+ * matching the instances. Renouncing or revoking the sole holder reverts
618
+ * `LastAdmin`, and granting the role to the zero address reverts
619
+ * `ZeroAdminGrant`, so the set can neither be emptied nor satisfied by a
620
+ * member nobody controls. Before audit-14 this succeeded and froze role
621
+ * administration permanently; it no longer does.
622
+ *
623
+ * A handover is still available and is the supported way out: grant the
624
+ * successor first, then renounce.
585
625
  *
586
626
  * @returns The transaction hash.
587
627
  */
@@ -1,6 +1,6 @@
1
1
  import type { Address, Hex, PublicClient } from "viem";
2
2
  import type { PreflightResult } from "../core/preflight.js";
3
- import type { DeploymentMode } from "./constants.js";
3
+ import { type DeploymentMode } from "./constants.js";
4
4
  import { type AirdropVariant } from "./errors.js";
5
5
  import type { ConfidentialAirdropFactoryClient } from "./factory.js";
6
6
  import type { EcdsaAirdropParams, MerkleAirdropParams } from "./types.js";
@@ -104,6 +104,49 @@ export interface AssertSaltAvailableArgs {
104
104
  * @alpha
105
105
  */
106
106
  export declare function assertSaltAvailable(args: AssertSaltAvailableArgs): Promise<void>;
107
+ /**
108
+ * The slice of {@link ConfidentialAirdropFactoryClient} {@link assertGasFeeAccepted}
109
+ * reads. Structural so this module keeps its runtime-import-free shape; the real
110
+ * client satisfies it as-is.
111
+ *
112
+ * @alpha
113
+ */
114
+ export interface GasFeeResolver {
115
+ address: Address;
116
+ resolveGasFee(creator: Address): Promise<bigint>;
117
+ }
118
+ /**
119
+ * Inputs for {@link assertGasFeeAccepted}.
120
+ *
121
+ * @alpha
122
+ */
123
+ export interface AssertGasFeeAcceptedArgs {
124
+ factory: GasFeeResolver;
125
+ /** The account that will send `create*`; fee overrides are per-creator. */
126
+ creator: Address;
127
+ /** `CommonAirdropParams.maxAcceptedGasFee`, in wei. */
128
+ maxAcceptedGasFee: bigint;
129
+ /** Label for the thrown error. */
130
+ method: string;
131
+ }
132
+ /**
133
+ * Refuse a create whose resolved per-claim fee exceeds the creator's declared bound.
134
+ *
135
+ * The contract enforces this too. Doing it here buys the reason: the revert
136
+ * carries the two numbers but not which factory knob produced them, and a
137
+ * creator who sees `resolvedFee` next to their own bound can tell a raised
138
+ * default from a custom override they did not know they had.
139
+ *
140
+ * The range check is not redundant with the comparison - a bound outside
141
+ * `uint96` silently wraps at the ABI edge and could encode as a SMALLER number
142
+ * than intended, turning "accept anything" into "accept almost nothing".
143
+ *
144
+ * @throws {@link InvalidArgumentError} when `maxAcceptedGasFee` is negative or above `UINT96_MAX`.
145
+ * @throws {@link GasFeeNotAcceptedError} when the factory resolves a fee above the bound.
146
+ *
147
+ * @alpha
148
+ */
149
+ export declare function assertGasFeeAccepted(args: AssertGasFeeAcceptedArgs): Promise<void>;
107
150
  /**
108
151
  * Inputs for {@link assertPredictionFresh}.
109
152
  *
@@ -163,7 +206,8 @@ export type PreflightCreateArgs = PreflightCreateCommon & ({
163
206
  * transaction, instead of throwing on the first problem.
164
207
  *
165
208
  * Same checks and the same typed errors the write path enforces: chain
166
- * support, the Merkle/UUPS refusal, the creator's effective
209
+ * support, the Merkle/UUPS refusal, the creator's resolved gas fee against
210
+ * `common.maxAcceptedGasFee`, the creator's effective
167
211
  * upgradeability policy when `mode: "uups"` is requested, the stock-wrapper
168
212
  * probe when `unwrappable` is set, a salt collision on the predicted
169
213
  * address, and - when `expectedInitCodeHash` is supplied - implementation
@@ -222,6 +266,40 @@ export interface ClaimPreflightTarget {
222
266
  signature: Hex;
223
267
  }) => Promise<boolean>;
224
268
  }
269
+ /**
270
+ * The slice of {@link ConfidentialAirdropFactoryClient} the genuineness check reads.
271
+ *
272
+ * @alpha
273
+ */
274
+ export interface AirdropRegistry {
275
+ readonly address: Address;
276
+ /**
277
+ * The chain this factory is bound to. Checked against the instance's own
278
+ * `chainId` before the registry is consulted - a registry answer from the
279
+ * wrong chain is worse than no answer, because it reads as a `true`.
280
+ */
281
+ readonly chainId: number;
282
+ isAirdrop(candidate: Address): Promise<boolean>;
283
+ }
284
+ /**
285
+ * Refuse an instance the canonical factory's registry does not contain.
286
+ *
287
+ * The factory has to be one the caller already trusts - taken from
288
+ * `src/core/addresses.ts`, not read off the instance. An instance that names
289
+ * its own factory proves nothing: a fake instance names a fake factory, which
290
+ * answers `true`.
291
+ *
292
+ * @throws {@link UnrecognisedAirdropError} when the registry does not contain `airdrop`.
293
+ *
294
+ * @alpha
295
+ */
296
+ export declare function assertKnownAirdrop(args: {
297
+ factory: AirdropRegistry;
298
+ airdrop: Address;
299
+ /** The chain the instance being vetted lives on. */
300
+ chainId: number;
301
+ method: string;
302
+ }): Promise<void>;
225
303
  /**
226
304
  * The signature material {@link preflightClaim} needs to check an ECDSA claim.
227
305
  *
@@ -265,6 +343,13 @@ export interface PreflightClaimArgs {
265
343
  * `isSignatureValid`, it adds the authorisation check to the report.
266
344
  */
267
345
  signature?: EcdsaClaimPreflightSignature | undefined;
346
+ /**
347
+ * The canonical factory, from `src/core/addresses.ts`. Supplying it adds the
348
+ * genuineness check (audit-10): every other blocker here describes a claim
349
+ * that will fail, but a claim against a look-alike instance SUCCEEDS and
350
+ * delivers nothing. Omitting it skips that one check.
351
+ */
352
+ factory?: AirdropRegistry | undefined;
268
353
  }
269
354
  /**
270
355
  * The three argument-relation rules the Merkle claim path enforces before it touches the