Skip to content

Commit 7375c63

Browse files
authored
Feat/adding support for recurring token allowance permissions (#750)
* update deps * add token-recurring permission support * chore:update deps * update ERC5792 capabilities * add recurring allowance capabilities * update rhinestone/module-sdk deps * chores: addressing review comments. * chore: fix build
1 parent 3621232 commit 7375c63

File tree

11 files changed

+696
-134
lines changed

11 files changed

+696
-134
lines changed

advanced/wallets/react-wallet-v2/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"@nextui-org/react": "1.0.8-beta.5",
2929
"@polkadot/keyring": "^10.1.2",
3030
"@polkadot/types": "^9.3.3",
31-
"@rhinestone/module-sdk": "0.1.18",
31+
"@reown/appkit-experimental": "1.1.5",
32+
"@rhinestone/module-sdk": "0.1.25",
3233
"@solana/web3.js": "1.89.2",
3334
"@taquito/signer": "^15.1.0",
3435
"@taquito/taquito": "^15.1.0",

advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ export const supportedEIP5792CapabilitiesForSCA: GetCapabilitiesResult = {
8383
permissions: {
8484
supported: true,
8585
signerTypes: ['keys'],
86-
permissionTypes: ['contract-call'],
86+
permissionTypes: [
87+
'contract-call',
88+
'native-token-recurring-allowance',
89+
'erc20-recurring-allowance'
90+
],
8791
policyTypes: []
8892
},
8993
atomicBatch: {

advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,93 +4,8 @@
44
export const EIP7715_METHOD = {
55
WALLET_GRANT_PERMISSIONS: 'wallet_grantPermissions'
66
}
7-
8-
export type Signer = MultiKeySigner
9-
export type KeyType = 'secp256k1' | 'secp256r1'
107
// The types of keys that are supported for the following `key` and `keys` signer types.
118
export enum SignerKeyType {
129
SECP256K1 = 0, // EOA - k1
1310
SECP256R1 = 1 // Passkey - r1
1411
}
15-
/*
16-
* A signer representing a multisig signer.
17-
* Each element of `publicKeys` are all explicitly the same `KeyType`, and the public keys are hex-encoded.
18-
*/
19-
export type MultiKeySigner = {
20-
type: 'keys'
21-
data: {
22-
keys: {
23-
type: KeyType
24-
publicKey: `0x${string}`
25-
}[]
26-
}
27-
}
28-
29-
export type Policy = {
30-
type: string
31-
data: Record<string, unknown>
32-
}
33-
// Enum for parameter operators
34-
enum ParamOperator {
35-
EQUAL = 'EQUAL',
36-
GREATER_THAN = 'GREATER_THAN',
37-
LESS_THAN = 'LESS_THAN'
38-
// Add other operators as needed
39-
}
40-
41-
// Enum for operation types
42-
enum Operation {
43-
Call = 'Call',
44-
DelegateCall = 'DelegateCall'
45-
}
46-
47-
// Type for a single argument condition
48-
type ArgumentCondition = {
49-
operator: ParamOperator
50-
value: any // You might want to be more specific based on your use case
51-
}
52-
53-
// Type for a single function permission
54-
type FunctionPermission = {
55-
functionName: string // Function name
56-
args: ArgumentCondition[] // An array of conditions, each corresponding to an argument for the function
57-
valueLimit: bigint // Maximum value that can be transferred for this specific function call
58-
operation?: Operation // (optional) whether this is a call or a delegatecall. Defaults to call
59-
}
60-
export type ContractCallPermission = {
61-
type: 'contract-call'
62-
data: {
63-
address: `0x${string}`
64-
abi: Record<string, unknown>[]
65-
functions: FunctionPermission[]
66-
}
67-
}
68-
69-
// Union type for all possible permissions
70-
export type Permission = ContractCallPermission
71-
72-
export type WalletGrantPermissionsRequest = {
73-
chainId: `0x${string}`
74-
address?: `0x${string}`
75-
expiry: number
76-
signer: Signer
77-
permissions: Permission[]
78-
policies: {
79-
type: string
80-
data: Record<string, unknown>
81-
}[]
82-
}
83-
84-
export type WalletGrantPermissionsResponse = WalletGrantPermissionsRequest & {
85-
context: `0x${string}`
86-
accountMeta?: {
87-
factory: `0x${string}`
88-
factoryData: `0x${string}`
89-
}
90-
signerMeta?: {
91-
// 7679 userOp building
92-
userOpBuilder?: `0x${string}`
93-
// 7710 delegation
94-
delegationManager?: `0x${string}`
95-
}
96-
}

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { SmartAccount, signerToSafeSmartAccount } from 'permissionless/accounts'
88
import { EntryPoint } from 'permissionless/types/entrypoint'
99
import { Address, Hex, createWalletClient, http, toHex } from 'viem'
1010
import { TRUSTED_SMART_SESSIONS_ATTERSTER_ADDRESS } from './builders/SmartSessionUtil'
11-
import { WalletGrantPermissionsRequest, WalletGrantPermissionsResponse } from '@/data/EIP7715Data'
11+
import {
12+
SmartSessionGrantPermissionsRequest,
13+
WalletGrantPermissionsResponse
14+
} from '@reown/appkit-experimental/smart-session'
1215
import { getContext } from './builders/ContextBuilderUtil'
1316
import { Execution, Module } from '@rhinestone/module-sdk'
1417

@@ -56,7 +59,7 @@ export class SafeSmartAccountLib extends SmartAccountLib {
5659

5760
/* 7715 method */
5861
async grantPermissions(
59-
grantPermissionsRequestParameters: WalletGrantPermissionsRequest
62+
grantPermissionsRequestParameters: SmartSessionGrantPermissionsRequest
6063
): Promise<WalletGrantPermissionsResponse> {
6164
if (!this.client?.account) {
6265
throw new Error('Client not initialized')

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/ContextBuilderUtil.ts

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { MULTIKEY_SIGNER_ADDRESSES, TIME_FRAME_POLICY_ADDRESSES } from './SmartSessionUtil'
2-
import type { Session, ChainSession, Account, ActionData } from '@rhinestone/module-sdk'
1+
import {
2+
MOCK_POLICY,
3+
MULTIKEY_SIGNER_ADDRESSES,
4+
TIME_FRAME_POLICY_ADDRESSES
5+
} from './SmartSessionUtil'
6+
import type { Session, ChainSession, Account, ActionData, PolicyData } from '@rhinestone/module-sdk'
37
const {
48
SMART_SESSIONS_ADDRESS,
59
SmartSessionMode,
@@ -28,10 +32,12 @@ import {
2832
MultiKeySigner,
2933
Permission,
3034
Signer,
31-
WalletGrantPermissionsRequest,
35+
SmartSessionGrantPermissionsRequest,
3236
ContractCallPermission,
33-
SignerKeyType
34-
} from '@/data/EIP7715Data'
37+
NativeTokenRecurringAllowancePermission,
38+
ERC20RecurringAllowancePermission
39+
} from '@reown/appkit-experimental/smart-session'
40+
import { SignerKeyType } from '@/data/EIP7715Data'
3541

3642
// Constants for error messages
3743
const ERROR_MESSAGES = {
@@ -76,7 +82,7 @@ async function fetchSessionData(
7682
account: Account,
7783
session: Session
7884
): Promise<{ sessionNonce: bigint; sessionDigest: Hex; permissionId: Hex }> {
79-
const permissionId = (await getPermissionId({ client: publicClient, session })) as Hex
85+
const permissionId = (await getPermissionId({ session })) as Hex
8086
const sessionNonce = await getSessionNonce({ client: publicClient, account, permissionId })
8187
const sessionDigest = await getSessionDigest({
8288
client: publicClient,
@@ -104,14 +110,11 @@ export async function getContext(
104110
{
105111
account,
106112
grantPermissionsRequest
107-
}: { account: Account; grantPermissionsRequest: WalletGrantPermissionsRequest }
113+
}: { account: Account; grantPermissionsRequest: SmartSessionGrantPermissionsRequest }
108114
): Promise<Hex> {
109115
if (!walletClient.account) throw new Error(ERROR_MESSAGES.ACCOUNT_UNDEFINED)
110116

111117
const { chainId: hexChainId } = grantPermissionsRequest
112-
console.log('walletClient.chain:', walletClient.chain)
113-
console.log('publicClient.chain:', publicClient.chain)
114-
console.log('hexChainId:', hexChainId)
115118
if (!walletClient.chain || !publicClient.chain || !hexChainId)
116119
throw new Error(ERROR_MESSAGES.CHAIN_UNDEFINED)
117120
if (toHex(walletClient.chain.id) !== hexChainId || toHex(publicClient.chain.id) !== hexChainId)
@@ -204,25 +207,25 @@ const adjustVInSignature = (
204207
}
205208

206209
/**
207-
* This method transforms the WalletGrantPermissionsRequest into a Session object
210+
* This method transforms the SmartSessionGrantPermissionsRequest into a Session object
208211
* The Session object includes permittied actions and policies.
209212
* It also includes the Session Validator Address(MultiKeySigner module) and Init Data needed for setting up the module.
210-
* @param WalletGrantPermissionsRequest
213+
* @param SmartSessionGrantPermissionsRequest
211214
* @returns
212215
*/
213216
function getSmartSession({
214217
chainId,
215218
expiry,
216219
permissions,
217220
signer
218-
}: WalletGrantPermissionsRequest): Session {
221+
}: SmartSessionGrantPermissionsRequest): Session {
219222
const chainIdNumber = parseInt(chainId, 16)
220223
const actions = getActionsFromPermissions(permissions, chainIdNumber, expiry)
221-
222224
const sessionValidator = getSessionValidatorAddress(signer, chainIdNumber)
223225
const sessionValidatorInitData = getSessionValidatorInitData(signer)
224226

225227
return {
228+
chainId: BigInt(chainId),
226229
sessionValidator,
227230
sessionValidatorInitData,
228231
salt: SESSION_SALT,
@@ -309,13 +312,23 @@ function isContractCallPermission(permission: Permission): permission is Contrac
309312
return permission.type === 'contract-call'
310313
}
311314

315+
function isNativeTokenRecurringAllowancePermission(
316+
permission: Permission
317+
): permission is NativeTokenRecurringAllowancePermission {
318+
return permission.type === 'native-token-recurring-allowance'
319+
}
320+
321+
function isERC20RecurringAllowancePermission(
322+
permission: Permission
323+
): permission is ERC20RecurringAllowancePermission {
324+
return permission.type === 'erc20-recurring-allowance'
325+
}
326+
312327
/**
313328
* This method processes the permissions array from the permissions request and returns the actions array
314-
* Note - Currently only 'contract-call' permission type is supported
315-
* - For each contract-call permission, it creates an action for each function in the permission
329+
* Note - For each permission, it creates an action data
316330
* - It also adds the TIME_FRAME_POLICY for each action as the actionPolicy
317331
* - The expiry time indicated in the permissions request is used as the expiry time for the actions
318-
* - Function Arguments are not supported in this version
319332
* @param permissions - Permissions array from the permissions request
320333
* @param chainId - Chain ID on which the actions are to be performed
321334
* @param expiry - Expiry time for the actions
@@ -327,12 +340,21 @@ function getActionsFromPermissions(
327340
expiry: number
328341
): ActionData[] {
329342
return permissions.reduce((actions: ActionData[], permission) => {
330-
if (!isContractCallPermission(permission)) {
331-
throw new Error(ERROR_MESSAGES.UNSUPPORTED_PERMISSION_TYPE(JSON.stringify(permission)))
332-
}
343+
switch (true) {
344+
case isContractCallPermission(permission):
345+
actions.push(
346+
...createActionForContractCall(permission as ContractCallPermission, chainId, expiry)
347+
)
348+
break
349+
350+
case isNativeTokenRecurringAllowancePermission(permission):
351+
case isERC20RecurringAllowancePermission(permission):
352+
actions.push(...createFallbackActionData(permission, chainId, expiry))
353+
break
333354

334-
const contractCallActions = createActionForContractCall(permission, chainId, expiry)
335-
actions.push(...contractCallActions)
355+
default:
356+
throw new Error(ERROR_MESSAGES.UNSUPPORTED_PERMISSION_TYPE(JSON.stringify(permission)))
357+
}
336358

337359
return actions
338360
}, [])
@@ -372,3 +394,29 @@ function createActionForContractCall(
372394
}
373395
})
374396
}
397+
398+
/**
399+
* This is a fallback action data which will be used to skip the functionSelector and address check.
400+
* */
401+
function createFallbackActionData(
402+
permission: Permission,
403+
chainId: number,
404+
expiry: number
405+
): ActionData[] {
406+
const fallbackActionSelector = '0x00000001'
407+
const fallbackActionAddress = '0x0000000000000000000000000000000000000001'
408+
409+
return [
410+
{
411+
actionTarget: fallbackActionAddress,
412+
actionTargetSelector: fallbackActionSelector,
413+
// Need atleast 1 actionPolicy, so hardcoding the TIME_FRAME_POLICY for now
414+
actionPolicies: [
415+
{
416+
policy: TIME_FRAME_POLICY_ADDRESSES[chainId],
417+
initData: encodePacked(['uint128', 'uint128'], [BigInt(expiry), BigInt(0)])
418+
}
419+
]
420+
}
421+
]
422+
}

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/CosignerService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import axios, { Method, AxiosError } from 'axios'
22
import { UserOperationWithBigIntAsHex } from './UserOpBuilder'
33
import { bigIntReplacer } from '@/utils/HelperUtil'
44
import { COSIGNER_BASE_URL } from '@/utils/ConstantsUtil'
5-
import { WalletGrantPermissionsRequest } from '@/data/EIP7715Data'
5+
import { SmartSessionGrantPermissionsRequest } from '@reown/appkit-experimental/smart-session'
66

77
//--Cosigner Types----------------------------------------------------------------------- //
8-
export type AddPermissionRequest = WalletGrantPermissionsRequest
8+
export type AddPermissionRequest = SmartSessionGrantPermissionsRequest
99

1010
export type AddPermissionResponse = {
1111
pci: string
@@ -164,7 +164,7 @@ export class CosignerService {
164164
getPermissionsContextRequest: GetPermissionsContextRequest
165165
): Promise<GetPermissionsContextResponse> {
166166
// need to change the method to use POST method and pass pci in the body with url as /{address}/getContext
167-
const url = `${this.baseUrl}/${encodeURIComponent(address)}/getContext`
167+
const url = `${this.baseUrl}/${encodeURIComponent(address)}/getcontext`
168168
return await sendCoSignerRequest<
169169
never,
170170
GetPermissionsContextResponse,

advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SmartSessionUtil.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import { Address } from 'viem'
2+
import { baseSepolia, sepolia } from 'viem/chains'
23

34
export const TIME_FRAME_POLICY_ADDRESSES: Record<number, Address> = {
4-
84532: '0x9A6c4974dcE237E01Ff35c602CA9555a3c0Fa5EF',
5-
11155111: '0x6E1FCe0ec6feaD8dBD2D36a5b9eCf8e33A538479'
5+
[baseSepolia.id]: '0x9A6c4974dcE237E01Ff35c602CA9555a3c0Fa5EF',
6+
[sepolia.id]: '0x6E1FCe0ec6feaD8dBD2D36a5b9eCf8e33A538479'
67
}
78

89
export const MULTIKEY_SIGNER_ADDRESSES: Record<number, Address> = {
9-
84532: '0x207b90941d9cff79A750C1E5c05dDaA17eA01B9F',
10-
11155111: '0x3cA2D7D588FA66248a49c1C885997e5017aF9Dc7'
10+
[baseSepolia.id]: '0xcaF0461410340F8F366f1F7F7716cF1D90b6bdA4',
11+
[sepolia.id]: '0x3cA2D7D588FA66248a49c1C885997e5017aF9Dc7'
1112
}
1213

1314
export const MOCK_VALIDATOR_ADDRESSES: Record<number, Address> = {
14-
84532: '0x8F8842B9b7346529484F282902Af173217411076',
15-
11155111: '0xaE15a31afb2770cE4c5C6131925564B03b597Fe3'
15+
[baseSepolia.id]: '0x8F8842B9b7346529484F282902Af173217411076',
16+
[sepolia.id]: '0xaE15a31afb2770cE4c5C6131925564B03b597Fe3'
1617
}
1718

1819
// All on sepolia
1920
export const SIMPLE_SIGNER = '0x6ff7E9992160bB25f5c67b0Ce389c28d8faD3Bfb' as Address
21+
export const MOCK_POLICY = '0xCBdFFA1e3b0bebAD9ea917910322332B2cfaeC26' as Address
2022
export const UNI_ACTION_POLICY = '0x237C7567Ac09D4DB7Dd48852a438F77a6bd65fc4' as Address
2123
export const USAGE_LIMIT_POLICY = '0x1f265E3beDc6ce93e1A36Dc80E1B1c65844F9861' as Address
2224
export const VALUE_LIMIT_POLICY = '0x6F0eC0c77cCAF4c25ff8FF7113D329caAA769688' as Address

advanced/wallets/react-wallet-v2/src/utils/EIP7715RequestHandlerUtils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
22
import { SessionTypes, SignClientTypes } from '@walletconnect/types'
33
import { getSdkError } from '@walletconnect/utils'
44
import SettingsStore from '@/store/SettingsStore'
5+
import { EIP7715_METHOD } from '@/data/EIP7715Data'
56
import {
6-
EIP7715_METHOD,
7-
WalletGrantPermissionsRequest,
7+
SmartSessionGrantPermissionsRequest,
88
WalletGrantPermissionsResponse
9-
} from '@/data/EIP7715Data'
9+
} from '@reown/appkit-experimental/smart-session'
1010
import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib'
1111
import { walletkit } from './WalletConnectUtil'
1212
import { smartAccountWallets } from './SmartAccountUtil'
@@ -49,7 +49,7 @@ export async function approveEIP7715Request(requestEvent: RequestEventArgs) {
4949
switch (request.method) {
5050
case EIP7715_METHOD.WALLET_GRANT_PERMISSIONS: {
5151
const wallet = getSmartAccountLibFromSession(requestSession, chainId)
52-
let grantPermissionsRequestParams: WalletGrantPermissionsRequest = request.params[0]
52+
let grantPermissionsRequestParams: SmartSessionGrantPermissionsRequest = request.params[0]
5353
if (
5454
wallet instanceof SafeSmartAccountLib
5555
//TODO:fix kernel grantPermissions

advanced/wallets/react-wallet-v2/src/utils/ERC7579AccountUtils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ export async function isERC7579ModuleInstalled(
135135
})
136136
const erc7579Module: Module = {
137137
module: moduleAddress,
138-
type: moduleType
138+
type: moduleType,
139+
initData: '0x',
140+
deInitData: '0x',
141+
additionalContext: '0x',
142+
address: moduleAddress
139143
}
140144
return await isModuleInstalled({
141145
client: publicClient, // The client object of type PublicClient from viem

0 commit comments

Comments
 (0)