Skip to content

Commit f7557a7

Browse files
pcarranzavnikgraf
andauthored
feat: use the Connect signaturePrivateKey as an additional owner in the smart account (#254)
Co-authored-by: Nik Graf <[email protected]>
1 parent 2403970 commit f7557a7

File tree

10 files changed

+150
-59
lines changed

10 files changed

+150
-59
lines changed

apps/connect/src/Boot.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ declare module '@tanstack/react-router' {
1818

1919
const queryClient = new QueryClient();
2020

21-
const storage = localStorage;
21+
const addressStorage = localStorage;
22+
const keysStorage = sessionStorage;
2223

2324
export function Boot() {
2425
// check if the user is already authenticated on initial render
2526
const initialRenderAuthCheckRef = useRef(false);
2627
// using a layout effect to avoid a re-render
2728
useLayoutEffect(() => {
2829
if (!initialRenderAuthCheckRef.current) {
29-
const accountAddress = Connect.loadAccountAddress(storage);
30+
const accountAddress = Connect.loadAccountAddress(addressStorage);
3031
if (accountAddress) {
31-
const keys = Connect.loadKeys(storage, accountAddress);
32+
const keys = Connect.loadKeys(keysStorage, accountAddress);
3233
if (keys) {
3334
// user is already authenticated, set state
3435
StoreConnect.store.send({

apps/connect/src/components/LogoutButton.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Loading } from '@/components/ui/Loading';
2+
import { Connect } from '@graphprotocol/hypergraph';
23
import { usePrivy } from '@privy-io/react-auth';
34
import { useRouter } from '@tanstack/react-router';
45
import { useState } from 'react';
@@ -9,6 +10,7 @@ export function LogoutButton() {
910
const [isLoading, setIsLoading] = useState(false);
1011
const disconnectWallet = async () => {
1112
setIsLoading(true);
13+
Connect.wipeAllAuthData(localStorage, sessionStorage);
1214
await privyLogout();
1315
router.navigate({
1416
to: '/login',

apps/connect/src/routes/authenticate.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Effect, Schema } from 'effect';
1212
import { TriangleAlert } from 'lucide-react';
1313
import { useEffect } from 'react';
1414
import { createWalletClient, custom } from 'viem';
15+
import { privateKeyToAccount } from 'viem/accounts';
1516

1617
const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET;
1718

@@ -381,9 +382,11 @@ function AuthenticateComponent() {
381382
})) ?? [];
382383
console.log('spaces', spaces);
383384

385+
const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`);
386+
console.log('local account', localAccount.address);
384387
// TODO: add additional actions (must be passed from the app)
385388
const permissionId = await Connect.createSmartSession(
386-
walletClient,
389+
localAccount,
387390
accountAddress,
388391
newAppIdentity.addressPrivateKey,
389392
CHAIN,
@@ -396,21 +399,22 @@ function AuthenticateComponent() {
396399
);
397400
console.log('smart session created');
398401
const smartAccountClient = await Connect.getSmartAccountWalletClient({
399-
owner: walletClient,
402+
owner: localAccount,
400403
address: accountAddress,
401404
chain: CHAIN,
402405
rpcUrl: import.meta.env.VITE_HYPERGRAPH_RPC_URL,
403406
});
404407

408+
console.log('encrypting app identity');
405409
const { ciphertext, nonce } = await Connect.encryptAppIdentity(
406410
signer,
407411
newAppIdentity.address,
408412
newAppIdentity.addressPrivateKey,
409413
permissionId,
410414
keys,
411415
);
416+
console.log('proving ownership');
412417
const { accountProof, keyProof } = await Identity.proveIdentityOwnership(
413-
walletClient,
414418
smartAccountClient,
415419
accountAddress,
416420
keys,

apps/connect/src/routes/login.lazy.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { type WalletClient, createWalletClient, custom } from 'viem';
88

99
const CHAIN = import.meta.env.VITE_HYPERGRAPH_CHAIN === 'geogenesis' ? Connect.GEOGENESIS : Connect.GEO_TESTNET;
1010
const syncServerUri = import.meta.env.VITE_HYPERGRAPH_SYNC_SERVER_ORIGIN;
11-
const storage = localStorage;
11+
const addressStorage = localStorage;
12+
const keysStorage = sessionStorage;
1213

1314
export const Route = createLazyFileRoute('/login')({
1415
component: () => <Login />,
@@ -52,7 +53,16 @@ function Login() {
5253
console.log(walletClient);
5354
console.log(rpcUrl);
5455
console.log(CHAIN);
55-
await Connect.login({ walletClient, signer, syncServerUri, storage, identityToken, rpcUrl, chain: CHAIN });
56+
await Connect.login({
57+
walletClient,
58+
signer,
59+
syncServerUri,
60+
addressStorage,
61+
keysStorage,
62+
identityToken,
63+
rpcUrl,
64+
chain: CHAIN,
65+
});
5666
},
5767
[identityToken, signMessage],
5868
);

apps/server/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { parse } from 'node:url';
12
import { Connect, Identity, Inboxes, Messages, SpaceEvents, Utils } from '@graphprotocol/hypergraph';
23
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
34
import cors from 'cors';
45
import { Effect, Exit, Schema } from 'effect';
56
import express, { type Request, type Response } from 'express';
6-
import { parse } from 'node:url';
77
import WebSocket, { WebSocketServer } from 'ws';
88
import { addAppIdentityToSpaces } from './handlers/add-app-identity-to-spaces.js';
99
import { applySpaceEvent } from './handlers/applySpaceEvent.js';

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 = (addressStorage: Storage, keysAndTokenStorage: Storage) => {
70+
const accountAddress = loadAccountAddress(addressStorage);
71+
wipeAccountAddress(addressStorage);
72+
if (accountAddress) {
73+
wipeKeys(keysAndTokenStorage, accountAddress);
74+
wipeSyncServerSessionToken(keysAndTokenStorage, accountAddress);
75+
}
76+
};

packages/hypergraph/src/connect/login.ts

Lines changed: 43 additions & 42 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,
@@ -29,17 +31,26 @@ export async function signup(
2931
smartAccountClient: SmartAccountClient,
3032
accountAddress: Address,
3133
syncServerUri: string,
32-
storage: Storage,
34+
addressStorage: Storage,
35+
keysStorage: Storage,
3336
identityToken: string,
37+
chain: Chain,
38+
rpcUrl: string,
3439
) {
3540
const keys = createIdentityKeys();
3641
const { ciphertext, nonce } = await encryptIdentity(signer, keys);
37-
const { accountProof, keyProof } = await proveIdentityOwnership(
38-
walletClient,
39-
smartAccountClient,
40-
accountAddress,
41-
keys,
42-
);
42+
43+
const localAccount = privateKeyToAccount(keys.signaturePrivateKey as `0x${string}`);
44+
// This will deploy the smart account if it's not deployed
45+
await addSmartAccountOwner(smartAccountClient, localAccount.address, chain, rpcUrl);
46+
const localSmartAccountClient = await getSmartAccountWalletClient({
47+
owner: localAccount,
48+
address: accountAddress,
49+
rpcUrl,
50+
chain,
51+
});
52+
53+
const { accountProof, keyProof } = await proveIdentityOwnership(localSmartAccountClient, accountAddress, keys);
4354

4455
const req: Messages.RequestConnectCreateIdentity = {
4556
keyBox: { signer: await signer.getAddress(), accountAddress, ciphertext, nonce },
@@ -64,8 +75,8 @@ export async function signup(
6475
if (!decoded.success) {
6576
throw new Error('Error creating identity');
6677
}
67-
storeKeys(storage, accountAddress, keys);
68-
storeAccountAddress(storage, accountAddress);
78+
storeKeys(keysStorage, accountAddress, keys);
79+
storeAccountAddress(addressStorage, accountAddress);
6980
return {
7081
accountAddress,
7182
keys,
@@ -76,7 +87,8 @@ export async function restoreKeys(
7687
signer: Signer,
7788
accountAddress: Address,
7889
syncServerUri: string,
79-
storage: Storage,
90+
addressStorage: Storage,
91+
keysStorage: Storage,
8092
identityToken: string,
8193
) {
8294
const res = await fetch(new URL('/connect/identity/encrypted', syncServerUri), {
@@ -93,8 +105,8 @@ export async function restoreKeys(
93105
const { keyBox } = decoded;
94106
const { ciphertext, nonce } = keyBox;
95107
const keys = await decryptIdentity(signer, ciphertext, nonce);
96-
storeKeys(storage, accountAddress, keys);
97-
storeAccountAddress(storage, accountAddress);
108+
storeKeys(keysStorage, accountAddress, keys);
109+
storeAccountAddress(addressStorage, accountAddress);
98110
return {
99111
accountAddress,
100112
keys,
@@ -103,8 +115,13 @@ export async function restoreKeys(
103115
throw new Error(`Error fetching identity ${res.status}`);
104116
}
105117

106-
const getAndDeploySmartAccount = async (walletClient: WalletClient, rpcUrl: string, chain: Chain, storage: Storage) => {
107-
const accountAddressFromStorage = loadAccountAddress(storage) as Hex;
118+
const getAndUpdateSmartAccount = async (
119+
walletClient: WalletClient,
120+
rpcUrl: string,
121+
chain: Chain,
122+
addressStorage: Storage,
123+
) => {
124+
const accountAddressFromStorage = loadAccountAddress(addressStorage) as Hex;
108125
const smartAccountParams: SmartAccountParams = {
109126
owner: walletClient,
110127
rpcUrl,
@@ -128,53 +145,34 @@ const getAndDeploySmartAccount = async (walletClient: WalletClient, rpcUrl: stri
128145
// Create the client again to ensure we have the 7579 config now
129146
return getSmartAccountWalletClient(smartAccountParams);
130147
}
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-
}
146148
return smartAccountWalletClient;
147149
};
148150

149151
export async function login({
150152
walletClient,
151153
signer,
152154
syncServerUri,
153-
storage,
155+
addressStorage,
156+
keysStorage,
154157
identityToken,
155158
rpcUrl,
156159
chain,
157160
}: {
158161
walletClient: WalletClient;
159162
signer: Signer;
160163
syncServerUri: string;
161-
storage: Storage;
164+
addressStorage: Storage;
165+
keysStorage: Storage;
162166
identityToken: string;
163167
rpcUrl: string;
164168
chain: Chain;
165169
}) {
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-
}
170+
const smartAccountWalletClient = await getAndUpdateSmartAccount(walletClient, rpcUrl, chain, addressStorage);
173171
if (!smartAccountWalletClient.account) {
174172
throw new Error('Smart account wallet client account not found');
175173
}
176174
const accountAddress = smartAccountWalletClient.account.address;
177-
// const keys = loadKeys(storage, accountAddress);
175+
178176
let authData: {
179177
accountAddress: Address;
180178
keys: IdentityKeys;
@@ -187,11 +185,14 @@ export async function login({
187185
smartAccountWalletClient,
188186
accountAddress,
189187
syncServerUri,
190-
storage,
188+
addressStorage,
189+
keysStorage,
191190
identityToken,
191+
chain,
192+
rpcUrl,
192193
);
193194
} else {
194-
authData = await restoreKeys(signer, accountAddress, syncServerUri, storage, identityToken);
195+
authData = await restoreKeys(signer, accountAddress, syncServerUri, addressStorage, keysStorage, identityToken);
195196
}
196197
store.send({ type: 'reset' });
197198
store.send({

0 commit comments

Comments
 (0)