diff --git a/advanced/wallets/react-wallet-v2/package.json b/advanced/wallets/react-wallet-v2/package.json index 28ddd9c9a..8b04acdd4 100644 --- a/advanced/wallets/react-wallet-v2/package.json +++ b/advanced/wallets/react-wallet-v2/package.json @@ -28,7 +28,7 @@ "@nextui-org/react": "1.0.8-beta.5", "@polkadot/keyring": "^10.1.2", "@polkadot/types": "^9.3.3", - "@rhinestone/module-sdk": "0.1.16", + "@rhinestone/module-sdk": "0.1.18", "@solana/web3.js": "1.89.2", "@taquito/signer": "^15.1.0", "@taquito/taquito": "^15.1.0", diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts index c328021d9..7f582816f 100644 --- a/advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts +++ b/advanced/wallets/react-wallet-v2/src/data/EIP5792Data.ts @@ -9,11 +9,12 @@ export const EIP5792_METHODS = { } // capability names as string literals -export type CapabilityName = 'atomicBatch' | 'paymasterService' | 'sessionKey' +export type CapabilityName = 'atomicBatch' | 'paymasterService' | 'permissions' // Capability type where each key is a capability name and value has `supported` field export type Capabilities = { [K in CapabilityName]?: { supported: boolean + [key: string]: any } } // GetCapabilitiesResult type using mapped types @@ -65,9 +66,26 @@ export const supportedEIP5792CapabilitiesForSCA: GetCapabilitiesResult = { paymasterService: { supported: true }, - // sessionKey: { - // supported: true, - // }, + permissions: { + supported: true, + signerTypes: ['keys'], + permissionTypes: ['contract-call'], + policyTypes: [] + }, + atomicBatch: { + supported: true + } + }, + '0x14a34': { + paymasterService: { + supported: true + }, + permissions: { + supported: true, + signerTypes: ['keys'], + permissionTypes: ['contract-call'], + policyTypes: [] + }, atomicBatch: { supported: true } diff --git a/advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts b/advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts index 61652e732..5a122c986 100644 --- a/advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts +++ b/advanced/wallets/react-wallet-v2/src/data/EIP7715Data.ts @@ -1,5 +1,3 @@ -import { Address } from 'viem' - /** * EIP7715Method */ @@ -7,76 +5,92 @@ export const EIP7715_METHOD = { WALLET_GRANT_PERMISSIONS: 'wallet_grantPermissions' } -// `data` is not necessary for this signer type as the wallet is both the signer and grantor of these permissions -export type WalletSigner = { - type: 'wallet' - data: {} -} - -// A signer representing a single key. -// `id` is a did:key identifier and can therefore represent both Secp256k1 or Secp256r1 keys, among other key types. -export type KeySigner = { - type: 'key' - data: { - id: string - } +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 `ids` is a did:key identifier just like the `key` signer. +/* + * 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: { - ids: string[] - address?: Address + keys: { + type: KeyType + publicKey: `0x${string}` + }[] } } -// An account that can be granted with permissions as in ERC-7710. -export type AccountSigner = { - type: 'account' - data: { - id: `0x${string}` - } +export type Policy = { + type: string + data: Record +} +// Enum for parameter operators +enum ParamOperator { + EQUAL = 'EQUAL', + GREATER_THAN = 'GREATER_THAN', + LESS_THAN = 'LESS_THAN' + // Add other operators as needed } -export enum SignerType { - EOA, - PASSKEY +// Enum for operation types +enum Operation { + Call = 'Call', + DelegateCall = 'DelegateCall' } -export type Signer = { - type: SignerType - data: string +// Type for a single argument condition +type ArgumentCondition = { + operator: ParamOperator + value: any // You might want to be more specific based on your use case } -export type Permission = { - type: PermissionType - policies: Policy[] - required: boolean - data: any +// 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 Policy = { - type: PolicyType - data: any +export type ContractCallPermission = { + type: 'contract-call' + data: { + address: `0x${string}` + abi: Record[] + 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 + }[] +} + +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}` + } } -export type PermissionType = - | 'native-token-transfer' - | 'erc20-token-transfer' - | 'erc721-token-transfer' - | 'erc1155-token-transfer' - | { - custom: any - } -export type PolicyType = - | 'gas-limit' - | 'call-limit' - | 'rate-limit' - | 'spent-limit' - | 'value-limit' - | 'time-frame' - | 'uni-action' - | 'simpler-signer' - | { - custom: any - } diff --git a/advanced/wallets/react-wallet-v2/src/data/ERC7579ModuleData.ts b/advanced/wallets/react-wallet-v2/src/data/ERC7579ModuleData.ts index b9cce6212..df879e969 100644 --- a/advanced/wallets/react-wallet-v2/src/data/ERC7579ModuleData.ts +++ b/advanced/wallets/react-wallet-v2/src/data/ERC7579ModuleData.ts @@ -5,8 +5,8 @@ const { MULTI_FACTOR_VALIDATOR_ADDRESS, OWNABLE_VALIDATOR_ADDRESS, WEBAUTHN_VALIDATOR_ADDRESS, - SCHEDULED_ORDERS_EXECUTER_ADDRESS, - SCHEDULED_TRANSFERS_EXECUTER_ADDRESS + SCHEDULED_ORDERS_EXECUTOR_ADDRESS, + SCHEDULED_TRANSFERS_EXECUTOR_ADDRESS } = require('@rhinestone/module-sdk') as typeof import('@rhinestone/module-sdk') export type ModuleView = @@ -65,7 +65,7 @@ export const supportedModules: Module[] = [ name: 'Schedule Orders Executor', type: 'executor', url: 'schedule-orders-executor', - moduleAddress: SCHEDULED_ORDERS_EXECUTER_ADDRESS, + moduleAddress: SCHEDULED_ORDERS_EXECUTOR_ADDRESS, description: `The Scheduled Orders module allows users to schedule swaps to be executed at a later time, with an optional recurring schedule. This module is an executor that is installed on an account and can be triggered by an automation service at the pre-specified time(s).`, moduleData: '' }, @@ -73,7 +73,7 @@ export const supportedModules: Module[] = [ name: 'Schedule Transfers Executor', type: 'executor', url: '/schedule-transfers-executor', - moduleAddress: SCHEDULED_TRANSFERS_EXECUTER_ADDRESS, + moduleAddress: SCHEDULED_TRANSFERS_EXECUTOR_ADDRESS, description: `The Scheduled Transfers module allows users to schedule token transfers to occur at a future time, with an optional recurring schedule. It is an executor that is installed on an account and can be triggered by an automation service at the pre-specified time(s).`, moduleData: '' } diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts index 162b280ff..c42f01462 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SafeSmartAccountLib.ts @@ -6,34 +6,19 @@ import { import { SmartAccountLib } from './SmartAccountLib' import { SmartAccount, signerToSafeSmartAccount } from 'permissionless/accounts' import { EntryPoint } from 'permissionless/types/entrypoint' -import { - Address, - Hex, - WalletGrantPermissionsParameters, - createWalletClient, - encodeFunctionData, - getAddress, - http, - parseAbi, - type WalletGrantPermissionsReturnType -} from 'viem' -import { MultiKeySigner } from 'viem/_types/experimental/erc7715/types/signer' -import { ModuleType } from 'permissionless/actions/erc7579' -import { - MOCK_VALIDATOR_ADDRESSES, - TRUSTED_SMART_SESSIONS_ATTERSTER_ADDRESS -} from './builders/SmartSessionUtil' -import { Permission } from '@/data/EIP7715Data' -import { getSmartSessionContext } from './builders/ContextBuilderUtil' -import { readContract } from 'viem/actions' +import { Address, Hex, createWalletClient, http, toHex } from 'viem' +import { TRUSTED_SMART_SESSIONS_ATTERSTER_ADDRESS } from './builders/SmartSessionUtil' +import { WalletGrantPermissionsRequest, WalletGrantPermissionsResponse } from '@/data/EIP7715Data' +import { getContext } from './builders/ContextBuilderUtil' import { Execution, Module } from '@rhinestone/module-sdk' const { SMART_SESSIONS_ADDRESS, - REGISTRY_ADDRESS, getTrustAttestersAction, getAccount, - getSmartSessionsValidator + getSmartSessionsValidator, + findTrustedAttesters, + installModule } = require('@rhinestone/module-sdk') as typeof import('@rhinestone/module-sdk') export class SafeSmartAccountLib extends SmartAccountLib { protected ERC_7579_LAUNCHPAD_ADDRESS: Address = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE' @@ -71,8 +56,8 @@ export class SafeSmartAccountLib extends SmartAccountLib { /* 7715 method */ async grantPermissions( - grantPermissionsRequestParameters: WalletGrantPermissionsParameters - ): Promise { + grantPermissionsRequestParameters: WalletGrantPermissionsRequest + ): Promise { if (!this.client?.account) { throw new Error('Client not initialized') } @@ -80,36 +65,30 @@ export class SafeSmartAccountLib extends SmartAccountLib { const walletClient = createWalletClient({ chain: this.chain, - account: this.client.account, + account: this.signer, transport: http() }) console.log('walletClient chainId:', walletClient.chain.id) - let permissionContext = '0x' - try { - permissionContext = await getSmartSessionContext({ - walletClient, - account: getAccount({ - address: this.client.account.address, - type: 'safe' - }), - permissions: [...grantPermissionsRequestParameters.permissions] as unknown as Permission[], - expiry: grantPermissionsRequestParameters.expiry, - signer: grantPermissionsRequestParameters.signer as MultiKeySigner - }) - } catch (error) { - console.error(`Error getting permission context: ${error}`) - throw error - } + + const permissionContext = await getContext(this.publicClient, walletClient, { + account: getAccount({ + address: this.client.account.address, + type: 'safe' + }), + grantPermissionsRequest: grantPermissionsRequestParameters + }) console.log(`Returning the permissions request`) return { - permissionsContext: permissionContext, - grantedPermissions: grantPermissionsRequestParameters.permissions, - expiry: grantPermissionsRequestParameters.expiry, - signerData: { - submitToAddress: this.client.account.address - } - } as WalletGrantPermissionsReturnType + ...grantPermissionsRequestParameters, + context: permissionContext as Hex, + chainId: toHex(this.chain.id), + accountMeta: { + factory: (await this.client.account.getFactory()) || '0x', + factoryData: (await this.client.account.getFactoryData()) || '0x' + }, + expiry: grantPermissionsRequestParameters.expiry + } } /** @@ -127,28 +106,22 @@ export class SafeSmartAccountLib extends SmartAccountLib { try { const setUpSmartAccountForSmartSession: Execution[] = [] - const [isAccountDeployed, doesSmartAccountTrustSmartSessionAttesters] = await Promise.all([ + const [isAccountDeployed, isSmartAccountTrustSmartSessionAttesters] = await Promise.all([ isSmartAccountDeployed(this.publicClient, this.client.account.address), this.isSmartAccountTrustSmartSessionAttesters() ]) let smartSessionValidatorInstalled = false - let mockValidatorInstalled = false - console.log(`SmartSession Address: ${SMART_SESSIONS_ADDRESS}`) - console.log(`mockValidator Address: ${MOCK_VALIDATOR_ADDRESSES[this.chain.id]}`) if (isAccountDeployed) { - ;[smartSessionValidatorInstalled, mockValidatorInstalled] = await Promise.all([ - this.isValidatorModuleInstalled(SMART_SESSIONS_ADDRESS as Address), - this.isValidatorModuleInstalled(MOCK_VALIDATOR_ADDRESSES[this.chain.id] as Address) - ]) + smartSessionValidatorInstalled = await this.isValidatorModuleInstalled( + SMART_SESSIONS_ADDRESS as Address + ) } - console.log({ smartSessionValidatorInstalled, mockValidatorInstalled }) if ( isAccountDeployed && smartSessionValidatorInstalled && - mockValidatorInstalled && - doesSmartAccountTrustSmartSessionAttesters + isSmartAccountTrustSmartSessionAttesters ) { console.log('Account is already set up with required modules') return @@ -157,30 +130,20 @@ export class SafeSmartAccountLib extends SmartAccountLib { console.log('Setting up the Account with required modules') if (!isAccountDeployed || !smartSessionValidatorInstalled) { - const smartSessionValidator: Module = { - module: SMART_SESSIONS_ADDRESS, - type: 'validator' - } - const installSmartSessionValidatorAction = this.getInstallModuleAction( - this.client.account.address, - smartSessionValidator - ) - setUpSmartAccountForSmartSession.push(installSmartSessionValidatorAction) - } + const smartSessionValidator: Module = getSmartSessionsValidator({}) + const installSmartSessionValidatorAction = await installModule({ + client: this.publicClient, + account: getAccount({ + address: this.client.account.address, + type: 'safe' + }), + module: smartSessionValidator + }) - if (!isAccountDeployed || !mockValidatorInstalled) { - const mockSignatureValidator: Module = { - module: MOCK_VALIDATOR_ADDRESSES[this.chain.id], - type: 'validator' - } - const installMockSignatureValidatorAction = this.getInstallModuleAction( - this.client.account.address, - mockSignatureValidator - ) - setUpSmartAccountForSmartSession.push(installMockSignatureValidatorAction) + setUpSmartAccountForSmartSession.push(installSmartSessionValidatorAction[0]) } - if (!doesSmartAccountTrustSmartSessionAttesters) { + if (!isSmartAccountTrustSmartSessionAttesters) { console.log('Smart Account do not trusted the attesters of the smartsessions module') console.log('Enable trusting the attesters of the smartsessions module') const trustAttestersAction = getTrustAttestersAction({ @@ -221,55 +184,14 @@ export class SafeSmartAccountLib extends SmartAccountLib { }) } - private getInstallModuleAction(accountAddress: Address, module: Module): Execution { - return { - target: accountAddress, - value: BigInt(0), - callData: encodeFunctionData({ - abi: [ - { - name: 'installModule', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { - type: 'uint256', - name: 'moduleTypeId' - }, - { - type: 'address', - name: 'module' - }, - { - type: 'bytes', - name: 'initData' - } - ], - outputs: [] - } - ], - functionName: 'installModule', - args: [ - this.parseModuleTypeId(module.type), - getAddress(module.module), - module.initData || '0x' - ] - }) - } - } - private async isSmartAccountTrustSmartSessionAttesters(): Promise { if (!this.client?.account) { throw new Error('Client not initialized') } - const attesters = await readContract(this.publicClient, { - address: REGISTRY_ADDRESS, - abi: parseAbi([ - 'function findTrustedAttesters(address smartAccount) view returns (address[])' - ]), - functionName: 'findTrustedAttesters', - args: [this.client.account.address] + const attesters = await findTrustedAttesters({ + client: this.publicClient, + accountAddress: this.client.account.address }) if (attesters.length > 0) { @@ -283,19 +205,4 @@ export class SafeSmartAccountLib extends SmartAccountLib { return false } - - private parseModuleTypeId(type: ModuleType): bigint { - switch (type) { - case 'validator': - return BigInt(1) - case 'executor': - return BigInt(2) - case 'fallback': - return BigInt(3) - case 'hook': - return BigInt(4) - default: - throw new Error('Invalid module type') - } - } } diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts index f44db06f0..df9d7964b 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/SmartAccountLib.ts @@ -6,7 +6,8 @@ import { http, createClient, HttpTransport, - Address + Address, + Chain } from 'viem' import { EIP155Wallet } from '../EIP155Lib' import { JsonRpcProvider } from '@ethersproject/providers' @@ -32,7 +33,6 @@ import { publicRPCUrl, UrlConfig } from '@/utils/SmartAccountUtil' -import { Chain } from '@/consts/smartAccounts' import { EntryPoint } from 'permissionless/types/entrypoint' import { Erc7579Actions, erc7579Actions } from 'permissionless/actions/erc7579' import { SmartAccount } from 'permissionless/accounts' @@ -120,8 +120,9 @@ export abstract class SmartAccountLib implements EIP155Wallet { }) this.publicClient = createPublicClient({ + chain: this.chain, transport: http(publicClientUrl({ chain: this.chain })) - }).extend(bundlerActions(this.entryPoint)) + }) this.paymasterClient = createPimlicoPaymasterClient({ transport: this.paymasterUrl, diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/ContextBuilderUtil.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/ContextBuilderUtil.ts index c53034f03..6f95c3f6f 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/ContextBuilderUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/ContextBuilderUtil.ts @@ -1,128 +1,83 @@ -import { decodeDIDToPublicKey, KEY_TYPES } from '@/utils/HelperUtil' -import { - MOCK_VALIDATOR_ADDRESSES, - MULTIKEY_SIGNER_ADDRESSES, - TIME_FRAME_POLICY_ADDRESSES -} from './SmartSessionUtil' -import type { Session, ChainSession, Account } from '@rhinestone/module-sdk' +import { MULTIKEY_SIGNER_ADDRESSES, TIME_FRAME_POLICY_ADDRESSES } from './SmartSessionUtil' +import type { Session, ChainSession, Account, ActionData } from '@rhinestone/module-sdk' const { SMART_SESSIONS_ADDRESS, SmartSessionMode, + encode1271Hash, getPermissionId, getSessionDigest, getSessionNonce, - encode1271Hash, encodeSmartSessionSignature, - hashChainSessions, - encodeUseOrEnableSmartSessionSignature, - decodeSmartSessionSignature + hashChainSessions } = require('@rhinestone/module-sdk') as typeof import('@rhinestone/module-sdk') import { - createPublicClient, + type Hex, + PublicClient, + type WalletClient, + concat, encodeAbiParameters, encodePacked, - Hex, - http, - pad, - parseAbiParameters, - PublicClient, - slice, + getAbiItem, toBytes, toFunctionSelector, - toHex, - WalletClient + toHex } from 'viem' import { publicKeyToAddress } from 'viem/accounts' -import { ENTRYPOINT_ADDRESS_V07, getAccountNonce } from 'permissionless' -import { parsePublicKey } from 'webauthn-p256' -import { MultiKeySigner, Permission, Signer, SignerType } from '@/data/EIP7715Data' - -type GetNonceWithContextParams = { - publicClient: PublicClient - account: Account - permissionsContext: Hex -} -type GetDummySignatureParams = { - publicClient: PublicClient - permissionsContext: Hex - account: Account -} -type FormatSignatureParams = { - publicClient: PublicClient - modifiedSignature: Hex - permissionsContext: Hex - account: Account -} -type GetSmartSessionContextParams = { - walletClient: WalletClient - account: Account - permissions: Permission[] - expiry: number - signer: MultiKeySigner -} +import { parsePublicKey as parsePasskeyPublicKey } from 'webauthn-p256' +import { + MultiKeySigner, + Permission, + Signer, + WalletGrantPermissionsRequest, + ContractCallPermission, + SignerKeyType +} from '@/data/EIP7715Data' -export async function getSmartSessionContext({ - walletClient, - account, - permissions, - expiry, - signer -}: GetSmartSessionContextParams): Promise<`0x${string}`> { - if (walletClient.chain === undefined) { - throw new Error('GetSmartSessionContextParams:Chain is undefined') - } - if (walletClient.account === undefined) { - throw new Error('wallet account is undefined') - } +// Constants for error messages +const ERROR_MESSAGES = { + CHAIN_UNDEFINED: 'getContext: chain is undefined', + MISMATCHED_CHAIN_ID: 'getContext: chain mismatch', + ACCOUNT_UNDEFINED: 'getContext: Wallet account is undefined', + CONTRACT_ADDRESS_UNDEFINED: 'Contract address is undefined', + FUNCTIONS_UNDEFINED: 'Functions is undefined', + UNSUPPORTED_SIGNER_TYPE: 'Unsupported signer type', + INVALID_SIGNATURE: 'Invalid signature', + FUNCTION_ABI_NOT_FOUND: (functionName: string) => + `Function ABI not found for function: ${functionName}`, + UNSUPPORTED_KEY_TYPE: (keyType: string) => `Unsupported key type: ${keyType}`, + UNSUPPORTED_PERMISSION_TYPE: (permissionType: string) => + `Unsupported permission type: ${permissionType}. Only 'contract-call' is allowed.` +} +// 32 byte salt for the session +const SESSION_SALT = toHex(toBytes('1', { size: 32 })) - const chainId = walletClient.chain.id - - let signers: Signer[] = [] - // if singer type if multiKeySigner - if (signer.type === 'keys') { - const publicKeys = signer.data.ids.map(id => decodeDIDToPublicKey(id)) - publicKeys.forEach(key => { - if (key.keyType === KEY_TYPES.secp256k1) { - const eoaAddress = publicKeyToAddress(key.key) - const signer = { type: SignerType.EOA, data: eoaAddress } - signers.push(signer) - } - if (key.keyType === KEY_TYPES.secp256r1) { - const passkeyPublicKey = parsePublicKey(key.key as `0x${string}`) - const signer = { - type: SignerType.PASSKEY, - data: encodeAbiParameters(parseAbiParameters('uint256, uint256'), [ - passkeyPublicKey.x, - passkeyPublicKey.y - ]) - } - signers.push(signer) - } - }) - } - const samplePermissionsSession: Session = getSamplePermissions(signers, chainId, { - permissions, - expiry - }) - const session: Session = { - ...samplePermissionsSession, - salt: toHex(toBytes(55, { size: 32 })) +// Build a ChainSession from given parameters +function buildChainSession( + chainId: number, + session: Session, + accountAddress: Hex, + sessionNonce: bigint +): ChainSession { + return { + chainId: BigInt(chainId), + session: { + ...session, + account: accountAddress, + smartSession: SMART_SESSIONS_ADDRESS, + mode: parseInt(SmartSessionMode.ENABLE, 16), + nonce: sessionNonce + } } - const publicClient = createPublicClient({ - chain: walletClient.chain, - transport: http() - }) - const permissionId = (await getPermissionId({ - client: publicClient, - session - })) as Hex - console.log('permissionId', permissionId) - const sessionNonce = await getSessionNonce({ - client: publicClient, - account, - permissionId - }) +} +// Fetch session data using the WalletClient +async function fetchSessionData( + publicClient: PublicClient, + account: Account, + session: Session +): Promise<{ sessionNonce: bigint; sessionDigest: Hex; permissionId: Hex }> { + const permissionId = (await getPermissionId({ client: publicClient, session })) as Hex + const sessionNonce = await getSessionNonce({ client: publicClient, account, permissionId }) const sessionDigest = await getSessionDigest({ client: publicClient, account, @@ -131,40 +86,74 @@ export async function getSmartSessionContext({ permissionId }) - const chainDigests = [ - { - chainId: BigInt(chainId), - sessionDigest - } - ] - - const chainSessions: ChainSession[] = [ - { - chainId: BigInt(chainId), - session: { - ...session, - account: account.address, - smartSession: SMART_SESSIONS_ADDRESS, - mode: 1, - nonce: sessionNonce - } - } - ] - const permissionEnableHash = hashChainSessions(chainSessions) + return { sessionNonce, sessionDigest, permissionId } +} + +/** + * 1. Check if walletClient account is defined, + * Note - currently walletClient account is the smartAccountSigner address not smartAccount + * 2. Check if walletClient chain is same as permissions request chain + * + * @param walletClient + * @param param + * @returns + */ +export async function getContext( + publicClient: PublicClient, + walletClient: WalletClient, + { + account, + grantPermissionsRequest + }: { account: Account; grantPermissionsRequest: WalletGrantPermissionsRequest } +): Promise { + 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) + throw new Error(ERROR_MESSAGES.MISMATCHED_CHAIN_ID) - // const formattedHash = encode1271Hash({ - // account, - // chainId: chainId, - // validator: account.address, - // hash: permissionEnableHash - // }) + const session: Session = getSmartSession(grantPermissionsRequest) - const permissionEnableSig = await walletClient.signMessage({ + // Fetch session data + const { sessionNonce, sessionDigest, permissionId } = await fetchSessionData( + publicClient, + account, + session + ) + + // Build chain session and hash it + const chainSession = buildChainSession( + walletClient.chain.id, + session, + account.address, + sessionNonce + ) + const chainSessionHash = hashChainSessions([chainSession]) + + // Encode chain session hash + const encodedChainSessionHash = encode1271Hash({ + account, + chainId: walletClient.chain.id, + validator: account.address, + hash: chainSessionHash + }) + + // Validate wallet account + if (!walletClient.account) throw new Error(ERROR_MESSAGES.ACCOUNT_UNDEFINED) + + // Sign the message + const encodedChainSessionSignature = await walletClient.signMessage({ account: walletClient.account, - // message: { raw: formattedHash } - message: { raw: permissionEnableHash } + message: { raw: toBytes(encodedChainSessionHash) } }) + // Adjust the signature + const permissionEnableSignature = adjustVInSignature('eth_sign', encodedChainSessionSignature) const encodedSmartSessionSignature = encodeSmartSessionSignature({ mode: SmartSessionMode.ENABLE, permissionId, @@ -172,139 +161,73 @@ export async function getSmartSessionContext({ enableSessionData: { enableSession: { chainDigestIndex: 0, - hashesAndChainIds: chainDigests, + hashesAndChainIds: [{ chainId: BigInt(walletClient.chain.id), sessionDigest }], sessionToEnable: session, - permissionEnableSig + permissionEnableSig: permissionEnableSignature }, - validator: MOCK_VALIDATOR_ADDRESSES[chainId], //account.address, - accountType: 'safe' + validator: account.address, + accountType: account.type } }) - const smartSessionContext = encodePacked( - ['address', 'bytes'], - [SMART_SESSIONS_ADDRESS, encodedSmartSessionSignature] - ) - return smartSessionContext + // Return the encoded packed data - this is the permission context which will be used for preparing the calls + return encodePacked(['address', 'bytes'], [SMART_SESSIONS_ADDRESS, encodedSmartSessionSignature]) } -export async function getDummySignature({ - publicClient, - permissionsContext, - account -}: GetDummySignatureParams) { - const validatorAddress = slice(permissionsContext, 0, 20) - if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { - throw new Error('getDummySignature:Invalid permission context') - } - - const smartSessionSignature = slice(permissionsContext, 20) - const { permissionId, enableSessionData } = decodeSmartSessionSignature({ - signature: smartSessionSignature, - account: account - }) - if (!enableSessionData) { - throw new Error('EnableSessionData is undefined, invalid smartSessionSignature') - } - const signerValidatorInitData = - enableSessionData?.enableSession.sessionToEnable.sessionValidatorInitData - const signers = decodeSigners(signerValidatorInitData) - console.log('signers', signers) - const dummySignatures: `0x${string}`[] = [] - const dummyECDSASignature: `0x${string}` = - '0xe8b94748580ca0b4993c9a1b86b5be851bfc076ff5ce3a1ff65bf16392acfcb800f9b4f1aef1555c7fce5599fffb17e7c635502154a0333ba21f3ae491839af51c' - const dummyPasskeySignature: `0x${string}` = '0x' - for (let i = 0; i < signers.length; i++) { - const signer = signers[i] - if (signer.type === 0) { - dummySignatures.push(dummyECDSASignature) - } else if (signer.type === 1) { - dummySignatures.push(dummyPasskeySignature) - } - } - const concatenatedDummySignature = encodeAbiParameters([{ type: 'bytes[]' }], [dummySignatures]) +// Adjust the V value in the signature +const adjustVInSignature = ( + signingMethod: 'eth_sign' | 'eth_signTypedData', + signature: string +): Hex => { + const ETHEREUM_V_VALUES = [0, 1, 27, 28] + const MIN_VALID_V_VALUE_FOR_SAFE_ECDSA = 27 - return encodeUseOrEnableSmartSessionSignature({ - account: account, - client: publicClient, - enableSessionData: enableSessionData, - permissionId: permissionId, - signature: concatenatedDummySignature - }) -} -export async function formatSignature({ - publicClient, - account, - modifiedSignature, - permissionsContext -}: FormatSignatureParams) { - const validatorAddress = slice(permissionsContext, 0, 20) - if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { - throw new Error('formatSignature:Invalid permission context') - } + let signatureV = Number.parseInt(signature.slice(-2), 16) - const smartSessionSignature = slice(permissionsContext, 20) - const { permissionId, enableSessionData } = decodeSmartSessionSignature({ - signature: smartSessionSignature, - account: account - }) + // Validate V value + if (!ETHEREUM_V_VALUES.includes(signatureV)) throw new Error(ERROR_MESSAGES.INVALID_SIGNATURE) - if (!enableSessionData) { - throw new Error('EnableSessionData is undefined, invalid smartSessionSignature') + // Adjust the signature based on the signing method + if (signingMethod === 'eth_sign') { + if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) { + signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA + } + signatureV += 4 + } else if (signingMethod === 'eth_signTypedData') { + if (signatureV < MIN_VALID_V_VALUE_FOR_SAFE_ECDSA) { + signatureV += MIN_VALID_V_VALUE_FOR_SAFE_ECDSA + } } - return encodeUseOrEnableSmartSessionSignature({ - account: account, - client: publicClient, - enableSessionData: enableSessionData, - permissionId: permissionId, - signature: modifiedSignature - }) + // Return the adjusted signature + return (signature.slice(0, -2) + signatureV.toString(16)) as Hex } -export async function getNonce({ - publicClient, - account, - permissionsContext -}: GetNonceWithContextParams): Promise { - const chainId = await publicClient.getChainId() - const validatorAddress = slice(permissionsContext, 0, 20) - if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { - throw new Error('getNonce:Invalid permission context') - } - return await getAccountNonce(publicClient, { - sender: account.address, - entryPoint: ENTRYPOINT_ADDRESS_V07, - key: BigInt( - pad(validatorAddress, { - dir: 'right', - size: 24 - }) || 0 - ) - }) -} +/** + * This method transforms the WalletGrantPermissionsRequest 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 + * @returns + */ +function getSmartSession({ + chainId, + expiry, + permissions, + signer +}: WalletGrantPermissionsRequest): Session { + const chainIdNumber = parseInt(chainId, 16) + const actions = getActionsFromPermissions(permissions, chainIdNumber, expiry) + + const sessionValidator = getSessionValidatorAddress(signer, chainIdNumber) + const sessionValidatorInitData = getSessionValidatorInitData(signer) -function getSamplePermissions( - signers: Signer[], - chainId: number, - { permissions, expiry }: { permissions: Permission[]; expiry: number } -): Session { - console.log({ expiry }) return { - sessionValidator: MULTIKEY_SIGNER_ADDRESSES[chainId], - sessionValidatorInitData: encodeMultiKeySignerInitData(signers), - salt: toHex(toBytes('1', { size: 32 })), + sessionValidator, + sessionValidatorInitData, + salt: SESSION_SALT, userOpPolicies: [], - actions: permissions.map(permission => ({ - actionTarget: permission.data.target, - actionTargetSelector: toFunctionSelector(permission.data.functionName), - actionPolicies: [ - { - policy: TIME_FRAME_POLICY_ADDRESSES[chainId], - initData: encodePacked(['uint128', 'uint128'], [BigInt(expiry), BigInt(0)]) // hardcoded for demo - } - ] - })), + actions, erc7739Policies: { allowedERC7739Content: [], erc1271Policies: [] @@ -312,44 +235,140 @@ function getSamplePermissions( } } -function encodeMultiKeySignerInitData(signers: Signer[]): Hex { - let encoded: Hex = encodePacked(['uint8'], [signers.length]) - for (const signer of signers) { - encoded = encodePacked(['bytes', 'uint8', 'bytes'], [encoded, signer.type, signer.data as Hex]) +// Type Guard for MultiKey Signer +function isMultiKeySigner(signer: Signer): signer is MultiKeySigner { + return signer.type === 'keys' +} + +/** + * This method processes the MultiKeySigner object from the permissions request and returns an array of SignerKeyType and data + * @param signer + * @returns + */ +function processMultiKeySigner(signer: MultiKeySigner): { type: SignerKeyType; data: string }[] { + return signer.data.keys.map(key => { + switch (key.type) { + case 'secp256k1': + return { + type: SignerKeyType.SECP256K1, + data: publicKeyToAddress(key.publicKey) + } + case 'secp256r1': + const { x, y } = parsePasskeyPublicKey(key.publicKey as Hex) + return { + type: SignerKeyType.SECP256R1, + data: encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [x, y]) + } + default: + throw new Error(ERROR_MESSAGES.UNSUPPORTED_KEY_TYPE(key.type)) + } + }) +} + +/** + * Get the Session Validator Address, based on signer type in permissions request + * Note - Currently only MultiKeySigner is supported + */ +function getSessionValidatorAddress(signer: Signer, chainId: number): Hex { + if (isMultiKeySigner(signer)) { + return MULTIKEY_SIGNER_ADDRESSES[chainId] } + throw new Error(ERROR_MESSAGES.UNSUPPORTED_SIGNER_TYPE) +} + +/** + * Get the Session Validator Init Data, based on signer type in permissions request + * Note - Currently only MultiKeySigner is supported + * This method return the init data in a format which can be used to initialize the MultiKeySigner module + * @param signer + * @returns + */ +function getSessionValidatorInitData(signer: Signer): Hex { + if (isMultiKeySigner(signer)) { + const processedSigners = processMultiKeySigner(signer) + return encodeMultiKeySignersInitData(processedSigners) + } + throw new Error(ERROR_MESSAGES.UNSUPPORTED_SIGNER_TYPE) +} + +/** + * This method encodes the signers array into a format which can be used to initialize the MultiKeySigner module + * @param signers + * @returns + */ +function encodeMultiKeySignersInitData(signers: { type: SignerKeyType; data: string }[]): Hex { + return signers.reduce( + (encoded, signer) => + concat([encoded, encodePacked(['uint8', 'bytes'], [signer.type, signer.data as Hex])]), + encodePacked(['uint8'], [signers.length]) as Hex + ) +} - return encoded +// Type Guard for Contract Call Permissions +function isContractCallPermission(permission: Permission): permission is ContractCallPermission { + return permission.type === 'contract-call' } -function decodeSigners(encodedData: `0x${string}`): Array<{ type: number; data: `0x${string}` }> { - let offset = 2 // Start after '0x' - const signers: Array<{ type: number; data: `0x${string}` }> = [] - - // Decode the number of signers - const signersCount = parseInt(encodedData.slice(offset, offset + 2), 16) - offset += 2 - - for (let i = 0; i < signersCount; i++) { - // Decode signer type - const signerType = parseInt(encodedData.slice(offset, offset + 2), 16) - offset += 2 - - // Determine data length based on signer type - let dataLength: number - if (signerType === 0) { - dataLength = 40 // 20 bytes - } else if (signerType === 1) { - dataLength = 128 // 64 bytes - } else { - throw new Error(`Unknown signer type: ${signerType}`) +/** + * 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 + * - 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 + * @returns + */ +function getActionsFromPermissions( + permissions: Permission[], + chainId: number, + expiry: number +): ActionData[] { + return permissions.reduce((actions: ActionData[], permission) => { + if (!isContractCallPermission(permission)) { + throw new Error(ERROR_MESSAGES.UNSUPPORTED_PERMISSION_TYPE(JSON.stringify(permission))) } - // Decode signer data - const signerData = `0x${encodedData.slice(offset, offset + dataLength)}` as `0x${string}` - offset += dataLength + const contractCallActions = createActionForContractCall(permission, chainId, expiry) + actions.push(...contractCallActions) - signers.push({ type: signerType, data: signerData }) - } + return actions + }, []) +} - return signers +// Create Action for Contract Call Permission +function createActionForContractCall( + permission: ContractCallPermission, + chainId: number, + expiry: number +): ActionData[] { + if (!permission.data.address) throw new Error(ERROR_MESSAGES.CONTRACT_ADDRESS_UNDEFINED) + if (!permission.data.functions || permission.data.functions.length === 0) + throw new Error(ERROR_MESSAGES.FUNCTIONS_UNDEFINED) + + return permission.data.functions.map(functionPermission => { + const functionName = functionPermission.functionName + const abi = permission.data.abi + const functionAbi = getAbiItem({ abi, name: functionName }) + + if (!functionAbi || functionAbi.type !== 'function') { + throw new Error(ERROR_MESSAGES.FUNCTION_ABI_NOT_FOUND(functionName)) + } + + const functionSelector = toFunctionSelector(functionAbi) + + return { + actionTarget: permission.data.address, + actionTargetSelector: functionSelector, + // 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)]) + } + ] + } + }) } diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/CosignerService.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/CosignerService.ts new file mode 100644 index 000000000..960b3e657 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/CosignerService.ts @@ -0,0 +1,201 @@ +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' + +//--Cosigner Types----------------------------------------------------------------------- // +export type AddPermissionRequest = WalletGrantPermissionsRequest + +export type AddPermissionResponse = { + pci: string + key: { + type: KeyType + publicKey: `0x${string}` + } +} + +export type ActivatePermissionsRequest = { + pci: string + context: `0x${string}` +} & AddPermissionRequest + +type RevokePermissionRequest = { + pci: string + signature: string +} + +type CoSignRequest = { + pci: string + userOp: UserOperationWithBigIntAsHex +} + +type CoSignResponse = { + signature: `0x${string}` +} + +type GetPermissionsContextRequest = { + pci: string +} + +type GetPermissionsContextResponse = { + context: `0x${string}` +} + +type GetPermissionsResponse = { + pci: string[] +} + +// -- Custom Error Class --------------------------------------------------- // +export class CoSignerApiError extends Error { + constructor(public status: number, message: string) { + super(message) + this.name = 'CoSignerApiError' + } +} + +// -- Helper Function for API Requests ------------------------------------- // +export async function sendCoSignerRequest< + TRequest, + TResponse, + TQueryParams extends Record +>({ + url, + method, + request, + queryParams = {} as TQueryParams, + headers, + transformRequest +}: { + url: string + method: Method + request?: TRequest + queryParams?: TQueryParams + headers: Record + transformRequest?: (data: TRequest) => unknown +}): Promise { + try { + const config = { + method, + url, + params: queryParams, + headers, + data: + method !== 'GET' + ? transformRequest + ? transformRequest(request as TRequest) + : request + : undefined + } + + const response = await axios(config) + return response.data + } catch (error) { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError + if (axiosError.response) { + throw new CoSignerApiError( + axiosError.response.status, + JSON.stringify(axiosError.response.data) + ) + } else { + throw new CoSignerApiError(500, 'Network error') + } + } + throw error + } +} + +// Class to interact with the WalletConnect CoSigner API +export class CosignerService { + private baseUrl: string + private projectId: string + + constructor(projectId: string) { + this.baseUrl = COSIGNER_BASE_URL + this.projectId = projectId + } + + async addPermission(address: string, data: AddPermissionRequest): Promise { + const url = `${this.baseUrl}/${encodeURIComponent(address)}` + + return await sendCoSignerRequest< + AddPermissionRequest, + AddPermissionResponse, + { projectId: string } + >({ + url, + method: 'POST', + request: data, + queryParams: { projectId: this.projectId }, + headers: { 'Content-Type': 'application/json' } + }) + } + + async activatePermissions( + address: string, + updateData: ActivatePermissionsRequest + ): Promise { + const url = `${this.baseUrl}/${encodeURIComponent(address)}/activate` + await sendCoSignerRequest({ + url, + method: 'POST', + request: updateData, + queryParams: { projectId: this.projectId }, + headers: { 'Content-Type': 'application/json' } + }) + } + + async coSignUserOperation(address: string, coSignData: CoSignRequest): Promise { + const url = `${this.baseUrl}/${encodeURIComponent(address)}/sign` + + return await sendCoSignerRequest({ + url, + method: 'POST', + request: coSignData, + queryParams: { projectId: this.projectId }, + headers: { 'Content-Type': 'application/json' }, + transformRequest: (value: CoSignRequest) => JSON.stringify(value, bigIntReplacer) + }) + } + + async getPermissionsContext( + address: string, + getPermissionsContextRequest: GetPermissionsContextRequest + ): Promise { + // 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` + return await sendCoSignerRequest< + never, + GetPermissionsContextResponse, + { projectId: string; pci: string } + >({ + url, + method: 'GET', + // request: getPermissionsContextRequest, + queryParams: { projectId: this.projectId, pci: getPermissionsContextRequest.pci }, + headers: { 'Content-Type': 'application/json' } + }) + } + + async getPermissions(address: string): Promise { + const url = `${this.baseUrl}/${encodeURIComponent(address)}` + return await sendCoSignerRequest({ + url, + method: 'GET', + queryParams: { projectId: this.projectId }, + headers: { 'Content-Type': 'application/json' } + }) + } + + async revokePermission(address: string, revokeData: RevokePermissionRequest): Promise { + const url = `${this.baseUrl}/${encodeURIComponent(address)}/revoke` + await sendCoSignerRequest({ + url, + method: 'POST', + request: revokeData, + queryParams: { projectId: this.projectId }, + headers: { 'Content-Type': 'application/json' } + }) + } +} diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts index 307450c85..f643d4311 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/SafeUserOpBuilder.ts @@ -1,10 +1,11 @@ import { generatePrivateKey, privateKeyToAccount, signMessage } from 'viem/accounts' import { - BuildUserOpRequestParams, - BuildUserOpResponseReturn, - SendUserOpRequestParams, - SendUserOpResponseReturn, - UserOpBuilder + PrepareCallsParams, + PrepareCallsReturnValue, + SendPreparedCallsParams, + SendPreparedCallsReturnValue, + UserOpBuilder, + UserOperationWithBigIntAsHex } from './UserOpBuilder' import { Address, @@ -31,8 +32,8 @@ import { import { bundlerUrl, paymasterUrl, publicClientUrl } from '@/utils/SmartAccountUtil' import { getChainById } from '@/utils/ChainUtil' import { SAFE_FALLBACK_HANDLER_STORAGE_SLOT } from '@/consts/smartAccounts' -import { formatSignature, getDummySignature, getNonce } from './ContextBuilderUtil' -import { WalletConnectCosigner } from './WalletConnectCosignerUtils' +import { formatSignature, getDummySignature, getNonce } from './UserOpBuilderUtil' +import { CosignerService } from './CosignerService' const { getAccount } = require('@rhinestone/module-sdk') as typeof import('@rhinestone/module-sdk') const ERC_7579_LAUNCHPAD_ADDRESS: Address = '0xEBe001b3D534B9B6E2500FB78E67a1A137f561CE' @@ -49,8 +50,10 @@ export class SafeUserOpBuilder implements UserOpBuilder { }) this.accountAddress = accountAddress } - - async fillUserOp(params: BuildUserOpRequestParams): Promise { + async prepareCalls( + projectId: string, + params: PrepareCallsParams + ): Promise { const privateKey = generatePrivateKey() const signer = privateKeyToAccount(privateKey) @@ -63,7 +66,16 @@ export class SafeUserOpBuilder implements UserOpBuilder { } const version = await this.getVersion() - + const safeAccount = await signerToSafeSmartAccount(this.publicClient, { + entryPoint: ENTRYPOINT_ADDRESS_V07, + signer: signer, + //@ts-ignore + safeVersion: version, + address: this.accountAddress, + safe4337ModuleAddress, + //@ts-ignore + erc7579LaunchpadAddress + }) const paymasterClient = createPimlicoPaymasterClient({ transport: http(paymasterUrl({ chain: this.chain }), { timeout: 30000 @@ -78,18 +90,6 @@ export class SafeUserOpBuilder implements UserOpBuilder { transport: bundlerTransport, entryPoint: ENTRYPOINT_ADDRESS_V07 }) - - const safeAccount = await signerToSafeSmartAccount(this.publicClient, { - entryPoint: ENTRYPOINT_ADDRESS_V07, - signer: signer, - //@ts-ignore - safeVersion: version, - address: this.accountAddress, - safe4337ModuleAddress, - //@ts-ignore - erc7579LaunchpadAddress - }) - const smartAccountClient = createSmartAccountClient({ account: safeAccount, entryPoint: ENTRYPOINT_ADDRESS_V07, @@ -97,7 +97,6 @@ export class SafeUserOpBuilder implements UserOpBuilder { bundlerTransport, middleware: { sponsorUserOperation: paymasterClient.sponsorUserOperation, - // params.capabilities.paymasterService && paymasterClient.sponsorUserOperation, // optional gasPrice: async () => (await pimlicoBundlerClient.getUserOperationGasPrice()).fast // if using pimlico bundler } }) @@ -107,20 +106,30 @@ export class SafeUserOpBuilder implements UserOpBuilder { type: 'safe' }) + const pci = params.capabilities.permissions?.context! + const cosignerService = new CosignerService(projectId) + const caip10AccountAddress = `eip155:${this.chain.id}:${this.accountAddress}` + const permissionsContext = await cosignerService.getPermissionsContext(caip10AccountAddress, { + pci + }) let nonce: bigint = await getNonce({ publicClient: this.publicClient, account, - permissionsContext: params.capabilities.permissions?.context! + permissionsContext: permissionsContext.context }) - - const callData = await smartAccountClient.account.encodeCallData(params.calls) + const callData = await smartAccountClient.account.encodeCallData( + params.calls.map(call => ({ + to: call.to, + value: BigInt(call.value), + data: call.data + })) + ) const dummySignature = await getDummySignature({ publicClient: this.publicClient, account, - permissionsContext: params.capabilities.permissions?.context! + permissionsContext: permissionsContext.context }) - const userOp = await smartAccountClient.prepareUserOperationRequest({ userOperation: { nonce: nonce, @@ -129,7 +138,6 @@ export class SafeUserOpBuilder implements UserOpBuilder { }, account: smartAccountClient.account }) - const hash = getUserOperationHash({ userOperation: userOp, chainId: this.chain.id, @@ -137,62 +145,82 @@ export class SafeUserOpBuilder implements UserOpBuilder { }) return { - userOp: { - ...userOp, - nonce: toHex(userOp.nonce), - callGasLimit: toHex(userOp.callGasLimit), - verificationGasLimit: toHex(userOp.verificationGasLimit), - preVerificationGas: toHex(userOp.preVerificationGas), - maxFeePerGas: toHex(userOp.maxFeePerGas), - maxPriorityFeePerGas: toHex(userOp.maxPriorityFeePerGas), - paymasterPostOpGasLimit: userOp.paymasterPostOpGasLimit - ? toHex(userOp.paymasterPostOpGasLimit) - : undefined, - paymasterVerificationGasLimit: userOp.paymasterVerificationGasLimit - ? toHex(userOp.paymasterVerificationGasLimit) - : undefined, - factory: userOp.factory, - factoryData: userOp.factoryData, - paymaster: userOp.paymaster, - paymasterData: userOp.paymasterData + context: params.capabilities.permissions?.context!, + preparedCalls: { + chainId: toHex(this.chain.id), + type: 'user-operation-v07', + data: userOp }, - hash + signatureRequest: { + hash: hash + } } } - async sendUserOpWithSignature( + async sendPreparedCalls( projectId: string, - params: SendUserOpRequestParams - ): Promise { + params: SendPreparedCallsParams + ): Promise { try { - const { chainId, userOp, permissionsContext, pci } = params + const { context, preparedCalls, signature } = params + const { chainId, data, type } = preparedCalls + const chainIdNumber = parseInt(chainId) + if (type !== 'user-operation-v07') { + throw new Error('Invalid preparedCalls type') + } + + //Get PermissionsContext from WalletConnectCosigner given pci + const pci = context + const cosignerService = new CosignerService(projectId) + const caip10AccountAddress = `eip155:${this.chain.id}:${this.accountAddress}` + const permissionsContext = await cosignerService.getPermissionsContext(caip10AccountAddress, { + pci + }) if (pci && projectId) { - const walletConnectCosigner = new WalletConnectCosigner(projectId) - const caip10AccountAddress = `eip155:${chainId}:${userOp.sender}` - const cosignResponse = await walletConnectCosigner.coSignUserOperation( - caip10AccountAddress, - { - pci, - userOp - } - ) - console.log('cosignResponse:', cosignResponse) - userOp.signature = cosignResponse.signature + const userOpWithBigIntAsHex: UserOperationWithBigIntAsHex = { + ...data, + nonce: toHex(BigInt(data.nonce)), + callGasLimit: toHex(BigInt(data.callGasLimit)), + verificationGasLimit: toHex(BigInt(data.verificationGasLimit)), + preVerificationGas: toHex(BigInt(data.preVerificationGas)), + maxFeePerGas: toHex(BigInt(data.maxFeePerGas)), + maxPriorityFeePerGas: toHex(BigInt(data.maxPriorityFeePerGas)), + paymasterPostOpGasLimit: data.paymasterPostOpGasLimit + ? toHex(BigInt(data.paymasterPostOpGasLimit)) + : undefined, + paymasterVerificationGasLimit: data.paymasterVerificationGasLimit + ? toHex(BigInt(data.paymasterVerificationGasLimit)) + : undefined, + factory: data.factory, + factoryData: data.factoryData, + paymaster: data.paymaster, + paymasterData: data.paymasterData, + signature: signature + } + const cosignerService = new CosignerService(projectId) + const caip10AccountAddress = `eip155:${chainIdNumber}:${userOpWithBigIntAsHex.sender}` + const cosignResponse = await cosignerService.coSignUserOperation(caip10AccountAddress, { + pci, + userOp: userOpWithBigIntAsHex + }) + data.signature = cosignResponse.signature } + const account = getAccount({ - address: userOp.sender, + address: data.sender, type: 'safe' }) - if (permissionsContext) { + if (permissionsContext.context) { const formattedSignature = await formatSignature({ publicClient: this.publicClient, account, - modifiedSignature: userOp.signature, - permissionsContext + modifiedSignature: data.signature, + permissionsContext: permissionsContext.context }) - userOp.signature = formattedSignature + data.signature = formattedSignature } + const bundlerTransport = http(bundlerUrl({ chain: this.chain }), { timeout: 30000 }) @@ -204,24 +232,22 @@ export class SafeUserOpBuilder implements UserOpBuilder { const userOpId = await pimlicoBundlerClient.sendUserOperation({ userOperation: { - ...userOp, - signature: userOp.signature, - callGasLimit: BigInt(userOp.callGasLimit), - nonce: BigInt(userOp.nonce), - preVerificationGas: BigInt(userOp.preVerificationGas), - verificationGasLimit: BigInt(userOp.verificationGasLimit), - maxFeePerGas: BigInt(userOp.maxFeePerGas), - maxPriorityFeePerGas: BigInt(userOp.maxPriorityFeePerGas), + ...data, + signature: data.signature, + callGasLimit: BigInt(data.callGasLimit), + nonce: BigInt(data.nonce), + preVerificationGas: BigInt(data.preVerificationGas), + verificationGasLimit: BigInt(data.verificationGasLimit), + maxFeePerGas: BigInt(data.maxFeePerGas), + maxPriorityFeePerGas: BigInt(data.maxPriorityFeePerGas), paymasterVerificationGasLimit: - userOp.paymasterVerificationGasLimit && BigInt(userOp.paymasterVerificationGasLimit), + data.paymasterVerificationGasLimit && BigInt(data.paymasterVerificationGasLimit), paymasterPostOpGasLimit: - userOp.paymasterPostOpGasLimit && BigInt(userOp.paymasterPostOpGasLimit) + data.paymasterPostOpGasLimit && BigInt(data.paymasterPostOpGasLimit) } }) - return { - userOpId - } + return `${toHex(this.chain.id)}:${userOpId}` } catch (e) { console.log(e) throw new Error('Failed to sign user operation with cosigner') diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts index b15f4b789..c90e0d7a2 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts @@ -1,7 +1,5 @@ import { Address, Hex } from 'viem' -type Call = { to: Address; value: bigint; data: Hex } - export type UserOperationWithBigIntAsHex = { sender: Address nonce: Hex @@ -21,37 +19,49 @@ export type UserOperationWithBigIntAsHex = { initCode?: never paymasterAndData?: never } -export type BuildUserOpRequestParams = { - chainId: number - account: Address - calls: Call[] - capabilities: { - paymasterService?: { url: string } - permissions?: { context: Hex } - } -} -export type BuildUserOpResponseReturn = { - userOp: UserOperationWithBigIntAsHex - hash: Hex -} export type ErrorResponse = { message: string error: string } -export type SendUserOpRequestParams = { - chainId: number - userOp: UserOperationWithBigIntAsHex - pci?: string - permissionsContext?: Hex + +export type PrepareCallsParams = { + from: `0x${string}` + chainId: `0x${string}` + calls: { + to: `0x${string}` + data: `0x${string}` + value: `0x${string}` + }[] + capabilities: Record } -export type SendUserOpResponseReturn = { - userOpId: Hex + +export type PrepareCallsReturnValue = { + preparedCalls: { + type: string + data: any + chainId: `0x${string}` + } + signatureRequest: { + hash: `0x${string}` // hash value of userOperation + } + context: string +} + +export type SendPreparedCallsParams = { + preparedCalls: { + type: string + data: any // userOp + chainId: `0x${string}` + } + signature: `0x${string}` + context: `0x${string}` } +export type SendPreparedCallsReturnValue = string export interface UserOpBuilder { - fillUserOp(params: BuildUserOpRequestParams): Promise - sendUserOpWithSignature( + prepareCalls(projectId: string, params: PrepareCallsParams): Promise + sendPreparedCalls( projectId: string, - params: SendUserOpRequestParams - ): Promise + params: SendPreparedCallsParams + ): Promise } diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilderUtil.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilderUtil.ts new file mode 100644 index 000000000..ede3fee75 --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilderUtil.ts @@ -0,0 +1,153 @@ +import type { Account } from '@rhinestone/module-sdk' +const { + SMART_SESSIONS_ADDRESS, + encodeUseOrEnableSmartSessionSignature, + decodeSmartSessionSignature +} = require('@rhinestone/module-sdk') as typeof import('@rhinestone/module-sdk') +import { encodeAbiParameters, Hex, pad, PublicClient, slice } from 'viem' +import { ENTRYPOINT_ADDRESS_V07, getAccountNonce } from 'permissionless' + +type GetNonceWithContextParams = { + publicClient: PublicClient + account: Account + permissionsContext: Hex +} +type GetDummySignatureParams = { + publicClient: PublicClient + permissionsContext: Hex + account: Account +} +type FormatSignatureParams = { + publicClient: PublicClient + modifiedSignature: Hex + permissionsContext: Hex + account: Account +} + +export async function getDummySignature({ + publicClient, + permissionsContext, + account +}: GetDummySignatureParams) { + const validatorAddress = slice(permissionsContext, 0, 20) + if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { + throw new Error('getDummySignature:Invalid permission context') + } + + const smartSessionSignature = slice(permissionsContext, 20) + const { permissionId, enableSessionData } = decodeSmartSessionSignature({ + signature: smartSessionSignature, + account: account + }) + if (!enableSessionData) { + throw new Error('EnableSessionData is undefined, invalid smartSessionSignature') + } + const signerValidatorInitData = + enableSessionData?.enableSession.sessionToEnable.sessionValidatorInitData + const signers = decodeSigners(signerValidatorInitData) + const dummySignatures: `0x${string}`[] = [] + const dummyECDSASignature: `0x${string}` = + '0xe8b94748580ca0b4993c9a1b86b5be851bfc076ff5ce3a1ff65bf16392acfcb800f9b4f1aef1555c7fce5599fffb17e7c635502154a0333ba21f3ae491839af51c' + const dummyPasskeySignature: `0x${string}` = + '0x00000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000001635bc6d0f68ff895cae8a288ecf7542a6a9cd555df784b73e1e2ea7e9104b1db15e9015d280cb19527881c625fee43fd3a405d5b0d199a8c8e6589a7381209e40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002549960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f47b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a22746278584e465339585f3442797231634d77714b724947422d5f3330613051685a36793775634d30424f45222c226f726967696e223a22687474703a2f2f6c6f63616c686f73743a33303030222c2263726f73734f726967696e223a66616c73652c20226f746865725f6b6579735f63616e5f62655f61646465645f68657265223a22646f206e6f7420636f6d7061726520636c69656e74446174614a534f4e20616761696e737420612074656d706c6174652e205365652068747470733a2f2f676f6f2e676c2f796162506578227d000000000000000000000000' + for (let i = 0; i < signers.length; i++) { + const signer = signers[i] + if (signer.type === 0) { + dummySignatures.push(dummyECDSASignature) + } else if (signer.type === 1) { + dummySignatures.push(dummyPasskeySignature) + } + } + const concatenatedDummySignature = encodeAbiParameters([{ type: 'bytes[]' }], [dummySignatures]) + + return await encodeUseOrEnableSmartSessionSignature({ + account: account, + client: publicClient, + enableSessionData: enableSessionData, + permissionId: permissionId, + signature: concatenatedDummySignature + }) +} +export async function formatSignature({ + publicClient, + account, + modifiedSignature, + permissionsContext +}: FormatSignatureParams) { + const validatorAddress = slice(permissionsContext, 0, 20) + if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { + throw new Error('formatSignature:Invalid permission context') + } + + const smartSessionSignature = slice(permissionsContext, 20) + const { permissionId, enableSessionData } = decodeSmartSessionSignature({ + signature: smartSessionSignature, + account: account + }) + + if (!enableSessionData) { + throw new Error('EnableSessionData is undefined, invalid smartSessionSignature') + } + + return encodeUseOrEnableSmartSessionSignature({ + account: account, + client: publicClient, + enableSessionData: enableSessionData, + permissionId: permissionId, + signature: modifiedSignature + }) +} +export async function getNonce({ + publicClient, + account, + permissionsContext +}: GetNonceWithContextParams): Promise { + const chainId = await publicClient.getChainId() + const validatorAddress = slice(permissionsContext, 0, 20) + if (validatorAddress.toLowerCase() !== SMART_SESSIONS_ADDRESS.toLowerCase()) { + throw new Error('getNonce:Invalid permission context') + } + + return await getAccountNonce(publicClient, { + sender: account.address, + entryPoint: ENTRYPOINT_ADDRESS_V07, + key: BigInt( + pad(validatorAddress, { + dir: 'right', + size: 24 + }) || 0 + ) + }) +} +function decodeSigners(encodedData: `0x${string}`): Array<{ type: number; data: `0x${string}` }> { + let offset = 2 // Start after '0x' + const signers: Array<{ type: number; data: `0x${string}` }> = [] + + // Decode the number of signers + const signersCount = parseInt(encodedData.slice(offset, offset + 2), 16) + offset += 2 + + for (let i = 0; i < signersCount; i++) { + // Decode signer type + const signerType = parseInt(encodedData.slice(offset, offset + 2), 16) + offset += 2 + + // Determine data length based on signer type + let dataLength: number + if (signerType === 0) { + dataLength = 40 // 20 bytes + } else if (signerType === 1) { + dataLength = 128 // 64 bytes + } else { + throw new Error(`Unknown signer type: ${signerType}`) + } + + // Decode signer data + const signerData = `0x${encodedData.slice(offset, offset + dataLength)}` as `0x${string}` + offset += dataLength + + signers.push({ type: signerType, data: signerData }) + } + + return signers +} diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/WalletConnectCosignerUtils.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/WalletConnectCosignerUtils.ts deleted file mode 100644 index fd529bc8c..000000000 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/WalletConnectCosignerUtils.ts +++ /dev/null @@ -1,169 +0,0 @@ -import axios, { AxiosError } from 'axios' -import { UserOperationWithBigIntAsHex } from './UserOpBuilder' -import { bigIntReplacer } from '@/utils/HelperUtil' -import { WC_COSIGNER_BASE_URL } from '@/utils/ConstantsUtil' - -// Define types for the request and response -type AddPermission = { - permissionType: string - data: string - required: boolean - onChainValidated: boolean -} - -type AddPermissionRequest = { - permission: AddPermission -} - -export type AddPermissionResponse = { - pci: string - key: string -} - -type Signer = { - type: string - data: { - ids: string[] - } -} - -type SignerData = { - userOpBuilder: string -} - -type PermissionsContext = { - signer: Signer - expiry: number - signerData: SignerData - factory?: string - factoryData?: string - permissionsContext: string -} - -type UpdatePermissionsContextRequest = { - pci: string - signature?: string - context: PermissionsContext -} - -type RevokePermissionRequest = { - pci: string - signature: string -} - -type CoSignRequest = { - pci: string - userOp: UserOperationWithBigIntAsHex -} - -type CoSignResponse = { - signature: `0x${string}` -} - -// Define a custom error type -export class CoSignerApiError extends Error { - constructor(public status: number, message: string) { - super(message) - this.name = 'CoSignerApiError' - } -} - -// Function to send requests to the CoSigner API -async function sendCoSignerRequest< - TRequest, - TResponse, - TQueryParams extends Record = Record ->(args: { - url: string - data: TRequest - queryParams?: TQueryParams - headers: Record - transformRequest?: (data: TRequest) => unknown -}): Promise { - const { url, data, queryParams = {}, headers, transformRequest } = args - const transformedData = transformRequest ? transformRequest(data) : data - - try { - const response = await axios.post(url, transformedData, { - params: queryParams, - headers - }) - - return response.data - } catch (error) { - if (axios.isAxiosError(error)) { - const axiosError = error as AxiosError - if (axiosError.response) { - throw new CoSignerApiError( - axiosError.response.status, - JSON.stringify(axiosError.response.data) - ) - } else { - throw new CoSignerApiError(500, 'Network error') - } - } - // Re-throw if it's not an Axios error - throw error - } -} - -// Class to interact with the WalletConnect CoSigner API -export class WalletConnectCosigner { - private baseUrl: string - private projectId: string - - constructor(projectId: string) { - this.baseUrl = WC_COSIGNER_BASE_URL - this.projectId = projectId - } - - async addPermission(address: string, permission: AddPermission): Promise { - const url = `${this.baseUrl}/${encodeURIComponent(address)}` - - return await sendCoSignerRequest< - AddPermissionRequest, - AddPermissionResponse, - { projectId: string } - >({ - url, - data: { permission }, - queryParams: { projectId: this.projectId }, - headers: { 'Content-Type': 'application/json' } - }) - } - - async updatePermissionsContext( - address: string, - updateData: UpdatePermissionsContextRequest - ): Promise { - const url = `${this.baseUrl}/${encodeURIComponent(address)}/context` - await sendCoSignerRequest({ - url, - data: updateData, - queryParams: { projectId: this.projectId }, - headers: { 'Content-Type': 'application/json' } - }) - } - - async revokePermission(address: string, revokeData: RevokePermissionRequest): Promise { - const url = `${this.baseUrl}/${encodeURIComponent(address)}/revoke` - await sendCoSignerRequest({ - url, - data: revokeData, - queryParams: { projectId: this.projectId }, - headers: { 'Content-Type': 'application/json' } - }) - } - - async coSignUserOperation(address: string, coSignData: CoSignRequest): Promise { - const url = `${this.baseUrl}/${encodeURIComponent(address)}/sign?version=1` - - return await sendCoSignerRequest({ - url, - data: coSignData, - queryParams: { projectId: this.projectId }, - headers: { 'Content-Type': 'application/json' }, - transformRequest: (value: CoSignRequest) => JSON.stringify(value, bigIntReplacer) - }) - } -} diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/build.ts b/advanced/wallets/react-wallet-v2/src/pages/api/build.ts deleted file mode 100644 index c71bf35e5..000000000 --- a/advanced/wallets/react-wallet-v2/src/pages/api/build.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - ErrorResponse, - BuildUserOpResponseReturn -} from '@/lib/smart-accounts/builders/UserOpBuilder' -import { getChainById } from '@/utils/ChainUtil' -import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil' -import { NextApiRequest, NextApiResponse } from 'next' - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === 'OPTIONS') { - res.status(200).end() - return - } - if (req.method !== 'POST') { - return res.status(405).json({ - message: 'Method not allowed', - error: 'Method not allowed' - }) - } - const chainId = req.body.chainId - const account = req.body.account - const chain = getChainById(chainId) - try { - const builder = await getUserOpBuilder({ - account, - chain - }) - - const response = await builder.fillUserOp(req.body) - - res.status(200).json(response) - } catch (error: any) { - return res.status(200).json({ - message: 'Unable to build userOp', - error: error.message - }) - } -} diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts b/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts deleted file mode 100644 index 82e3fb2f9..000000000 --- a/advanced/wallets/react-wallet-v2/src/pages/api/sendUserOp.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - ErrorResponse, - SendUserOpResponseReturn -} from '@/lib/smart-accounts/builders/UserOpBuilder' -import { getChainById } from '@/utils/ChainUtil' -import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil' -import { NextApiRequest, NextApiResponse } from 'next' - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method === 'OPTIONS') { - res.status(200).end() - return - } - if (req.method !== 'POST') { - return res.status(405).json({ - message: 'Method not allowed', - error: 'Method not allowed' - }) - } - if (!req.body.chainId) { - return res.status(400).json({ - message: 'chainId is required', - error: 'chainId is required' - }) - } - if (!req.body.userOp) { - return res.status(400).json({ - message: 'userOp is required', - error: 'userOp is required' - }) - } - if ( - req.query.projectId === undefined || - req.query.projectId === '' || - typeof req.query.projectId !== 'string' - ) { - return res.status(400).json({ - message: 'invalid projectId', - error: 'invalid projectId' - }) - } - const projectId = req.query.projectId - const chainId = req.body.chainId - const account = req.body.userOp.sender - const chain = getChainById(chainId) - try { - const builder = await getUserOpBuilder({ - account, - chain - }) - const response = await builder.sendUserOpWithSignature(projectId, req.body) - - res.status(200).json(response) - } catch (error: any) { - return res.status(200).json({ - message: 'Unable to send userOp', - error: error.message - }) - } -} diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts b/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts new file mode 100644 index 000000000..27936f07f --- /dev/null +++ b/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts @@ -0,0 +1,144 @@ +import { + ErrorResponse, + PrepareCallsParams, + PrepareCallsReturnValue, + SendPreparedCallsParams, + SendPreparedCallsReturnValue +} from '@/lib/smart-accounts/builders/UserOpBuilder' +import { getChainById } from '@/utils/ChainUtil' +import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil' +import { NextApiRequest, NextApiResponse } from 'next' + +type JsonRpcRequest = { + jsonrpc: '2.0' + id: number | string + method: string + params: any[] +} + +type JsonRpcResponse = { + jsonrpc: '2.0' + id: number | string | null + result?: T + error?: { + code: number + message: string + data?: any + } +} + +type SupportedMethods = 'wallet_prepareCalls' | 'wallet_sendPreparedCalls' + +const ERROR_CODES = { + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32000 +} + +function createErrorResponse( + id: number | string | null, + code: number, + message: string, + data?: any +): JsonRpcResponse { + return { + jsonrpc: '2.0', + id, + error: { code, message, data } + } +} + +async function handlePrepareCalls( + projectId: string, + params: PrepareCallsParams[] +): Promise { + const [data] = params + const chainId = parseInt(data.chainId, 16) + const account = data.from + const chain = getChainById(chainId) + const builder = await getUserOpBuilder({ account, chain }) + return builder.prepareCalls(projectId, data) +} + +async function handleSendPreparedCalls( + projectId: string, + params: SendPreparedCallsParams[] +): Promise { + const [data] = params + const chainId = parseInt(data.preparedCalls.chainId, 16) + const account = data.preparedCalls.data.sender + const chain = getChainById(chainId) + const builder = await getUserOpBuilder({ account, chain }) + return builder.sendPreparedCalls(projectId, data) +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse< + JsonRpcResponse + > +) { + if (req.method === 'OPTIONS') { + return res.status(200).end() + } + + if (req.method !== 'POST') { + return res + .status(405) + .json(createErrorResponse(null, ERROR_CODES.INVALID_REQUEST, 'Invalid Request')) + } + + const jsonRpcRequest: JsonRpcRequest = req.body + const { id, method, params } = jsonRpcRequest + + if (!['wallet_prepareCalls', 'wallet_sendPreparedCalls'].includes(method)) { + return res + .status(200) + .json(createErrorResponse(id, ERROR_CODES.METHOD_NOT_FOUND, `${method} method not found`)) + } + + const projectId = req.query.projectId as string + if (!projectId) { + return res + .status(200) + .json(createErrorResponse(id, ERROR_CODES.INVALID_PARAMS, 'Invalid projectId')) + } + + try { + let response: PrepareCallsReturnValue | SendPreparedCallsReturnValue + + switch (method as SupportedMethods) { + case 'wallet_prepareCalls': + response = await handlePrepareCalls(projectId, params as PrepareCallsParams[]) + return res.status(200).json({ + jsonrpc: '2.0', + id, + result: [response] as PrepareCallsReturnValue[] + }) + + case 'wallet_sendPreparedCalls': + response = await handleSendPreparedCalls(projectId, params as SendPreparedCallsParams[]) + return res.status(200).json({ + jsonrpc: '2.0', + id, + result: [response] as SendPreparedCallsReturnValue[] + }) + + default: + throw new Error(`Unsupported method: ${method}`) + } + } catch (error: any) { + console.error(error) + return res + .status(200) + .json( + createErrorResponse( + id, + ERROR_CODES.INTERNAL_ERROR, + `${method}: Internal error`, + error.message + ) + ) + } +} diff --git a/advanced/wallets/react-wallet-v2/src/utils/ConstantsUtil.ts b/advanced/wallets/react-wallet-v2/src/utils/ConstantsUtil.ts index 0f462d5b6..c3dc1a74e 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/ConstantsUtil.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/ConstantsUtil.ts @@ -1 +1 @@ -export const WC_COSIGNER_BASE_URL = 'https://rpc.walletconnect.org/v1/sessions' +export const COSIGNER_BASE_URL = 'https://rpc.walletconnect.org/v1/sessions' diff --git a/advanced/wallets/react-wallet-v2/src/utils/EIP7715RequestHandlerUtils.ts b/advanced/wallets/react-wallet-v2/src/utils/EIP7715RequestHandlerUtils.ts index aa290dd47..accef33e0 100644 --- a/advanced/wallets/react-wallet-v2/src/utils/EIP7715RequestHandlerUtils.ts +++ b/advanced/wallets/react-wallet-v2/src/utils/EIP7715RequestHandlerUtils.ts @@ -2,15 +2,18 @@ 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, + WalletGrantPermissionsResponse +} from '@/data/EIP7715Data' import { SafeSmartAccountLib } from '@/lib/smart-accounts/SafeSmartAccountLib' import { web3wallet } from './WalletConnectUtil' import { smartAccountWallets } from './SmartAccountUtil' import { KernelSmartAccountLib } from '@/lib/smart-accounts/KernelSmartAccountLib' -import { WalletGrantPermissionsParameters, WalletGrantPermissionsReturnType } from 'viem' type RequestEventArgs = Omit -function getSmartWalletAddressFromSession(requestSession: SessionTypes.Struct, chainId: string) { +function getSmartAccountLibFromSession(requestSession: SessionTypes.Struct, chainId: string) { const sessionAccounts = requestSession.namespaces['eip155'].accounts.filter(value => value.startsWith(chainId) ) @@ -45,12 +48,16 @@ export async function approveEIP7715Request(requestEvent: RequestEventArgs) { SettingsStore.setActiveChainId(chainId) switch (request.method) { case EIP7715_METHOD.WALLET_GRANT_PERMISSIONS: { - const wallet = getSmartWalletAddressFromSession(requestSession, chainId) - let grantPermissionsRequestParams: WalletGrantPermissionsParameters = request.params[0] - if (wallet instanceof SafeSmartAccountLib || wallet instanceof KernelSmartAccountLib) { - const grantPermissionsResponse: WalletGrantPermissionsReturnType = + const wallet = getSmartAccountLibFromSession(requestSession, chainId) + let grantPermissionsRequestParams: WalletGrantPermissionsRequest = request.params[0] + if ( + wallet instanceof SafeSmartAccountLib + //TODO:fix kernel grantPermissions + // || wallet instanceof KernelSmartAccountLib + ) { + const grantPermissionsResponse: WalletGrantPermissionsResponse = await wallet.grantPermissions(grantPermissionsRequestParams) - return formatJsonRpcResult(id, grantPermissionsResponse) + return formatJsonRpcResult(id, grantPermissionsResponse) } // for any other wallet instance return un_supported @@ -66,3 +73,9 @@ export function rejectEIP7715Request(request: RequestEventArgs) { return formatJsonRpcError(id, getSdkError('USER_REJECTED').message) } + +export function createErrorResponse(request: RequestEventArgs, errorMessage: string) { + const { id } = request + + return formatJsonRpcError(id, errorMessage) +} diff --git a/advanced/wallets/react-wallet-v2/src/views/SessionGrantPermissionsModal.tsx b/advanced/wallets/react-wallet-v2/src/views/SessionGrantPermissionsModal.tsx index a57e4c4c3..2746e0fd4 100644 --- a/advanced/wallets/react-wallet-v2/src/views/SessionGrantPermissionsModal.tsx +++ b/advanced/wallets/react-wallet-v2/src/views/SessionGrantPermissionsModal.tsx @@ -9,7 +9,11 @@ import { web3wallet } from '@/utils/WalletConnectUtil' import RequestModal from '../components/RequestModal' import { useCallback, useState } from 'react' import PermissionDetailsCard from '@/components/PermissionDetailsCard' -import { approveEIP7715Request, rejectEIP7715Request } from '@/utils/EIP7715RequestHandlerUtils' +import { + approveEIP7715Request, + createErrorResponse, + rejectEIP7715Request +} from '@/utils/EIP7715RequestHandlerUtils' import { GrantPermissionsParameters } from 'viem/experimental' // import { GrantPermissionsRequestParams } from '@/data/EIP7715Data' @@ -46,6 +50,11 @@ export default function SessionGrantPermissionsModal() { } } catch (e) { styledToast((e as Error).message, 'error') + const response = createErrorResponse(requestEvent, (e as Error).message) + await web3wallet.respondSessionRequest({ + topic, + response + }) } finally { setIsLoadingApprove(false) ModalStore.close() diff --git a/advanced/wallets/react-wallet-v2/yarn.lock b/advanced/wallets/react-wallet-v2/yarn.lock index 8ed760497..37fe3bc4f 100644 --- a/advanced/wallets/react-wallet-v2/yarn.lock +++ b/advanced/wallets/react-wallet-v2/yarn.lock @@ -2077,10 +2077,10 @@ "@react-types/grid" "^3.2.6" "@react-types/shared" "^3.23.1" -"@rhinestone/module-sdk@0.1.16": - version "0.1.16" - resolved "https://registry.yarnpkg.com/@rhinestone/module-sdk/-/module-sdk-0.1.16.tgz#8bbc5ed193ad4f3656a0ac1aa1f438771734d182" - integrity sha512-Cn+JJ/8J6f5rk9ihagCoQ815bqve0YOpPCzkxEIqFzsyVYOuEUrEbX5ED9KJhoyyZSuAk2Ar8yxNGJBlS56RvA== +"@rhinestone/module-sdk@0.1.18": + version "0.1.18" + resolved "https://registry.yarnpkg.com/@rhinestone/module-sdk/-/module-sdk-0.1.18.tgz#24bdf5235ccdbf7a595955cf002941243dead862" + integrity sha512-mYWB9P1Q7TE++QbkE6+DTBcWZTYykEhdzsyw9O4uUi2CCE/KUFwKfU0PmxjOTLDw4zcUxL8ponAys8avOJOYRg== dependencies: solady "^0.0.235" tslib "^2.7.0"