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.
@@ -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.