diff --git a/packages/smart-accounts-kit/CHANGELOG.md b/packages/smart-accounts-kit/CHANGELOG.md index aaf9c694..c7f3c93e 100644 --- a/packages/smart-accounts-kit/CHANGELOG.md +++ b/packages/smart-accounts-kit/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Experimental `createx402DelegationProvider` added to @metamask/smart-accounts-kit/experimental ([#248](https://github.com/MetaMask/smart-accounts-kit/pull/248)) +- Experimental `createx402DelegationProvider` added to @metamask/smart-accounts-kit/experimental ([#248](https://github.com/MetaMask/smart-accounts-kit/pull/248), [#251](https://github.com/MetaMask/smart-accounts-kit/pull/251)) - New `generateSalt` function added to @metamask/smart-accounts-kit/utils ([#248](https://github.com/MetaMask/smart-accounts-kit/pull/248)) - ERC-7715 `token-approval-revocation` permission type ([#226](https://github.com/MetaMask/smart-accounts-kit/pull/226), [#237](https://github.com/MetaMask/smart-accounts-kit/pull/237)) - `CaveatBuilder` for `ApprovalRevocationEnforcer`, deployment address added to `SmartAccountsEnvironment` ([#226](https://github.com/metamask/smart-accounts-kit/pull/226), [#237](https://github.com/MetaMask/smart-accounts-kit/pull/237)) diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProvider.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProvider.ts index 329dcff6..f42c11d5 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProvider.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProvider.ts @@ -42,9 +42,10 @@ export function createx402DelegationProvider( ): Promise => { const { account, + createDelegationConfig, delegationManager, existingDelegations, - createDelegationConfig, + rootDelegator, } = await resolveDelegationCreationContext(config, requirements); const delegation = createOpenDelegation(createDelegationConfig); @@ -76,7 +77,7 @@ export function createx402DelegationProvider( return { delegationManager, permissionContext, - delegator: delegation.delegator, + delegator: rootDelegator, }; }; } diff --git a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts index c7a7b7a2..9f2de1a8 100644 --- a/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts +++ b/packages/smart-accounts-kit/src/experimental/x402DelegationProviderUtils.ts @@ -63,9 +63,10 @@ type EnsureExpirySufficientlyConstrainedParams = { */ export type DelegationCreationContext = { account: Account; + createDelegationConfig: Parameters[0]; delegationManager: Address; existingDelegations: Delegation[]; - createDelegationConfig: Parameters[0]; + rootDelegator: Address; }; /** @@ -552,10 +553,14 @@ export const resolveDelegationCreationContext = async ( }; } + const rootDelegator = + existingDelegations[existingDelegations.length - 1]?.delegator ?? from; + return { account, + createDelegationConfig, delegationManager, existingDelegations, - createDelegationConfig, + rootDelegator, }; }; diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProvider.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProvider.test.ts index 09f99d51..3467b2bf 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProvider.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProvider.test.ts @@ -48,9 +48,11 @@ const mockPayeeEnforcer = '0x2000000000000000000000000000000000000004' as Hex; const mockTimestampEnforcer = '0x2000000000000000000000000000000000000005' as Hex; const mockDelegator = '0x3000000000000000000000000000000000000003' as Hex; +const mockRootDelegator = '0x3100000000000000000000000000000000000003' as Hex; const mockSignature = '0xabc123' as Hex; const mockTypedData = { domain: {}, message: {} }; const mockPermissionContext = '0xfeed' as Hex; +const mockParentPermissionContext = '0xbeef' as Hex; const mockAllowedCalldataTerms = '0x3333' as Hex; const mockGeneratedSalt = '0x1111111111111111111111111111111111111111111111111111111111111111' as Hex; @@ -141,7 +143,16 @@ describe('createx402DelegationProvider', () => { mockTypedData, ); delegationMocks.encodeDelegations.mockReturnValue(mockPermissionContext); - delegationMocks.decodeDelegations.mockReturnValue([]); + delegationMocks.decodeDelegations.mockReturnValue([ + { + delegate: '0xb00000000000000000000000000000000000000b', + delegator: mockRootDelegator, + authority: mockAuthority, + caveats: [], + salt: '0x44', + signature: '0x55', + }, + ]); }); it('creates and signs a delegation using default from/salt values', async () => { @@ -151,6 +162,7 @@ describe('createx402DelegationProvider', () => { account, environment, caveats: [], + parentPermissionContext: mockParentPermissionContext, }); const result = await provider(mockRequirements); @@ -182,16 +194,19 @@ describe('createx402DelegationProvider', () => { }, ); expect(account.signTypedData).toHaveBeenCalledWith(mockTypedData); + const decodedDelegations = + delegationMocks.decodeDelegations.mock.results[0]?.value ?? []; expect(delegationMocks.encodeDelegations).toHaveBeenCalledWith([ { ...delegationMocks.createOpenDelegation.mock.results[0]?.value, signature: mockSignature, }, + ...decodedDelegations, ]); expect(result).toStrictEqual({ delegationManager: mockDelegationManager, permissionContext: mockPermissionContext, - delegator: mockDelegator, + delegator: mockRootDelegator, }); }); @@ -202,6 +217,7 @@ describe('createx402DelegationProvider', () => { account, environment, caveats: [], + parentPermissionContext: mockParentPermissionContext, }); await provider({ @@ -223,6 +239,7 @@ describe('createx402DelegationProvider', () => { account, environment, caveats: [], + parentPermissionContext: mockParentPermissionContext, }); await expect( @@ -240,6 +257,7 @@ describe('createx402DelegationProvider', () => { const provider = createx402DelegationProvider({ account, environment: createMockEnvironment(), + parentPermissionContext: mockParentPermissionContext, } as x402DelegationProviderConfig); await expect(provider(mockRequirements)).rejects.toThrow( diff --git a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts index 3807d610..0c3dfa7d 100644 --- a/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts +++ b/packages/smart-accounts-kit/test/experimental/x402DelegationProviderUtils.test.ts @@ -509,6 +509,7 @@ describe('x402DelegationProviderUtils', () => { environment: baseEnvironment, }), ); + expect(result.rootDelegator).toBe(mockAccount.address); } finally { getEnvironmentSpy.mockRestore(); } @@ -556,6 +557,40 @@ describe('x402DelegationProviderUtils', () => { environment: baseEnvironment, }), ); + expect(result.rootDelegator).toBe( + '0xba000000000000000000000000000000000000ba', + ); + }); + + it('uses from as rootDelegator when parentPermissionContext is not provided', async () => { + const from = '0xca000000000000000000000000000000000000ca' as const; + + const result = await resolveDelegationCreationContext( + { + account: mockAccount, + environment: baseEnvironment, + caveats: [], + from, + salt: `0x${'99'.repeat(32)}`, + }, + { + scheme: 'exact', + network: 'eip155:1', + asset: facilitatorA, + amount: '1', + payTo: payee, + maxTimeoutSeconds: 120, + extra: { facilitatorAddresses: [facilitatorA] }, + }, + ); + + expect(result.existingDelegations).toStrictEqual([]); + expect(result.rootDelegator).toBe(from); + expect(result.createDelegationConfig).toEqual( + expect.objectContaining({ + from, + }), + ); }); it('uses deferred caveats and deferred parent permission context', async () => { @@ -652,12 +687,14 @@ describe('x402DelegationProviderUtils', () => { }); it('does not throw when facilitators are missing and redeemers are optional', async () => { + const parentDelegation = makeDelegation([]); await expect( resolveDelegationCreationContext( { account: mockAccount, environment: baseEnvironment, salt: `0x${'33'.repeat(32)}`, + parentPermissionContext: [parentDelegation], }, { scheme: 'exact', @@ -697,11 +734,13 @@ describe('x402DelegationProviderUtils', () => { }); it('adds redeemer caveat from redeemers config addresses when facilitators are missing', async () => { + const parentDelegation = makeDelegation([]); const result = await resolveDelegationCreationContext( { account: mockAccount, environment: baseEnvironment, salt: `0x${'33'.repeat(32)}`, + parentPermissionContext: [parentDelegation], redeemers: { requireRedeemers: true, addresses: [facilitatorB], @@ -732,6 +771,7 @@ describe('x402DelegationProviderUtils', () => { }); it('resolves deferred redeemers configuration and addresses', async () => { + const parentDelegation = makeDelegation([]); const deferredRedeemerAddresses = vi.fn(async () => [facilitatorB]); const deferredRedeemersConfig = vi.fn(async () => ({ requireRedeemers: true, @@ -743,6 +783,7 @@ describe('x402DelegationProviderUtils', () => { account: mockAccount, environment: baseEnvironment, salt: `0x${'33'.repeat(32)}`, + parentPermissionContext: [parentDelegation], redeemers: deferredRedeemersConfig, }, { @@ -775,6 +816,7 @@ describe('x402DelegationProviderUtils', () => { it('resolves deferred expirySeconds and applies timestamp caveat', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2024-01-01T00:00:00Z')); + const parentDelegation = makeDelegation([]); const deferredExpirySeconds = vi.fn(async () => 120); const result = await resolveDelegationCreationContext( @@ -784,6 +826,7 @@ describe('x402DelegationProviderUtils', () => { caveats: [], salt: `0x${'66'.repeat(32)}`, expirySeconds: deferredExpirySeconds, + parentPermissionContext: [parentDelegation], }, { scheme: 'exact', @@ -816,6 +859,7 @@ describe('x402DelegationProviderUtils', () => { it('resolves synchronous deferred expirySeconds and applies timestamp caveat', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2024-01-01T00:00:00Z')); + const parentDelegation = makeDelegation([]); const deferredExpirySeconds = vi.fn(() => 90); const result = await resolveDelegationCreationContext( @@ -825,6 +869,7 @@ describe('x402DelegationProviderUtils', () => { caveats: [], salt: `0x${'66'.repeat(32)}`, expirySeconds: deferredExpirySeconds, + parentPermissionContext: [parentDelegation], }, { scheme: 'exact',