From e9c8f8c0ab5e501c57ed9289e96d4bbdc0d7dd5c Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 28 May 2026 15:53:51 +1200 Subject: [PATCH 1/6] When createx402DelegationProvider, resolve the redeemers as the intersection of specified redeemer addresses, and facilitatorAddresses - if only one of the two is specified, that value is used --- .../x402DelegationProviderUtils.ts | 65 +++++++++++++++---- .../x402DelegationProviderUtils.test.ts | 15 ++--- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index 9f2de1a8..0b920e47 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -34,7 +34,7 @@ type EnsureRedeemerSufficientlyConstrainedParams = { redeemerEnforcer: Hex; caveats: Caveat[]; existingDelegations: Delegation[]; - redeemerAddresses: Hex[]; + redeemerAddresses: Address[] | undefined; requireRedeemers: boolean; }; @@ -58,6 +58,14 @@ type EnsureExpirySufficientlyConstrainedParams = { expirySeconds: number; }; +/** + * Inputs for resolving allowed redeemer addresses. + */ +type ResolveRedeemerAddressesParams = { + facilitatorAddresses?: Address[]; + redeemerAddresses?: Address[]; +}; + /** * Resolved context required to build and sign an x402 delegation. */ @@ -236,6 +244,42 @@ export const ensureExpirySufficientlyConstrained = ({ return [...caveats, timestampCaveat]; }; +/** + * Resolves the allowed redeemer addresses from the union of facilitator specified and caller specified redeemer addresses. + * If either are undefined, the other is returned. + * + * @param options - Redeemer address inputs. + * @param options.facilitatorAddresses - Optional facilitator addresses from the PaymentRequirements. + * @param options.redeemerAddresses - Optional redeemer addresses from the RedeemersConfig. + * @returns The allowed redeemer addresses. + */ +const resolveRedeemerAddresses = ({ + facilitatorAddresses, + redeemerAddresses, +}: ResolveRedeemerAddressesParams): Address[] | undefined => { + if (!facilitatorAddresses) { + if (!redeemerAddresses) { + return undefined; + } + return redeemerAddresses; + } + + if (!redeemerAddresses) { + return facilitatorAddresses; + } + + const normalizedFacilitatorAddresses = + facilitatorAddresses.map(normalizeAddress); + + const normalizedRedeemerAddressSet = new Set( + redeemerAddresses.map(normalizeAddress), + ); + + return normalizedFacilitatorAddresses.filter((address) => + normalizedRedeemerAddressSet.has(address), + ); +}; + /** * Ensures caveats include a sufficiently strict redeemer constraint. * @@ -258,9 +302,9 @@ export const ensureRedeemerSufficientlyConstrained = ({ redeemerAddresses, requireRedeemers, }: EnsureRedeemerSufficientlyConstrainedParams): Caveat[] => { - const redeemerAddressNormalized = normalizeAddress(redeemerEnforcer); + const redeemerEnforcerNormalized = normalizeAddress(redeemerEnforcer); - if (redeemerAddresses.length === 0) { + if (!redeemerAddresses || redeemerAddresses.length === 0) { if (!requireRedeemers) { return caveats; } @@ -269,7 +313,7 @@ export const ensureRedeemerSufficientlyConstrained = ({ caveats, existingDelegations, ({ enforcer }) => - normalizeAddress(enforcer) === redeemerAddressNormalized, + normalizeAddress(enforcer) === redeemerEnforcerNormalized, ); if (!hasExistingRedeemerCaveat) { @@ -288,7 +332,7 @@ export const ensureRedeemerSufficientlyConstrained = ({ caveats, existingDelegations, (caveat) => { - if (normalizeAddress(caveat.enforcer) !== redeemerAddressNormalized) { + if (normalizeAddress(caveat.enforcer) !== redeemerEnforcerNormalized) { return false; } @@ -420,17 +464,16 @@ export const resolvex402DelegationCaveats = ({ isScopeOptional: true, }); - const allRedeemerAddresses = new Set( - [...(facilitatorAddresses ?? []), ...(redeemerAddresses ?? [])].map( - normalizeAddress, - ), - ); + const resolvedRedeemerAddresses = resolveRedeemerAddresses({ + facilitatorAddresses, + redeemerAddresses, + }); const caveatsWithRedeemer = ensureRedeemerSufficientlyConstrained({ redeemerEnforcer, caveats: initialCaveats, existingDelegations, - redeemerAddresses: Array.from(allRedeemerAddresses), + redeemerAddresses: resolvedRedeemerAddresses, requireRedeemers, }); diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts index 0c3dfa7d..ec252297 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts @@ -448,15 +448,15 @@ describe('x402DelegationProviderUtils', () => { ).toThrow('Expiry seconds must be a positive number'); }); - it('enforces redeemer caveats from the union of facilitator and configured redeemer addresses', () => { + it('enforces redeemer caveats from the intersection of facilitator and configured redeemer addresses', () => { const result = resolvex402DelegationCaveats({ environment: baseEnvironment, caveatsConfig: [], existingDelegations: [], - facilitatorAddresses: [facilitatorA], + facilitatorAddresses: [facilitatorA, facilitatorB], payee, requireRedeemers: true, - redeemerAddresses: [facilitatorB], + redeemerAddresses: [facilitatorB, facilitatorC], }); const redeemerCaveat = result.find( @@ -467,12 +467,9 @@ describe('x402DelegationProviderUtils', () => { throw new Error('Expected redeemer caveat to be present'); } - expect(decodeRedeemerTerms(redeemerCaveat.terms).redeemers).toEqual( - expect.arrayContaining([facilitatorA, facilitatorB]), - ); - expect(decodeRedeemerTerms(redeemerCaveat.terms).redeemers).toHaveLength( - 2, - ); + expect(decodeRedeemerTerms(redeemerCaveat.terms).redeemers).toEqual([ + facilitatorB, + ]); }); }); From fc0351e995d57ee437035365e2a4732eb343e883 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Thu, 28 May 2026 16:43:09 +1200 Subject: [PATCH 2/6] Add changelog entry --- packages/smart-accounts-kit/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/smart-accounts-kit/CHANGELOG.md b/packages/smart-accounts-kit/CHANGELOG.md index 8669fa0f..4ddb0a3a 100644 --- a/packages/smart-accounts-kit/CHANGELOG.md +++ b/packages/smart-accounts-kit/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Experimental `createx402DelegationProvider` now resolves redeemers as the intersection of `facilitatorAddresses` and `redeemer.addresses` ([#256](https://github.com/MetaMask/smart-accounts-kit/pull/256)) + ## [1.6.0] ### Added From 7422f30d5556486440700a75c69e5eeb061ad826 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 29 May 2026 12:33:39 +1200 Subject: [PATCH 3/6] Throw if the redeemer addresses resolve to an empty array --- .../x402DelegationProviderUtils.ts | 16 ++++++--- .../x402DelegationProviderUtils.test.ts | 34 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index 0b920e47..1bf64084 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -251,7 +251,7 @@ export const ensureExpirySufficientlyConstrained = ({ * @param options - Redeemer address inputs. * @param options.facilitatorAddresses - Optional facilitator addresses from the PaymentRequirements. * @param options.redeemerAddresses - Optional redeemer addresses from the RedeemersConfig. - * @returns The allowed redeemer addresses. + * @returns The allowed redeemer addresses, or undefined if none are specified. */ const resolveRedeemerAddresses = ({ facilitatorAddresses, @@ -275,9 +275,11 @@ const resolveRedeemerAddresses = ({ redeemerAddresses.map(normalizeAddress), ); - return normalizedFacilitatorAddresses.filter((address) => - normalizedRedeemerAddressSet.has(address), + const redeemerAddressesIntersection = normalizedFacilitatorAddresses.filter( + (address) => normalizedRedeemerAddressSet.has(address), ); + + return redeemerAddressesIntersection; }; /** @@ -304,7 +306,13 @@ export const ensureRedeemerSufficientlyConstrained = ({ }: EnsureRedeemerSufficientlyConstrainedParams): Caveat[] => { const redeemerEnforcerNormalized = normalizeAddress(redeemerEnforcer); - if (!redeemerAddresses || redeemerAddresses.length === 0) { + if (redeemerAddresses?.length === 0) { + throw new Error( + 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + ); + } + + if (!redeemerAddresses) { if (!requireRedeemers) { return caveats; } diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts index ec252297..e3359404 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts @@ -77,6 +77,34 @@ describe('x402DelegationProviderUtils', () => { }); describe('ensureRedeemerSufficientlyConstrained', () => { + it('throws when redeemer addresses are empty and redeemers are not required', () => { + expect(() => + ensureRedeemerSufficientlyConstrained({ + redeemerEnforcer, + caveats: [], + existingDelegations: [], + redeemerAddresses: [], + requireRedeemers: false, + }), + ).toThrow( + 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + ); + }); + + it('throws when redeemer addresses are empty and redeemers are required', () => { + expect(() => + ensureRedeemerSufficientlyConstrained({ + redeemerEnforcer, + caveats: [], + existingDelegations: [], + redeemerAddresses: [], + requireRedeemers: true, + }), + ).toThrow( + 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + ); + }); + it('returns caveats unchanged when redeemer addresses are missing and redeemers are optional', () => { const initialCaveats: Caveat[] = []; @@ -84,7 +112,7 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: initialCaveats, existingDelegations: [], - redeemerAddresses: [], + redeemerAddresses: undefined, requireRedeemers: false, }); @@ -97,7 +125,7 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: [], existingDelegations: [], - redeemerAddresses: [], + redeemerAddresses: undefined, requireRedeemers: true, }), ).toThrow('Redeemer must be constrained'); @@ -124,7 +152,7 @@ describe('x402DelegationProviderUtils', () => { }, ]), ], - redeemerAddresses: [], + redeemerAddresses: undefined, requireRedeemers: true, }); From da3400bad3f35cefde97242384d706ad925de527 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Fri, 29 May 2026 12:55:34 +1200 Subject: [PATCH 4/6] Update error message for clarity --- .../src/experimental/x402DelegationProviderUtils.ts | 2 +- .../test/experimental/x402DelegationProviderUtils.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index 1bf64084..ba0695ee 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -308,7 +308,7 @@ export const ensureRedeemerSufficientlyConstrained = ({ if (redeemerAddresses?.length === 0) { throw new Error( - 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', ); } diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts index e3359404..8bb756f8 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts @@ -87,7 +87,7 @@ describe('x402DelegationProviderUtils', () => { requireRedeemers: false, }), ).toThrow( - 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', ); }); @@ -101,7 +101,7 @@ describe('x402DelegationProviderUtils', () => { requireRedeemers: true, }), ).toThrow( - 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap.', + 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', ); }); From 9c3a2a08951a581acbc2f1a73fa70e120f1412f0 Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:11:57 +1200 Subject: [PATCH 5/6] Minor refactor of redeemer address resolution, and clarity improvements to error messages. --- .../x402DelegationProviderUtils.ts | 77 ++++++++++--------- .../x402DelegationProviderUtils.test.ts | 15 ++-- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index ba0695ee..de7fa349 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -34,6 +34,7 @@ type EnsureRedeemerSufficientlyConstrainedParams = { redeemerEnforcer: Hex; caveats: Caveat[]; existingDelegations: Delegation[]; + facilitatorAddresses: Address[] | undefined; redeemerAddresses: Address[] | undefined; requireRedeemers: boolean; }; @@ -61,10 +62,10 @@ type EnsureExpirySufficientlyConstrainedParams = { /** * Inputs for resolving allowed redeemer addresses. */ -type ResolveRedeemerAddressesParams = { - facilitatorAddresses?: Address[]; - redeemerAddresses?: Address[]; -}; +type ResolveRedeemerAddressesParams = Pick< + EnsureRedeemerSufficientlyConstrainedParams, + 'facilitatorAddresses' | 'redeemerAddresses' +>; /** * Resolved context required to build and sign an x402 delegation. @@ -245,13 +246,16 @@ export const ensureExpirySufficientlyConstrained = ({ }; /** - * Resolves the allowed redeemer addresses from the union of facilitator specified and caller specified redeemer addresses. - * If either are undefined, the other is returned. + * Resolves allowed redeemer addresses from facilitator and configured redeemer inputs. + * + * If both inputs are provided, returns their intersection. + * If only one input is provided, returns that input. + * If neither input is provided, returns undefined. * * @param options - Redeemer address inputs. * @param options.facilitatorAddresses - Optional facilitator addresses from the PaymentRequirements. * @param options.redeemerAddresses - Optional redeemer addresses from the RedeemersConfig. - * @returns The allowed redeemer addresses, or undefined if none are specified. + * @returns Resolved allowed redeemer addresses, or undefined when no input is provided. */ const resolveRedeemerAddresses = ({ facilitatorAddresses, @@ -288,40 +292,44 @@ const resolveRedeemerAddresses = ({ * Returns the caveat list unchanged when an existing redeemer caveat is already * strict enough, or appends a new redeemer caveat scoped to facilitator addresses. * - * @param options0 - Redeemer constraint evaluation inputs. - * @param options0.redeemerEnforcer - Address of the redeemer enforcer caveat contract. - * @param options0.caveats - Currently resolved caveats for the delegation being created. - * @param options0.existingDelegations - Existing parent-chain delegations to inspect for inherited constraints. - * @param options0.redeemerAddresses - Optional addresses to which redemption should be constrained. - * @param options0.requireRedeemers - Whether at least one redeemer constraint must exist when no redeemer addresses are supplied. + * @param config - Redeemer constraint evaluation inputs. + * @param config.redeemerEnforcer - Address of the redeemer enforcer caveat contract. + * @param config.caveats - Currently resolved caveats for the delegation being created. + * @param config.existingDelegations - Existing parent-chain delegations to inspect for inherited constraints. + * @param config.redeemerAddresses - Optional addresses to which redemption should be constrained. + * @param config.requireRedeemers - Whether at least one redeemer constraint must exist. * @returns The original caveats when sufficiently constrained, otherwise caveats with a redeemer caveat appended. - * @throws If no facilitator addresses are provided and no redeemer constraint exists. + * @throws If either facilitatorAddresses and/or redeemerAddresses are provided, but the intersection is empty. + * @throws If requireRedeemers is true, but no valid redeemer addresses are provided, and no existing redeemer caveat is found in the existingDelegations. */ -export const ensureRedeemerSufficientlyConstrained = ({ - redeemerEnforcer, - caveats, - existingDelegations, - redeemerAddresses, - requireRedeemers, -}: EnsureRedeemerSufficientlyConstrainedParams): Caveat[] => { - const redeemerEnforcerNormalized = normalizeAddress(redeemerEnforcer); - +export const ensureRedeemerSufficientlyConstrained = ( + config: EnsureRedeemerSufficientlyConstrainedParams, +): Caveat[] => { + // `redeemerAddresses` is the intersection of `facilitatorAddresses` and `redeemerAddresses`. + // If either is undefined, it implies no constraint is specified by that value, and the other is returned. + // If both are undefined, it implies that no constraint is specified overall. + const redeemerAddresses = resolveRedeemerAddresses(config); + + // If the result is defined, but zero-length, it implies no redeemers are allowed, which is a non-satisfiable constraint, so an error is thrown. if (redeemerAddresses?.length === 0) { throw new Error( - 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', + 'No valid redeemer addresses were resolved. If both `redeemers.addresses` and `extra.facilitatorAddresses` are provided, they must overlap. If only one is provided, it must include at least one address.', ); } + const redeemerEnforcer = normalizeAddress(config.redeemerEnforcer); + + const { caveats, existingDelegations } = config; + if (!redeemerAddresses) { - if (!requireRedeemers) { + if (!config.requireRedeemers) { return caveats; } const hasExistingRedeemerCaveat = hasMatchingCaveats( caveats, existingDelegations, - ({ enforcer }) => - normalizeAddress(enforcer) === redeemerEnforcerNormalized, + ({ enforcer }) => normalizeAddress(enforcer) === redeemerEnforcer, ); if (!hasExistingRedeemerCaveat) { @@ -333,14 +341,13 @@ export const ensureRedeemerSufficientlyConstrained = ({ return caveats; } - const redeemerAddressesNormalized = redeemerAddresses.map(normalizeAddress); - const redeemerAddressesSet = new Set(redeemerAddressesNormalized); + const redeemerAddressesSet = new Set(redeemerAddresses.map(normalizeAddress)); const hasSupersedingRedeemerCaveat = hasMatchingCaveats( caveats, existingDelegations, (caveat) => { - if (normalizeAddress(caveat.enforcer) !== redeemerEnforcerNormalized) { + if (normalizeAddress(caveat.enforcer) !== redeemerEnforcer) { return false; } @@ -348,7 +355,7 @@ export const ensureRedeemerSufficientlyConstrained = ({ caveat.terms, ).redeemers.map(normalizeAddress); - // If this redeemer caveat only allows facilitator addresses, it is sufficiently constrained. + // If this redeemer caveat only allows redeemer addresses, it is sufficiently constrained. return allowedRedeemerAddresses.every((item) => redeemerAddressesSet.has(item), ); @@ -472,16 +479,12 @@ export const resolvex402DelegationCaveats = ({ isScopeOptional: true, }); - const resolvedRedeemerAddresses = resolveRedeemerAddresses({ - facilitatorAddresses, - redeemerAddresses, - }); - const caveatsWithRedeemer = ensureRedeemerSufficientlyConstrained({ redeemerEnforcer, + facilitatorAddresses, + redeemerAddresses, caveats: initialCaveats, existingDelegations, - redeemerAddresses: resolvedRedeemerAddresses, requireRedeemers, }); diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts index 8bb756f8..64cc8ba0 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts @@ -83,12 +83,11 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: [], existingDelegations: [], + facilitatorAddresses: undefined, redeemerAddresses: [], requireRedeemers: false, }), - ).toThrow( - 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', - ); + ).toThrow('No valid redeemer addresses were resolved.'); }); it('throws when redeemer addresses are empty and redeemers are required', () => { @@ -97,12 +96,11 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: [], existingDelegations: [], + facilitatorAddresses: undefined, redeemerAddresses: [], requireRedeemers: true, }), - ).toThrow( - 'No valid redeemer addresses were resolved. The intersection of `redeemers.addresses` and `extra.facilitatorAddresses` must be non-empty.', - ); + ).toThrow('No valid redeemer addresses were resolved.'); }); it('returns caveats unchanged when redeemer addresses are missing and redeemers are optional', () => { @@ -112,6 +110,7 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: initialCaveats, existingDelegations: [], + facilitatorAddresses: undefined, redeemerAddresses: undefined, requireRedeemers: false, }); @@ -125,6 +124,7 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: [], existingDelegations: [], + facilitatorAddresses: undefined, redeemerAddresses: undefined, requireRedeemers: true, }), @@ -152,6 +152,7 @@ describe('x402DelegationProviderUtils', () => { }, ]), ], + facilitatorAddresses: undefined, redeemerAddresses: undefined, requireRedeemers: true, }); @@ -172,6 +173,7 @@ describe('x402DelegationProviderUtils', () => { redeemerEnforcer, caveats: initialCaveats, existingDelegations: [], + facilitatorAddresses: undefined, redeemerAddresses: [facilitatorA, facilitatorB], requireRedeemers: true, }); @@ -196,6 +198,7 @@ describe('x402DelegationProviderUtils', () => { }, ]), ], + facilitatorAddresses: undefined, redeemerAddresses: [facilitatorA, facilitatorB], requireRedeemers: true, }); From a1e3c414ea63374045f4e5a0afb5160cdaee02be Mon Sep 17 00:00:00 2001 From: Jeff Smale <6363749+jeffsmale90@users.noreply.github.com> Date: Tue, 2 Jun 2026 11:50:28 +1200 Subject: [PATCH 6/6] Use non-normalized enforcer address in caveat struct --- .../src/experimental/x402DelegationProviderUtils.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index de7fa349..c2d8c05e 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -317,9 +317,9 @@ export const ensureRedeemerSufficientlyConstrained = ( ); } - const redeemerEnforcer = normalizeAddress(config.redeemerEnforcer); + const { caveats, existingDelegations, redeemerEnforcer } = config; - const { caveats, existingDelegations } = config; + const redeemerEnforcerNormalized = normalizeAddress(redeemerEnforcer); if (!redeemerAddresses) { if (!config.requireRedeemers) { @@ -329,7 +329,8 @@ export const ensureRedeemerSufficientlyConstrained = ( const hasExistingRedeemerCaveat = hasMatchingCaveats( caveats, existingDelegations, - ({ enforcer }) => normalizeAddress(enforcer) === redeemerEnforcer, + ({ enforcer }) => + normalizeAddress(enforcer) === redeemerEnforcerNormalized, ); if (!hasExistingRedeemerCaveat) { @@ -347,7 +348,7 @@ export const ensureRedeemerSufficientlyConstrained = ( caveats, existingDelegations, (caveat) => { - if (normalizeAddress(caveat.enforcer) !== redeemerEnforcer) { + if (normalizeAddress(caveat.enforcer) !== redeemerEnforcerNormalized) { return false; }