Skip to content

Commit 6c32096

Browse files
committed
implement the missing pieces for smart accounts
1 parent 942c3b2 commit 6c32096

File tree

2 files changed

+181
-40
lines changed

2 files changed

+181
-40
lines changed

packages/hypergraph/src/identity/abis.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,28 @@ export const smartSessionsAbi = [
4444
type: 'function',
4545
},
4646
];
47+
48+
// ABI for the Safe7579 module, only with the functions we need
49+
export const safe7579Abi = [
50+
{
51+
type: 'function',
52+
name: 'isModuleInstalled',
53+
inputs: [
54+
{
55+
name: 'moduleType',
56+
type: 'uint256',
57+
internalType: 'uint256',
58+
},
59+
{ name: 'module', type: 'address', internalType: 'address' },
60+
{ name: 'additionalContext', type: 'bytes', internalType: 'bytes' },
61+
],
62+
outputs: [
63+
{
64+
name: '',
65+
type: 'bool',
66+
internalType: 'bool',
67+
},
68+
],
69+
stateMutability: 'view',
70+
},
71+
];

packages/hypergraph/src/identity/smart-account.ts

Lines changed: 156 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
1923
import { 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';
4051
import { bytesToHex } from '../utils/hexBytesAddressUtils.js';
41-
import { safeModuleManagerAbi, smartSessionsAbi } from './abis.js';
52+
import { safe7579Abi, safeModuleManagerAbi, smartSessionsAbi } from './abis.js';
4253

4354
const DEFAULT_RPC_URL = 'https://rpc-geo-genesis-h0q2s21xx8.t.conduit.xyz';
4455
/**
@@ -55,6 +66,8 @@ const ERC7579_LAUNCHPAD_ADDRESS = '0x7579011aB74c46090561ea277Ba79D510c6C00ff';
5566
const SPACE_FACTORY_ADDRESS = '0x0000000000000000000000000000000000000000'; // TODO: add address
5667
const SPACE_FACTORY_CREATE_SPACE_SELECTOR = '0x00000000'; // TODO: add selector
5768

69+
const MODULE_TYPE_VALIDATOR = 1;
70+
5871
// TODO: add details for testnet too
5972
export 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.
208240
export 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.
315427
export 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

Comments
 (0)