From a20ce96a57d058d5a9a7a558fc7a6e32c342fd57 Mon Sep 17 00:00:00 2001 From: Snowmanzx <105658828+Snowmanzx@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:34:36 +0300 Subject: [PATCH] fix: use wallet-substituted account in returned spend permission requestSpendPermission lets the wallet replace message.account via mutableData and computes permissionHash from the wallet-returned message, but built the returned permission object from the original pre-substitution typed data. When the wallet substituted the account, permission.account and permissionHash described different permissions. Carry the signed message through a variable and use it for both. Added a test that exercises the substitution. --- .../methods/requestSpendPermission.test.ts | 39 +++++++++++++++++++ .../methods/requestSpendPermission.ts | 7 +++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.test.ts b/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.test.ts index 3f63970d..e70405c2 100644 --- a/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.test.ts +++ b/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.test.ts @@ -327,6 +327,45 @@ describe('requestSpendPermission', () => { }); }); + it('returns the wallet-substituted account from signedData, not the original', async () => { + const capabilities: WalletSignCapabilities = { + spendPermission: { + requireBalance: true, + }, + }; + + // Wallet substitutes message.account via mutableData (e.g. EOA to smart wallet) + const substitutedAccount = '0xabcdef0123456789abcdef0123456789abcdef01' as `0x${string}`; + const substitutedTypedData: SpendPermissionTypedData = { + ...mockTypedData, + message: { + ...mockTypedData.message, + account: substitutedAccount, + }, + }; + + (createSpendPermissionTypedData as Mock).mockReturnValue(mockTypedData); + (mockProviderRequest as Mock).mockResolvedValue({ + signature: mockSignature, + signedData: substitutedTypedData, + }); + (getHash as Mock).mockResolvedValue(mockPermissionHash); + + const result = await requestSpendPermission({ + ...mockRequestData, + capabilities, + }); + + // permissionHash is derived from the substituted message + expect(getHash).toHaveBeenCalledWith({ + permission: substitutedTypedData.message, + chainId: mockRequestData.chainId, + }); + // the returned permission reflects the substituted account, not the original + expect(result.permission).toEqual(substitutedTypedData.message); + expect(result.permission).not.toEqual(mockTypedData.message); + }); + it('should handle invalid wallet_sign response', async () => { const capabilities: WalletSignCapabilities = { spendPermission: { diff --git a/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts b/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts index 850262b3..382a9b23 100644 --- a/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts +++ b/packages/account-sdk/src/interface/public-utilities/spend-permission/methods/requestSpendPermission.ts @@ -78,6 +78,7 @@ const requestSpendPermissionFn = async ( // Check if we should use wallet_sign (when capabilities are provided) or eth_signTypedData_v4 let signature: string; let permissionHash: string; + let permissionMessage: typeof typedData.message; if (capabilities) { // Use wallet_sign with capabilities @@ -122,6 +123,9 @@ const requestSpendPermissionFn = async ( }; signature = signResult.signature; + // Use the wallet-returned (post-substitution) message: mutableData allows the + // wallet to replace message.account, and permissionHash below is computed from it. + permissionMessage = signResult.signedData.message; permissionHash = await getHash({ permission: signResult.signedData.message, chainId, @@ -135,6 +139,7 @@ const requestSpendPermissionFn = async ( }) as Promise, getHash({ permission: typedData.message, chainId }), ]); + permissionMessage = typedData.message; } const permission: SpendPermission = { @@ -142,7 +147,7 @@ const requestSpendPermissionFn = async ( permissionHash, signature, chainId, - permission: typedData.message, + permission: permissionMessage, }; return permission;