Skip to content

Commit fa0b380

Browse files
committed
spike: functional namespace creation
1 parent 3ff7dd4 commit fa0b380

File tree

23 files changed

+358
-418
lines changed

23 files changed

+358
-418
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
"tsx": "^4.0.0",
4747
"wait-port": "^1.1.0"
4848
},
49+
"pnpm": {
50+
"overrides": {
51+
"nx>axios": "^1.12.0"
52+
}
53+
},
4954
"engines": {
5055
"node": ">=18"
5156
},

packages/demo/backend/.env.example

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ PORT=3000
77
PRIVY_APP_ID=your_privy_app_id_here
88
PRIVY_APP_SECRET=your_privy_app_secret_here
99

10-
# Clerk Configuration
11-
CLERK_SECRET_KEY=your_clerk_secret_key_here
12-
CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here
13-
1410
# Local chain
1511
RPC_URL=http://127.0.0.1:9545
1612
FAUCET_ADMIN_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
@@ -19,4 +15,6 @@ FAUCET_ADDRESS=0x75538B7C906e09A5080b7CA08e794287e2DFd1d3
1915
# Gas sponsorship
2016
BASE_SEPOLIA_BUNDER_URL=SET_YOUR_GAS_SPONSOR_URL_HERE
2117
UNICHAIN_BUNDLER_URL=SET_YOUR_GAS_SPONSOR_URL_HERE
22-
UNICHAIN_BUNDLER_SPONSORSHIP_POLICY=SET_A_POLICY_HERE
18+
UNICHAIN_BUNDLER_SPONSORSHIP_POLICY=SET_A_POLICY_HERE
19+
20+
SESSION_SIGNER_PK=

