@coinbase/create-cdp-app 0.0.23 → 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 +125 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-nextjs/README.md +25 -2
- package/template-nextjs/env.example +4 -0
- package/template-nextjs/src/app/api/onramp/buy-options/route.ts +90 -0
- package/template-nextjs/src/app/api/onramp/buy-quote/route.ts +106 -0
- package/template-nextjs/src/app/globals.css +28 -4
- package/template-nextjs/src/components/FundWallet.tsx +49 -0
- package/template-nextjs/src/components/SignedInScreen.tsx +5 -1
- package/template-nextjs/src/components/SignedInScreenWithOnramp.tsx +112 -0
- package/template-nextjs/src/components/SmartAccountTransaction.tsx +3 -6
- package/template-nextjs/src/components/UserBalance.tsx +11 -11
- package/template-nextjs/src/components/theme.ts +1 -0
- package/template-nextjs/src/lib/cdp-auth.ts +48 -0
- package/template-nextjs/src/lib/onramp-api.ts +66 -0
- package/template-nextjs/src/lib/to-camel-case.ts +39 -0
- package/template-react/src/SmartAccountTransaction.tsx +3 -6
- package/template-react/src/index.css +28 -4
- package/template-react/src/theme.ts +1 -0
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
@@ -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
|
-
|
|
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
|
|
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: #
|
|
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, #
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
href="
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
>
|
|
37
|
-
|
|
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
|
-
|
|
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: #
|
|
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, #
|
|
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)",
|