cdp-docs-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/bin/cdp-docs.js +48 -0
- package/bin/cdp-setup.js +91 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +212 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -0
- package/src/templates/docs/exporting.md +0 -0
- package/src/templates/docs/fund.md +122 -0
- package/src/templates/docs/importing.md +273 -0
- package/src/templates/docs/managing-accounts.md +366 -0
- package/src/templates/docs/policies.md +730 -0
- package/src/templates/docs/transfer.md +366 -0
- package/src/templates/docs/wallet-accounts.md +203 -0
- package/src/templates/docs/wallet-start.md +741 -0
- package/src/templates/prompt/INTEGRATION-SUMMARY.md +151 -0
- package/src/templates/prompt/SETUP-CDP-WALLET.md +142 -0
- package/src/templates/prompt/cdp-wallet.md +1155 -0
- package/src/templates/prompt/context.md +105 -0
- package/src/templates/prompt/directory.md +97 -0
@@ -0,0 +1,1155 @@
|
|
1
|
+
You are given a task to integrate **Coinbase Developer Platform (CDP) Wallet API v2** into this codebase
|
2
|
+
|
3
|
+
The codebase should support:
|
4
|
+
- Next.js App Router
|
5
|
+
- Tailwind CSS
|
6
|
+
- TypeScript
|
7
|
+
|
8
|
+
If it doesn't, provide instructions on how to setup the project, install Tailwind or TypeScript.
|
9
|
+
|
10
|
+
## 📝 Implementation Lessons Learned
|
11
|
+
|
12
|
+
Based on recent integration experience, here are **critical issues to avoid**:
|
13
|
+
|
14
|
+
### ⚠️ Common Pitfalls & Solutions
|
15
|
+
|
16
|
+
1. **TypeScript Type Safety**: Always handle potentially undefined API responses
|
17
|
+
```typescript
|
18
|
+
// ❌ Causes: "Type 'undefined' is not assignable to parameter"
|
19
|
+
wallet.setAccounts(result.accounts)
|
20
|
+
|
21
|
+
// ✅ Safe handling with fallback
|
22
|
+
wallet.setAccounts(result.accounts || [])
|
23
|
+
```
|
24
|
+
|
25
|
+
2. **CDP SDK Response Structure**: The SDK returns paginated objects, not direct arrays
|
26
|
+
```typescript
|
27
|
+
// ❌ Incorrect assumption
|
28
|
+
const accounts = await cdp.evm.listAccounts()
|
29
|
+
return accounts.map(...) // Error: accounts is an object, not array
|
30
|
+
|
31
|
+
// ✅ Correct understanding of CDP API
|
32
|
+
const response = await cdp.evm.listAccounts()
|
33
|
+
return response.accounts?.map(...) || []
|
34
|
+
```
|
35
|
+
|
36
|
+
3. **Mock Implementation Linting**: Handle unused parameters in mock functions
|
37
|
+
```typescript
|
38
|
+
// Add this pattern for mock functions:
|
39
|
+
const _ = { address, network }; // Acknowledge unused params for mock implementation
|
40
|
+
```
|
41
|
+
|
42
|
+
4. **Documentation Alignment**: Always reference official CDP docs before assuming API structures
|
43
|
+
|
44
|
+
### 🛡️ Prevention Strategy
|
45
|
+
|
46
|
+
- Run `npm run build` frequently during development
|
47
|
+
- Test TypeScript compilation before committing
|
48
|
+
- Reference CDP docs for exact API response formats
|
49
|
+
- Use optional chaining (`?.`) and fallbacks for uncertain data
|
50
|
+
|
51
|
+
---
|
52
|
+
|
53
|
+
## Prerequisites Check
|
54
|
+
|
55
|
+
Your project should have:
|
56
|
+
- Node.js 22+
|
57
|
+
- Next.js 15+ with App Router
|
58
|
+
- TypeScript configured
|
59
|
+
- Tailwind CSS configured
|
60
|
+
|
61
|
+
If missing any of these, set them up first before proceeding.
|
62
|
+
|
63
|
+
## Installation
|
64
|
+
|
65
|
+
Install the required dependencies:
|
66
|
+
```bash
|
67
|
+
npm install @coinbase/cdp-sdk dotenv viem
|
68
|
+
```
|
69
|
+
|
70
|
+
> `@coinbase/cdp-sdk` - Coinbase Developer Platform SDK
|
71
|
+
> `dotenv` - Environment variable loading
|
72
|
+
> `viem` - Ethereum utilities (used for transaction receipts)
|
73
|
+
|
74
|
+
## Environment Setup
|
75
|
+
|
76
|
+
Create `.env.local` in your project root with these **required** values:
|
77
|
+
```bash
|
78
|
+
CDP_API_KEY_ID=your_key_id_here
|
79
|
+
CDP_API_KEY_SECRET=your_key_secret_here
|
80
|
+
CDP_WALLET_SECRET=your_wallet_secret_here
|
81
|
+
```
|
82
|
+
|
83
|
+
**⚠️ IMPORTANT**: Never commit this file. Add `.env.local` to your `.gitignore`.
|
84
|
+
|
85
|
+
To get these values:
|
86
|
+
1. Visit [CDP Portal](https://portal.cdp.coinbase.com/)
|
87
|
+
2. Create API keys under "API Keys" section
|
88
|
+
3. Generate a wallet secret for signing transactions
|
89
|
+
|
90
|
+
## Core CDP Client Setup
|
91
|
+
|
92
|
+
Create `src/lib/cdp.ts`:
|
93
|
+
```typescript
|
94
|
+
'use server'
|
95
|
+
import { CdpClient } from '@coinbase/cdp-sdk'
|
96
|
+
import 'dotenv/config'
|
97
|
+
|
98
|
+
if (!process.env.CDP_API_KEY_ID) {
|
99
|
+
throw new Error('CDP_API_KEY_ID environment variable is required')
|
100
|
+
}
|
101
|
+
|
102
|
+
if (!process.env.CDP_API_KEY_SECRET) {
|
103
|
+
throw new Error('CDP_API_KEY_SECRET environment variable is required')
|
104
|
+
}
|
105
|
+
|
106
|
+
if (!process.env.CDP_WALLET_SECRET) {
|
107
|
+
throw new Error('CDP_WALLET_SECRET environment variable is required')
|
108
|
+
}
|
109
|
+
|
110
|
+
export const cdp = new CdpClient({
|
111
|
+
apiKeyId: process.env.CDP_API_KEY_ID,
|
112
|
+
apiKeySecret: process.env.CDP_API_KEY_SECRET,
|
113
|
+
walletSecret: process.env.CDP_WALLET_SECRET,
|
114
|
+
})
|
115
|
+
|
116
|
+
// Helper function to close CDP client properly
|
117
|
+
export async function closeCdp() {
|
118
|
+
await cdp.close()
|
119
|
+
}
|
120
|
+
```
|
121
|
+
|
122
|
+
## Wallet Utilities
|
123
|
+
|
124
|
+
Create `src/lib/wallet-utils.ts`:
|
125
|
+
```typescript
|
126
|
+
'use server'
|
127
|
+
import { cdp } from './cdp'
|
128
|
+
import { parseEther } from 'viem'
|
129
|
+
|
130
|
+
export type NetworkType = 'base-sepolia' | 'base-mainnet' | 'ethereum-sepolia' | 'ethereum-mainnet'
|
131
|
+
export type TokenType = 'eth' | 'usdc'
|
132
|
+
|
133
|
+
// EVM Account Management
|
134
|
+
export async function createOrGetEvmAccount(name: string) {
|
135
|
+
try {
|
136
|
+
const account = await cdp.evm.getOrCreateAccount({ name })
|
137
|
+
return {
|
138
|
+
success: true,
|
139
|
+
account: {
|
140
|
+
address: account.address,
|
141
|
+
name: account.name,
|
142
|
+
network: account.network,
|
143
|
+
}
|
144
|
+
}
|
145
|
+
} catch (error) {
|
146
|
+
return {
|
147
|
+
success: false,
|
148
|
+
error: error instanceof Error ? error.message : 'Failed to create account'
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
// Create Smart Account (for gas sponsorship)
|
154
|
+
export async function createSmartAccount(ownerAccountName: string, smartAccountName: string) {
|
155
|
+
try {
|
156
|
+
const ownerAccount = await cdp.evm.getOrCreateAccount({ name: ownerAccountName })
|
157
|
+
const smartAccount = await cdp.evm.getOrCreateSmartAccount({
|
158
|
+
owner: ownerAccount,
|
159
|
+
name: smartAccountName
|
160
|
+
})
|
161
|
+
|
162
|
+
return {
|
163
|
+
success: true,
|
164
|
+
smartAccount: {
|
165
|
+
address: smartAccount.address,
|
166
|
+
name: smartAccount.name,
|
167
|
+
owner: ownerAccount.address,
|
168
|
+
}
|
169
|
+
}
|
170
|
+
} catch (error) {
|
171
|
+
return {
|
172
|
+
success: false,
|
173
|
+
error: error instanceof Error ? error.message : 'Failed to create smart account'
|
174
|
+
}
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|
178
|
+
// Import existing private key
|
179
|
+
export async function importEvmAccount(privateKey: string, name: string) {
|
180
|
+
try {
|
181
|
+
const account = await cdp.evm.importAccount({ privateKey, name })
|
182
|
+
return {
|
183
|
+
success: true,
|
184
|
+
account: {
|
185
|
+
address: account.address,
|
186
|
+
name: account.name,
|
187
|
+
network: account.network,
|
188
|
+
}
|
189
|
+
}
|
190
|
+
} catch (error) {
|
191
|
+
return {
|
192
|
+
success: false,
|
193
|
+
error: error instanceof Error ? error.message : 'Failed to import account'
|
194
|
+
}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
|
198
|
+
// Request testnet funds
|
199
|
+
export async function requestFaucet(address: string, network: NetworkType, token: TokenType = 'eth') {
|
200
|
+
try {
|
201
|
+
const result = await cdp.evm.requestFaucet({
|
202
|
+
address,
|
203
|
+
network,
|
204
|
+
token,
|
205
|
+
})
|
206
|
+
|
207
|
+
return {
|
208
|
+
success: true,
|
209
|
+
transactionHash: result.transactionHash,
|
210
|
+
message: `Successfully requested ${token.toUpperCase()} from faucet`
|
211
|
+
}
|
212
|
+
} catch (error) {
|
213
|
+
return {
|
214
|
+
success: false,
|
215
|
+
error: error instanceof Error ? error.message : 'Failed to request faucet funds'
|
216
|
+
}
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
// Send EVM transaction
|
221
|
+
export async function sendEvmTransaction(
|
222
|
+
address: string,
|
223
|
+
network: NetworkType,
|
224
|
+
to: string,
|
225
|
+
valueEth: string
|
226
|
+
) {
|
227
|
+
try {
|
228
|
+
const result = await cdp.evm.sendTransaction({
|
229
|
+
address,
|
230
|
+
network,
|
231
|
+
transaction: {
|
232
|
+
to,
|
233
|
+
value: parseEther(valueEth),
|
234
|
+
},
|
235
|
+
})
|
236
|
+
|
237
|
+
return {
|
238
|
+
success: true,
|
239
|
+
transactionHash: result.transactionHash,
|
240
|
+
explorerUrl: getExplorerUrl(network, result.transactionHash)
|
241
|
+
}
|
242
|
+
} catch (error) {
|
243
|
+
return {
|
244
|
+
success: false,
|
245
|
+
error: error instanceof Error ? error.message : 'Failed to send transaction'
|
246
|
+
}
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
// Get account balance
|
251
|
+
export async function getAccountBalance(address: string, network: NetworkType) {
|
252
|
+
try {
|
253
|
+
const balance = await cdp.evm.getBalance({
|
254
|
+
address,
|
255
|
+
network,
|
256
|
+
token: 'eth'
|
257
|
+
})
|
258
|
+
|
259
|
+
return {
|
260
|
+
success: true,
|
261
|
+
balance: balance.toString(),
|
262
|
+
balanceFormatted: `${Number(balance) / 1e18} ETH`
|
263
|
+
}
|
264
|
+
} catch (error) {
|
265
|
+
return {
|
266
|
+
success: false,
|
267
|
+
error: error instanceof Error ? error.message : 'Failed to get balance'
|
268
|
+
}
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
// List all accounts
|
273
|
+
export async function listAccounts() {
|
274
|
+
try {
|
275
|
+
const accounts = await cdp.evm.listAccounts()
|
276
|
+
return {
|
277
|
+
success: true,
|
278
|
+
accounts: accounts.map(account => ({
|
279
|
+
address: account.address,
|
280
|
+
name: account.name,
|
281
|
+
network: account.network,
|
282
|
+
}))
|
283
|
+
}
|
284
|
+
} catch (error) {
|
285
|
+
return {
|
286
|
+
success: false,
|
287
|
+
error: error instanceof Error ? error.message : 'Failed to list accounts'
|
288
|
+
}
|
289
|
+
}
|
290
|
+
}
|
291
|
+
|
292
|
+
// Solana account management
|
293
|
+
export async function createSolanaAccount(name: string) {
|
294
|
+
try {
|
295
|
+
const account = await cdp.solana.getOrCreateAccount({ name })
|
296
|
+
return {
|
297
|
+
success: true,
|
298
|
+
account: {
|
299
|
+
address: account.address,
|
300
|
+
name: account.name,
|
301
|
+
network: account.network,
|
302
|
+
}
|
303
|
+
}
|
304
|
+
} catch (error) {
|
305
|
+
return {
|
306
|
+
success: false,
|
307
|
+
error: error instanceof Error ? error.message : 'Failed to create Solana account'
|
308
|
+
}
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
// Request Solana testnet funds
|
313
|
+
export async function requestSolanaFaucet(address: string) {
|
314
|
+
try {
|
315
|
+
const result = await cdp.solana.requestFaucet({
|
316
|
+
address,
|
317
|
+
network: 'solana-devnet',
|
318
|
+
})
|
319
|
+
|
320
|
+
return {
|
321
|
+
success: true,
|
322
|
+
transactionHash: result.transactionHash,
|
323
|
+
message: 'Successfully requested SOL from devnet faucet'
|
324
|
+
}
|
325
|
+
} catch (error) {
|
326
|
+
return {
|
327
|
+
success: false,
|
328
|
+
error: error instanceof Error ? error.message : 'Failed to request Solana faucet funds'
|
329
|
+
}
|
330
|
+
}
|
331
|
+
}
|
332
|
+
|
333
|
+
// Helper function to get explorer URLs
|
334
|
+
function getExplorerUrl(network: NetworkType, txHash: string): string {
|
335
|
+
const explorers = {
|
336
|
+
'base-sepolia': 'https://sepolia.basescan.org/tx/',
|
337
|
+
'base-mainnet': 'https://basescan.org/tx/',
|
338
|
+
'ethereum-sepolia': 'https://sepolia.etherscan.io/tx/',
|
339
|
+
'ethereum-mainnet': 'https://etherscan.io/tx/',
|
340
|
+
}
|
341
|
+
|
342
|
+
return explorers[network] + txHash
|
343
|
+
}
|
344
|
+
```
|
345
|
+
|
346
|
+
## React Hook for Wallet State
|
347
|
+
|
348
|
+
Create `src/lib/hooks/use-wallet.ts`:
|
349
|
+
```typescript
|
350
|
+
'use client'
|
351
|
+
import { useState, useCallback } from 'react'
|
352
|
+
|
353
|
+
export interface WalletAccount {
|
354
|
+
address: string
|
355
|
+
name: string
|
356
|
+
network?: string
|
357
|
+
}
|
358
|
+
|
359
|
+
export interface WalletState {
|
360
|
+
accounts: WalletAccount[]
|
361
|
+
selectedAccount: WalletAccount | null
|
362
|
+
isLoading: boolean
|
363
|
+
error: string | null
|
364
|
+
}
|
365
|
+
|
366
|
+
export function useWallet() {
|
367
|
+
const [state, setState] = useState<WalletState>({
|
368
|
+
accounts: [],
|
369
|
+
selectedAccount: null,
|
370
|
+
isLoading: false,
|
371
|
+
error: null,
|
372
|
+
})
|
373
|
+
|
374
|
+
const setLoading = useCallback((loading: boolean) => {
|
375
|
+
setState(prev => ({ ...prev, isLoading: loading, error: null }))
|
376
|
+
}, [])
|
377
|
+
|
378
|
+
const setError = useCallback((error: string) => {
|
379
|
+
setState(prev => ({ ...prev, error, isLoading: false }))
|
380
|
+
}, [])
|
381
|
+
|
382
|
+
const setAccounts = useCallback((accounts: WalletAccount[]) => {
|
383
|
+
setState(prev => ({
|
384
|
+
...prev,
|
385
|
+
accounts,
|
386
|
+
selectedAccount: accounts.length > 0 && !prev.selectedAccount ? accounts[0] : prev.selectedAccount
|
387
|
+
}))
|
388
|
+
}, [])
|
389
|
+
|
390
|
+
const selectAccount = useCallback((account: WalletAccount) => {
|
391
|
+
setState(prev => ({ ...prev, selectedAccount: account }))
|
392
|
+
}, [])
|
393
|
+
|
394
|
+
const clearError = useCallback(() => {
|
395
|
+
setState(prev => ({ ...prev, error: null }))
|
396
|
+
}, [])
|
397
|
+
|
398
|
+
return {
|
399
|
+
...state,
|
400
|
+
setLoading,
|
401
|
+
setError,
|
402
|
+
setAccounts,
|
403
|
+
selectAccount,
|
404
|
+
clearError,
|
405
|
+
}
|
406
|
+
}
|
407
|
+
```
|
408
|
+
|
409
|
+
## Wallet Dashboard Component
|
410
|
+
|
411
|
+
Create `src/components/ui/wallet-dashboard.tsx`:
|
412
|
+
```typescript
|
413
|
+
'use client'
|
414
|
+
import { useState, useEffect } from 'react'
|
415
|
+
import { useWallet } from '@/lib/hooks/use-wallet'
|
416
|
+
import {
|
417
|
+
createOrGetEvmAccount,
|
418
|
+
listAccounts,
|
419
|
+
requestFaucet,
|
420
|
+
sendEvmTransaction,
|
421
|
+
getAccountBalance,
|
422
|
+
createSmartAccount,
|
423
|
+
importEvmAccount,
|
424
|
+
} from '@/lib/wallet-utils'
|
425
|
+
|
426
|
+
export function WalletDashboard() {
|
427
|
+
const wallet = useWallet()
|
428
|
+
const [newAccountName, setNewAccountName] = useState('')
|
429
|
+
const [importKey, setImportKey] = useState('')
|
430
|
+
const [balance, setBalance] = useState<string>('')
|
431
|
+
const [recipient, setRecipient] = useState('')
|
432
|
+
const [amount, setAmount] = useState('')
|
433
|
+
|
434
|
+
// Load accounts on mount
|
435
|
+
useEffect(() => {
|
436
|
+
loadAccounts()
|
437
|
+
}, [])
|
438
|
+
|
439
|
+
// Load balance when account is selected
|
440
|
+
useEffect(() => {
|
441
|
+
if (wallet.selectedAccount) {
|
442
|
+
loadBalance()
|
443
|
+
}
|
444
|
+
}, [wallet.selectedAccount])
|
445
|
+
|
446
|
+
const loadAccounts = async () => {
|
447
|
+
wallet.setLoading(true)
|
448
|
+
const result = await listAccounts()
|
449
|
+
|
450
|
+
if (result.success) {
|
451
|
+
wallet.setAccounts(result.accounts)
|
452
|
+
} else {
|
453
|
+
wallet.setError(result.error || 'Failed to load accounts')
|
454
|
+
}
|
455
|
+
wallet.setLoading(false)
|
456
|
+
}
|
457
|
+
|
458
|
+
const loadBalance = async () => {
|
459
|
+
if (!wallet.selectedAccount) return
|
460
|
+
|
461
|
+
const result = await getAccountBalance(wallet.selectedAccount.address, 'base-sepolia')
|
462
|
+
if (result.success) {
|
463
|
+
setBalance(result.balanceFormatted)
|
464
|
+
}
|
465
|
+
}
|
466
|
+
|
467
|
+
const handleCreateAccount = async () => {
|
468
|
+
if (!newAccountName.trim()) return
|
469
|
+
|
470
|
+
wallet.setLoading(true)
|
471
|
+
const result = await createOrGetEvmAccount(newAccountName)
|
472
|
+
|
473
|
+
if (result.success) {
|
474
|
+
await loadAccounts()
|
475
|
+
setNewAccountName('')
|
476
|
+
} else {
|
477
|
+
wallet.setError(result.error || 'Failed to create account')
|
478
|
+
}
|
479
|
+
wallet.setLoading(false)
|
480
|
+
}
|
481
|
+
|
482
|
+
const handleImportAccount = async () => {
|
483
|
+
if (!importKey.trim() || !newAccountName.trim()) return
|
484
|
+
|
485
|
+
wallet.setLoading(true)
|
486
|
+
const result = await importEvmAccount(importKey, newAccountName)
|
487
|
+
|
488
|
+
if (result.success) {
|
489
|
+
await loadAccounts()
|
490
|
+
setImportKey('')
|
491
|
+
setNewAccountName('')
|
492
|
+
} else {
|
493
|
+
wallet.setError(result.error || 'Failed to import account')
|
494
|
+
}
|
495
|
+
wallet.setLoading(false)
|
496
|
+
}
|
497
|
+
|
498
|
+
const handleRequestFaucet = async () => {
|
499
|
+
if (!wallet.selectedAccount) return
|
500
|
+
|
501
|
+
wallet.setLoading(true)
|
502
|
+
const result = await requestFaucet(wallet.selectedAccount.address, 'base-sepolia')
|
503
|
+
|
504
|
+
if (result.success) {
|
505
|
+
setTimeout(loadBalance, 5000) // Refresh balance after 5 seconds
|
506
|
+
} else {
|
507
|
+
wallet.setError(result.error || 'Failed to request faucet')
|
508
|
+
}
|
509
|
+
wallet.setLoading(false)
|
510
|
+
}
|
511
|
+
|
512
|
+
const handleSendTransaction = async () => {
|
513
|
+
if (!wallet.selectedAccount || !recipient || !amount) return
|
514
|
+
|
515
|
+
wallet.setLoading(true)
|
516
|
+
const result = await sendEvmTransaction(
|
517
|
+
wallet.selectedAccount.address,
|
518
|
+
'base-sepolia',
|
519
|
+
recipient,
|
520
|
+
amount
|
521
|
+
)
|
522
|
+
|
523
|
+
if (result.success) {
|
524
|
+
alert(`Transaction sent! View on explorer: ${result.explorerUrl}`)
|
525
|
+
setTimeout(loadBalance, 5000) // Refresh balance after 5 seconds
|
526
|
+
setRecipient('')
|
527
|
+
setAmount('')
|
528
|
+
} else {
|
529
|
+
wallet.setError(result.error || 'Failed to send transaction')
|
530
|
+
}
|
531
|
+
wallet.setLoading(false)
|
532
|
+
}
|
533
|
+
|
534
|
+
return (
|
535
|
+
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
536
|
+
<div className="text-center">
|
537
|
+
<h1 className="text-3xl font-bold text-gray-900">CDP Wallet Dashboard</h1>
|
538
|
+
<p className="text-gray-600 mt-2">Manage your Coinbase Developer Platform wallets</p>
|
539
|
+
</div>
|
540
|
+
|
541
|
+
{wallet.error && (
|
542
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
543
|
+
<div className="flex justify-between items-center">
|
544
|
+
<p className="text-red-800">{wallet.error}</p>
|
545
|
+
<button
|
546
|
+
onClick={wallet.clearError}
|
547
|
+
className="text-red-600 hover:text-red-800"
|
548
|
+
>
|
549
|
+
✕
|
550
|
+
</button>
|
551
|
+
</div>
|
552
|
+
</div>
|
553
|
+
)}
|
554
|
+
|
555
|
+
<div className="grid md:grid-cols-2 gap-6">
|
556
|
+
{/* Account Creation */}
|
557
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
558
|
+
<h2 className="text-xl font-semibold mb-4">Create New Account</h2>
|
559
|
+
<div className="space-y-4">
|
560
|
+
<input
|
561
|
+
type="text"
|
562
|
+
placeholder="Account name"
|
563
|
+
value={newAccountName}
|
564
|
+
onChange={(e) => setNewAccountName(e.target.value)}
|
565
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
566
|
+
/>
|
567
|
+
<button
|
568
|
+
onClick={handleCreateAccount}
|
569
|
+
disabled={wallet.isLoading || !newAccountName.trim()}
|
570
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
571
|
+
>
|
572
|
+
{wallet.isLoading ? 'Creating...' : 'Create Account'}
|
573
|
+
</button>
|
574
|
+
</div>
|
575
|
+
</div>
|
576
|
+
|
577
|
+
{/* Account Import */}
|
578
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
579
|
+
<h2 className="text-xl font-semibold mb-4">Import Account</h2>
|
580
|
+
<div className="space-y-4">
|
581
|
+
<input
|
582
|
+
type="text"
|
583
|
+
placeholder="Account name"
|
584
|
+
value={newAccountName}
|
585
|
+
onChange={(e) => setNewAccountName(e.target.value)}
|
586
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
587
|
+
/>
|
588
|
+
<input
|
589
|
+
type="password"
|
590
|
+
placeholder="Private key (0x...)"
|
591
|
+
value={importKey}
|
592
|
+
onChange={(e) => setImportKey(e.target.value)}
|
593
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
594
|
+
/>
|
595
|
+
<button
|
596
|
+
onClick={handleImportAccount}
|
597
|
+
disabled={wallet.isLoading || !importKey.trim() || !newAccountName.trim()}
|
598
|
+
className="w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
599
|
+
>
|
600
|
+
{wallet.isLoading ? 'Importing...' : 'Import Account'}
|
601
|
+
</button>
|
602
|
+
</div>
|
603
|
+
</div>
|
604
|
+
</div>
|
605
|
+
|
606
|
+
{/* Account List */}
|
607
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
608
|
+
<div className="flex justify-between items-center mb-4">
|
609
|
+
<h2 className="text-xl font-semibold">Your Accounts</h2>
|
610
|
+
<button
|
611
|
+
onClick={loadAccounts}
|
612
|
+
disabled={wallet.isLoading}
|
613
|
+
className="bg-gray-600 text-white py-1 px-3 rounded text-sm hover:bg-gray-700 disabled:opacity-50"
|
614
|
+
>
|
615
|
+
Refresh
|
616
|
+
</button>
|
617
|
+
</div>
|
618
|
+
|
619
|
+
{wallet.accounts.length === 0 ? (
|
620
|
+
<p className="text-gray-500 text-center py-8">No accounts found. Create or import an account to get started.</p>
|
621
|
+
) : (
|
622
|
+
<div className="space-y-2">
|
623
|
+
{wallet.accounts.map((account) => (
|
624
|
+
<div
|
625
|
+
key={account.address}
|
626
|
+
onClick={() => wallet.selectAccount(account)}
|
627
|
+
className={`p-3 rounded-md cursor-pointer border-2 transition-colors ${
|
628
|
+
wallet.selectedAccount?.address === account.address
|
629
|
+
? 'border-blue-500 bg-blue-50'
|
630
|
+
: 'border-gray-200 hover:border-gray-300'
|
631
|
+
}`}
|
632
|
+
>
|
633
|
+
<div className="flex justify-between items-center">
|
634
|
+
<div>
|
635
|
+
<p className="font-medium">{account.name}</p>
|
636
|
+
<p className="text-sm text-gray-600 font-mono">{account.address}</p>
|
637
|
+
</div>
|
638
|
+
{wallet.selectedAccount?.address === account.address && (
|
639
|
+
<span className="text-blue-600 text-sm font-medium">Selected</span>
|
640
|
+
)}
|
641
|
+
</div>
|
642
|
+
</div>
|
643
|
+
))}
|
644
|
+
</div>
|
645
|
+
)}
|
646
|
+
</div>
|
647
|
+
|
648
|
+
{/* Account Actions */}
|
649
|
+
{wallet.selectedAccount && (
|
650
|
+
<div className="grid md:grid-cols-2 gap-6">
|
651
|
+
{/* Account Info & Faucet */}
|
652
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
653
|
+
<h2 className="text-xl font-semibold mb-4">Account Details</h2>
|
654
|
+
<div className="space-y-4">
|
655
|
+
<div>
|
656
|
+
<p className="text-sm text-gray-600">Name</p>
|
657
|
+
<p className="font-medium">{wallet.selectedAccount.name}</p>
|
658
|
+
</div>
|
659
|
+
<div>
|
660
|
+
<p className="text-sm text-gray-600">Address</p>
|
661
|
+
<p className="font-mono text-sm break-all">{wallet.selectedAccount.address}</p>
|
662
|
+
</div>
|
663
|
+
<div>
|
664
|
+
<p className="text-sm text-gray-600">Balance (Base Sepolia)</p>
|
665
|
+
<p className="font-medium">{balance || 'Loading...'}</p>
|
666
|
+
</div>
|
667
|
+
<button
|
668
|
+
onClick={handleRequestFaucet}
|
669
|
+
disabled={wallet.isLoading}
|
670
|
+
className="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
671
|
+
>
|
672
|
+
{wallet.isLoading ? 'Requesting...' : 'Request Test ETH'}
|
673
|
+
</button>
|
674
|
+
</div>
|
675
|
+
</div>
|
676
|
+
|
677
|
+
{/* Send Transaction */}
|
678
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6">
|
679
|
+
<h2 className="text-xl font-semibold mb-4">Send Transaction</h2>
|
680
|
+
<div className="space-y-4">
|
681
|
+
<input
|
682
|
+
type="text"
|
683
|
+
placeholder="Recipient address (0x...)"
|
684
|
+
value={recipient}
|
685
|
+
onChange={(e) => setRecipient(e.target.value)}
|
686
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
687
|
+
/>
|
688
|
+
<input
|
689
|
+
type="text"
|
690
|
+
placeholder="Amount in ETH (e.g., 0.001)"
|
691
|
+
value={amount}
|
692
|
+
onChange={(e) => setAmount(e.target.value)}
|
693
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
694
|
+
/>
|
695
|
+
<button
|
696
|
+
onClick={handleSendTransaction}
|
697
|
+
disabled={wallet.isLoading || !recipient || !amount}
|
698
|
+
className="w-full bg-orange-600 text-white py-2 px-4 rounded-md hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
699
|
+
>
|
700
|
+
{wallet.isLoading ? 'Sending...' : 'Send Transaction'}
|
701
|
+
</button>
|
702
|
+
</div>
|
703
|
+
</div>
|
704
|
+
</div>
|
705
|
+
)}
|
706
|
+
</div>
|
707
|
+
)
|
708
|
+
}
|
709
|
+
```
|
710
|
+
|
711
|
+
## Demo Usage Example
|
712
|
+
|
713
|
+
Create `examples/wallet-demo.ts`:
|
714
|
+
```typescript
|
715
|
+
import { cdp, closeCdp } from '@/lib/cdp'
|
716
|
+
import { parseEther } from 'viem'
|
717
|
+
|
718
|
+
export async function demoWallet() {
|
719
|
+
try {
|
720
|
+
console.log('🚀 Starting CDP Wallet Demo...')
|
721
|
+
|
722
|
+
// 1. Create or fetch an account
|
723
|
+
console.log('📝 Creating account...')
|
724
|
+
const account = await cdp.evm.getOrCreateAccount({ name: 'DemoAccount' })
|
725
|
+
console.log(`✅ Account created: ${account.address}`)
|
726
|
+
|
727
|
+
// 2. Fund with Sepolia ETH faucet
|
728
|
+
console.log('💰 Requesting test funds...')
|
729
|
+
const faucetResult = await cdp.evm.requestFaucet({
|
730
|
+
address: account.address,
|
731
|
+
network: 'base-sepolia',
|
732
|
+
token: 'eth',
|
733
|
+
})
|
734
|
+
console.log(`✅ Faucet transaction: ${faucetResult.transactionHash}`)
|
735
|
+
|
736
|
+
// Wait a moment for funds to arrive
|
737
|
+
console.log('⏳ Waiting for funds to arrive...')
|
738
|
+
await new Promise(resolve => setTimeout(resolve, 10000))
|
739
|
+
|
740
|
+
// 3. Send tiny transfer to burn address
|
741
|
+
console.log('🔥 Sending test transaction...')
|
742
|
+
const { transactionHash } = await cdp.evm.sendTransaction({
|
743
|
+
address: account.address,
|
744
|
+
network: 'base-sepolia',
|
745
|
+
transaction: {
|
746
|
+
to: '0x0000000000000000000000000000000000000000',
|
747
|
+
value: parseEther('0.000001'),
|
748
|
+
},
|
749
|
+
})
|
750
|
+
|
751
|
+
const explorerUrl = `https://sepolia.basescan.org/tx/${transactionHash}`
|
752
|
+
console.log(`✅ Transaction sent: ${explorerUrl}`)
|
753
|
+
|
754
|
+
return {
|
755
|
+
success: true,
|
756
|
+
account: account.address,
|
757
|
+
transactionHash,
|
758
|
+
explorerUrl
|
759
|
+
}
|
760
|
+
} catch (error) {
|
761
|
+
console.error('❌ Demo failed:', error)
|
762
|
+
return {
|
763
|
+
success: false,
|
764
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
765
|
+
}
|
766
|
+
} finally {
|
767
|
+
await closeCdp()
|
768
|
+
}
|
769
|
+
}
|
770
|
+
|
771
|
+
// Usage in a server action or API route:
|
772
|
+
// const result = await demoWallet()
|
773
|
+
// console.log(result)
|
774
|
+
```
|
775
|
+
|
776
|
+
## Integration Steps
|
777
|
+
|
778
|
+
1. **Install dependencies** (already done above)
|
779
|
+
2. **Create environment file**:
|
780
|
+
```bash
|
781
|
+
touch .env.local
|
782
|
+
# Add your CDP credentials to .env.local
|
783
|
+
```
|
784
|
+
3. **Copy all the code files above** to their respective locations
|
785
|
+
4. **Create utils file** `src/lib/utils.ts` if it doesn't exist:
|
786
|
+
```typescript
|
787
|
+
import { type ClassValue, clsx } from "clsx"
|
788
|
+
import { twMerge } from "tailwind-merge"
|
789
|
+
|
790
|
+
export function cn(...inputs: ClassValue[]) {
|
791
|
+
return twMerge(clsx(inputs))
|
792
|
+
}
|
793
|
+
```
|
794
|
+
5. **Add to your main page** `src/app/page.tsx`:
|
795
|
+
```typescript
|
796
|
+
import { WalletDashboard } from '@/components/ui/wallet-dashboard'
|
797
|
+
|
798
|
+
export default function Home() {
|
799
|
+
return (
|
800
|
+
<main className="min-h-screen bg-gray-50 py-8">
|
801
|
+
<WalletDashboard />
|
802
|
+
</main>
|
803
|
+
)
|
804
|
+
}
|
805
|
+
```
|
806
|
+
|
807
|
+
## Testing the Integration
|
808
|
+
|
809
|
+
1. **Run the development server**:
|
810
|
+
```bash
|
811
|
+
npm run dev
|
812
|
+
```
|
813
|
+
|
814
|
+
2. **Visit** `http://localhost:3000` to see the wallet dashboard
|
815
|
+
|
816
|
+
3. **Test the flow**:
|
817
|
+
- Create a new account
|
818
|
+
- Request test ETH from the faucet
|
819
|
+
- Send a small transaction
|
820
|
+
- Verify the transaction on BaseScan
|
821
|
+
|
822
|
+
## Security Notes
|
823
|
+
|
824
|
+
- ✅ All wallet operations happen server-side
|
825
|
+
- ✅ Private keys never leave the CDP service
|
826
|
+
- ✅ Environment variables are not exposed to client
|
827
|
+
- ✅ Server actions protect sensitive operations
|
828
|
+
|
829
|
+
## Done When
|
830
|
+
|
831
|
+
- ✅ `npm run dev` launches without errors
|
832
|
+
- ✅ Can create and fund accounts via the dashboard
|
833
|
+
- ✅ Transactions show up on Base Sepolia explorer
|
834
|
+
- ✅ No CDP secrets are leaked to client bundle
|
835
|
+
- ✅ All wallet operations work through the UI
|
836
|
+
|
837
|
+
## Advanced Features
|
838
|
+
|
839
|
+
For additional features like Smart Accounts, Policies, or Solana support, refer to the utility functions in `wallet-utils.ts` - they're ready to be integrated into your UI components.
|
840
|
+
|
841
|
+
---
|
842
|
+
|
843
|
+
## 🚨 Troubleshooting Common Issues
|
844
|
+
|
845
|
+
### TypeScript Compilation Errors
|
846
|
+
|
847
|
+
#### Issue: "Type 'undefined' is not assignable to parameter"
|
848
|
+
```
|
849
|
+
Argument of type '{ address: string; name: string; }[] | undefined' is not assignable to parameter of type 'WalletAccount[]'
|
850
|
+
```
|
851
|
+
|
852
|
+
**Root Cause**: CDP SDK functions can return undefined results, but TypeScript expects guaranteed types.
|
853
|
+
|
854
|
+
**Solution**: Always provide fallback values when handling API responses:
|
855
|
+
|
856
|
+
```typescript
|
857
|
+
// ❌ Problematic - direct assignment without null check
|
858
|
+
const result = await listAccounts()
|
859
|
+
if (result.success) {
|
860
|
+
wallet.setAccounts(result.accounts) // Error: accounts could be undefined
|
861
|
+
}
|
862
|
+
|
863
|
+
// ✅ Fixed - provide fallback empty array
|
864
|
+
const result = await listAccounts()
|
865
|
+
if (result.success) {
|
866
|
+
wallet.setAccounts(result.accounts || []) // Safe assignment
|
867
|
+
}
|
868
|
+
```
|
869
|
+
|
870
|
+
#### Issue: ESLint "unused parameter" warnings in mock functions
|
871
|
+
```
|
872
|
+
'address' is defined but never used.
|
873
|
+
'network' is defined but never used.
|
874
|
+
```
|
875
|
+
|
876
|
+
**Root Cause**: Mock implementations don't use all parameters, causing linter warnings.
|
877
|
+
|
878
|
+
**Solution**: Acknowledge unused parameters explicitly:
|
879
|
+
|
880
|
+
```typescript
|
881
|
+
// ❌ Problematic - linter complains about unused params
|
882
|
+
export async function requestFaucet(address: string, network: NetworkType, token: TokenType = 'eth') {
|
883
|
+
try {
|
884
|
+
return { success: true, transactionHash: 'mock-tx-hash' }
|
885
|
+
} catch (error) {
|
886
|
+
// ...
|
887
|
+
}
|
888
|
+
}
|
889
|
+
|
890
|
+
// ✅ Fixed - acknowledge unused params for mock implementation
|
891
|
+
export async function requestFaucet(address: string, network: NetworkType, token: TokenType = 'eth') {
|
892
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
893
|
+
const _ = { address, network }; // Acknowledge unused params for mock implementation
|
894
|
+
try {
|
895
|
+
return { success: true, transactionHash: 'mock-tx-hash' }
|
896
|
+
} catch (error) {
|
897
|
+
// ...
|
898
|
+
}
|
899
|
+
}
|
900
|
+
```
|
901
|
+
|
902
|
+
### CDP SDK API Structure Issues
|
903
|
+
|
904
|
+
#### Issue: Incorrect assumption about `listAccounts()` response format
|
905
|
+
|
906
|
+
**Root Cause**: Assuming CDP SDK returns arrays directly when it returns paginated response objects.
|
907
|
+
|
908
|
+
**Problem Code**:
|
909
|
+
```typescript
|
910
|
+
// ❌ Wrong - treats response as direct array
|
911
|
+
const accounts = await cdp.evm.listAccounts()
|
912
|
+
return accounts.map(account => ({ ... })) // Error: accounts is not an array
|
913
|
+
```
|
914
|
+
|
915
|
+
**Solution**: Reference the official CDP documentation for correct response structure:
|
916
|
+
|
917
|
+
```typescript
|
918
|
+
// ✅ Correct - handles paginated response object
|
919
|
+
const response = await cdp.evm.listAccounts()
|
920
|
+
// According to CDP docs: { accounts: Account[], nextPageToken?: string }
|
921
|
+
return {
|
922
|
+
success: true,
|
923
|
+
accounts: response.accounts?.map((account: { address: string; name?: string }) => ({
|
924
|
+
address: account.address,
|
925
|
+
name: account.name || 'Unnamed Account',
|
926
|
+
})) || []
|
927
|
+
}
|
928
|
+
```
|
929
|
+
|
930
|
+
### Build and Runtime Errors
|
931
|
+
|
932
|
+
#### Issue: Build fails with "Cannot find module" errors
|
933
|
+
|
934
|
+
**Root Cause**: Missing dependencies or incorrect import paths.
|
935
|
+
|
936
|
+
**Solution**: Ensure all required packages are installed:
|
937
|
+
```bash
|
938
|
+
npm install @coinbase/cdp-sdk dotenv viem clsx tailwind-merge
|
939
|
+
```
|
940
|
+
|
941
|
+
#### Issue: Runtime errors about missing environment variables
|
942
|
+
|
943
|
+
**Root Cause**: CDP client requires specific environment variables that aren't set.
|
944
|
+
|
945
|
+
**Solution**: Verify `.env.local` contains all required variables:
|
946
|
+
```bash
|
947
|
+
CDP_API_KEY_ID=your_actual_key_id
|
948
|
+
CDP_API_KEY_SECRET=your_actual_key_secret
|
949
|
+
CDP_WALLET_SECRET=your_actual_wallet_secret
|
950
|
+
```
|
951
|
+
|
952
|
+
### Prevention Best Practices
|
953
|
+
|
954
|
+
1. **Documentation First**: Always consult [CDP documentation](https://docs.cdp.coinbase.com/) before implementing API calls
|
955
|
+
2. **TypeScript Strict Mode**: Use strict TypeScript settings to catch type issues early
|
956
|
+
3. **Incremental Testing**: Test each wallet function individually before UI integration
|
957
|
+
4. **Error Boundaries**: Implement proper error handling for all CDP SDK calls
|
958
|
+
5. **Mock Consistency**: Use consistent patterns for mock implementations during development
|
959
|
+
|
960
|
+
### Development Workflow Recommendations
|
961
|
+
|
962
|
+
1. **Build Frequently**: Run `npm run build` after implementing each function
|
963
|
+
2. **Check Types**: Use `npx tsc --noEmit` to check TypeScript without building
|
964
|
+
3. **Lint Regularly**: Run `npm run lint` to catch code quality issues
|
965
|
+
4. **Test API Responses**: Log CDP SDK responses to understand actual data structures
|
966
|
+
5. **Reference Examples**: Use the official CDP SDK examples as reference implementations
|
967
|
+
|
968
|
+
### Quick Debug Checklist
|
969
|
+
|
970
|
+
- [ ] All environment variables are set in `.env.local`
|
971
|
+
- [ ] CDP SDK imports are correct (`@coinbase/cdp-sdk`)
|
972
|
+
- [ ] Server actions are marked with `'use server'`
|
973
|
+
- [ ] Response handling includes null/undefined checks
|
974
|
+
- [ ] TypeScript errors are resolved in build output
|
975
|
+
- [ ] ESLint warnings are addressed appropriately
|
976
|
+
|
977
|
+
---
|
978
|
+
|
979
|
+
**🎉 Integration Complete!** Your Next.js app now has full CDP Wallet functionality with a beautiful dashboard interface.
|
980
|
+
|
981
|
+
---
|
982
|
+
|
983
|
+
## 🚨 CRITICAL FIXES APPLIED - READ BEFORE IMPLEMENTING
|
984
|
+
|
985
|
+
### ⚡ **Next.js App Router & React Hook Issues**
|
986
|
+
|
987
|
+
#### 1. **Server Action Export Restrictions**
|
988
|
+
```typescript
|
989
|
+
// ❌ WRONG - 'use server' files can ONLY export async functions
|
990
|
+
'use server'
|
991
|
+
export const cdp = new CdpClient({...}) // ❌ Objects not allowed
|
992
|
+
export type NetworkType = '...' // ❌ Types not allowed
|
993
|
+
|
994
|
+
// ✅ CORRECT - separate files for objects/types
|
995
|
+
// cdp.ts (no 'use server' directive)
|
996
|
+
export const cdp = new CdpClient({...})
|
997
|
+
|
998
|
+
// types.ts (separate file for types)
|
999
|
+
export type NetworkType = '...'
|
1000
|
+
|
1001
|
+
// wallet-utils.ts ('use server' with only async functions)
|
1002
|
+
'use server'
|
1003
|
+
export async function createAccount() {...} // ✅ Only async functions
|
1004
|
+
```
|
1005
|
+
|
1006
|
+
#### 2. **React Hook Infinite Loop**
|
1007
|
+
```typescript
|
1008
|
+
// ❌ WRONG - causes infinite re-renders
|
1009
|
+
const loadAccounts = useCallback(async () => {
|
1010
|
+
// ...
|
1011
|
+
}, [wallet]) // ❌ wallet object recreated every render
|
1012
|
+
|
1013
|
+
useEffect(() => {
|
1014
|
+
loadAccounts()
|
1015
|
+
}, [loadAccounts]) // ❌ loadAccounts changes every render = infinite loop
|
1016
|
+
|
1017
|
+
// ✅ CORRECT - stable dependencies or disable exhaustive-deps
|
1018
|
+
const loadAccounts = async () => {
|
1019
|
+
// ... function body
|
1020
|
+
}
|
1021
|
+
|
1022
|
+
useEffect(() => {
|
1023
|
+
loadAccounts()
|
1024
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
1025
|
+
}, []) // ✅ Empty dependency array or specific stable props only
|
1026
|
+
```
|
1027
|
+
|
1028
|
+
---
|
1029
|
+
|
1030
|
+
## 🚨 CRITICAL FIXES APPLIED - READ BEFORE IMPLEMENTING
|
1031
|
+
|
1032
|
+
**Problem**: The original implementation had multiple TypeScript errors due to incorrect assumptions about the CDP SDK v2 API structure.
|
1033
|
+
|
1034
|
+
### ❌ **Problems Identified & Fixed**:
|
1035
|
+
|
1036
|
+
#### 1. **Account Objects Don't Have Network Property**
|
1037
|
+
```typescript
|
1038
|
+
// ❌ WRONG - accounts don't have network property
|
1039
|
+
account: {
|
1040
|
+
address: account.address,
|
1041
|
+
name: account.name,
|
1042
|
+
network: account.network, // ❌ Property doesn't exist
|
1043
|
+
}
|
1044
|
+
|
1045
|
+
// ✅ CORRECT - omit network property
|
1046
|
+
account: {
|
1047
|
+
address: account.address,
|
1048
|
+
name: account.name || name,
|
1049
|
+
}
|
1050
|
+
```
|
1051
|
+
|
1052
|
+
#### 2. **Address Type Safety Issues**
|
1053
|
+
```typescript
|
1054
|
+
// ❌ WRONG - string type not assignable to `0x${string}`
|
1055
|
+
const result = await cdp.evm.sendTransaction({
|
1056
|
+
address, // ❌ Type error
|
1057
|
+
transaction: { to } // ❌ Type error
|
1058
|
+
})
|
1059
|
+
|
1060
|
+
// ✅ CORRECT - ensure proper hex format
|
1061
|
+
const formattedAddress = address.startsWith('0x') ? address as `0x${string}` : `0x${address}` as `0x${string}`
|
1062
|
+
const formattedTo = to.startsWith('0x') ? to as `0x${string}` : `0x${to}` as `0x${string}`
|
1063
|
+
```
|
1064
|
+
|
1065
|
+
#### 3. **Private Key Import Type Issues**
|
1066
|
+
```typescript
|
1067
|
+
// ❌ WRONG - string not assignable to `0x${string}`
|
1068
|
+
await cdp.evm.importAccount({ privateKey, name })
|
1069
|
+
|
1070
|
+
// ✅ CORRECT - format private key properly
|
1071
|
+
const formattedPrivateKey = privateKey.startsWith('0x') ? privateKey as `0x${string}` : `0x${privateKey}` as `0x${string}`
|
1072
|
+
await cdp.evm.importAccount({ privateKey: formattedPrivateKey, name })
|
1073
|
+
```
|
1074
|
+
|
1075
|
+
#### 4. **Balance Checking Not Available in CDP SDK v2**
|
1076
|
+
```typescript
|
1077
|
+
// ❌ WRONG - getBalance method doesn't exist
|
1078
|
+
const balance = await cdp.evm.getBalance({ address, network, token: 'eth' })
|
1079
|
+
|
1080
|
+
// ✅ CORRECT - CDP SDK v2 doesn't provide balance checking
|
1081
|
+
export async function getAccountBalance(address: string, network: NetworkType) {
|
1082
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
1083
|
+
const _ = { address, network }; // Acknowledge unused params for mock implementation
|
1084
|
+
return {
|
1085
|
+
success: true,
|
1086
|
+
balance: "0",
|
1087
|
+
balanceFormatted: "0 ETH"
|
1088
|
+
}
|
1089
|
+
}
|
1090
|
+
```
|
1091
|
+
|
1092
|
+
#### 5. **Network Type Restrictions**
|
1093
|
+
```typescript
|
1094
|
+
// ❌ WRONG - includes mainnet networks not supported by faucets
|
1095
|
+
export type NetworkType = 'base-sepolia' | 'base-mainnet' | 'ethereum-sepolia' | 'ethereum-mainnet'
|
1096
|
+
|
1097
|
+
// ✅ CORRECT - only testnet networks for faucets
|
1098
|
+
export type NetworkType = 'base-sepolia' | 'ethereum-sepolia'
|
1099
|
+
```
|
1100
|
+
|
1101
|
+
#### 6. **Solana API Structure Differences**
|
1102
|
+
```typescript
|
1103
|
+
// ❌ WRONG - Solana faucet has different API
|
1104
|
+
const result = await cdp.solana.requestFaucet({
|
1105
|
+
address,
|
1106
|
+
network: 'solana-devnet', // ❌ Network param not supported
|
1107
|
+
})
|
1108
|
+
return { transactionHash: result.transactionHash } // ❌ Property doesn't exist
|
1109
|
+
|
1110
|
+
// ✅ CORRECT - Solana faucet API structure
|
1111
|
+
const result = await cdp.solana.requestFaucet({
|
1112
|
+
address,
|
1113
|
+
token: 'sol',
|
1114
|
+
})
|
1115
|
+
return { transactionHash: result.signature } // ✅ Use signature property
|
1116
|
+
```
|
1117
|
+
|
1118
|
+
#### 7. **Unused Parameter Handling**
|
1119
|
+
```typescript
|
1120
|
+
// ❌ WRONG - linter complains about unused params in mock functions
|
1121
|
+
export async function getAccountBalance(address: string, network: NetworkType) {
|
1122
|
+
// address and network unused - linter error
|
1123
|
+
}
|
1124
|
+
|
1125
|
+
// ✅ CORRECT - acknowledge unused params explicitly
|
1126
|
+
export async function getAccountBalance(address: string, network: NetworkType) {
|
1127
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
1128
|
+
const _ = { address, network }; // Acknowledge unused params for mock implementation
|
1129
|
+
}
|
1130
|
+
```
|
1131
|
+
|
1132
|
+
### 🛡️ **Prevention Strategy for Future Implementations**:
|
1133
|
+
|
1134
|
+
1. **Always Reference Official CDP Docs**: Check the actual API structure at [docs.cdp.coinbase.com](https://docs.cdp.coinbase.com/) before assuming property names
|
1135
|
+
2. **Type Safety First**: Use TypeScript's strict mode and handle `0x${string}` types properly
|
1136
|
+
3. **Test Incrementally**: Run `npm run build` after each function to catch type errors early
|
1137
|
+
4. **Understand API Limitations**: CDP SDK v2 doesn't include all features - some require external RPC providers
|
1138
|
+
5. **Network Restrictions**: Faucets only work on testnets, not mainnet networks
|
1139
|
+
6. **Platform Differences**: Solana and EVM APIs have different structures and property names
|
1140
|
+
|
1141
|
+
---
|
1142
|
+
|
1143
|
+
## 📋 Implementation Guidance for AI Agents
|
1144
|
+
|
1145
|
+
When implementing this CDP Wallet integration, **use `INTEGRATION-SUMMARY.md` as your step-by-step checklist**. That file contains a comprehensive task-by-task breakdown of the entire implementation process.
|
1146
|
+
|
1147
|
+
Go through `INTEGRATION-SUMMARY.md` systematically:
|
1148
|
+
1. Check off each prerequisite and dependency
|
1149
|
+
2. Create each required file in the specified order
|
1150
|
+
3. Implement each CDP API function with proper error handling
|
1151
|
+
4. Build each UI component with the documented patterns
|
1152
|
+
5. Complete all testing and validation steps
|
1153
|
+
6. Verify security checklist items
|
1154
|
+
|
1155
|
+
The `INTEGRATION-SUMMARY.md` file will ensure you don't miss any critical steps and implement the integration correctly from start to finish.
|