Skip to content

Commit f38831e

Browse files
authored
Merge pull request #49 from ethereum-optimism/harry/paymaster
Implement starter wallet abstraction
2 parents 99be9ec + a7213c3 commit f38831e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2851
-1606
lines changed

packages/demo/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@eth-optimism/verbs-sdk": "workspace:*",
4040
"@eth-optimism/viem": "^0.4.13",
4141
"@hono/node-server": "^1.14.0",
42+
"@privy-io/server-auth": "^1.31.1",
4243
"commander": "^13.1.0",
4344
"dotenv": "^16.4.5",
4445
"envalid": "^8.1.0",

packages/demo/backend/src/app.integration.spec.ts

Lines changed: 85 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,54 +12,93 @@ import { router } from './router.js'
1212
vi.mock('./config/verbs.js', () => ({
1313
initializeVerbs: vi.fn(),
1414
getVerbs: vi.fn(() => ({
15-
createWallet: vi.fn((userId: string) =>
16-
Promise.resolve({
17-
id: `wallet-${userId}`,
18-
address: `0x${userId.padEnd(40, '0')}`,
19-
getBalance: () =>
20-
Promise.resolve([
21-
{ symbol: 'USDC', balance: 1000000n },
22-
{ symbol: 'MORPHO', balance: 500000n },
23-
]),
24-
}),
25-
),
26-
getWallet: vi.fn((userId: string) => {
27-
// Simulate some users existing and others not
28-
if (userId.includes('non-existent')) {
29-
return Promise.resolve(null)
30-
}
31-
return Promise.resolve({
32-
id: `wallet-${userId}`,
33-
address: `0x${userId.padEnd(40, '0')}`,
34-
getBalance: () =>
35-
Promise.resolve([
36-
{ symbol: 'USDC', balance: 1000000n },
37-
{ symbol: 'MORPHO', balance: 500000n },
38-
]),
39-
})
40-
}),
41-
getAllWallets: vi.fn(() =>
42-
Promise.resolve([
43-
{
44-
id: 'wallet-1',
45-
address: '0x1111111111111111111111111111111111111111',
15+
wallet: {
16+
createWalletWithEmbeddedSigner: vi.fn(() =>
17+
Promise.resolve({
18+
id: `wallet-1`,
19+
signer: {
20+
address: `0x1111111111111111111111111111111111111111`,
21+
},
22+
getAddress: () =>
23+
Promise.resolve(`0x1111111111111111111111111111111111111111`),
4624
getBalance: () =>
4725
Promise.resolve([
4826
{ symbol: 'USDC', balance: 1000000n },
4927
{ symbol: 'MORPHO', balance: 500000n },
5028
]),
29+
}),
30+
),
31+
getSmartWalletWithEmbeddedSigner: vi.fn(
32+
({ walletId: userId }: { walletId: string }) => {
33+
// Simulate some users existing and others not
34+
if (userId.includes('non-existent')) {
35+
return Promise.resolve(null)
36+
}
37+
return Promise.resolve({
38+
id: `wallet-${userId}`,
39+
getAddress: () => Promise.resolve(`0x${userId.padEnd(40, '0')}`),
40+
getBalance: () =>
41+
Promise.resolve([
42+
{ symbol: 'USDC', balance: 1000000n },
43+
{ symbol: 'MORPHO', balance: 500000n },
44+
]),
45+
})
5146
},
52-
{
53-
id: 'wallet-2',
54-
address: '0x2222222222222222222222222222222222222222',
55-
getBalance: () =>
56-
Promise.resolve([
57-
{ symbol: 'USDC', balance: 2000000n },
58-
{ symbol: 'MORPHO', balance: 750000n },
59-
]),
60-
},
61-
]),
62-
),
47+
),
48+
smartWalletProvider: {
49+
getWalletAddress: vi.fn(({ owners }: { owners: string[] }) => {
50+
return Promise.resolve(owners[0])
51+
}),
52+
getWallet: vi.fn(
53+
({
54+
walletAddress,
55+
signer,
56+
ownerIndex,
57+
}: {
58+
walletAddress: string
59+
signer: string
60+
ownerIndex: number
61+
}) => {
62+
return {
63+
address: walletAddress,
64+
getAddress: () => Promise.resolve(walletAddress),
65+
signer,
66+
ownerIndex,
67+
}
68+
},
69+
),
70+
},
71+
embeddedWalletProvider: {
72+
getAllWallets: vi.fn(() =>
73+
Promise.resolve([
74+
{
75+
id: 'wallet-1',
76+
address: '0x1111111111111111111111111111111111111111',
77+
getBalance: () =>
78+
Promise.resolve([
79+
{ symbol: 'USDC', balance: 1000000n },
80+
{ symbol: 'MORPHO', balance: 500000n },
81+
]),
82+
signer: vi.fn().mockResolvedValue({
83+
address: '0x1111111111111111111111111111111111111111',
84+
}),
85+
},
86+
{
87+
id: 'wallet-2',
88+
address: '0x2222222222222222222222222222222222222222',
89+
getBalance: () =>
90+
Promise.resolve([
91+
{ symbol: 'USDC', balance: 2000000n },
92+
{ symbol: 'MORPHO', balance: 750000n },
93+
]),
94+
signer: vi.fn().mockResolvedValue({
95+
address: '0x2222222222222222222222222222222222222222',
96+
}),
97+
},
98+
]),
99+
),
100+
},
101+
},
63102
lend: {
64103
getVaults: vi.fn(() =>
65104
Promise.resolve([
@@ -186,11 +225,12 @@ describe('HTTP API Integration', () => {
186225

187226
expect(response.statusCode).toBe(200)
188227
const data = (await response.body.json()) as any
189-
190-
expect(data).toHaveProperty('address')
228+
expect(data).toHaveProperty('privyAddress')
229+
expect(data).toHaveProperty('smartWalletAddress')
191230
expect(data).toHaveProperty('userId')
192231
expect(data.userId).toBe(testUserId)
193-
expect(data.address).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/) // Basic address format validation
232+
expect(data.smartWalletAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/)
233+
expect(data.privyAddress).toMatch(/^0x[a-zA-Z0-9\-]{1,}$/)
194234
})
195235

196236
it('should get an existing wallet', async () => {

packages/demo/backend/src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,5 @@ class VerbsApp extends App {
8484
}
8585
}
8686

87+
export * from '@/types/index.js'
8788
export { VerbsApp }

packages/demo/backend/src/config/env.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ export const env = cleanEnv(process.env, {
4545
PRIVY_APP_ID: str({ devDefault: 'dummy' }),
4646
PRIVY_APP_SECRET: str({ devDefault: 'dummy' }),
4747
LOCAL_DEV: bool({ default: false }),
48-
RPC_URL: str({ default: 'http://127.0.0.1:9545' }),
48+
BASE_SEPOLIA_RPC_URL: str({ default: undefined }),
49+
UNICHAIN_RPC_URL: str({ default: undefined }),
4950
FAUCET_ADMIN_PRIVATE_KEY: str({
5051
default:
5152
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
5253
}),
5354
FAUCET_ADDRESS: str({
5455
default: getFaucetAddressDefault(),
5556
}),
57+
BASE_SEPOLIA_BUNDER_URL: str({ default: undefined }),
5658
})

packages/demo/backend/src/config/verbs.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,49 @@
1-
import {
2-
initVerbs,
3-
type VerbsConfig,
4-
type VerbsInterface,
5-
} from '@eth-optimism/verbs-sdk'
6-
import { unichain } from 'viem/chains'
1+
import { Verbs, type VerbsConfig } from '@eth-optimism/verbs-sdk'
2+
import { PrivyClient } from '@privy-io/server-auth'
3+
import { baseSepolia, unichain } from 'viem/chains'
74

85
import { env } from './env.js'
96

10-
let verbsInstance: VerbsInterface
7+
let verbsInstance: Verbs
118

129
export function createVerbsConfig(): VerbsConfig {
1310
return {
1411
wallet: {
15-
type: 'privy',
16-
appId: env.PRIVY_APP_ID,
17-
appSecret: env.PRIVY_APP_SECRET,
12+
embeddedWalletConfig: {
13+
provider: {
14+
type: 'privy',
15+
privyClient: new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET),
16+
},
17+
},
18+
smartWalletConfig: {
19+
provider: {
20+
type: 'default',
21+
},
22+
},
1823
},
1924
lend: {
2025
type: 'morpho',
2126
},
2227
chains: [
2328
{
2429
chainId: unichain.id,
25-
rpcUrl: env.RPC_URL,
30+
rpcUrl: env.UNICHAIN_RPC_URL || unichain.rpcUrls.default.http[0],
31+
},
32+
{
33+
chainId: baseSepolia.id,
34+
rpcUrl: env.BASE_SEPOLIA_RPC_URL || baseSepolia.rpcUrls.default.http[0],
35+
bundlerUrl: env.BASE_SEPOLIA_BUNDER_URL,
2636
},
2737
],
2838
}
2939
}
3040

3141
export function initializeVerbs(config?: VerbsConfig): void {
3242
const verbsConfig = config || createVerbsConfig()
33-
verbsInstance = initVerbs(verbsConfig)
43+
verbsInstance = new Verbs(verbsConfig)
3444
}
3545

36-
export function getVerbs(): VerbsInterface {
46+
export function getVerbs() {
3747
if (!verbsInstance) {
3848
throw new Error('Verbs SDK not initialized. Call initializeVerbs() first.')
3949
}

packages/demo/backend/src/controllers/lend.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Context } from 'hono'
22
import type { Address } from 'viem'
3+
import { baseSepolia } from 'viem/chains'
34
import { z } from 'zod'
45

56
import { validateRequest } from '../helpers/validation.js'
67
import * as lendService from '../services/lend.js'
8+
import { serializeBigInt } from '../utils/serializers.js'
79

810
const DepositRequestSchema = z.object({
911
body: z.object({
@@ -117,10 +119,16 @@ export class LendController {
117119
const {
118120
body: { walletId, amount, token },
119121
} = validation.data
120-
const lendTransaction = await lendService.deposit(walletId, amount, token)
122+
const lendTransaction = await lendService.deposit(
123+
walletId,
124+
amount,
125+
token,
126+
baseSepolia.id,
127+
)
121128
const result = await lendService.executeLendTransaction(
122129
walletId,
123130
lendTransaction,
131+
baseSepolia.id,
124132
)
125133

126134
return c.json({
@@ -132,10 +140,11 @@ export class LendController {
132140
apy: result.apy,
133141
timestamp: result.timestamp,
134142
slippage: result.slippage,
135-
transactionData: result.transactionData,
143+
transactionData: serializeBigInt(result.transactionData),
136144
},
137145
})
138146
} catch (error) {
147+
console.error('Failed to deposit', error)
139148
return c.json(
140149
{
141150
error: 'Failed to deposit',

packages/demo/backend/src/controllers/wallet.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import type { Context } from 'hono'
2+
import type { Address } from 'viem'
3+
import { z } from 'zod'
4+
15
import type {
26
CreateWalletResponse,
37
GetAllWalletsResponse,
48
GetWalletResponse,
5-
} from '@eth-optimism/verbs-sdk'
6-
import type { Context } from 'hono'
7-
import type { Address } from 'viem'
8-
import { z } from 'zod'
9+
} from '@/types/service.js'
910

1011
import { validateRequest } from '../helpers/validation.js'
1112
import * as walletService from '../services/wallet.js'
@@ -58,13 +59,16 @@ export class WalletController {
5859
const {
5960
params: { userId },
6061
} = validation.data
61-
const wallet = await walletService.createWallet(userId)
62+
const { privyAddress, smartWalletAddress } =
63+
await walletService.createWallet()
6264

6365
return c.json({
64-
address: wallet.address,
66+
privyAddress,
67+
smartWalletAddress,
6568
userId,
6669
} satisfies CreateWalletResponse)
6770
} catch (error) {
71+
console.error(error)
6872
return c.json(
6973
{
7074
error: 'Failed to create wallet',
@@ -86,7 +90,7 @@ export class WalletController {
8690
const {
8791
params: { userId },
8892
} = validation.data
89-
const wallet = await walletService.getWallet(userId)
93+
const { wallet } = await walletService.getWallet(userId)
9094

9195
if (!wallet) {
9296
return c.json(
@@ -97,12 +101,14 @@ export class WalletController {
97101
404,
98102
)
99103
}
104+
const walletAddress = await wallet.getAddress()
100105

101106
return c.json({
102-
address: wallet.address,
107+
address: walletAddress,
103108
userId,
104109
} satisfies GetWalletResponse)
105110
} catch (error) {
111+
console.error(error)
106112
return c.json(
107113
{
108114
error: 'Failed to get wallet',
@@ -125,15 +131,19 @@ export class WalletController {
125131
query: { limit, cursor },
126132
} = validation.data
127133
const wallets = await walletService.getAllWallets({ limit, cursor })
134+
const walletsData = await Promise.all(
135+
wallets.map(async ({ wallet, id }) => ({
136+
address: await wallet.getAddress(),
137+
id,
138+
})),
139+
)
128140

129141
return c.json({
130-
wallets: wallets.map((wallet) => ({
131-
address: wallet.address,
132-
id: wallet.id,
133-
})),
142+
wallets: walletsData,
134143
count: wallets.length,
135144
} satisfies GetAllWalletsResponse)
136145
} catch (error) {
146+
console.error(error)
137147
return c.json(
138148
{
139149
error: 'Failed to get wallets',

0 commit comments

Comments
 (0)