@@ -13,8 +13,12 @@ import {
1313 getOwnableValidatorMockSignature ,
1414 getPermissionId ,
1515 getSmartSessionsValidator ,
16+ getSpendingLimitsPolicy ,
1617 getSudoPolicy ,
18+ getTimeFramePolicy ,
1719 getUniversalActionPolicy ,
20+ getUsageLimitPolicy ,
21+ getValueLimitPolicy ,
1822} from '@rhinestone/module-sdk' ;
1923import { privateKeyToAccount } from 'viem/accounts' ;
2024
@@ -28,17 +32,24 @@ import {
2832 type AbiFunction ,
2933 type Address ,
3034 type Chain ,
35+ ContractFunctionExecutionError ,
3136 type Hex ,
37+ type SignableMessage ,
3238 type WalletClient ,
3339 createPublicClient ,
3440 encodeFunctionData ,
3541 toBytes ,
3642 toFunctionSelector ,
3743 toHex ,
3844} from 'viem' ;
39- import { type UserOperation , entryPoint07Address , getUserOperationHash } from 'viem/account-abstraction' ;
45+ import {
46+ type UserOperation ,
47+ type WaitForUserOperationReceiptReturnType ,
48+ entryPoint07Address ,
49+ getUserOperationHash ,
50+ } from 'viem/account-abstraction' ;
4051import { bytesToHex } from '../utils/hexBytesAddressUtils.js' ;
41- import { safeModuleManagerAbi , smartSessionsAbi } from './abis.js' ;
52+ import { safe7579Abi , safeModuleManagerAbi , smartSessionsAbi } from './abis.js' ;
4253
4354const DEFAULT_RPC_URL = 'https://rpc-geo-genesis-h0q2s21xx8.t.conduit.xyz' ;
4455/**
@@ -55,6 +66,8 @@ const ERC7579_LAUNCHPAD_ADDRESS = '0x7579011aB74c46090561ea277Ba79D510c6C00ff';
5566const SPACE_FACTORY_ADDRESS = '0x0000000000000000000000000000000000000000' ; // TODO: add address
5667const SPACE_FACTORY_CREATE_SPACE_SELECTOR = '0x00000000' ; // TODO: add selector
5768
69+ const MODULE_TYPE_VALIDATOR = 1 ;
70+
5871// TODO: add details for testnet too
5972export const GEOGENESIS = {
6073 id : Number ( '80451' ) ,
@@ -74,15 +87,31 @@ export const GEOGENESIS = {
7487 } ,
7588} ;
7689
77- type Action = {
90+ export type Action = {
7891 actionTarget : Address ;
7992 actionTargetSelector : Hex ;
8093 actionPolicies : { policy : Address ; address : Address ; initData : Hex } [ ] ;
8194} ;
8295
96+ // We re-export these functions to allow creating sessions with policies for
97+ // additional actions without needing the Rhinestone module SDK.
98+ export {
99+ getSudoPolicy ,
100+ getUniversalActionPolicy ,
101+ getSpendingLimitsPolicy ,
102+ getTimeFramePolicy ,
103+ getUsageLimitPolicy ,
104+ getValueLimitPolicy ,
105+ } ;
106+
107+ export type SmartSessionClient = {
108+ sendTransaction : < const calls extends readonly unknown [ ] > ( { calls } : { calls : calls } ) => Promise < string > ;
109+ signMessage : ( { message } : { message : SignableMessage } ) => Promise < Hex > ;
110+ } ;
111+
83112// Gets the legacy Geo smart account wallet client. If the smart account returned
84113// by this function is deployed, it means it might need to be updated to have the 7579 module installed
85- export const getLegacySmartAccountWalletClient = async (
114+ const getLegacySmartAccountWalletClient = async (
86115 walletClient : WalletClient ,
87116 chain : Chain = GEOGENESIS ,
88117 rpcUrl : string = DEFAULT_RPC_URL ,
@@ -129,9 +158,11 @@ export const getLegacySmartAccountWalletClient = async (
129158 return smartAccountClient ;
130159} ;
131160
132- export const get7579SmartAccountWalletClient = async (
161+ // Gets the 7579 smart account wallet client. This is the new type of smart account that
162+ // includes the session keys validator and the 7579 module.
163+ const get7579SmartAccountWalletClient = async (
133164 walletClient : WalletClient ,
134- address : `0x${ string } ` | undefined ,
165+ address : Hex | undefined ,
135166 chain : Chain = GEOGENESIS ,
136167 rpcUrl : string = DEFAULT_RPC_URL ,
137168 apiKey : string = DEFAULT_API_KEY ,
@@ -152,16 +183,16 @@ export const get7579SmartAccountWalletClient = async (
152183 } ) ;
153184 const smartSessionsValidator = getSmartSessionsValidator ( { } ) ;
154185
155- const safeAccountParams : ToSafeSmartAccountParameters < '0.7' , `0x${ string } ` > = {
186+ const safeAccountParams : ToSafeSmartAccountParameters < '0.7' , Hex > = {
156187 client : publicClient ,
157188 owners : [ walletClient ] ,
158189 version : '1.4.1' as const ,
159190 entryPoint : {
160191 address : entryPoint07Address ,
161192 version : '0.7' as const ,
162193 } ,
163- safe4337ModuleAddress : SAFE_7579_MODULE_ADDRESS as `0x${ string } ` ,
164- erc7579LaunchpadAddress : ERC7579_LAUNCHPAD_ADDRESS as `0x${ string } ` ,
194+ safe4337ModuleAddress : SAFE_7579_MODULE_ADDRESS as Hex ,
195+ erc7579LaunchpadAddress : ERC7579_LAUNCHPAD_ADDRESS as Hex ,
165196 attesters : [ ] ,
166197 attestersThreshold : 0 ,
167198 validators : [
@@ -205,32 +236,105 @@ export const get7579SmartAccountWalletClient = async (
205236 return smartAccountClient as unknown as SmartAccountClient ;
206237} ;
207238
239+ // Checks if the smart account is deployed.
208240export const isSmartAccountDeployed = async ( smartAccountClient : SmartAccountClient ) : Promise < boolean > => {
209241 if ( ! smartAccountClient . account ) {
210242 throw new Error ( 'Invalid smart account' ) ;
211243 }
212244 return smartAccountClient . account . isDeployed ( ) ;
213245} ;
214246
215- export const legacySmartAccountNeedsUpdate = async (
247+ // Gets the smart account wallet client. This is the main function to use to get a smart account wallet client.
248+ // It will return the 7579 smart account wallet client if the smart account is deployed, otherwise it will return the legacy smart account wallet client, that might need to be updated.
249+ // You can use smartAccountNeedsUpdate to check if the smart account needs to be updated, and then call updateLegacySmartAccount to update it,
250+ // which requires executing a user operation.
251+ export const getSmartAccountWalletClient = async (
252+ walletClient : WalletClient ,
253+ address : Hex | undefined ,
254+ chain : Chain ,
255+ rpcUrl : string ,
256+ apiKey : string ,
257+ ) : Promise < SmartAccountClient > => {
258+ if ( address ) {
259+ return get7579SmartAccountWalletClient ( walletClient , address , chain , rpcUrl , apiKey ) ;
260+ }
261+ const legacyClient = await getLegacySmartAccountWalletClient ( walletClient , chain , rpcUrl , apiKey ) ;
262+ if ( await isSmartAccountDeployed ( legacyClient ) ) {
263+ return legacyClient ;
264+ }
265+ return get7579SmartAccountWalletClient ( walletClient , undefined , chain , rpcUrl , apiKey ) ;
266+ } ;
267+
268+ // Checks if the smart account has the 7579 module installed, the smart sessions validator installed, and the ownable validator installed.
269+ export const legacySmartAccountUpdateStatus = async (
216270 smartAccountClient : SmartAccountClient ,
217271 chain : Chain ,
218272 rpcUrl : string ,
219- ) : Promise < boolean > => {
273+ ) : Promise < { has7579Module : boolean ; hasSmartSessionsValidator : boolean ; hasOwnableValidator : boolean } > => {
220274 if ( ! smartAccountClient . account ) {
221275 throw new Error ( 'Invalid smart account' ) ;
222276 }
223277 // We assume the smart account is deployed, so we just need to check if it has the 7579 module and smart sesions validator installed
224- // TODO: call the isModuleInstalled function from the Safe7579 ABI on the
278+ // TODO: call the isModuleInstalled function from the safe7579Abi on the
225279 // smart account, checking if the smart sessions validator is installed. This would fail
226280 // if the smart account doesn't have the 7579 module installed.
227- return false ;
281+ const transport = http ( rpcUrl ) ;
282+ const publicClient = createPublicClient ( {
283+ transport,
284+ chain,
285+ } ) ;
286+ const smartSessionsValidator = getSmartSessionsValidator ( { } ) ;
287+ let isSmartSessionsValidatorInstalled = false ;
288+ try {
289+ isSmartSessionsValidatorInstalled = ( await publicClient . readContract ( {
290+ abi : safe7579Abi ,
291+ address : smartAccountClient . account . address ,
292+ functionName : 'isModuleInstalled' ,
293+ args : [ MODULE_TYPE_VALIDATOR , smartSessionsValidator . address , '0x' ] ,
294+ } ) ) as boolean ;
295+ } catch ( error ) {
296+ if ( error instanceof ContractFunctionExecutionError && error . details . includes ( 'execution reverted' ) ) {
297+ // If the smart account doesn't have the 7579 module installed, the isModuleInstalled function will revert
298+ return { has7579Module : false , hasSmartSessionsValidator : false , hasOwnableValidator : false } ;
299+ }
300+ throw error ;
301+ }
302+ const ownableValidator = getOwnableValidator ( {
303+ owners : [ smartAccountClient . account . address ] ,
304+ threshold : 1 ,
305+ } ) ;
306+ // This shouldn't throw because by now we know the smart account has the 7579 module installed
307+ const isOwnableValidatorInstalled = ( await publicClient . readContract ( {
308+ abi : safe7579Abi ,
309+ address : smartAccountClient . account . address ,
310+ functionName : 'isModuleInstalled' ,
311+ args : [ MODULE_TYPE_VALIDATOR , ownableValidator . address , '0x' ] ,
312+ } ) ) as boolean ;
313+ return {
314+ has7579Module : true ,
315+ hasSmartSessionsValidator : isSmartSessionsValidatorInstalled ,
316+ hasOwnableValidator : isOwnableValidatorInstalled ,
317+ } ;
318+ } ;
319+
320+ // Checks if the smart account needs to be updated from a legacy ERC-4337 smart account to an ERC-7579 smart account
321+ // with support for smart sessions.
322+ export const smartAccountNeedsUpdate = async (
323+ smartAccountClient : SmartAccountClient ,
324+ chain : Chain ,
325+ rpcUrl : string ,
326+ ) : Promise < boolean > => {
327+ const updateStatus = await legacySmartAccountUpdateStatus ( smartAccountClient , chain , rpcUrl ) ;
328+ return ! updateStatus . has7579Module || ! updateStatus . hasSmartSessionsValidator || ! updateStatus . hasOwnableValidator ;
228329} ;
229330
230331// Legacy Geo smart accounts (i.e. the ones that don't have the 7579 module installed)
231- // need to be updated to have the 7579 module installed
232- // with the ownable and smart sessions validators.
233- export const updateLegacySmartAccount = async ( smartAccountClient : SmartAccountClient ) => {
332+ // need to be updated to have the 7579 module installed with the ownable and smart sessions validators.
333+ export const updateLegacySmartAccount = async (
334+ smartAccountClient : SmartAccountClient ,
335+ chain : Chain ,
336+ rpcUrl : string ,
337+ ) : Promise < WaitForUserOperationReceiptReturnType | undefined > => {
234338 if ( ! smartAccountClient . account ?. address ) {
235339 throw new Error ( 'Invalid smart account' ) ;
236340 }
@@ -255,46 +359,54 @@ export const updateLegacySmartAccount = async (smartAccountClient: SmartAccountC
255359 ] ,
256360 } ) ;
257361
258- const calls = [
259- {
362+ const updateStatus = await legacySmartAccountUpdateStatus ( smartAccountClient , chain , rpcUrl ) ;
363+ const calls = [ ] ;
364+ if ( ! updateStatus . has7579Module ) {
365+ calls . push ( {
260366 to : smartAccountClient . account . address ,
261367 data : encodeFunctionData ( {
262368 abi : safeModuleManagerAbi ,
263369 functionName : 'enableModule' ,
264- args : [ SAFE_7579_MODULE_ADDRESS as `0x${ string } ` ] ,
370+ args : [ SAFE_7579_MODULE_ADDRESS as Hex ] ,
265371 } ) ,
266372 value : BigInt ( 0 ) ,
267- } ,
268- {
373+ } ) ;
374+ calls . push ( {
269375 to : smartAccountClient . account . address ,
270376 data : encodeFunctionData ( {
271377 abi : safeModuleManagerAbi ,
272378 functionName : 'setFallbackHandler' ,
273- args : [ SAFE_7579_MODULE_ADDRESS as `0x${ string } ` ] ,
379+ args : [ SAFE_7579_MODULE_ADDRESS as Hex ] ,
274380 } ) ,
275381 value : BigInt ( 0 ) ,
276- } ,
277- {
382+ } ) ;
383+ calls . push ( {
278384 to : smartAccountClient . account . address ,
279385 data : encodeFunctionData ( {
280386 abi : safeModuleManagerAbi ,
281387 functionName : 'disableModule' ,
282- args : [ SAFE_4337_MODULE_ADDRESS as `0x${ string } ` ] ,
388+ args : [ SAFE_4337_MODULE_ADDRESS as Hex ] ,
283389 } ) ,
284390 value : BigInt ( 0 ) ,
285- } ,
286- {
391+ } ) ;
392+ }
393+ if ( ! updateStatus . hasOwnableValidator ) {
394+ calls . push ( {
287395 to : installValidatorsTx [ 0 ] . to ,
288396 data : installValidatorsTx [ 0 ] . data ,
289397 value : installValidatorsTx [ 0 ] . value ,
290- } ,
291- {
398+ } ) ;
399+ }
400+ if ( ! updateStatus . hasSmartSessionsValidator ) {
401+ calls . push ( {
292402 to : installValidatorsTx [ 1 ] . to ,
293403 data : installValidatorsTx [ 1 ] . data ,
294404 value : installValidatorsTx [ 1 ] . value ,
295- } ,
296- ] ;
297-
405+ } ) ;
406+ }
407+ if ( calls . length === 0 ) {
408+ return ;
409+ }
298410 const tx = await smartAccountClient . sendUserOperation ( {
299411 calls,
300412 } ) ;
@@ -311,11 +423,11 @@ export const updateLegacySmartAccount = async (smartAccountClient: SmartAccountC
311423// enable it on the smart account.
312424// It will prompt the user to sign the message to enable the session, and then
313425// execute the transaction to enable the session.
314- // It will return the permissionId.
426+ // It will return the permissionId that can be used to create a smart session client .
315427export const createSmartSession = async (
316428 walletClient : WalletClient ,
317429 smartAccountClient : SmartAccountClient ,
318- sessionPrivateKey : `0x${ string } ` ,
430+ sessionPrivateKey : Hex ,
319431 chain : Chain ,
320432 rpcUrl : string ,
321433 {
@@ -324,10 +436,10 @@ export const createSmartSession = async (
324436 additionalActions = [ ] ,
325437 } : {
326438 allowCreateSpace ?: boolean ;
327- spaceAddresses ?: `0x${ string } ` [ ] ;
439+ spaceAddresses ?: Hex [ ] ;
328440 additionalActions ?: Action [ ] ;
329441 } = { } ,
330- ) => {
442+ ) : Promise < Hex > => {
331443 if ( ! smartAccountClient . account ) {
332444 throw new Error ( 'Invalid smart account' ) ;
333445 }
@@ -395,7 +507,7 @@ export const createSmartSession = async (
395507 threshold : 1 ,
396508 owners : [ sessionKeyAccount . address ] ,
397509 } ) ,
398- salt : bytesToHex ( randomBytes ( 32 ) ) as `0x${ string } ` ,
510+ salt : bytesToHex ( randomBytes ( 32 ) ) as Hex ,
399511 userOpPolicies : [ getSudoPolicy ( ) ] ,
400512 erc7739Policies : {
401513 allowedERC7739Content : [ ] ,
@@ -485,13 +597,14 @@ export const createSmartSession = async (
485597
486598// This is the function that we use on the end user app to create a smart session client that can send transactions to the smart account.
487599// The session must have previously been created by the createSmartSession function.
488- export const getSmartSessionClient = async (
600+ // The client also includes a signMessage function that can be used to sign messages with the session key.
601+ export const getSmartSessionClient = (
489602 smartAccountClient : SmartAccountClient ,
490- sessionPrivateKey : `0x${ string } ` ,
603+ sessionPrivateKey : Hex ,
491604 permissionId : Hex ,
492605 chain : Chain ,
493606 rpcUrl : string ,
494- ) => {
607+ ) : SmartSessionClient => {
495608 if ( ! smartAccountClient . account ) {
496609 throw new Error ( 'Invalid smart account' ) ;
497610 }
@@ -547,5 +660,8 @@ export const getSmartSessionClient = async (
547660
548661 return smartAccountClient . sendUserOperation ( userOperation as UserOperation ) ;
549662 } ,
663+ signMessage : async ( { message } : { message : SignableMessage } ) => {
664+ return sessionKeyAccount . signMessage ( { message } ) ;
665+ } ,
550666 } ;
551667} ;
0 commit comments