Skip to content

Commit

Permalink
Feat/adding support for recurring token allowance permissions (#750)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KannuSingh authored Oct 23, 2024
1 parent 3621232 commit 7375c63
Show file tree
Hide file tree
Showing 11 changed files with 696 additions and 134 deletions.
3 changes: 2 additions & 1 deletion advanced/wallets/react-wallet-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"@nextui-org/react": "1.0.8-beta.5",
"@polkadot/keyring": "^10.1.2",
"@polkadot/types": "^9.3.3",
"@rhinestone/module-sdk": "0.1.18",
"@reown/appkit-experimental": "1.1.5",
"@rhinestone/module-sdk": "0.1.25",
"@solana/web3.js": "1.89.2",
"@taquito/signer": "^15.1.0",
"@taquito/taquito": "^15.1.0",
Expand Down
6 changes: 5 additions & 1 deletion advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ export const supportedEIP5792CapabilitiesForSCA: GetCapabilitiesResult = {
permissions: {
supported: true,
signerTypes: ['keys'],
permissionTypes: ['contract-call'],
permissionTypes: [
'contract-call',
'native-token-recurring-allowance',
'erc20-recurring-allowance'
],
policyTypes: []
},
atomicBatch: {
Expand Down
85 changes: 0 additions & 85 deletions advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,93 +4,8 @@
export const EIP7715_METHOD = {
WALLET_GRANT_PERMISSIONS: 'wallet_grantPermissions'
}

export type Signer = MultiKeySigner
export type KeyType = 'secp256k1' | 'secp256r1'
// The types of keys that are supported for the following `key` and `keys` signer types.
export enum SignerKeyType {
SECP256K1 = 0, // EOA - k1
SECP256R1 = 1 // Passkey - r1
}
/*
* A signer representing a multisig signer.
* Each element of `publicKeys` are all explicitly the same `KeyType`, and the public keys are hex-encoded.
*/
export type MultiKeySigner = {
type: 'keys'
data: {
keys: {
type: KeyType
publicKey: `0x${string}`
}[]
}
}

export type Policy = {
type: string
data: Record<string, unknown>
}
// Enum for parameter operators
enum ParamOperator {
EQUAL = 'EQUAL',
GREATER_THAN = 'GREATER_THAN',
LESS_THAN = 'LESS_THAN'
// Add other operators as needed
}

// Enum for operation types
enum Operation {
Call = 'Call',
DelegateCall = 'DelegateCall'
}

// Type for a single argument condition
type ArgumentCondition = {
operator: ParamOperator
value: any // You might want to be more specific based on your use case
}

// Type for a single function permission
type FunctionPermission = {
functionName: string // Function name
args: ArgumentCondition[] // An array of conditions, each corresponding to an argument for the function
valueLimit: bigint // Maximum value that can be transferred for this specific function call
operation?: Operation // (optional) whether this is a call or a delegatecall. Defaults to call
}
export type ContractCallPermission = {
type: 'contract-call'
data: {
address: `0x${string}`
abi: Record<string, unknown>[]
functions: FunctionPermission[]
}
}

// Union type for all possible permissions
export type Permission = ContractCallPermission

export type WalletGrantPermissionsRequest = {
chainId: `0x${string}`
address?: `0x${string}`
expiry: number
signer: Signer
permissions: Permission[]
policies: {
type: string
data: Record<string, unknown>
}[]
}

export type WalletGrantPermissionsResponse = WalletGrantPermissionsRequest & {
context: `0x${string}`
accountMeta?: {
factory: `0x${string}`
factoryData: `0x${string}`
}
signerMeta?: {
// 7679 userOp building
userOpBuilder?: `0x${string}`
// 7710 delegation
delegationManager?: `0x${string}`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { SmartAccount, signerToSafeSmartAccount } from 'permissionless/accounts'
import { EntryPoint } from 'permissionless/types/entrypoint'
import { Address, Hex, createWalletClient, http, toHex } from 'viem'
import { TRUSTED_SMART_SESSIONS_ATTERSTER_ADDRESS } from './builders/SmartSessionUtil'
import { WalletGrantPermissionsRequest, WalletGrantPermissionsResponse } from '@/data/EIP7715Data'
import {
SmartSessionGrantPermissionsRequest,
WalletGrantPermissionsResponse
} from '@reown/appkit-experimental/smart-session'
import { getContext } from './builders/ContextBuilderUtil'
import { Execution, Module } from '@rhinestone/module-sdk'

Expand Down Expand Up @@ -56,7 +59,7 @@ export class SafeSmartAccountLib extends SmartAccountLib {

/* 7715 method */
async grantPermissions(
grantPermissionsRequestParameters: WalletGrantPermissionsRequest
grantPermissionsRequestParameters: SmartSessionGrantPermissionsRequest
): Promise<WalletGrantPermissionsResponse> {
if (!this.client?.account) {
throw new Error('Client not initialized')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { MULTIKEY_SIGNER_ADDRESSES, TIME_FRAME_POLICY_ADDRESSES } from './SmartSessionUtil'
import type { Session, ChainSession, Account, ActionData } from '@rhinestone/module-sdk'
import {
MOCK_POLICY,
MULTIKEY_SIGNER_ADDRESSES,
TIME_FRAME_POLICY_ADDRESSES
} from './SmartSessionUtil'
import type { Session, ChainSession, Account, ActionData, PolicyData } from '@rhinestone/module-sdk'
const {
SMART_SESSIONS_ADDRESS,
SmartSessionMode,
Expand Down Expand Up @@ -28,10 +32,12 @@ import {
MultiKeySigner,
Permission,
Signer,
WalletGrantPermissionsRequest,
SmartSessionGrantPermissionsRequest,
ContractCallPermission,
SignerKeyType
} from '@/data/EIP7715Data'
NativeTokenRecurringAllowancePermission,
ERC20RecurringAllowancePermission
} from '@reown/appkit-experimental/smart-session'
import { SignerKeyType } from '@/data/EIP7715Data'

// Constants for error messages
const ERROR_MESSAGES = {
Expand Down Expand Up @@ -76,7 +82,7 @@ async function fetchSessionData(
account: Account,
session: Session
): Promise<{ sessionNonce: bigint; sessionDigest: Hex; permissionId: Hex }> {
const permissionId = (await getPermissionId({ client: publicClient, session })) as Hex
const permissionId = (await getPermissionId({ session })) as Hex
const sessionNonce = await getSessionNonce({ client: publicClient, account, permissionId })
const sessionDigest = await getSessionDigest({
client: publicClient,
Expand Down Expand Up @@ -104,14 +110,11 @@ export async function getContext(
{
account,
grantPermissionsRequest
}: { account: Account; grantPermissionsRequest: WalletGrantPermissionsRequest }
}: { account: Account; grantPermissionsRequest: SmartSessionGrantPermissionsRequest }
): Promise<Hex> {
if (!walletClient.account) throw new Error(ERROR_MESSAGES.ACCOUNT_UNDEFINED)

const { chainId: hexChainId } = grantPermissionsRequest
console.log('walletClient.chain:', walletClient.chain)
console.log('publicClient.chain:', publicClient.chain)
console.log('hexChainId:', hexChainId)
if (!walletClient.chain || !publicClient.chain || !hexChainId)
throw new Error(ERROR_MESSAGES.CHAIN_UNDEFINED)
if (toHex(walletClient.chain.id) !== hexChainId || toHex(publicClient.chain.id) !== hexChainId)
Expand Down Expand Up @@ -204,25 +207,25 @@ const adjustVInSignature = (
}

/**
* This method transforms the WalletGrantPermissionsRequest into a Session object
* This method transforms the SmartSessionGrantPermissionsRequest into a Session object
* The Session object includes permittied actions and policies.
* It also includes the Session Validator Address(MultiKeySigner module) and Init Data needed for setting up the module.
* @param WalletGrantPermissionsRequest
* @param SmartSessionGrantPermissionsRequest
* @returns
*/
function getSmartSession({
chainId,
expiry,
permissions,
signer
}: WalletGrantPermissionsRequest): Session {
}: SmartSessionGrantPermissionsRequest): Session {
const chainIdNumber = parseInt(chainId, 16)
const actions = getActionsFromPermissions(permissions, chainIdNumber, expiry)

const sessionValidator = getSessionValidatorAddress(signer, chainIdNumber)
const sessionValidatorInitData = getSessionValidatorInitData(signer)

return {
chainId: BigInt(chainId),
sessionValidator,
sessionValidatorInitData,
salt: SESSION_SALT,
Expand Down Expand Up @@ -309,13 +312,23 @@ function isContractCallPermission(permission: Permission): permission is Contrac
return permission.type === 'contract-call'
}

function isNativeTokenRecurringAllowancePermission(
permission: Permission
): permission is NativeTokenRecurringAllowancePermission {
return permission.type === 'native-token-recurring-allowance'
}

function isERC20RecurringAllowancePermission(
permission: Permission
): permission is ERC20RecurringAllowancePermission {
return permission.type === 'erc20-recurring-allowance'
}

/**
* This method processes the permissions array from the permissions request and returns the actions array
* Note - Currently only 'contract-call' permission type is supported
* - For each contract-call permission, it creates an action for each function in the permission
* Note - For each permission, it creates an action data
* - It also adds the TIME_FRAME_POLICY for each action as the actionPolicy
* - The expiry time indicated in the permissions request is used as the expiry time for the actions
* - Function Arguments are not supported in this version
* @param permissions - Permissions array from the permissions request
* @param chainId - Chain ID on which the actions are to be performed
* @param expiry - Expiry time for the actions
Expand All @@ -327,12 +340,21 @@ function getActionsFromPermissions(
expiry: number
): ActionData[] {
return permissions.reduce((actions: ActionData[], permission) => {
if (!isContractCallPermission(permission)) {
throw new Error(ERROR_MESSAGES.UNSUPPORTED_PERMISSION_TYPE(JSON.stringify(permission)))
}
switch (true) {
case isContractCallPermission(permission):
actions.push(
...createActionForContractCall(permission as ContractCallPermission, chainId, expiry)
)
break

case isNativeTokenRecurringAllowancePermission(permission):
case isERC20RecurringAllowancePermission(permission):
actions.push(...createFallbackActionData(permission, chainId, expiry))
break

const contractCallActions = createActionForContractCall(permission, chainId, expiry)
actions.push(...contractCallActions)
default:
throw new Error(ERROR_MESSAGES.UNSUPPORTED_PERMISSION_TYPE(JSON.stringify(permission)))
}

return actions
}, [])
Expand Down Expand Up @@ -372,3 +394,29 @@ function createActionForContractCall(
}
})
}

/**
* This is a fallback action data which will be used to skip the functionSelector and address check.
* */
function createFallbackActionData(
permission: Permission,
chainId: number,
expiry: number
): ActionData[] {
const fallbackActionSelector = '0x00000001'
const fallbackActionAddress = '0x0000000000000000000000000000000000000001'

return [
{
actionTarget: fallbackActionAddress,
actionTargetSelector: fallbackActionSelector,
// Need atleast 1 actionPolicy, so hardcoding the TIME_FRAME_POLICY for now
actionPolicies: [
{
policy: TIME_FRAME_POLICY_ADDRESSES[chainId],
initData: encodePacked(['uint128', 'uint128'], [BigInt(expiry), BigInt(0)])
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import axios, { Method, AxiosError } from 'axios'
import { UserOperationWithBigIntAsHex } from './UserOpBuilder'
import { bigIntReplacer } from '@/utils/HelperUtil'
import { COSIGNER_BASE_URL } from '@/utils/ConstantsUtil'
import { WalletGrantPermissionsRequest } from '@/data/EIP7715Data'
import { SmartSessionGrantPermissionsRequest } from '@reown/appkit-experimental/smart-session'

//--Cosigner Types----------------------------------------------------------------------- //
export type AddPermissionRequest = WalletGrantPermissionsRequest
export type AddPermissionRequest = SmartSessionGrantPermissionsRequest

export type AddPermissionResponse = {
pci: string
Expand Down Expand Up @@ -164,7 +164,7 @@ export class CosignerService {
getPermissionsContextRequest: GetPermissionsContextRequest
): Promise<GetPermissionsContextResponse> {
// need to change the method to use POST method and pass pci in the body with url as /{address}/getContext
const url = `${this.baseUrl}/${encodeURIComponent(address)}/getContext`
const url = `${this.baseUrl}/${encodeURIComponent(address)}/getcontext`
return await sendCoSignerRequest<
never,
GetPermissionsContextResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { Address } from 'viem'
import { baseSepolia, sepolia } from 'viem/chains'

export const TIME_FRAME_POLICY_ADDRESSES: Record<number, Address> = {
84532: '0x9A6c4974dcE237E01Ff35c602CA9555a3c0Fa5EF',
11155111: '0x6E1FCe0ec6feaD8dBD2D36a5b9eCf8e33A538479'
[baseSepolia.id]: '0x9A6c4974dcE237E01Ff35c602CA9555a3c0Fa5EF',
[sepolia.id]: '0x6E1FCe0ec6feaD8dBD2D36a5b9eCf8e33A538479'
}

export const MULTIKEY_SIGNER_ADDRESSES: Record<number, Address> = {
84532: '0x207b90941d9cff79A750C1E5c05dDaA17eA01B9F',
11155111: '0x3cA2D7D588FA66248a49c1C885997e5017aF9Dc7'
[baseSepolia.id]: '0xcaF0461410340F8F366f1F7F7716cF1D90b6bdA4',
[sepolia.id]: '0x3cA2D7D588FA66248a49c1C885997e5017aF9Dc7'
}

export const MOCK_VALIDATOR_ADDRESSES: Record<number, Address> = {
84532: '0x8F8842B9b7346529484F282902Af173217411076',
11155111: '0xaE15a31afb2770cE4c5C6131925564B03b597Fe3'
[baseSepolia.id]: '0x8F8842B9b7346529484F282902Af173217411076',
[sepolia.id]: '0xaE15a31afb2770cE4c5C6131925564B03b597Fe3'
}

// All on sepolia
export const SIMPLE_SIGNER = '0x6ff7E9992160bB25f5c67b0Ce389c28d8faD3Bfb' as Address
export const MOCK_POLICY = '0xCBdFFA1e3b0bebAD9ea917910322332B2cfaeC26' as Address
export const UNI_ACTION_POLICY = '0x237C7567Ac09D4DB7Dd48852a438F77a6bd65fc4' as Address
export const USAGE_LIMIT_POLICY = '0x1f265E3beDc6ce93e1A36Dc80E1B1c65844F9861' as Address
export const VALUE_LIMIT_POLICY = '0x6F0eC0c77cCAF4c25ff8FF7113D329caAA769688' as Address
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils'
import { SessionTypes, SignClientTypes } from '@walletconnect/types'
import { getSdkError } from '@walletconnect/utils'
import SettingsStore from '@/store/SettingsStore'
import { EIP7715_METHOD } from '@/data/EIP7715Data'
import {
EIP7715_METHOD,
WalletGrantPermissionsRequest,
SmartSessionGrantPermissionsRequest,
WalletGrantPermissionsResponse
} from '@/data/EIP7715Data'
} from '@reown/appkit-experimental/smart-session'
import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib'
import { walletkit } from './WalletConnectUtil'
import { smartAccountWallets } from './SmartAccountUtil'
Expand Down Expand Up @@ -49,7 +49,7 @@ export async function approveEIP7715Request(requestEvent: RequestEventArgs) {
switch (request.method) {
case EIP7715_METHOD.WALLET_GRANT_PERMISSIONS: {
const wallet = getSmartAccountLibFromSession(requestSession, chainId)
let grantPermissionsRequestParams: WalletGrantPermissionsRequest = request.params[0]
let grantPermissionsRequestParams: SmartSessionGrantPermissionsRequest = request.params[0]
if (
wallet instanceof SafeSmartAccountLib
//TODO:fix kernel grantPermissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ export async function isERC7579ModuleInstalled(
})
const erc7579Module: Module = {
module: moduleAddress,
type: moduleType
type: moduleType,
initData: '0x',
deInitData: '0x',
additionalContext: '0x',
address: moduleAddress
}
return await isModuleInstalled({
client: publicClient, // The client object of type PublicClient from viem
Expand Down
Loading

0 comments on commit 7375c63

Please sign in to comment.