@coinbase/create-cdp-app 0.0.24 → 0.0.25

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/dist/index.js CHANGED
@@ -11,13 +11,22 @@ function prepareAppDirectory(targetDir, shouldOverwrite) {
11
11
  }
12
12
  return root;
13
13
  }
14
- function customizePackageJson(templateDir, appName) {
14
+ function customizePackageJson(templateDir, appName, includeSdk) {
15
15
  const packageJsonPath = path.join(templateDir, "package.json");
16
16
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
17
17
  packageJson.name = appName;
18
+ if (includeSdk) {
19
+ packageJson.dependencies["@coinbase/cdp-sdk"] = "latest";
20
+ }
18
21
  return JSON.stringify(packageJson, null, 2) + "\n";
19
22
  }
20
- function customizeEnv(templateDir, projectId, useSmartAccounts) {
23
+ function customizeEnv({
24
+ templateDir,
25
+ projectId,
26
+ useSmartAccounts,
27
+ apiKeyId,
28
+ apiKeySecret
29
+ }) {
21
30
  const exampleEnvPath = path.join(templateDir, "env.example");
22
31
  const exampleEnv = fs.readFileSync(exampleEnvPath, "utf-8");
23
32
  let envContent = exampleEnv.replace(/(.*PROJECT_ID=).*(\r?\n|$)/, `$1${projectId}
@@ -30,6 +39,13 @@ function customizeEnv(templateDir, projectId, useSmartAccounts) {
30
39
  `$1${accountType}
31
40
  `
32
41
  );
42
+ if (apiKeyId && apiKeySecret) {
43
+ envContent = envContent.replace(/# CDP_API_KEY_ID=.*/, `CDP_API_KEY_ID=${apiKeyId}`);
44
+ envContent = envContent.replace(
45
+ /# CDP_API_KEY_SECRET=.*/,
46
+ `CDP_API_KEY_SECRET=${apiKeySecret}`
47
+ );
48
+ }
33
49
  return envContent;
34
50
  }
35
51
  function customizeConfig(templateDir, useSmartAccounts, isNextjs) {
@@ -57,23 +73,45 @@ function customizeConfig(templateDir, useSmartAccounts, isNextjs) {
57
73
  }
58
74
  return configContent;
59
75
  }
60
- function copyFileSelectively(filePath, destPath, useSmartAccounts) {
76
+ function copyFileSelectively({
77
+ filePath,
78
+ destPath,
79
+ useSmartAccounts,
80
+ enableOnramp
81
+ }) {
61
82
  const stat = fs.statSync(filePath);
62
83
  if (stat.isDirectory()) {
63
- copyDirSelectively(filePath, destPath, useSmartAccounts);
84
+ const baseDir = path.basename(filePath);
85
+ if (!enableOnramp && (baseDir === "api" || baseDir === "lib")) return;
86
+ copyDirSelectively({ srcDir: filePath, destDir: destPath, useSmartAccounts, enableOnramp });
64
87
  } else {
65
88
  const fileName = path.basename(filePath);
66
89
  if (useSmartAccounts && fileName === "EOATransaction.tsx") return;
67
90
  if (!useSmartAccounts && fileName === "SmartAccountTransaction.tsx") return;
91
+ if (!enableOnramp && ["FundWallet.tsx", "SignedInScreenWithOnramp.tsx"].includes(fileName))
92
+ return;
93
+ if (enableOnramp) {
94
+ if (fileName === "SignedInScreen.tsx") return;
95
+ if (fileName === "SignedInScreenWithOnramp.tsx") {
96
+ const newDestPath = destPath.replace("SignedInScreenWithOnramp.tsx", "SignedInScreen.tsx");
97
+ fs.copyFileSync(filePath, newDestPath);
98
+ return;
99
+ }
100
+ }
68
101
  fs.copyFileSync(filePath, destPath);
69
102
  }
70
103
  }
71
- function copyDirSelectively(srcDir, destDir, useSmartAccounts) {
104
+ function copyDirSelectively({
105
+ srcDir,
106
+ destDir,
107
+ useSmartAccounts,
108
+ enableOnramp
109
+ }) {
72
110
  fs.mkdirSync(destDir, { recursive: true });
73
111
  for (const file of fs.readdirSync(srcDir)) {
74
112
  const srcFile = path.resolve(srcDir, file);
75
113
  const destFile = path.resolve(destDir, file);
76
- copyFileSelectively(srcFile, destFile, useSmartAccounts);
114
+ copyFileSelectively({ filePath: srcFile, destPath: destFile, useSmartAccounts, enableOnramp });
77
115
  }
78
116
  }
79
117
  function isDirEmpty(dirPath) {
@@ -107,12 +145,30 @@ const fileRenames = {
107
145
  };
108
146
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
109
147
  async function init() {
110
- const { appName, template, targetDirectory, projectId, useSmartAccounts } = await getAppDetails();
148
+ const {
149
+ appName,
150
+ template,
151
+ targetDirectory,
152
+ projectId,
153
+ useSmartAccounts,
154
+ enableOnramp,
155
+ apiKeyId,
156
+ apiKeySecret
157
+ } = await getAppDetails();
111
158
  console.log(`
112
159
  Scaffolding app in ${targetDirectory}...`);
113
160
  const root = prepareAppDirectory(targetDirectory);
114
161
  const templateDir = path.resolve(fileURLToPath(import.meta.url), "../..", `template-${template}`);
115
- copyTemplateFiles(templateDir, root, appName, projectId, useSmartAccounts);
162
+ copyTemplateFiles({
163
+ templateDir,
164
+ root,
165
+ appName,
166
+ projectId,
167
+ useSmartAccounts,
168
+ enableOnramp,
169
+ apiKeyId,
170
+ apiKeySecret
171
+ });
116
172
  printNextSteps(root);
117
173
  }
118
174
  async function getAppDetails() {
@@ -164,6 +220,36 @@ async function getAppDetails() {
164
220
  ),
165
221
  initial: false
166
222
  },
223
+ {
224
+ type: (_, { template }) => template === "nextjs" ? "confirm" : null,
225
+ name: "enableOnramp",
226
+ message: reset(
227
+ "Enable Coinbase Onramp? (Onramp enables users to buy crypto with fiat) -- NOTE: EXPERIMENTAL FEATURE"
228
+ ),
229
+ initial: false
230
+ },
231
+ {
232
+ type: (_, { enableOnramp }) => enableOnramp ? "text" : null,
233
+ name: "apiKeyId",
234
+ message: reset("CDP API Key ID (Create at https://portal.cdp.coinbase.com/api-keys):"),
235
+ validate: (value) => {
236
+ if (!value) {
237
+ return "API Key ID is required for Onramp";
238
+ }
239
+ return true;
240
+ }
241
+ },
242
+ {
243
+ type: (_, { enableOnramp }) => enableOnramp ? "password" : null,
244
+ name: "apiKeySecret",
245
+ message: reset("CDP API Key Secret (paste your private key - it will be hidden):"),
246
+ validate: (value) => {
247
+ if (!value) {
248
+ return "API Key Secret is required for Onramp";
249
+ }
250
+ return true;
251
+ }
252
+ },
167
253
  {
168
254
  type: "confirm",
169
255
  name: "corsConfirmation",
@@ -198,7 +284,10 @@ async function getAppDetails() {
198
284
  template: result.template,
199
285
  targetDirectory: targetDir,
200
286
  projectId: result.projectId,
201
- useSmartAccounts: result.useSmartAccounts
287
+ useSmartAccounts: result.useSmartAccounts,
288
+ enableOnramp: result.enableOnramp,
289
+ apiKeyId: result.apiKeyId,
290
+ apiKeySecret: result.apiKeySecret
202
291
  };
203
292
  } catch (cancelled) {
204
293
  if (cancelled instanceof Error) {
@@ -217,25 +306,48 @@ function printNextSteps(appRoot) {
217
306
  console.log(`${packageManager} install`);
218
307
  console.log(devCommand);
219
308
  }
220
- function copyTemplateFiles(templateDir, root, appName, projectId, useSmartAccounts) {
309
+ function copyTemplateFiles({
310
+ templateDir,
311
+ root,
312
+ appName,
313
+ projectId,
314
+ useSmartAccounts,
315
+ enableOnramp,
316
+ apiKeyId,
317
+ apiKeySecret
318
+ }) {
221
319
  const writeFileToTarget = (file, content) => {
222
320
  const targetPath = path.join(root, fileRenames[file] ?? file);
223
321
  if (content) {
224
322
  fs.writeFileSync(targetPath, content);
225
323
  } else {
226
- copyFileSelectively(path.join(templateDir, file), targetPath, useSmartAccounts);
324
+ copyFileSelectively({
325
+ filePath: path.join(templateDir, file),
326
+ destPath: targetPath,
327
+ useSmartAccounts,
328
+ enableOnramp
329
+ });
227
330
  }
228
331
  };
229
332
  const isNextjs = templateDir.includes("nextjs");
230
333
  const files = fs.readdirSync(templateDir);
231
334
  for (const file of files) {
232
335
  if (file === "package.json") {
233
- const customizedPackageJson = customizePackageJson(templateDir, appName);
336
+ const customizedPackageJson = customizePackageJson(templateDir, appName, enableOnramp);
234
337
  writeFileToTarget(file, customizedPackageJson);
235
338
  } else if (file === "env.example" && projectId) {
236
339
  writeFileToTarget(file);
237
- const customizedEnv = customizeEnv(templateDir, projectId, useSmartAccounts);
340
+ const customizedEnv = customizeEnv({
341
+ templateDir,
342
+ projectId,
343
+ useSmartAccounts,
344
+ apiKeyId,
345
+ apiKeySecret
346
+ });
238
347
  console.log("Copying CDP Project ID to .env");
348
+ if (apiKeyId && apiKeySecret) {
349
+ console.log("Copying CDP API credentials to .env");
350
+ }
239
351
  if (useSmartAccounts) {
240
352
  console.log("Configuring Smart Accounts in environment");
241
353
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Prepare the app directory\n *\n * @param targetDir - The target directory for the app\n * @param shouldOverwrite - Whether to overwrite the existing directory\n * @returns The path to the prepared app directory\n */\nexport function prepareAppDirectory(targetDir: string, shouldOverwrite: boolean): string {\n const root = path.join(process.cwd(), targetDir);\n\n if (shouldOverwrite) {\n emptyDir(root);\n } else if (!fs.existsSync(root)) {\n fs.mkdirSync(root, { recursive: true });\n }\n\n return root;\n}\n\n/**\n * Customize package.json for the new app\n *\n * @param templateDir - The directory containing the template files\n * @param appName - The name of the app\n * @returns The customized package.json content\n */\nexport function customizePackageJson(templateDir: string, appName: string): string {\n const packageJsonPath = path.join(templateDir, \"package.json\");\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, \"utf-8\"));\n packageJson.name = appName;\n return JSON.stringify(packageJson, null, 2) + \"\\n\";\n}\n\n/**\n * Set up the .env file for the new app\n *\n * @param templateDir - The directory containing the template files\n * @param projectId - The CDP Project ID\n * @param useSmartAccounts - Whether to enable smart accounts\n * @returns The customized .env content\n */\nexport function customizeEnv(\n templateDir: string,\n projectId: string,\n useSmartAccounts?: boolean,\n): string {\n const exampleEnvPath = path.join(templateDir, \"env.example\");\n const exampleEnv = fs.readFileSync(exampleEnvPath, \"utf-8\");\n\n let envContent = exampleEnv.replace(/(.*PROJECT_ID=).*(\\r?\\n|$)/, `$1${projectId}\\n`);\n\n // Set the account type based on user choice\n const accountType = useSmartAccounts ? \"evm-smart\" : \"evm-eoa\";\n const prefix = templateDir.includes(\"nextjs\") ? \"NEXT_PUBLIC_\" : \"VITE_\";\n envContent = envContent.replace(\n new RegExp(`(${prefix}CDP_CREATE_ACCOUNT_TYPE=).*(\\r?\\n|$)`),\n `$1${accountType}\\n`,\n );\n\n return envContent;\n}\n\n/**\n * Customize configuration files for smart accounts\n *\n * @param templateDir - The directory containing the template files\n * @param useSmartAccounts - Whether to enable smart accounts\n * @param isNextjs - Whether this is a Next.js template\n * @returns The customized config content\n */\nexport function customizeConfig(\n templateDir: string,\n useSmartAccounts: boolean,\n isNextjs: boolean,\n): string | null {\n if (!useSmartAccounts) return null;\n\n const configFileName = isNextjs ? \"src/components/Providers.tsx\" : \"src/config.ts\";\n const configPath = path.join(templateDir, configFileName);\n\n if (!fs.existsSync(configPath)) return null;\n\n let configContent = fs.readFileSync(configPath, \"utf-8\");\n\n if (isNextjs) {\n // For Next.js Providers.tsx\n configContent = configContent.replace(\n /const CDP_CONFIG: Config = \\{[\\s\\S]*?\\};/,\n `const CDP_CONFIG: Config = {\n projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID ?? \"\",\n createAccountOnLogin: process.env.NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE === \"evm-smart\" ? \"evm-smart\" : \"evm-eoa\",\n};`,\n );\n } else {\n // For React config.ts\n configContent = configContent.replace(\n /export const CDP_CONFIG: Config = \\{[\\s\\S]*?\\};/,\n `export const CDP_CONFIG: Config = {\n projectId: import.meta.env.VITE_CDP_PROJECT_ID,\n createAccountOnLogin: import.meta.env.VITE_CDP_CREATE_ACCOUNT_TYPE === \"evm-smart\" ? \"evm-smart\" : \"evm-eoa\",\n};`,\n );\n }\n\n return configContent;\n}\n\n/**\n * Copy a file or directory recursively\n *\n * @param filePath - The source path\n * @param destPath - The destination path\n */\nexport function copyFile(filePath: string, destPath: string): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n copyDir(filePath, destPath);\n } else {\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a file or directory recursively with selective filtering for transaction components\n *\n * @param filePath - The source path\n * @param destPath - The destination path\n * @param useSmartAccounts - Whether to use Smart Accounts\n */\nexport function copyFileSelectively(\n filePath: string,\n destPath: string,\n useSmartAccounts?: boolean,\n): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n copyDirSelectively(filePath, destPath, useSmartAccounts);\n } else {\n const fileName = path.basename(filePath);\n // Skip transaction components that don't match the user's choice\n if (useSmartAccounts && fileName === \"EOATransaction.tsx\") return;\n if (!useSmartAccounts && fileName === \"SmartAccountTransaction.tsx\") return;\n\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a directory recursively\n *\n * @param srcDir - The source directory path\n * @param destDir - The destination directory path\n */\nfunction copyDir(srcDir: string, destDir: string): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFile(srcFile, destFile);\n }\n}\n\n/**\n * Copy a directory recursively with selective filtering\n *\n * @param srcDir - The source directory path\n * @param destDir - The destination directory path\n * @param useSmartAccounts - Whether to use Smart Accounts\n */\nfunction copyDirSelectively(srcDir: string, destDir: string, useSmartAccounts?: boolean): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFileSelectively(srcFile, destFile, useSmartAccounts);\n }\n}\n\n/**\n * Check if a directory is empty\n *\n * @param dirPath - The path to the directory\n * @returns True if the directory is empty, false otherwise\n */\nexport function isDirEmpty(dirPath: string): boolean {\n const files = fs.readdirSync(dirPath);\n return files.length === 0 || (files.length === 1 && files[0] === \".git\");\n}\n\n/**\n * Empty a directory while preserving .git\n *\n * @param dirPath - The path to the directory\n */\nfunction emptyDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n return;\n }\n for (const file of fs.readdirSync(dirPath)) {\n if (file === \".git\") {\n continue;\n }\n fs.rmSync(path.resolve(dirPath, file), { recursive: true, force: true });\n }\n}\n\n/**\n * Detect which package manager invoked the create command\n *\n * @returns The detected package manager or 'pnpm' as default\n */\nexport function detectPackageManager(): \"npm\" | \"pnpm\" | \"yarn\" {\n const userAgent = process.env.npm_config_user_agent;\n\n if (userAgent) {\n if (userAgent.startsWith(\"yarn\")) return \"yarn\";\n if (userAgent.startsWith(\"pnpm\")) return \"pnpm\";\n if (userAgent.startsWith(\"npm\")) return \"npm\";\n }\n\n return \"npm\"; // Default to npm if we can't detect\n}\n","#!/usr/bin/env node\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { red, green, reset } from \"kolorist\";\nimport prompts from \"prompts\";\n\nimport {\n prepareAppDirectory,\n isDirEmpty,\n customizePackageJson,\n copyFileSelectively,\n customizeEnv,\n customizeConfig,\n detectPackageManager,\n} from \"./utils.js\";\n\n// Available templates for app creation\nconst TEMPLATES = [\n {\n name: \"react\",\n display: \"React Single Page App\",\n color: green,\n },\n {\n name: \"nextjs\",\n display: \"Next.js Full Stack App\",\n color: green,\n },\n];\n\nconst defaultTargetDir = \"cdp-app\";\n\nconst fileRenames: Record<string, string | undefined> = {\n _gitignore: \".gitignore\",\n};\n\ninterface AppOptions {\n appName: string;\n template: string;\n targetDirectory: string;\n projectId: string;\n useSmartAccounts: boolean;\n}\n\nconst uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Initialize a new CDP app\n */\nasync function init(): Promise<void> {\n const { appName, template, targetDirectory, projectId, useSmartAccounts } = await getAppDetails();\n\n console.log(`\\nScaffolding app in ${targetDirectory}...`);\n\n const root = prepareAppDirectory(targetDirectory, false);\n const templateDir = path.resolve(fileURLToPath(import.meta.url), \"../..\", `template-${template}`);\n\n copyTemplateFiles(templateDir, root, appName, projectId, useSmartAccounts);\n printNextSteps(root);\n}\n\n/**\n * Get app details from command line arguments or prompt the user\n *\n * @returns The app details\n */\nasync function getAppDetails(): Promise<AppOptions> {\n // Get target directory from command line args (first non-option argument)\n let targetDir = process.argv[2];\n const defaultAppName = targetDir ?? defaultTargetDir;\n\n try {\n const result = await prompts(\n [\n {\n type: targetDir ? null : \"text\",\n name: \"appName\",\n message: reset(\"App Name:\"),\n initial: defaultAppName,\n onState: state => {\n targetDir = String(state.value).trim() || defaultAppName;\n },\n },\n {\n type: \"select\",\n name: \"template\",\n message: reset(\"Template:\"),\n initial: 0,\n choices: TEMPLATES.map(template => ({\n title: template.color(template.display || template.name),\n value: template.name,\n })),\n },\n {\n type: \"text\",\n name: \"projectId\",\n message: reset(\n \"CDP Project ID (Find your project ID at https://portal.cdp.coinbase.com/projects/overview):\",\n ),\n validate: value => {\n if (!value) {\n return \"Project ID is required\";\n } else if (!uuidRegex.test(value)) {\n return \"Project ID must be a valid UUID\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: \"confirm\",\n name: \"useSmartAccounts\",\n message: reset(\n \"Enable Smart Accounts? (Smart Accounts enable gasless transactions and improved UX):\",\n ),\n initial: false,\n },\n {\n type: \"confirm\",\n name: \"corsConfirmation\",\n message: reset(\n \"Confirm you have whitelisted 'http://localhost:3000' at https://portal.cdp.coinbase.com/products/embedded-wallets/domains:\",\n ),\n initial: true,\n },\n {\n type: () => (!fs.existsSync(targetDir) || isDirEmpty(targetDir) ? null : \"confirm\"),\n name: \"overwrite\",\n message: () =>\n (targetDir === \".\" ? \"Current directory\" : `Target directory \"${targetDir}\"`) +\n \" is not empty. Remove existing files and continue?\",\n },\n {\n type: (_, { overwrite }: { overwrite?: boolean }) => {\n if (overwrite === false) {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n }\n return null;\n },\n name: \"overwriteChecker\",\n },\n ],\n {\n onCancel: () => {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n },\n },\n );\n\n return {\n appName: result.appName,\n template: result.template,\n targetDirectory: targetDir,\n projectId: result.projectId,\n useSmartAccounts: result.useSmartAccounts,\n };\n } catch (cancelled: unknown) {\n if (cancelled instanceof Error) {\n console.log(cancelled.message);\n }\n process.exit(0);\n }\n}\n\n/**\n * Print next steps for the user\n *\n * @param appRoot - The root directory of the app\n */\nfunction printNextSteps(appRoot: string): void {\n const packageManager = detectPackageManager();\n\n console.log(green(\"\\nDone. Now run your app:\\n\"));\n if (appRoot !== process.cwd()) {\n console.log(`cd ${path.relative(process.cwd(), appRoot)}`);\n }\n const devCommand = packageManager === \"npm\" ? \"npm run dev\" : `${packageManager} dev`;\n console.log(`${packageManager} install`);\n console.log(devCommand);\n}\n\n/**\n * Copy template files to the app directory\n *\n * @param templateDir - The directory containing the template files\n * @param root - The root directory of the app\n * @param appName - The name of the app\n * @param projectId - The CDP Project ID\n * @param useSmartAccounts - Whether to enable smart accounts\n */\nfunction copyTemplateFiles(\n templateDir: string,\n root: string,\n appName: string,\n projectId?: string,\n useSmartAccounts?: boolean,\n): void {\n const writeFileToTarget = (file: string, content?: string) => {\n const targetPath = path.join(root, fileRenames[file] ?? file);\n if (content) {\n fs.writeFileSync(targetPath, content);\n } else {\n copyFileSelectively(path.join(templateDir, file), targetPath, useSmartAccounts);\n }\n };\n\n const isNextjs = templateDir.includes(\"nextjs\");\n\n const files = fs.readdirSync(templateDir);\n for (const file of files) {\n if (file === \"package.json\") {\n const customizedPackageJson = customizePackageJson(templateDir, appName);\n writeFileToTarget(file, customizedPackageJson);\n } else if (file === \"env.example\" && projectId) {\n writeFileToTarget(file);\n const customizedEnv = customizeEnv(templateDir, projectId, useSmartAccounts);\n console.log(\"Copying CDP Project ID to .env\");\n if (useSmartAccounts) {\n console.log(\"Configuring Smart Accounts in environment\");\n }\n writeFileToTarget(\".env\", customizedEnv);\n } else {\n writeFileToTarget(file);\n }\n }\n\n // Handle smart account configuration in config files\n if (useSmartAccounts) {\n const configFileName = isNextjs ? \"src/components/Providers.tsx\" : \"src/config.ts\";\n const customizedConfig = customizeConfig(templateDir, useSmartAccounts, isNextjs);\n if (customizedConfig) {\n console.log(\"Configuring Smart Accounts in application config\");\n writeFileToTarget(configFileName, customizedConfig);\n }\n }\n\n // Generate the appropriate Transaction.tsx component\n const transactionFileName = isNextjs ? \"src/components/Transaction.tsx\" : \"src/Transaction.tsx\";\n const transactionContent = generateTransactionComponent(useSmartAccounts);\n writeFileToTarget(transactionFileName, transactionContent);\n}\n\n/**\n * Generate the appropriate Transaction component based on account type\n *\n * @param useSmartAccounts - Whether to use Smart Accounts\n * @returns The generated Transaction component content\n */\nfunction generateTransactionComponent(useSmartAccounts?: boolean): string {\n if (useSmartAccounts) {\n return `export { default } from \"./SmartAccountTransaction\";`;\n } else {\n return `export { default } from \"./EOATransaction\";`;\n }\n}\n\ninit().catch(e => {\n console.error(e);\n process.exit(1);\n});\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/utils.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Prepare the app directory\n *\n * @param targetDir - The target directory for the app\n * @param shouldOverwrite - Whether to overwrite the existing directory\n * @returns The path to the prepared app directory\n */\nexport function prepareAppDirectory(targetDir: string, shouldOverwrite: boolean): string {\n const root = path.join(process.cwd(), targetDir);\n\n if (shouldOverwrite) {\n emptyDir(root);\n } else if (!fs.existsSync(root)) {\n fs.mkdirSync(root, { recursive: true });\n }\n\n return root;\n}\n\n/**\n * Customize package.json for the new app\n *\n * @param templateDir - The directory containing the template files\n * @param appName - The name of the app\n * @param includeSdk - Whether to include the CDP SDK in the dependencies\n * @returns The customized package.json content\n */\nexport function customizePackageJson(\n templateDir: string,\n appName: string,\n includeSdk?: boolean,\n): string {\n const packageJsonPath = path.join(templateDir, \"package.json\");\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, \"utf-8\"));\n packageJson.name = appName;\n if (includeSdk) {\n packageJson.dependencies[\"@coinbase/cdp-sdk\"] = \"latest\";\n }\n return JSON.stringify(packageJson, null, 2) + \"\\n\";\n}\n\n/**\n * Set up the .env file for the new app\n *\n * @param params - The parameters for the function\n * @param params.templateDir - The directory containing the template files\n * @param params.projectId - The CDP Project ID\n * @param params.useSmartAccounts - Whether to enable smart accounts\n * @param params.apiKeyId - The API Key ID\n * @param params.apiKeySecret - The API Key Secret\n * @returns The customized .env content\n */\nexport function customizeEnv({\n templateDir,\n projectId,\n useSmartAccounts,\n apiKeyId,\n apiKeySecret,\n}: {\n templateDir: string;\n projectId: string;\n useSmartAccounts?: boolean;\n apiKeyId?: string;\n apiKeySecret?: string;\n}): string {\n const exampleEnvPath = path.join(templateDir, \"env.example\");\n const exampleEnv = fs.readFileSync(exampleEnvPath, \"utf-8\");\n\n let envContent = exampleEnv.replace(/(.*PROJECT_ID=).*(\\r?\\n|$)/, `$1${projectId}\\n`);\n\n // Set the account type based on user choice\n const accountType = useSmartAccounts ? \"evm-smart\" : \"evm-eoa\";\n const prefix = templateDir.includes(\"nextjs\") ? \"NEXT_PUBLIC_\" : \"VITE_\";\n envContent = envContent.replace(\n new RegExp(`(${prefix}CDP_CREATE_ACCOUNT_TYPE=).*(\\r?\\n|$)`),\n `$1${accountType}\\n`,\n );\n\n // Replace CDP API credentials if provided\n if (apiKeyId && apiKeySecret) {\n // Replace the commented API Key ID\n envContent = envContent.replace(/# CDP_API_KEY_ID=.*/, `CDP_API_KEY_ID=${apiKeyId}`);\n // Replace the commented API Key Secret\n envContent = envContent.replace(\n /# CDP_API_KEY_SECRET=.*/,\n `CDP_API_KEY_SECRET=${apiKeySecret}`,\n );\n }\n\n return envContent;\n}\n\n/**\n * Customize configuration files for smart accounts\n *\n * @param templateDir - The directory containing the template files\n * @param useSmartAccounts - Whether to enable smart accounts\n * @param isNextjs - Whether this is a Next.js template\n * @returns The customized config content\n */\nexport function customizeConfig(\n templateDir: string,\n useSmartAccounts: boolean,\n isNextjs: boolean,\n): string | null {\n if (!useSmartAccounts) return null;\n\n const configFileName = isNextjs ? \"src/components/Providers.tsx\" : \"src/config.ts\";\n const configPath = path.join(templateDir, configFileName);\n\n if (!fs.existsSync(configPath)) return null;\n\n let configContent = fs.readFileSync(configPath, \"utf-8\");\n\n if (isNextjs) {\n // For Next.js Providers.tsx\n configContent = configContent.replace(\n /const CDP_CONFIG: Config = \\{[\\s\\S]*?\\};/,\n `const CDP_CONFIG: Config = {\n projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID ?? \"\",\n createAccountOnLogin: process.env.NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE === \"evm-smart\" ? \"evm-smart\" : \"evm-eoa\",\n};`,\n );\n } else {\n // For React config.ts\n configContent = configContent.replace(\n /export const CDP_CONFIG: Config = \\{[\\s\\S]*?\\};/,\n `export const CDP_CONFIG: Config = {\n projectId: import.meta.env.VITE_CDP_PROJECT_ID,\n createAccountOnLogin: import.meta.env.VITE_CDP_CREATE_ACCOUNT_TYPE === \"evm-smart\" ? \"evm-smart\" : \"evm-eoa\",\n};`,\n );\n }\n\n return configContent;\n}\n\n/**\n * Copy a file or directory recursively\n *\n * @param filePath - The source path\n * @param destPath - The destination path\n */\nexport function copyFile(filePath: string, destPath: string): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n copyDir(filePath, destPath);\n } else {\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a file or directory recursively with selective filtering for transaction components\n *\n * @param params - The parameters for the function\n * @param params.filePath - The source path\n * @param params.destPath - The destination path\n * @param params.useSmartAccounts - Whether to use Smart Accounts\n * @param params.enableOnramp - Whether to include Onramp\n */\nexport function copyFileSelectively({\n filePath,\n destPath,\n useSmartAccounts,\n enableOnramp,\n}: {\n filePath: string;\n destPath: string;\n useSmartAccounts?: boolean;\n enableOnramp?: boolean;\n}): void {\n const stat = fs.statSync(filePath);\n if (stat.isDirectory()) {\n const baseDir = path.basename(filePath);\n // skip api and lib directories if Onramp is not enabled\n if (!enableOnramp && (baseDir === \"api\" || baseDir === \"lib\")) return;\n // copy the directory\n copyDirSelectively({ srcDir: filePath, destDir: destPath, useSmartAccounts, enableOnramp });\n } else {\n const fileName = path.basename(filePath);\n\n // Skip transaction components that don't match the user's choice\n if (useSmartAccounts && fileName === \"EOATransaction.tsx\") return;\n if (!useSmartAccounts && fileName === \"SmartAccountTransaction.tsx\") return;\n\n // Skip Onramp files if the user didn't enable Onramp\n if (!enableOnramp && [\"FundWallet.tsx\", \"SignedInScreenWithOnramp.tsx\"].includes(fileName))\n return;\n // Onramp-specific SignedInScreen\n if (enableOnramp) {\n // Skip the default SignedInScreen.tsx file\n if (fileName === \"SignedInScreen.tsx\") return;\n // Copy the SignedInScreenWithOnramp.tsx file to SignedInScreen.tsx\n if (fileName === \"SignedInScreenWithOnramp.tsx\") {\n const newDestPath = destPath.replace(\"SignedInScreenWithOnramp.tsx\", \"SignedInScreen.tsx\");\n fs.copyFileSync(filePath, newDestPath);\n return;\n }\n }\n\n fs.copyFileSync(filePath, destPath);\n }\n}\n\n/**\n * Copy a directory recursively\n *\n * @param srcDir - The source directory path\n * @param destDir - The destination directory path\n */\nfunction copyDir(srcDir: string, destDir: string): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFile(srcFile, destFile);\n }\n}\n\n/**\n * Copy a directory recursively with selective filtering\n *\n * @param params - The parameters for the function\n * @param params.srcDir - The source directory path\n * @param params.destDir - The destination directory path\n * @param params.useSmartAccounts - Whether to use Smart Accounts\n * @param params.enableOnramp - Whether to include Onramp\n */\nfunction copyDirSelectively({\n srcDir,\n destDir,\n useSmartAccounts,\n enableOnramp,\n}: {\n srcDir: string;\n destDir: string;\n useSmartAccounts?: boolean;\n enableOnramp?: boolean;\n}): void {\n fs.mkdirSync(destDir, { recursive: true });\n for (const file of fs.readdirSync(srcDir)) {\n const srcFile = path.resolve(srcDir, file);\n const destFile = path.resolve(destDir, file);\n copyFileSelectively({ filePath: srcFile, destPath: destFile, useSmartAccounts, enableOnramp });\n }\n}\n\n/**\n * Check if a directory is empty\n *\n * @param dirPath - The path to the directory\n * @returns True if the directory is empty, false otherwise\n */\nexport function isDirEmpty(dirPath: string): boolean {\n const files = fs.readdirSync(dirPath);\n return files.length === 0 || (files.length === 1 && files[0] === \".git\");\n}\n\n/**\n * Empty a directory while preserving .git\n *\n * @param dirPath - The path to the directory\n */\nfunction emptyDir(dirPath: string): void {\n if (!fs.existsSync(dirPath)) {\n return;\n }\n for (const file of fs.readdirSync(dirPath)) {\n if (file === \".git\") {\n continue;\n }\n fs.rmSync(path.resolve(dirPath, file), { recursive: true, force: true });\n }\n}\n\n/**\n * Detect which package manager invoked the create command\n *\n * @returns The detected package manager or 'pnpm' as default\n */\nexport function detectPackageManager(): \"npm\" | \"pnpm\" | \"yarn\" {\n const userAgent = process.env.npm_config_user_agent;\n\n if (userAgent) {\n if (userAgent.startsWith(\"yarn\")) return \"yarn\";\n if (userAgent.startsWith(\"pnpm\")) return \"pnpm\";\n if (userAgent.startsWith(\"npm\")) return \"npm\";\n }\n\n return \"npm\"; // Default to npm if we can't detect\n}\n","#!/usr/bin/env node\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { red, green, reset } from \"kolorist\";\nimport prompts from \"prompts\";\n\nimport {\n prepareAppDirectory,\n isDirEmpty,\n customizePackageJson,\n copyFileSelectively,\n customizeEnv,\n customizeConfig,\n detectPackageManager,\n} from \"./utils.js\";\n\n// Available templates for app creation\nconst TEMPLATES = [\n {\n name: \"react\",\n display: \"React Single Page App\",\n color: green,\n },\n {\n name: \"nextjs\",\n display: \"Next.js Full Stack App\",\n color: green,\n },\n];\n\nconst defaultTargetDir = \"cdp-app\";\n\nconst fileRenames: Record<string, string | undefined> = {\n _gitignore: \".gitignore\",\n};\n\ninterface AppOptions {\n appName: string;\n template: string;\n targetDirectory: string;\n projectId: string;\n useSmartAccounts: boolean;\n enableOnramp: boolean;\n apiKeyId?: string;\n apiKeySecret?: string;\n}\n\nconst uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Initialize a new CDP app\n */\nasync function init(): Promise<void> {\n const {\n appName,\n template,\n targetDirectory,\n projectId,\n useSmartAccounts,\n enableOnramp,\n apiKeyId,\n apiKeySecret,\n } = await getAppDetails();\n\n console.log(`\\nScaffolding app in ${targetDirectory}...`);\n\n const root = prepareAppDirectory(targetDirectory, false);\n const templateDir = path.resolve(fileURLToPath(import.meta.url), \"../..\", `template-${template}`);\n\n copyTemplateFiles({\n templateDir,\n root,\n appName,\n projectId,\n useSmartAccounts,\n enableOnramp,\n apiKeyId,\n apiKeySecret,\n });\n printNextSteps(root);\n}\n\n/**\n * Get app details from command line arguments or prompt the user\n *\n * @returns The app details\n */\nasync function getAppDetails(): Promise<AppOptions> {\n // Get target directory from command line args (first non-option argument)\n let targetDir = process.argv[2];\n const defaultAppName = targetDir ?? defaultTargetDir;\n\n try {\n const result = await prompts(\n [\n {\n type: targetDir ? null : \"text\",\n name: \"appName\",\n message: reset(\"App Name:\"),\n initial: defaultAppName,\n onState: state => {\n targetDir = String(state.value).trim() || defaultAppName;\n },\n },\n {\n type: \"select\",\n name: \"template\",\n message: reset(\"Template:\"),\n initial: 0,\n choices: TEMPLATES.map(template => ({\n title: template.color(template.display || template.name),\n value: template.name,\n })),\n },\n {\n type: \"text\",\n name: \"projectId\",\n message: reset(\n \"CDP Project ID (Find your project ID at https://portal.cdp.coinbase.com/projects/overview):\",\n ),\n validate: value => {\n if (!value) {\n return \"Project ID is required\";\n } else if (!uuidRegex.test(value)) {\n return \"Project ID must be a valid UUID\";\n }\n return true;\n },\n initial: \"\",\n },\n {\n type: \"confirm\",\n name: \"useSmartAccounts\",\n message: reset(\n \"Enable Smart Accounts? (Smart Accounts enable gasless transactions and improved UX):\",\n ),\n initial: false,\n },\n {\n type: (_, { template }: { template?: string }) =>\n template === \"nextjs\" ? \"confirm\" : null,\n name: \"enableOnramp\",\n message: reset(\n \"Enable Coinbase Onramp? (Onramp enables users to buy crypto with fiat) -- NOTE: EXPERIMENTAL FEATURE\",\n ),\n initial: false,\n },\n {\n type: (_, { enableOnramp }: { enableOnramp?: boolean }) => (enableOnramp ? \"text\" : null),\n name: \"apiKeyId\",\n message: reset(\"CDP API Key ID (Create at https://portal.cdp.coinbase.com/api-keys):\"),\n validate: value => {\n if (!value) {\n return \"API Key ID is required for Onramp\";\n }\n return true;\n },\n },\n {\n type: (_, { enableOnramp }: { enableOnramp?: boolean }) =>\n enableOnramp ? \"password\" : null,\n name: \"apiKeySecret\",\n message: reset(\"CDP API Key Secret (paste your private key - it will be hidden):\"),\n validate: value => {\n if (!value) {\n return \"API Key Secret is required for Onramp\";\n }\n return true;\n },\n },\n {\n type: \"confirm\",\n name: \"corsConfirmation\",\n message: reset(\n \"Confirm you have whitelisted 'http://localhost:3000' at https://portal.cdp.coinbase.com/products/embedded-wallets/domains:\",\n ),\n initial: true,\n },\n {\n type: () => (!fs.existsSync(targetDir) || isDirEmpty(targetDir) ? null : \"confirm\"),\n name: \"overwrite\",\n message: () =>\n (targetDir === \".\" ? \"Current directory\" : `Target directory \"${targetDir}\"`) +\n \" is not empty. Remove existing files and continue?\",\n },\n {\n type: (_, { overwrite }: { overwrite?: boolean }) => {\n if (overwrite === false) {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n }\n return null;\n },\n name: \"overwriteChecker\",\n },\n ],\n {\n onCancel: () => {\n throw new Error(red(\"✖\") + \" Operation cancelled\");\n },\n },\n );\n\n return {\n appName: result.appName,\n template: result.template,\n targetDirectory: targetDir,\n projectId: result.projectId,\n useSmartAccounts: result.useSmartAccounts,\n enableOnramp: result.enableOnramp,\n apiKeyId: result.apiKeyId,\n apiKeySecret: result.apiKeySecret,\n };\n } catch (cancelled: unknown) {\n if (cancelled instanceof Error) {\n console.log(cancelled.message);\n }\n process.exit(0);\n }\n}\n\n/**\n * Print next steps for the user\n *\n * @param appRoot - The root directory of the app\n */\nfunction printNextSteps(appRoot: string): void {\n const packageManager = detectPackageManager();\n\n console.log(green(\"\\nDone. Now run your app:\\n\"));\n if (appRoot !== process.cwd()) {\n console.log(`cd ${path.relative(process.cwd(), appRoot)}`);\n }\n const devCommand = packageManager === \"npm\" ? \"npm run dev\" : `${packageManager} dev`;\n console.log(`${packageManager} install`);\n console.log(devCommand);\n}\n\n/**\n * Copy template files to the app directory\n *\n * @param params - The parameters for the function\n * @param params.templateDir - The directory containing the template files\n * @param params.root - The root directory of the app\n * @param params.appName - The name of the app\n * @param params.projectId - The CDP Project ID\n * @param params.useSmartAccounts - Whether to enable smart accounts\n * @param params.enableOnramp - Whether to include Onramp\n * @param params.apiKeyId - The API Key ID\n * @param params.apiKeySecret - The API Key Secret\n */\nfunction copyTemplateFiles({\n templateDir,\n root,\n appName,\n projectId,\n useSmartAccounts,\n enableOnramp,\n apiKeyId,\n apiKeySecret,\n}: {\n templateDir: string;\n root: string;\n appName: string;\n projectId?: string;\n useSmartAccounts?: boolean;\n enableOnramp?: boolean;\n apiKeyId?: string;\n apiKeySecret?: string;\n}): void {\n const writeFileToTarget = (file: string, content?: string) => {\n const targetPath = path.join(root, fileRenames[file] ?? file);\n if (content) {\n fs.writeFileSync(targetPath, content);\n } else {\n copyFileSelectively({\n filePath: path.join(templateDir, file),\n destPath: targetPath,\n useSmartAccounts,\n enableOnramp,\n });\n }\n };\n\n const isNextjs = templateDir.includes(\"nextjs\");\n\n const files = fs.readdirSync(templateDir);\n for (const file of files) {\n if (file === \"package.json\") {\n const customizedPackageJson = customizePackageJson(templateDir, appName, enableOnramp);\n writeFileToTarget(file, customizedPackageJson);\n } else if (file === \"env.example\" && projectId) {\n writeFileToTarget(file);\n const customizedEnv = customizeEnv({\n templateDir,\n projectId,\n useSmartAccounts,\n apiKeyId,\n apiKeySecret,\n });\n console.log(\"Copying CDP Project ID to .env\");\n if (apiKeyId && apiKeySecret) {\n console.log(\"Copying CDP API credentials to .env\");\n }\n if (useSmartAccounts) {\n console.log(\"Configuring Smart Accounts in environment\");\n }\n writeFileToTarget(\".env\", customizedEnv);\n } else {\n writeFileToTarget(file);\n }\n }\n\n // Handle smart account configuration in config files\n if (useSmartAccounts) {\n const configFileName = isNextjs ? \"src/components/Providers.tsx\" : \"src/config.ts\";\n const customizedConfig = customizeConfig(templateDir, useSmartAccounts, isNextjs);\n if (customizedConfig) {\n console.log(\"Configuring Smart Accounts in application config\");\n writeFileToTarget(configFileName, customizedConfig);\n }\n }\n\n // Generate the appropriate Transaction.tsx component\n const transactionFileName = isNextjs ? \"src/components/Transaction.tsx\" : \"src/Transaction.tsx\";\n const transactionContent = generateTransactionComponent(useSmartAccounts);\n writeFileToTarget(transactionFileName, transactionContent);\n}\n\n/**\n * Generate the appropriate Transaction component based on account type\n *\n * @param useSmartAccounts - Whether to use Smart Accounts\n * @returns The generated Transaction component content\n */\nfunction generateTransactionComponent(useSmartAccounts?: boolean): string {\n if (useSmartAccounts) {\n return `export { default } from \"./SmartAccountTransaction\";`;\n } else {\n return `export { default } from \"./EOATransaction\";`;\n }\n}\n\ninit().catch(e => {\n console.error(e);\n process.exit(1);\n});\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/create-cdp-app",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -7,13 +7,18 @@ This project was generated with [`@coinbase/create-cdp-app`](https://coinbase.gi
7
7
  ```
8
8
  src/
9
9
  ├── app/ # Next.js App Router directory
10
+ │ ├── api/ # API endpoints
11
+ │ │ └── onramp/ # Onramp-related endpoints
12
+ │ │ ├── buy-quote/ # Buy quote generation endpoint for exchange rate and purchase URL
13
+ │ │ └── buy-options/ # Available crypto assets and payment currencies
10
14
  │ ├── favicon.ico # Application favicon
11
15
  │ ├── globals.css # Global styles and theme variables
12
16
  │ ├── layout.tsx # Root layout with providers and global UI
13
17
  │ └── page.tsx # Home page component
14
18
 
15
- └── components/ # Reusable React components
19
+ ├── components/ # Reusable React components
16
20
  ├── ClientApp.tsx # Client-side application wrapper
21
+ ├── FundWallet.tsx # Example Fund flow
17
22
  ├── Header.tsx # Navigation header with authentication status
18
23
  ├── Icons.tsx # Reusable icon components
19
24
  ├── Loading.tsx # Loading state component
@@ -21,8 +26,13 @@ src/
21
26
  ├── SignInScreen.tsx # Authentication screen with CDP sign-in flow
22
27
  ├── SignedInScreen.tsx # Screen displayed after successful authentication
23
28
  ├── theme.ts # Theme configuration and styling constants
24
- ├── Transaction.tsx # Example transaction flow using CDP Hooks
29
+ ├── Transaction.tsx # Example transaction flow
25
30
  └── UserBalance.tsx # Component to display user's wallet balance
31
+
32
+ └── lib/ # Shared utilities and helpers
33
+ ├── cdp-auth.ts # CDP API authentication utilities
34
+ ├── onramp-api.ts # CDP Onramp API utilities
35
+ └── to-camel-case.ts # Utility for converting snakecase-keyed objects to camelcase-keyed objects
26
36
  ```
27
37
 
28
38
  ## Getting Started
@@ -36,6 +46,18 @@ First, make sure you have your CDP Project ID:
36
46
 
37
47
  Then, copy the `env.example` file to `.env`, and populate the `NEXT_PUBLIC_CDP_PROJECT_ID` with your project id.
38
48
 
49
+ ### CDP API credentials (Optional)
50
+
51
+ If you enabled Onramp during setup, your `.env` file will already contain the CDP API credentials. If you want to add Onramp later:
52
+
53
+ 1. Go to [CDP API Keys](https://portal.cdp.coinbase.com/api-keys) to create an API key
54
+ 2. Add the following to your `.env` file:
55
+ ```
56
+ CDP_API_KEY_ID=your-api-key-id
57
+ CDP_API_KEY_SECRET=your-api-key-secret
58
+ ```
59
+
60
+
39
61
  Now you can start the development server:
40
62
 
41
63
  Using npm:
@@ -76,6 +98,7 @@ This template comes with:
76
98
  - Built-in TypeScript support
77
99
  - ESLint with Next.js configuration
78
100
  - Viem for type-safe Ethereum interactions
101
+ - Optional Onramp API integration
79
102
 
80
103
  ## Learn More
81
104
 
@@ -3,3 +3,7 @@ NEXT_PUBLIC_CDP_PROJECT_ID=example-id
3
3
 
4
4
  # Account type: "evm-eoa" for regular accounts, "evm-smart" for Smart Accounts (gasless transactions and improved UX)
5
5
  NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE=evm-eoa
6
+
7
+ # CDP API Key
8
+ # CDP_API_KEY_ID=your-api-key-id
9
+ # CDP_API_KEY_SECRET=your-api-key-secret
@@ -0,0 +1,90 @@
1
+ import {
2
+ type unstable_FetchBuyOptions as FetchBuyOptions,
3
+ type unstable_OnrampBuyOptionsSnakeCaseResponse as OnrampBuyOptionsSnakeCaseResponse,
4
+ } from "@coinbase/cdp-react";
5
+ import { NextRequest, NextResponse } from "next/server";
6
+
7
+ import { generateCDPJWT, getCDPCredentials, ONRAMP_API_BASE_URL } from "@/lib/cdp-auth";
8
+ import { convertSnakeToCamelCase } from "@/lib/to-camel-case";
9
+
10
+ type OnrampBuyOptionsResponseRaw = OnrampBuyOptionsSnakeCaseResponse;
11
+ type OnrampBuyOptionsResponse = Awaited<ReturnType<FetchBuyOptions>>;
12
+
13
+ /**
14
+ * Fetches available buy options (payment currencies and purchasable assets) for onramp
15
+ *
16
+ * @param request - NextRequest object
17
+ * @returns NextResponse object
18
+ */
19
+ export async function GET(request: NextRequest) {
20
+ try {
21
+ // Validate CDP credentials are configured
22
+ try {
23
+ getCDPCredentials();
24
+ } catch (_error) {
25
+ return NextResponse.json({ error: "CDP API credentials not configured" }, { status: 500 });
26
+ }
27
+
28
+ /**
29
+ * Extract query parameters
30
+ * Note: While the API documentation shows all parameters as optional,
31
+ * the backend currently requires the 'country' parameter
32
+ */
33
+ const searchParams = request.nextUrl.searchParams;
34
+ const country = searchParams.get("country");
35
+ const subdivision = searchParams.get("subdivision");
36
+ const networks = searchParams.get("networks");
37
+
38
+ // Build query string
39
+ const queryParams = new URLSearchParams();
40
+ if (country) queryParams.append("country", country);
41
+ if (subdivision) queryParams.append("subdivision", subdivision);
42
+ if (networks) queryParams.append("networks", networks);
43
+
44
+ const queryString = queryParams.toString();
45
+ const apiPath = "/onramp/v1/buy/options";
46
+ const fullPath = apiPath + (queryString ? `?${queryString}` : "");
47
+
48
+ // Generate JWT for CDP API authentication
49
+ const jwt = await generateCDPJWT({
50
+ requestMethod: "GET",
51
+ requestHost: new URL(ONRAMP_API_BASE_URL).hostname,
52
+ requestPath: apiPath,
53
+ });
54
+
55
+ // Call CDP API to get buy options
56
+ const response = await fetch(`${ONRAMP_API_BASE_URL}${fullPath}`, {
57
+ method: "GET",
58
+ headers: {
59
+ Authorization: `Bearer ${jwt}`,
60
+ "Content-Type": "application/json",
61
+ },
62
+ });
63
+
64
+ if (!response.ok) {
65
+ console.error("CDP API error:", response.statusText);
66
+ const errorText = await response.text();
67
+ console.error("Error details:", errorText);
68
+
69
+ try {
70
+ const errorData = JSON.parse(errorText);
71
+ return NextResponse.json(
72
+ { error: errorData.message || "Failed to fetch buy options" },
73
+ { status: response.status },
74
+ );
75
+ } catch {
76
+ return NextResponse.json(
77
+ { error: "Failed to fetch buy options" },
78
+ { status: response.status },
79
+ );
80
+ }
81
+ }
82
+
83
+ const data: OnrampBuyOptionsResponseRaw = await response.json();
84
+ const dataCamelCase: OnrampBuyOptionsResponse = convertSnakeToCamelCase(data);
85
+ return NextResponse.json(dataCamelCase);
86
+ } catch (error) {
87
+ console.error("Error fetching buy options:", error);
88
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
89
+ }
90
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ type unstable_FetchBuyQuote as FetchBuyQuote,
3
+ type unstable_OnrampBuyQuoteSnakeCaseResponse as OnrampBuyQuoteSnakeCaseResponse,
4
+ } from "@coinbase/cdp-react";
5
+ import { NextRequest, NextResponse } from "next/server";
6
+
7
+ import { generateCDPJWT, getCDPCredentials, ONRAMP_API_BASE_URL } from "@/lib/cdp-auth";
8
+ import { convertSnakeToCamelCase } from "@/lib/to-camel-case";
9
+
10
+ type OnrampBuyQuoteRequest = Parameters<FetchBuyQuote>[0];
11
+ type OnrampBuyQuoteResponseRaw = OnrampBuyQuoteSnakeCaseResponse;
12
+ type OnrampBuyQuoteResponse = Awaited<ReturnType<FetchBuyQuote>>;
13
+
14
+ /**
15
+ * Creates a buy quote for onramp purchase
16
+ *
17
+ * @param request - Buy quote request parameters
18
+ * @returns Buy quote with fees and onramp URL
19
+ */
20
+ export async function POST(request: NextRequest) {
21
+ try {
22
+ const body: OnrampBuyQuoteRequest = await request.json();
23
+
24
+ // Validate CDP credentials are configured
25
+ try {
26
+ getCDPCredentials();
27
+ } catch (_error) {
28
+ return NextResponse.json({ error: "CDP API credentials not configured" }, { status: 500 });
29
+ }
30
+
31
+ // Validate required fields
32
+
33
+ // Note we don't require the wallet info because this endpoint is used to get an exchange rate. Only the onramp URL requires the wallet info.
34
+
35
+ if (
36
+ !body.purchaseCurrency ||
37
+ !body.paymentAmount ||
38
+ !body.paymentCurrency ||
39
+ !body.paymentMethod ||
40
+ !body.country
41
+ ) {
42
+ return NextResponse.json({ error: "Missing required parameters" }, { status: 400 });
43
+ }
44
+
45
+ // Validate US subdivision requirement
46
+ if (body.country === "US" && !body.subdivision) {
47
+ return NextResponse.json({ error: "State/subdivision is required for US" }, { status: 400 });
48
+ }
49
+
50
+ // Generate JWT for CDP API authentication
51
+ const jwt = await generateCDPJWT({
52
+ requestMethod: "POST",
53
+ requestHost: new URL(ONRAMP_API_BASE_URL).hostname,
54
+ requestPath: "/onramp/v1/buy/quote",
55
+ });
56
+
57
+ // Prepare request body for buy quote API
58
+ const requestBody = {
59
+ purchaseCurrency: body.purchaseCurrency,
60
+ purchaseNetwork: body.purchaseNetwork, // Use the wallet's network
61
+ paymentAmount: body.paymentAmount,
62
+ paymentCurrency: body.paymentCurrency,
63
+ paymentMethod: body.paymentMethod,
64
+ country: body.country,
65
+ subdivision: body.subdivision,
66
+ destinationAddress: body.destinationAddress, // Include to get one-click-buy URL
67
+ };
68
+
69
+ // Call CDP Onramp API to get buy quote and URL
70
+ const response = await fetch(`${ONRAMP_API_BASE_URL}/onramp/v1/buy/quote`, {
71
+ method: "POST",
72
+ headers: {
73
+ Authorization: `Bearer ${jwt}`,
74
+ "Content-Type": "application/json",
75
+ },
76
+ body: JSON.stringify(requestBody),
77
+ });
78
+
79
+ if (!response.ok) {
80
+ console.error("CDP API error:", response.statusText);
81
+ const errorText = await response.text();
82
+ console.error("Error details:", errorText);
83
+
84
+ try {
85
+ const errorData = JSON.parse(errorText);
86
+ return NextResponse.json(
87
+ { error: errorData.message || "Failed to create buy quote" },
88
+ { status: response.status },
89
+ );
90
+ } catch {
91
+ return NextResponse.json(
92
+ { error: "Failed to create buy quote" },
93
+ { status: response.status },
94
+ );
95
+ }
96
+ }
97
+
98
+ // convert response data to camelCase until migration to API v2 which will return camelCase data
99
+ const data: OnrampBuyQuoteResponseRaw = await response.json();
100
+ const dataCamelCase: OnrampBuyQuoteResponse = convertSnakeToCamelCase(data);
101
+ return NextResponse.json(dataCamelCase);
102
+ } catch (error) {
103
+ console.error("Error creating buy quote:", error);
104
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
105
+ }
106
+ }
@@ -1,5 +1,6 @@
1
1
  :root {
2
2
  --cdp-example-page-bg-color: #eaeaea;
3
+ --cdp-example-page-bg-alt-color: #eef0f3;
3
4
  --cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
4
5
  --cdp-example-bg-skeleton-color: rgba(0, 0, 0, 0.1);
5
6
  --cdp-example-text-color: #111111;
@@ -30,6 +31,7 @@
30
31
  @media (prefers-color-scheme: dark) {
31
32
  :root {
32
33
  --cdp-example-page-bg-color: #0a0b0d;
34
+ --cdp-example-page-bg-alt-color: #333333;
33
35
  --cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
34
36
  --cdp-example-bg-skeleton-color: rgba(255, 255, 255, 0.1);
35
37
  --cdp-example-text-color: #fafafa;
@@ -167,7 +169,7 @@ header .wallet-address {
167
169
  }
168
170
 
169
171
  .main {
170
- padding: 0.5rem;
172
+ padding: 4rem 0.5rem;
171
173
  width: 100%;
172
174
  }
173
175
 
@@ -200,7 +202,7 @@ header .wallet-address {
200
202
 
201
203
  .smart-badge {
202
204
  background-color: rgba(76, 175, 80, 0.15);
203
- color: #4CAF50;
205
+ color: #4caf50;
204
206
  border-radius: 12px;
205
207
  padding: 0.2rem 0.6rem;
206
208
  font-size: 0.7rem;
@@ -220,6 +222,10 @@ header .wallet-address {
220
222
  font-size: 0.9rem;
221
223
  }
222
224
 
225
+ .smart-account-info:last-child {
226
+ margin-bottom: 0;
227
+ }
228
+
223
229
  .success-message {
224
230
  background-color: rgba(76, 175, 80, 0.1);
225
231
  border: 1px solid rgba(76, 175, 80, 0.3);
@@ -227,7 +233,7 @@ header .wallet-address {
227
233
  padding: 0.75rem;
228
234
  margin: 0.5rem 0;
229
235
  font-size: 0.9rem;
230
- color: var(--cdp-example-success-color, #4CAF50);
236
+ color: var(--cdp-example-success-color, #4caf50);
231
237
  }
232
238
 
233
239
  .card {
@@ -313,6 +319,24 @@ header .wallet-address {
313
319
  min-width: 11.75rem;
314
320
  }
315
321
 
322
+ .page-heading {
323
+ font-size: 1.5rem;
324
+ font-weight: 500;
325
+ line-height: 1;
326
+ margin: 0 0 1.5rem;
327
+ }
328
+
329
+ .page-divider {
330
+ width: 60%;
331
+ margin: 4rem 0;
332
+ }
333
+
334
+ .small-text {
335
+ font-size: 0.875rem;
336
+ line-height: 1.5;
337
+ margin: 0;
338
+ }
339
+
316
340
  @media (min-width: 540px) {
317
341
  .header-inner {
318
342
  flex-direction: row;
@@ -332,7 +356,7 @@ header .wallet-address {
332
356
  }
333
357
 
334
358
  .main {
335
- padding: 1rem;
359
+ padding: 4rem 1rem;
336
360
  }
337
361
  }
338
362
 
@@ -0,0 +1,49 @@
1
+ import {
2
+ unstable_FundModal as FundModal,
3
+ type unstable_FundModalProps as FundModalProps,
4
+ } from "@coinbase/cdp-react";
5
+ import { useCallback } from "react";
6
+
7
+ import { getBuyOptions, createBuyQuote } from "@/lib/onramp-api";
8
+
9
+ /**
10
+ * A component that wraps the FundModal component
11
+ *
12
+ * @param props - The props for the FundWallet component
13
+ * @param props.onSuccess - The callback function to call when the onramp purchase is successful
14
+ * @returns The FundWallet component
15
+ */
16
+ export default function FundWallet({ onSuccess }: { onSuccess: () => void }) {
17
+ const fetchBuyQuote: FundModalProps["fetchBuyQuote"] = useCallback(async params => {
18
+ return createBuyQuote(params);
19
+ }, []);
20
+
21
+ const fetchBuyOptions: FundModalProps["fetchBuyOptions"] = useCallback(async params => {
22
+ return getBuyOptions(params);
23
+ }, []);
24
+
25
+ return (
26
+ <>
27
+ <FundModal
28
+ country="US"
29
+ subdivision="CA"
30
+ cryptoCurrency="ETH"
31
+ fiatCurrency="USD"
32
+ fetchBuyQuote={fetchBuyQuote}
33
+ fetchBuyOptions={fetchBuyOptions}
34
+ network="base"
35
+ presetAmountInputs={[10, 25, 50]}
36
+ onSuccess={onSuccess}
37
+ />
38
+ <p className="small-text">
39
+ Warning: this will cost real money unless you{" "}
40
+ <a
41
+ href="https://docs.cdp.coinbase.com/onramp-&-offramp/developer-guidance/faq#can-i-test-my-onramp-integration-by-creating-mock-buys-and-sends%3F"
42
+ target="_blank"
43
+ >
44
+ enable mock buys and sends
45
+ </a>
46
+ </p>
47
+ </>
48
+ );
49
+ }
@@ -50,7 +50,11 @@ export default function SignedInScreen() {
50
50
  <main className="main flex-col-container flex-grow">
51
51
  <div className="main-inner flex-col-container">
52
52
  <div className="card card--user-balance">
53
- <UserBalance balance={formattedBalance} />
53
+ <UserBalance
54
+ balance={formattedBalance}
55
+ faucetName="Base Sepolia Faucet"
56
+ faucetUrl="https://portal.cdp.coinbase.com/products/faucet"
57
+ />
54
58
  </div>
55
59
  <div className="card card--transaction">
56
60
  {isSignedIn && evmAddress && (
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import { useEvmAddress, useIsSignedIn } from "@coinbase/cdp-hooks";
4
+ import { useCallback, useEffect, useMemo, useState } from "react";
5
+ import {
6
+ createPublicClient,
7
+ http,
8
+ formatEther,
9
+ type PublicClient,
10
+ type Transport,
11
+ type Address,
12
+ } from "viem";
13
+ import { baseSepolia, base } from "viem/chains";
14
+
15
+ import FundWallet from "@/components/FundWallet";
16
+ import Header from "@/components/Header";
17
+ import Transaction from "@/components/Transaction";
18
+ import UserBalance from "@/components/UserBalance";
19
+
20
+ /**
21
+ * Create a viem client to access user's balance on the Base network
22
+ */
23
+ const client = createPublicClient({
24
+ chain: base,
25
+ transport: http(),
26
+ });
27
+
28
+ /**
29
+ * Create a viem client to access user's balance on the Base Sepolia network
30
+ */
31
+ const sepoliaClient = createPublicClient({
32
+ chain: baseSepolia,
33
+ transport: http(),
34
+ });
35
+
36
+ const useBalance = (
37
+ address: Address | null,
38
+ client: PublicClient<Transport, typeof base | typeof baseSepolia, undefined, undefined>,
39
+ poll = false,
40
+ ) => {
41
+ const [balance, setBalance] = useState<bigint | undefined>(undefined);
42
+
43
+ const formattedBalance = useMemo(() => {
44
+ if (balance === undefined) return undefined;
45
+ return formatEther(balance);
46
+ }, [balance]);
47
+
48
+ const getBalance = useCallback(async () => {
49
+ if (!address) return;
50
+ const balance = await client.getBalance({ address });
51
+ setBalance(balance);
52
+ }, [address, client]);
53
+
54
+ useEffect(() => {
55
+ if (!poll) {
56
+ return;
57
+ }
58
+ getBalance();
59
+ const interval = setInterval(getBalance, 500);
60
+ return () => clearInterval(interval);
61
+ }, [getBalance, poll]);
62
+
63
+ return { balance, formattedBalance, getBalance };
64
+ };
65
+
66
+ /**
67
+ * The Signed In screen
68
+ */
69
+ export default function SignedInScreen() {
70
+ const { isSignedIn } = useIsSignedIn();
71
+ const { evmAddress } = useEvmAddress();
72
+
73
+ const { formattedBalance, getBalance } = useBalance(evmAddress, client, true);
74
+ const { formattedBalance: formattedBalanceSepolia, getBalance: getBalanceSepolia } = useBalance(
75
+ evmAddress,
76
+ sepoliaClient,
77
+ true,
78
+ );
79
+
80
+ return (
81
+ <>
82
+ <Header />
83
+ <main className="main flex-col-container flex-grow">
84
+ <p className="page-heading">Fund your wallet on Base</p>
85
+ <div className="main-inner flex-col-container">
86
+ <div className="card card--user-balance">
87
+ <UserBalance balance={formattedBalance} />
88
+ </div>
89
+ <div className="card card--transaction">
90
+ {isSignedIn && evmAddress && <FundWallet onSuccess={getBalance} />}
91
+ </div>
92
+ </div>
93
+ <hr className="page-divider" />
94
+ <p className="page-heading">Send a transaction on Base Sepolia</p>
95
+ <div className="main-inner flex-col-container">
96
+ <div className="card card--user-balance">
97
+ <UserBalance
98
+ balance={formattedBalanceSepolia}
99
+ faucetName="Base Sepolia Faucet"
100
+ faucetUrl="https://portal.cdp.coinbase.com/products/faucet"
101
+ />
102
+ </div>
103
+ <div className="card card--transaction">
104
+ {isSignedIn && evmAddress && (
105
+ <Transaction balance={formattedBalanceSepolia} onSuccess={getBalanceSepolia} />
106
+ )}
107
+ </div>
108
+ </div>
109
+ </main>
110
+ </>
111
+ );
112
+ }
@@ -108,8 +108,9 @@ export default function SmartAccountTransaction(props: Props) {
108
108
  <p>
109
109
  This example transaction sends a tiny amount of ETH from your wallet to itself.
110
110
  </p>
111
- <p>
112
- Get some from{" "}
111
+ <p className="smart-account-info">
112
+ ℹ️ <strong>Note:</strong> Even though this is a gasless transaction, you still
113
+ need ETH in your account to send it. Get some from{" "}
113
114
  <a
114
115
  href="https://portal.cdp.coinbase.com/products/faucet"
115
116
  target="_blank"
@@ -118,10 +119,6 @@ export default function SmartAccountTransaction(props: Props) {
118
119
  Base Sepolia Faucet
119
120
  </a>
120
121
  </p>
121
- <p className="smart-account-info">
122
- ℹ️ <strong>Note:</strong> Even though this is a gasless transaction, you still
123
- need ETH in your account to send it.
124
- </p>
125
122
  </>
126
123
  )}
127
124
  {!smartAccount && (
@@ -3,6 +3,8 @@ import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkelet
3
3
 
4
4
  interface Props {
5
5
  balance?: string;
6
+ faucetUrl?: string;
7
+ faucetName?: string;
6
8
  }
7
9
 
8
10
  /**
@@ -13,7 +15,7 @@ interface Props {
13
15
  * @returns A component that displays the user's balance.
14
16
  */
15
17
  export default function UserBalance(props: Props) {
16
- const { balance } = props;
18
+ const { balance, faucetUrl, faucetName } = props;
17
19
  return (
18
20
  <>
19
21
  <h2 className="card-title">Available balance</h2>
@@ -27,16 +29,14 @@ export default function UserBalance(props: Props) {
27
29
  </span>
28
30
  )}
29
31
  </p>
30
- <p>
31
- Get testnet ETH from{" "}
32
- <a
33
- href="https://portal.cdp.coinbase.com/products/faucet"
34
- target="_blank"
35
- rel="noopener noreferrer"
36
- >
37
- Base Sepolia Faucet
38
- </a>
39
- </p>
32
+ {faucetUrl && faucetName && (
33
+ <p>
34
+ Get testnet ETH from{" "}
35
+ <a href={faucetUrl} target="_blank" rel="noopener noreferrer">
36
+ {faucetName}
37
+ </a>
38
+ </p>
39
+ )}
40
40
  </>
41
41
  );
42
42
  }
@@ -1,6 +1,7 @@
1
1
  import { type Theme } from "@coinbase/cdp-react/theme";
2
2
 
3
3
  export const theme: Partial<Theme> = {
4
+ "colors-bg-alternate": "var(--cdp-example-page-bg-alt-color)",
4
5
  "colors-bg-default": "var(--cdp-example-card-bg-color)",
5
6
  "colors-bg-overlay": "var(--cdp-example-bg-overlay-color)",
6
7
  "colors-bg-skeleton": "var(--cdp-example-bg-skeleton-color)",
@@ -0,0 +1,48 @@
1
+ import { generateJwt } from "@coinbase/cdp-sdk/auth";
2
+
3
+ interface CDPAuthConfig {
4
+ requestMethod: string;
5
+ requestHost: string;
6
+ requestPath: string;
7
+ audience?: string[];
8
+ }
9
+
10
+ /**
11
+ * Get CDP API credentials from environment variables
12
+ *
13
+ * @throws Error if credentials are not configured
14
+ */
15
+ export function getCDPCredentials() {
16
+ const apiKeyId = process.env.CDP_API_KEY_ID;
17
+ const apiKeySecret = process.env.CDP_API_KEY_SECRET;
18
+
19
+ if (!apiKeyId || !apiKeySecret) {
20
+ throw new Error("CDP API credentials not configured");
21
+ }
22
+
23
+ return { apiKeyId, apiKeySecret };
24
+ }
25
+
26
+ /**
27
+ * Generate JWT token for CDP API authentication
28
+ *
29
+ * @param config - Configuration for JWT generation
30
+ * @returns JWT token string
31
+ */
32
+ export async function generateCDPJWT(config: CDPAuthConfig): Promise<string> {
33
+ const { apiKeyId, apiKeySecret } = getCDPCredentials();
34
+
35
+ return generateJwt({
36
+ apiKeyId,
37
+ apiKeySecret,
38
+ requestMethod: config.requestMethod,
39
+ requestHost: config.requestHost,
40
+ requestPath: config.requestPath,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * Base URL for ONRAMP API
46
+ * Can change to api.cdp.coinbase.com/platform once session token endpoints are supported in v2 API
47
+ */
48
+ export const ONRAMP_API_BASE_URL = "https://api.developer.coinbase.com";
@@ -0,0 +1,66 @@
1
+ import {
2
+ type unstable_FetchBuyOptions as FetchBuyOptions,
3
+ type unstable_FetchBuyQuote as FetchBuyQuote,
4
+ } from "@coinbase/cdp-react/components/Fund";
5
+
6
+ /**
7
+ * Fetches available buy options for onramp
8
+ *
9
+ * @param params - Query parameters for buy options
10
+ * @returns Buy options including payment currencies and purchasable assets
11
+ */
12
+ export const getBuyOptions: FetchBuyOptions = async params => {
13
+ try {
14
+ const queryParams = new URLSearchParams();
15
+ queryParams.append("country", params.country);
16
+ if (params?.subdivision) queryParams.append("subdivision", params.subdivision);
17
+
18
+ const queryString = queryParams.toString();
19
+ const url = `/api/onramp/buy-options${queryString ? `?${queryString}` : ""}`;
20
+
21
+ const response = await fetch(url, {
22
+ method: "GET",
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ },
26
+ });
27
+
28
+ if (!response.ok) {
29
+ const errorData = await response.json();
30
+ throw new Error(errorData.error || "Failed to fetch buy options");
31
+ }
32
+
33
+ return await response.json();
34
+ } catch (error) {
35
+ console.error("Error fetching buy options:", error);
36
+ throw error;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Creates a buy quote for onramp purchase
42
+ *
43
+ * @param request - Buy quote request parameters
44
+ * @returns Buy quote with fees and onramp URL
45
+ */
46
+ export const createBuyQuote: FetchBuyQuote = async request => {
47
+ try {
48
+ const response = await fetch("/api/onramp/buy-quote", {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ },
53
+ body: JSON.stringify(request),
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const errorData = await response.json();
58
+ throw new Error(errorData.error || "Failed to create buy quote");
59
+ }
60
+
61
+ return await response.json();
62
+ } catch (error) {
63
+ console.error("Error creating buy quote:", error);
64
+ throw error;
65
+ }
66
+ };
@@ -0,0 +1,39 @@
1
+ type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
2
+ ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
3
+ : S;
4
+
5
+ type CamelizeKeys<T> = T extends readonly unknown[]
6
+ ? { [K in keyof T]: CamelizeKeys<T[K]> }
7
+ : T extends object
8
+ ? {
9
+ [K in keyof T as SnakeToCamelCase<K & string>]: CamelizeKeys<T[K]>;
10
+ }
11
+ : T;
12
+
13
+ /**
14
+ * Converts snake_case keys to camelCase in an object or array of objects.
15
+ *
16
+ * @param {T} obj - The object, array, or string to convert. (required)
17
+ * @returns {T} The converted object, array, or string.
18
+ */
19
+ export const convertSnakeToCamelCase = <T>(obj: T): CamelizeKeys<T> => {
20
+ if (Array.isArray(obj)) {
21
+ return obj.map(item => convertSnakeToCamelCase(item)) as CamelizeKeys<T>;
22
+ }
23
+
24
+ if (obj !== null && typeof obj === "object") {
25
+ return Object.keys(obj).reduce((acc, key) => {
26
+ const camelCaseKey = toCamelCase(key);
27
+ (acc as Record<string, unknown>)[camelCaseKey] = convertSnakeToCamelCase(
28
+ (obj as Record<string, unknown>)[key],
29
+ );
30
+ return acc;
31
+ }, {} as CamelizeKeys<T>);
32
+ }
33
+
34
+ return obj as CamelizeKeys<T>;
35
+ };
36
+
37
+ const toCamelCase = (str: string) => {
38
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
39
+ };
@@ -107,8 +107,9 @@ function SmartAccountTransaction(props: Props) {
107
107
  <p>
108
108
  This example transaction sends a tiny amount of ETH from your wallet to itself.
109
109
  </p>
110
- <p>
111
- Get some from{" "}
110
+ <p className="smart-account-info">
111
+ ℹ️ <strong>Note:</strong> Even though this is a gasless transaction, you still
112
+ need ETH in your account to send it. Get some from{" "}
112
113
  <a
113
114
  href="https://portal.cdp.coinbase.com/products/faucet"
114
115
  target="_blank"
@@ -117,10 +118,6 @@ function SmartAccountTransaction(props: Props) {
117
118
  Base Sepolia Faucet
118
119
  </a>
119
120
  </p>
120
- <p className="smart-account-info">
121
- ℹ️ <strong>Note:</strong> Even though this is a gasless transaction, you still
122
- need ETH in your account to send it.
123
- </p>
124
121
  </>
125
122
  )}
126
123
  {!smartAccount && (
@@ -1,5 +1,6 @@
1
1
  :root {
2
2
  --cdp-example-page-bg-color: #eaeaea;
3
+ --cdp-example-page-bg-alt-color: #eef0f3;
3
4
  --cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
4
5
  --cdp-example-bg-skeleton-color: rgba(0, 0, 0, 0.1);
5
6
  --cdp-example-text-color: #111111;
@@ -30,6 +31,7 @@
30
31
  @media (prefers-color-scheme: dark) {
31
32
  :root {
32
33
  --cdp-example-page-bg-color: #0a0b0d;
34
+ --cdp-example-page-bg-alt-color: #333333;
33
35
  --cdp-example-bg-overlay-color: rgba(0, 0, 0, 0.25);
34
36
  --cdp-example-bg-skeleton-color: rgba(255, 255, 255, 0.1);
35
37
  --cdp-example-text-color: #fafafa;
@@ -167,7 +169,7 @@ header .wallet-address {
167
169
  }
168
170
 
169
171
  .main {
170
- padding: 0.5rem;
172
+ padding: 4rem 0.5rem;
171
173
  width: 100%;
172
174
  }
173
175
 
@@ -200,7 +202,7 @@ header .wallet-address {
200
202
 
201
203
  .smart-badge {
202
204
  background-color: rgba(76, 175, 80, 0.15);
203
- color: #4CAF50;
205
+ color: #4caf50;
204
206
  border-radius: 12px;
205
207
  padding: 0.2rem 0.6rem;
206
208
  font-size: 0.7rem;
@@ -220,6 +222,10 @@ header .wallet-address {
220
222
  font-size: 0.9rem;
221
223
  }
222
224
 
225
+ .smart-account-info:last-child {
226
+ margin-bottom: 0;
227
+ }
228
+
223
229
  .success-message {
224
230
  background-color: rgba(76, 175, 80, 0.1);
225
231
  border: 1px solid rgba(76, 175, 80, 0.3);
@@ -227,7 +233,7 @@ header .wallet-address {
227
233
  padding: 0.75rem;
228
234
  margin: 0.5rem 0;
229
235
  font-size: 0.9rem;
230
- color: var(--cdp-example-success-color, #4CAF50);
236
+ color: var(--cdp-example-success-color, #4caf50);
231
237
  }
232
238
 
233
239
  .card {
@@ -313,6 +319,24 @@ header .wallet-address {
313
319
  min-width: 11.75rem;
314
320
  }
315
321
 
322
+ .page-heading {
323
+ font-size: 1.5rem;
324
+ font-weight: 500;
325
+ line-height: 1;
326
+ margin: 0 0 1.5rem;
327
+ }
328
+
329
+ .page-divider {
330
+ width: 60%;
331
+ margin: 4rem 0;
332
+ }
333
+
334
+ .small-text {
335
+ font-size: 0.875rem;
336
+ line-height: 1.5;
337
+ margin: 0;
338
+ }
339
+
316
340
  @media (min-width: 540px) {
317
341
  .header-inner {
318
342
  flex-direction: row;
@@ -332,7 +356,7 @@ header .wallet-address {
332
356
  }
333
357
 
334
358
  .main {
335
- padding: 1rem;
359
+ padding: 4rem 1rem;
336
360
  }
337
361
  }
338
362
 
@@ -1,6 +1,7 @@
1
1
  import { type Theme } from "@coinbase/cdp-react/theme";
2
2
 
3
3
  export const theme: Partial<Theme> = {
4
+ "colors-bg-alternate": "var(--cdp-example-page-bg-alt-color)",
4
5
  "colors-bg-default": "var(--cdp-example-card-bg-color)",
5
6
  "colors-bg-overlay": "var(--cdp-example-bg-overlay-color)",
6
7
  "colors-bg-skeleton": "var(--cdp-example-bg-skeleton-color)",