packages/demo/backend/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"attribution": "tsx scripts/attribution.ts"
3737
},
3838
"dependencies": {
39-
"@clerk/backend": "^2.12.0",
4039
"@eth-optimism/utils-app": "^0.0.6",
4140
"@eth-optimism/verbs-sdk": "workspace:*",
4241
"@eth-optimism/viem": "^0.4.13",

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ export const env = cleanEnv(process.env, {
4444
PORT: port({ default: 3000 }),
4545
PRIVY_APP_ID: str({ devDefault: 'dummy' }),
4646
PRIVY_APP_SECRET: str({ devDefault: 'dummy' }),
47-
CLERK_SECRET_KEY: str({ devDefault: 'dummy' }),
48-
CLERK_PUBLISHABLE_KEY: str({ devDefault: 'dummy' }),
4947
LOCAL_DEV: bool({ default: false }),
5048
BASE_SEPOLIA_RPC_URL: str({ default: undefined }),
5149
UNICHAIN_RPC_URL: str({ default: undefined }),
@@ -59,4 +57,5 @@ export const env = cleanEnv(process.env, {
5957
BASE_SEPOLIA_BUNDER_URL: str({ devDefault: 'dummy' }),
6058
UNICHAIN_BUNDLER_URL: str({ devDefault: 'dummy' }),
6159
UNICHAIN_BUNDLER_SPONSORSHIP_POLICY: str({ devDefault: 'dummy' }),
60+
SESSION_SIGNER_PK: str(),
6261
})

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ export function createVerbsConfig(): VerbsConfig<'privy'> {
1313
provider: {
1414
type: 'privy',
1515
config: {
16-
privyClient: new PrivyClient(
17-
env.PRIVY_APP_ID,
18-
env.PRIVY_APP_SECRET,
19-
),
16+
privyClient: getPrivyClient(),
2017
},
2118
},
2219
},
@@ -69,5 +66,9 @@ export function getVerbs() {
6966
}
7067

7168
export function getPrivyClient() {
72-
return new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET)
69+
const privy = new PrivyClient(env.PRIVY_APP_ID, env.PRIVY_APP_SECRET)
70+
if (env.SESSION_SIGNER_PK) {
71+
privy.walletApi.updateAuthorizationKey(env.SESSION_SIGNER_PK)
72+
}
73+
return privy
7374
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { Context } from 'hono'
33
import type { Address } from 'viem'
44
import { z } from 'zod'
55

6+
import type { AuthContext } from '@/middleware/auth.js'
7+
68
import { validateRequest } from '../helpers/validation.js'
79
import * as lendService from '../services/lend.js'
810
import { serializeBigInt } from '../utils/serializers.js'
@@ -126,6 +128,39 @@ export class LendController {
126128
const {
127129
body: { walletId, amount, tokenAddress, chainId },
128130
} = validation.data
131+
const auth = c.get('auth') as AuthContext | undefined
132+
133+
// TODO (https://github.com/ethereum-optimism/verbs/issues/124): enforce auth and clean
134+
// up this route.
135+
if (auth && auth.userId) {
136+
const lendTransaction = await lendService.depositWithUserWallet(
137+
auth.userId,
138+
amount,
139+
tokenAddress as Address,
140+
chainId as SupportedChainId,
141+
)
142+
143+
const result = await lendService.executeLendTransactionWithUserWallet(
144+
auth.userId,
145+
lendTransaction,
146+
chainId as SupportedChainId,
147+
)
148+
149+
return c.json({
150+
transaction: {
151+
blockExplorerUrl: result.blockExplorerUrl,
152+
hash: result.hash,
153+
amount: result.amount.toString(),
154+
asset: result.asset,
155+
marketId: result.marketId,
156+
apy: result.apy,
157+
timestamp: result.timestamp,
158+
slippage: result.slippage,
159+
transactionData: serializeBigInt(result.transactionData),
160+
},
161+
})
162+
}
163+
129164
const lendTransaction = await lendService.deposit(
130165
walletId,
131166
amount,

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Context } from 'hono'
22
import type { Address } from 'viem'
33
import { z } from 'zod'
44

5+
import type { AuthContext } from '@/middleware/auth.js'
56
import type {
67
CreateWalletResponse,
78
GetAllWalletsResponse,
@@ -159,6 +160,19 @@ export class WalletController {
159160
const {
160161
params: { userId },
161162
} = validation.data
163+
const auth = c.get('auth') as AuthContext | undefined
164+
165+
// TODO (https://github.com/ethereum-optimism/verbs/issues/124): enforce auth and clean
166+
// up this route.
167+
if (auth && auth.userId) {
168+
const wallet = await walletService.getUserWallet(auth.userId)
169+
if (!wallet) {
170+
throw new Error('Wallet not found')
171+
}
172+
const balance = await walletService.getWalletBalance(wallet)
173+
return c.json({ balance: serializeBigInt(balance) })
174+
}
175+
162176
const balance = await walletService.getBalance(userId)
163177

164178
return c.json({ balance: serializeBigInt(balance) })
@@ -185,8 +199,23 @@ export class WalletController {
185199
const {
186200
params: { userId },
187201
} = validation.data
202+
const auth = c.get('auth') as AuthContext | undefined
203+
// TODO (https://github.com/ethereum-optimism/verbs/issues/124): enforce auth and clean
204+
// up this route.
205+
if (auth && auth.userId) {
206+
const wallet = await walletService.getUserWallet(auth.userId)
207+
if (!wallet) {
208+
throw new Error('Wallet not found')
209+
}
210+
const result = await walletService.fundWallet(wallet)
211+
return c.json(result)
212+
}
188213

189-
const result = await walletService.fundWallet(userId)
214+
const wallet = await walletService.getWallet(userId)
215+
if (!wallet) {
216+
throw new Error('Wallet not found')
217+
}
218+
const result = await walletService.fundWallet(wallet)
190219

191220
return c.json(result)
192221
} catch (error) {
Lines changed: 28 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,45 @@
1-
import { verifyToken } from '@clerk/backend'
21
import type { Context, Next } from 'hono'
32

4-
import { env } from '../config/env.js'
3+
import { getPrivyClient } from '@/config/verbs.js'
54

65
export interface AuthContext {
7-
userId: string
8-
clerkUserId: string
9-
privyAuthKey?: string
6+
userId?: string
107
}
118

129
export async function authMiddleware(c: Context, next: Next) {
1310
const authHeader = c.req.header('Authorization')
1411

1512
if (!authHeader?.startsWith('Bearer ')) {
16-
return c.json({ error: 'Missing or invalid authorization header' }, 401)
13+
// TODO (https://github.com/ethereum-optimism/verbs/issues/124): enforce auth
14+
// Fail silently
15+
await next()
16+
return
1717
}
1818

19-
const token = authHeader.substring(7)
19+
const accessToken = parseAuthorizationHeader(authHeader)
20+
const authContext: AuthContext = {}
2021

2122
try {
22-
const verifiedToken = await verifyToken(token, {
23-
secretKey: env.CLERK_SECRET_KEY,
24-
// Accept both the publishable key and localhost origins for development
25-
authorizedParties: [
26-
env.CLERK_PUBLISHABLE_KEY,
27-
'http://localhost:5173',
28-
'localhost:5173',
29-
],
30-
})
31-
32-
if (!verifiedToken) {
33-
return c.json({ error: 'Invalid token' }, 401)
34-
}
35-
36-
const userId = verifiedToken.sub
37-
38-
const authContext: AuthContext = {
39-
userId,
40-
clerkUserId: userId,
41-
}
42-
43-
try {
44-
// Get Privy authorization key for the authenticated user
45-
// This enables user-owned wallets via authenticated signers
46-
const authKeyResponse = await fetch(
47-
`https://auth.privy.io/api/v1/authorization/init`,
48-
{
49-
method: 'POST',
50-
headers: {
51-
'Content-Type': 'application/json',
52-
'privy-app-id': env.PRIVY_APP_ID,
53-
Authorization: `Bearer ${token}`,
54-
},
55-
},
56-
)
23+
const privy = getPrivyClient()
24+
const verifiedPrivy = await privy
25+
.verifyAuthToken(accessToken)
26+
.catch((err) => {
27+
console.error('❌ Auth middleware: Token verification failed:', err)
28+
throw c.json({ error: 'Invalid or expired token' }, 401)
29+
})
30+
const userId = verifiedPrivy.userId
31+
authContext.userId = userId
32+
} catch {
33+
// TODO (https://github.com/ethereum-optimism/verbs/issues/124): enforce auth
34+
// Silently continue without Privy auth key if request fails
35+
}
5736

58-
if (authKeyResponse.ok) {
59-
const authKeyData = await authKeyResponse.json()
60-
authContext.privyAuthKey = authKeyData.authorizationKey
61-
}
62-
} catch {
63-
// Silently continue without Privy auth key if request fails
64-
}
37+
c.set('auth', authContext)
38+
await next()
39+
}
6540

66-
c.set('auth', authContext)
67-
await next()
68-
} catch (error) {
69-
console.error('❌ Auth middleware: Token verification failed:', error)
70-
return c.json({ error: 'Invalid or expired token' }, 401)
71-
}
41+
const parseAuthorizationHeader = (value: string) => {
42+
return value.replace('Bearer', '').trim()
7243
}
44+
45+
export const PRIVY_TOKEN_COOKIE_KEY = 'privy-token'

packages/demo/backend/src/router.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url'
55

66
import { LendController } from './controllers/lend.js'
77
import { WalletController } from './controllers/wallet.js'
8+
import { authMiddleware } from './middleware/auth.js'
89

910
export const router = new Hono()
1011

@@ -44,8 +45,12 @@ router.get('/version', (c) => {
4445
router.get('/wallets', walletController.getAllWallets)
4546
router.post('/wallet/:userId', walletController.createWallet)
4647
router.get('/wallet/:userId', walletController.getWallet)
47-
router.get('/wallet/:userId/balance', walletController.getBalance)
48-
router.post('/wallet/:userId/fund', walletController.fundWallet)
48+
router.get(
49+
'/wallet/:userId/balance',
50+
authMiddleware,
51+
walletController.getBalance,
52+
)
53+
router.post('/wallet/:userId/fund', authMiddleware, walletController.fundWallet)
4954
router.post('/wallet/send', walletController.sendTokens)
5055

5156
// Lend endpoints
@@ -55,4 +60,4 @@ router.get(
5560
'/lend/market/:vaultAddress/balance/:walletId',
5661
lendController.getMarketBalance,
5762
)
58-
router.post('/lend/deposit', lendController.deposit)
63+
router.post('/lend/deposit', authMiddleware, lendController.deposit)

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { Address } from 'viem'
88
import { baseSepolia, unichain } from 'viem/chains'
99

1010
import { getVerbs } from '../config/verbs.js'
11-
import { getWallet } from './wallet.js'
11+
import { getUserWallet, getWallet } from './wallet.js'
1212

1313
interface MarketBalanceResult {
1414
balance: bigint
@@ -112,6 +112,27 @@ export async function formatMarketBalanceResponse(
112112
}
113113
}
114114

115+
export async function depositWithUserWallet(
116+
userId: string,
117+
amount: number,
118+
tokenAddress: Address,
119+
chainId: SupportedChainId,
120+
): Promise<LendTransaction> {
121+
const wallet = await getUserWallet(userId)
122+
123+
if (!wallet) {
124+
throw new Error(`Wallet not found for user ID: ${userId}`)
125+
}
126+
127+
if ('lendExecute' in wallet && typeof wallet.lendExecute === 'function') {
128+
return await wallet.lendExecute(amount, tokenAddress, chainId)
129+
} else {
130+
throw new Error(
131+
'Lend functionality not yet implemented for this wallet type.',
132+
)
133+
}
134+
}
135+
115136
export async function deposit(
116137
walletId: string,
117138
amount: number,
@@ -133,6 +154,38 @@ export async function deposit(
133154
}
134155
}
135156

157+
export async function executeLendTransactionWithUserWallet(
158+
userId: string,
159+
lendTransaction: LendTransaction,
160+
chainId: SupportedChainId,
161+
): Promise<LendTransaction & { blockExplorerUrl: string }> {
162+
const wallet = await getUserWallet(userId)
163+
164+
if (!wallet) {
165+
throw new Error(`Wallet not found for user ID: ${userId}`)
166+
}
167+
168+
if (!lendTransaction.transactionData) {
169+
throw new Error('No transaction data available for execution')
170+
}
171+
172+
const depositHash = lendTransaction.transactionData.approval
173+
? await wallet.sendBatch(
174+
[
175+
lendTransaction.transactionData.approval,
176+
lendTransaction.transactionData.deposit,
177+
],
178+
chainId,
179+
)
180+
: await wallet.send(lendTransaction.transactionData.deposit, chainId)
181+
182+
return {
183+
...lendTransaction,
184+
hash: depositHash,
185+
blockExplorerUrl: await getBlockExplorerUrl(chainId),
186+
}
187+
}
188+
136189
export async function executeLendTransaction(
137190
walletId: string,
138191
lendTransaction: LendTransaction,

0 commit comments

Comments
 (0)