Skip to content

Commit 8ab6d62

Browse files
committed
feat: use the Connect signaturePrivateKey as an additional owner in the smart account
1 parent e5d148d commit 8ab6d62

File tree

7 files changed

+113
-43
lines changed

7 files changed

+113
-43
lines changed

apps/connect/src/components/logout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Connect } from '@graphprotocol/hypergraph';
12
import { usePrivy } from '@privy-io/react-auth';
23
import { useRouter } from '@tanstack/react-router';
34
import { Loader2 } from 'lucide-react';
@@ -10,6 +11,7 @@ export function Logout() {
1011
const [isLoading, setIsLoading] = useState(false);
1112
const disconnectWallet = async () => {
1213
setIsLoading(true);
14+
Connect.wipeAllAuthData(localStorage);
1315
await privyLogout();
1416
router.navigate({
1517
to: '/login',

apps/connect/src/routes/authenticate.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import { Button } from '@/components/ui/button';
33
import { usePrivateSpaces } from '@/hooks/use-private-spaces';
44
import { type PublicSpaceData, usePublicSpaces } from '@/hooks/use-public-spaces';
55
import { Connect, Identity, Key, type Messages, StoreConnect, Utils } from '@graphprotocol/hypergraph';
6-
import { GEOGENESIS, GEO_TESTNET, getSmartAccountWalletClient } from '@graphprotocol/hypergraph/connect/smart-account';
6+
77
import { useIdentityToken, usePrivy, useWallets } from '@privy-io/react-auth';
88
import { createFileRoute } from '@tanstack/react-router';
99
import { createStore } from '@xstate/store';
1010
import { useSelector } from '@xstate/store/react';
1111
import { Effect, Schema } from 'effect';
1212
import { useEffect } from 'react';
1313
import { createWalletClient, custom } from 'viem';
14+
import { privateKeyToAccount } from 'viem/accounts';
1415

15-
const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? GEOGENESIS : GEO_TESTNET;
16+
const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET;
1617

1718
type AuthenticateSearch = {
1819
data: unknown;
@@ -380,9 +381,11 @@ function AuthenticateComponent() {
380381
})) ?? [];
381382
console.log('spaces', spaces);
382383

384+
const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`);
385+
console.log('local account', localAccount.address);
383386
// TODO: add additional actions (must be passed from the app)
384387
const permissionId = await Connect.createSmartSession(
385-
walletClient,
388+
localAccount,
386389
accountAddress,
387390
newAppIdentity.addressPrivateKey,
388391
CHAIN,
@@ -394,22 +397,23 @@ function AuthenticateComponent() {
394397
},
395398
);
396399
console.log('smart session created');
397-
const smartAccountClient = await getSmartAccountWalletClient({
398-
owner: walletClient,
400+
const smartAccountClient = await Connect.getSmartAccountWalletClient({
401+
owner: localAccount,
399402
address: accountAddress,
400403
chain: CHAIN,
401404
rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL,
402405
});
403406

407+
console.log('encrypting app identity');
404408
const { ciphertext, nonce } = await Connect.encryptAppIdentity(
405409
signer,
406410
newAppIdentity.address,
407411
newAppIdentity.addressPrivateKey,
408412
permissionId,
409413
keys,
410414
);
415+
console.log('proving ownership');
411416
const { accountProof, keyProof } = await Identity.proveIdentityOwnership(
412-
walletClient,
413417
smartAccountClient,
414418
accountAddress,
415419
keys,

packages/hypergraph/src/connect/abis.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ export const safeOwnerManagerAbi = [
5252
stateMutability: 'nonpayable',
5353
type: 'function',
5454
},
55+
{
56+
inputs: [
57+
{
58+
internalType: 'address',
59+
name: 'owner',
60+
type: 'address',
61+
},
62+
],
63+
name: 'isOwner',
64+
outputs: [
65+
{
66+
internalType: 'bool',
67+
name: '',
68+
type: 'bool',
69+
},
70+
],
71+
stateMutability: 'view',
72+
type: 'function',
73+
},
5574
];
5675

5776
// We only use this for revokeEnableSignature to use as a noop when creating a smart session

packages/hypergraph/src/connect/auth-storage.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ export const storeAccountAddress = (storage: Storage, accountId: string) => {
6565
export const wipeAccountAddress = (storage: Storage) => {
6666
storage.removeItem(buildAccountAddressStorageKey());
6767
};
68+
69+
export const wipeAllAuthData = (storage: Storage) => {
70+
const accountAddress = loadAccountAddress(storage);
71+
wipeAccountAddress(storage);
72+
if (accountAddress) {
73+
wipeKeys(storage, accountAddress);
74+
wipeSyncServerSessionToken(storage, accountAddress);
75+
}
76+
};

packages/hypergraph/src/connect/login.ts

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import * as Schema from 'effect/Schema';
22
import type { SmartAccountClient } from 'permissionless';
33
import type { Address, Chain, Hex, WalletClient } from 'viem';
4+
import { privateKeyToAccount } from 'viem/accounts';
45
import { proveIdentityOwnership } from '../identity/prove-ownership.js';
56
import * as Messages from '../messages/index.js';
67
import { store } from '../store-connect.js';
7-
import { loadAccountAddress, storeAccountAddress, storeKeys, wipeAccountAddress } from './auth-storage.js';
8+
import { loadAccountAddress, storeAccountAddress, storeKeys } from './auth-storage.js';
89
import { createIdentityKeys } from './create-identity-keys.js';
910
import { decryptIdentity, encryptIdentity } from './identity-encryption.js';
1011
import {
1112
type SmartAccountParams,
13+
addSmartAccountOwner,
1214
getSmartAccountWalletClient,
1315
isSmartAccountDeployed,
1416
smartAccountNeedsUpdate,
@@ -31,15 +33,23 @@ export async function signup(
3133
syncServerUri: string,
3234
storage: Storage,
3335
identityToken: string,
36+
chain: Chain,
37+
rpcUrl: string,
3438
) {
3539
const keys = createIdentityKeys();
3640
const { ciphertext, nonce } = await encryptIdentity(signer, keys);
37-
const { accountProof, keyProof } = await proveIdentityOwnership(
38-
walletClient,
39-
smartAccountClient,
40-
accountAddress,
41-
keys,
42-
);
41+
42+
const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`);
43+
// This will deploy the smart account if it's not deployed
44+
await addSmartAccountOwner(smartAccountClient, localAccount.address, chain, rpcUrl);
45+
const localSmartAccountClient = await getSmartAccountWalletClient({
46+
owner: localAccount,
47+
address: accountAddress,
48+
rpcUrl,
49+
chain,
50+
});
51+
52+
const { accountProof, keyProof } = await proveIdentityOwnership(localSmartAccountClient, accountAddress, keys);
4353

4454
const req: Messages.RequestConnectCreateIdentity = {
4555
keyBox: { signer: await signer.getAddress(), accountAddress, ciphertext, nonce },
@@ -103,7 +113,7 @@ export async function restoreKeys(
103113
throw new Error(`Error fetching identity ${res.status}`);
104114
}
105115

106-
const getAndDeploySmartAccount = async (walletClient: WalletClient, rpcUrl: string, chain: Chain, storage: Storage) => {
116+
const getAndUpdateSmartAccount = async (walletClient: WalletClient, rpcUrl: string, chain: Chain, storage: Storage) => {
107117
const accountAddressFromStorage = loadAccountAddress(storage) as Hex;
108118
const smartAccountParams: SmartAccountParams = {
109119
owner: walletClient,
@@ -128,21 +138,6 @@ const getAndDeploySmartAccount = async (walletClient: WalletClient, rpcUrl: stri
128138
// Create the client again to ensure we have the 7579 config now
129139
return getSmartAccountWalletClient(smartAccountParams);
130140
}
131-
if (!(await isSmartAccountDeployed(smartAccountWalletClient))) {
132-
// TODO: remove this once we manage to get counterfactual signatures working
133-
console.log('sending dummy userOp to deploy smart account');
134-
if (!walletClient.account) {
135-
throw new Error('Wallet client account not found');
136-
}
137-
const tx = await smartAccountWalletClient.sendUserOperation({
138-
calls: [{ to: walletClient.account.address, data: '0x' }],
139-
account: smartAccountWalletClient.account,
140-
});
141-
142-
console.log('tx', tx);
143-
const receipt = await smartAccountWalletClient.waitForUserOperationReceipt({ hash: tx });
144-
console.log('receipt', receipt);
145-
}
146141
return smartAccountWalletClient;
147142
};
148143

@@ -163,13 +158,7 @@ export async function login({
163158
rpcUrl: string;
164159
chain: Chain;
165160
}) {
166-
let smartAccountWalletClient: SmartAccountClient;
167-
try {
168-
smartAccountWalletClient = await getAndDeploySmartAccount(walletClient, rpcUrl, chain, storage);
169-
} catch (error) {
170-
wipeAccountAddress(storage);
171-
smartAccountWalletClient = await getAndDeploySmartAccount(walletClient, rpcUrl, chain, storage);
172-
}
161+
const smartAccountWalletClient = await getAndUpdateSmartAccount(walletClient, rpcUrl, chain, storage);
173162
if (!smartAccountWalletClient.account) {
174163
throw new Error('Smart account wallet client account not found');
175164
}
@@ -189,6 +178,8 @@ export async function login({
189178
syncServerUri,
190179
storage,
191180
identityToken,
181+
chain,
182+
rpcUrl,
192183
);
193184
} else {
194185
authData = await restoreKeys(signer, accountAddress, syncServerUri, storage, identityToken);

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

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
ContractFunctionExecutionError,
3737
type Hex,
3838
type Narrow,
39+
type PrivateKeyAccount,
3940
type SignableMessage,
4041
type WalletClient,
4142
createPublicClient,
@@ -582,13 +583,61 @@ const getSpaceActions = (space: { address: Hex; type: 'personal' | 'public' }) =
582583
return actions;
583584
};
584585

586+
export const addSmartAccountOwner = async (
587+
smartAccountClient: SmartAccountClient,
588+
newOwner: Address,
589+
chain: Chain,
590+
rpcUrl: string,
591+
) => {
592+
if (!smartAccountClient.account) {
593+
throw new Error('Invalid smart account');
594+
}
595+
const publicClient = createPublicClient({
596+
transport: http(rpcUrl),
597+
chain,
598+
});
599+
if (await isSmartAccountDeployed(smartAccountClient)) {
600+
const isOwner = await publicClient.readContract({
601+
abi: safeOwnerManagerAbi,
602+
address: smartAccountClient.account.address,
603+
functionName: 'isOwner',
604+
args: [newOwner],
605+
});
606+
if (isOwner) {
607+
return;
608+
}
609+
}
610+
611+
const tx = await smartAccountClient.sendUserOperation({
612+
calls: [
613+
{
614+
to: smartAccountClient.account.address,
615+
data: encodeFunctionData({
616+
abi: safeOwnerManagerAbi,
617+
functionName: 'addOwnerWithThreshold',
618+
args: [newOwner, BigInt(1)],
619+
}),
620+
value: BigInt(0),
621+
},
622+
],
623+
account: smartAccountClient.account,
624+
});
625+
const receipt = await smartAccountClient.waitForUserOperationReceipt({
626+
hash: tx,
627+
});
628+
if (!receipt.success) {
629+
throw new Error('Transaction to add smart account owner failed');
630+
}
631+
return receipt;
632+
};
633+
585634
// This is the function that the Connect app uses to create a smart session and
586635
// enable it on the smart account.
587636
// It will prompt the user to sign the message to enable the session, and then
588637
// execute the transaction to enable the session.
589638
// It will return the permissionId that can be used to create a smart session client.
590639
export const createSmartSession = async (
591-
owner: WalletClient,
640+
owner: WalletClient | PrivateKeyAccount,
592641
accountAddress: Hex,
593642
sessionPrivateKey: Hex,
594643
chain: Chain,
@@ -624,9 +673,6 @@ export const createSmartSession = async (
624673
if (!smartAccountClient.chain) {
625674
throw new Error('Invalid smart account chain');
626675
}
627-
if (!owner.account) {
628-
throw new Error('Invalid wallet client');
629-
}
630676

631677
const sessionKeyAccount = privateKeyToAccount(sessionPrivateKey);
632678
const transport = http(rpcUrl);
@@ -751,9 +797,9 @@ export const createSmartSession = async (
751797

752798
console.log('signing session details');
753799
// This will prompt the user to sign the message to enable the session
754-
sessionDetails.enableSessionData.enableSession.permissionEnableSig = await owner.signMessage({
800+
sessionDetails.enableSessionData.enableSession.permissionEnableSig = await (owner as WalletClient).signMessage({
755801
message: { raw: sessionDetails.permissionEnableHash },
756-
account: owner.account.address,
802+
account: (owner as WalletClient).account?.address ?? (owner as PrivateKeyAccount).address,
757803
});
758804
console.log('session details signed');
759805
const smartSessions = getSmartSessionsValidator({});

packages/hypergraph/src/identity/prove-ownership.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export const accountProofDomain = {
2020
};
2121

2222
export const proveIdentityOwnership = async (
23-
walletClient: WalletClient,
2423
smartAccountClient: SmartAccountClient,
2524
accountAddress: string,
2625
keys: IdentityKeys,

0 commit comments

Comments
 (0)