@coinbase/create-cdp-app 0.0.22 → 0.0.24
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 +79 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/template-nextjs/env.example +2 -0
- package/template-nextjs/src/app/globals.css +38 -0
- package/template-nextjs/src/components/{Transaction.tsx → EOATransaction.tsx} +6 -4
- package/template-nextjs/src/components/Header.tsx +6 -1
- package/template-nextjs/src/components/Providers.tsx +2 -0
- package/template-nextjs/src/components/SmartAccountTransaction.tsx +181 -0
- package/template-react/env.example +3 -0
- package/template-react/src/{Transaction.tsx → EOATransaction.tsx} +7 -5
- package/template-react/src/Header.tsx +6 -1
- package/template-react/src/SmartAccountTransaction.tsx +182 -0
- package/template-react/src/config.ts +5 -1
- package/template-react/src/index.css +38 -0
package/dist/index.js
CHANGED
|
@@ -17,27 +17,63 @@ function customizePackageJson(templateDir, appName) {
|
|
|
17
17
|
packageJson.name = appName;
|
|
18
18
|
return JSON.stringify(packageJson, null, 2) + "\n";
|
|
19
19
|
}
|
|
20
|
-
function customizeEnv(templateDir, projectId) {
|
|
20
|
+
function customizeEnv(templateDir, projectId, useSmartAccounts) {
|
|
21
21
|
const exampleEnvPath = path.join(templateDir, "env.example");
|
|
22
22
|
const exampleEnv = fs.readFileSync(exampleEnvPath, "utf-8");
|
|
23
|
-
|
|
23
|
+
let envContent = exampleEnv.replace(/(.*PROJECT_ID=).*(\r?\n|$)/, `$1${projectId}
|
|
24
24
|
`);
|
|
25
|
+
const accountType = useSmartAccounts ? "evm-smart" : "evm-eoa";
|
|
26
|
+
const prefix = templateDir.includes("nextjs") ? "NEXT_PUBLIC_" : "VITE_";
|
|
27
|
+
envContent = envContent.replace(
|
|
28
|
+
new RegExp(`(${prefix}CDP_CREATE_ACCOUNT_TYPE=).*(\r?
|
|
29
|
+
|$)`),
|
|
30
|
+
`$1${accountType}
|
|
31
|
+
`
|
|
32
|
+
);
|
|
25
33
|
return envContent;
|
|
26
34
|
}
|
|
27
|
-
function
|
|
35
|
+
function customizeConfig(templateDir, useSmartAccounts, isNextjs) {
|
|
36
|
+
if (!useSmartAccounts) return null;
|
|
37
|
+
const configFileName = isNextjs ? "src/components/Providers.tsx" : "src/config.ts";
|
|
38
|
+
const configPath = path.join(templateDir, configFileName);
|
|
39
|
+
if (!fs.existsSync(configPath)) return null;
|
|
40
|
+
let configContent = fs.readFileSync(configPath, "utf-8");
|
|
41
|
+
if (isNextjs) {
|
|
42
|
+
configContent = configContent.replace(
|
|
43
|
+
/const CDP_CONFIG: Config = \{[\s\S]*?\};/,
|
|
44
|
+
`const CDP_CONFIG: Config = {
|
|
45
|
+
projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID ?? "",
|
|
46
|
+
createAccountOnLogin: process.env.NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE === "evm-smart" ? "evm-smart" : "evm-eoa",
|
|
47
|
+
};`
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
configContent = configContent.replace(
|
|
51
|
+
/export const CDP_CONFIG: Config = \{[\s\S]*?\};/,
|
|
52
|
+
`export const CDP_CONFIG: Config = {
|
|
53
|
+
projectId: import.meta.env.VITE_CDP_PROJECT_ID,
|
|
54
|
+
createAccountOnLogin: import.meta.env.VITE_CDP_CREATE_ACCOUNT_TYPE === "evm-smart" ? "evm-smart" : "evm-eoa",
|
|
55
|
+
};`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return configContent;
|
|
59
|
+
}
|
|
60
|
+
function copyFileSelectively(filePath, destPath, useSmartAccounts) {
|
|
28
61
|
const stat = fs.statSync(filePath);
|
|
29
62
|
if (stat.isDirectory()) {
|
|
30
|
-
|
|
63
|
+
copyDirSelectively(filePath, destPath, useSmartAccounts);
|
|
31
64
|
} else {
|
|
65
|
+
const fileName = path.basename(filePath);
|
|
66
|
+
if (useSmartAccounts && fileName === "EOATransaction.tsx") return;
|
|
67
|
+
if (!useSmartAccounts && fileName === "SmartAccountTransaction.tsx") return;
|
|
32
68
|
fs.copyFileSync(filePath, destPath);
|
|
33
69
|
}
|
|
34
70
|
}
|
|
35
|
-
function
|
|
71
|
+
function copyDirSelectively(srcDir, destDir, useSmartAccounts) {
|
|
36
72
|
fs.mkdirSync(destDir, { recursive: true });
|
|
37
73
|
for (const file of fs.readdirSync(srcDir)) {
|
|
38
74
|
const srcFile = path.resolve(srcDir, file);
|
|
39
75
|
const destFile = path.resolve(destDir, file);
|
|
40
|
-
|
|
76
|
+
copyFileSelectively(srcFile, destFile, useSmartAccounts);
|
|
41
77
|
}
|
|
42
78
|
}
|
|
43
79
|
function isDirEmpty(dirPath) {
|
|
@@ -71,12 +107,12 @@ const fileRenames = {
|
|
|
71
107
|
};
|
|
72
108
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
73
109
|
async function init() {
|
|
74
|
-
const { appName, template, targetDirectory, projectId } = await getAppDetails();
|
|
110
|
+
const { appName, template, targetDirectory, projectId, useSmartAccounts } = await getAppDetails();
|
|
75
111
|
console.log(`
|
|
76
112
|
Scaffolding app in ${targetDirectory}...`);
|
|
77
113
|
const root = prepareAppDirectory(targetDirectory);
|
|
78
114
|
const templateDir = path.resolve(fileURLToPath(import.meta.url), "../..", `template-${template}`);
|
|
79
|
-
copyTemplateFiles(templateDir, root, appName, projectId);
|
|
115
|
+
copyTemplateFiles(templateDir, root, appName, projectId, useSmartAccounts);
|
|
80
116
|
printNextSteps(root);
|
|
81
117
|
}
|
|
82
118
|
async function getAppDetails() {
|
|
@@ -120,6 +156,14 @@ async function getAppDetails() {
|
|
|
120
156
|
},
|
|
121
157
|
initial: ""
|
|
122
158
|
},
|
|
159
|
+
{
|
|
160
|
+
type: "confirm",
|
|
161
|
+
name: "useSmartAccounts",
|
|
162
|
+
message: reset(
|
|
163
|
+
"Enable Smart Accounts? (Smart Accounts enable gasless transactions and improved UX):"
|
|
164
|
+
),
|
|
165
|
+
initial: false
|
|
166
|
+
},
|
|
123
167
|
{
|
|
124
168
|
type: "confirm",
|
|
125
169
|
name: "corsConfirmation",
|
|
@@ -153,7 +197,8 @@ async function getAppDetails() {
|
|
|
153
197
|
appName: result.appName,
|
|
154
198
|
template: result.template,
|
|
155
199
|
targetDirectory: targetDir,
|
|
156
|
-
projectId: result.projectId
|
|
200
|
+
projectId: result.projectId,
|
|
201
|
+
useSmartAccounts: result.useSmartAccounts
|
|
157
202
|
};
|
|
158
203
|
} catch (cancelled) {
|
|
159
204
|
if (cancelled instanceof Error) {
|
|
@@ -172,29 +217,51 @@ function printNextSteps(appRoot) {
|
|
|
172
217
|
console.log(`${packageManager} install`);
|
|
173
218
|
console.log(devCommand);
|
|
174
219
|
}
|
|
175
|
-
function copyTemplateFiles(templateDir, root, appName, projectId) {
|
|
220
|
+
function copyTemplateFiles(templateDir, root, appName, projectId, useSmartAccounts) {
|
|
176
221
|
const writeFileToTarget = (file, content) => {
|
|
177
222
|
const targetPath = path.join(root, fileRenames[file] ?? file);
|
|
178
223
|
if (content) {
|
|
179
224
|
fs.writeFileSync(targetPath, content);
|
|
180
225
|
} else {
|
|
181
|
-
|
|
226
|
+
copyFileSelectively(path.join(templateDir, file), targetPath, useSmartAccounts);
|
|
182
227
|
}
|
|
183
228
|
};
|
|
229
|
+
const isNextjs = templateDir.includes("nextjs");
|
|
184
230
|
const files = fs.readdirSync(templateDir);
|
|
185
231
|
for (const file of files) {
|
|
186
232
|
if (file === "package.json") {
|
|
187
233
|
const customizedPackageJson = customizePackageJson(templateDir, appName);
|
|
188
234
|
writeFileToTarget(file, customizedPackageJson);
|
|
189
235
|
} else if (file === "env.example" && projectId) {
|
|
190
|
-
const customizedEnv = customizeEnv(templateDir, projectId);
|
|
191
236
|
writeFileToTarget(file);
|
|
237
|
+
const customizedEnv = customizeEnv(templateDir, projectId, useSmartAccounts);
|
|
192
238
|
console.log("Copying CDP Project ID to .env");
|
|
239
|
+
if (useSmartAccounts) {
|
|
240
|
+
console.log("Configuring Smart Accounts in environment");
|
|
241
|
+
}
|
|
193
242
|
writeFileToTarget(".env", customizedEnv);
|
|
194
243
|
} else {
|
|
195
244
|
writeFileToTarget(file);
|
|
196
245
|
}
|
|
197
246
|
}
|
|
247
|
+
if (useSmartAccounts) {
|
|
248
|
+
const configFileName = isNextjs ? "src/components/Providers.tsx" : "src/config.ts";
|
|
249
|
+
const customizedConfig = customizeConfig(templateDir, useSmartAccounts, isNextjs);
|
|
250
|
+
if (customizedConfig) {
|
|
251
|
+
console.log("Configuring Smart Accounts in application config");
|
|
252
|
+
writeFileToTarget(configFileName, customizedConfig);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const transactionFileName = isNextjs ? "src/components/Transaction.tsx" : "src/Transaction.tsx";
|
|
256
|
+
const transactionContent = generateTransactionComponent(useSmartAccounts);
|
|
257
|
+
writeFileToTarget(transactionFileName, transactionContent);
|
|
258
|
+
}
|
|
259
|
+
function generateTransactionComponent(useSmartAccounts) {
|
|
260
|
+
if (useSmartAccounts) {
|
|
261
|
+
return `export { default } from "./SmartAccountTransaction";`;
|
|
262
|
+
} else {
|
|
263
|
+
return `export { default } from "./EOATransaction";`;
|
|
264
|
+
}
|
|
198
265
|
}
|
|
199
266
|
init().catch((e) => {
|
|
200
267
|
console.error(e);
|
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 * @returns The customized .env content\n */\nexport function customizeEnv(templateDir: string, projectId: string): string {\n const exampleEnvPath = path.join(templateDir, \"env.example\");\n const exampleEnv = fs.readFileSync(exampleEnvPath, \"utf-8\");\n // Replace the project ID in the env file\n const envContent = exampleEnv.replace(/(.*PROJECT_ID=).*(\\r?\\n|$)/, `$1${projectId}\\n`);\n return envContent;\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 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 * 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 copyFile,\n customizeEnv,\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}\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 } = 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);\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: \"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 };\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 */\nfunction copyTemplateFiles(\n templateDir: string,\n root: string,\n appName: string,\n projectId?: 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 copyFile(path.join(templateDir, file), targetPath);\n }\n };\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 const customizedEnv = customizeEnv(templateDir, projectId);\n writeFileToTarget(file);\n console.log(\"Copying CDP Project ID to .env\");\n writeFileToTarget(\".env\", customizedEnv);\n } else {\n writeFileToTarget(file);\n }\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 * @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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"}
|
package/package.json
CHANGED
|
@@ -180,6 +180,12 @@ header .wallet-address {
|
|
|
180
180
|
width: 100%;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
.title-container {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 0.75rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
.site-title {
|
|
184
190
|
font-size: 1.2rem;
|
|
185
191
|
font-weight: 400;
|
|
@@ -192,6 +198,38 @@ header .wallet-address {
|
|
|
192
198
|
display: none;
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
.smart-badge {
|
|
202
|
+
background-color: rgba(76, 175, 80, 0.15);
|
|
203
|
+
color: #4CAF50;
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
padding: 0.2rem 0.6rem;
|
|
206
|
+
font-size: 0.7rem;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
letter-spacing: 0.025em;
|
|
209
|
+
text-transform: uppercase;
|
|
210
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
211
|
+
display: inline-block;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.smart-account-info {
|
|
215
|
+
background-color: rgba(76, 175, 80, 0.1);
|
|
216
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
217
|
+
border-radius: 0.5rem;
|
|
218
|
+
padding: 0.75rem;
|
|
219
|
+
margin: 0.5rem 0;
|
|
220
|
+
font-size: 0.9rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.success-message {
|
|
224
|
+
background-color: rgba(76, 175, 80, 0.1);
|
|
225
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
226
|
+
border-radius: 0.5rem;
|
|
227
|
+
padding: 0.75rem;
|
|
228
|
+
margin: 0.5rem 0;
|
|
229
|
+
font-size: 0.9rem;
|
|
230
|
+
color: var(--cdp-example-success-color, #4CAF50);
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
.card {
|
|
196
234
|
align-items: center;
|
|
197
235
|
background-color: var(--cdp-example-card-bg-color);
|
|
@@ -13,14 +13,14 @@ interface Props {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* This component demonstrates how to send an EVM transaction using
|
|
16
|
+
* This component demonstrates how to send an EVM transaction using EOA (Externally Owned Accounts).
|
|
17
17
|
*
|
|
18
|
-
* @param {Props} props - The props for the
|
|
18
|
+
* @param {Props} props - The props for the EOATransaction component.
|
|
19
19
|
* @param {string} [props.balance] - The user's balance.
|
|
20
20
|
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
21
21
|
* @returns A component that displays a transaction form and a transaction hash.
|
|
22
22
|
*/
|
|
23
|
-
export default function
|
|
23
|
+
export default function EOATransaction(props: Props) {
|
|
24
24
|
const { balance, onSuccess } = props;
|
|
25
25
|
const { evmAddress } = useEvmAddress();
|
|
26
26
|
const [transactionHash, setTransactionHash] = useState("");
|
|
@@ -93,7 +93,9 @@ export default function Transaction(props: Props) {
|
|
|
93
93
|
)}
|
|
94
94
|
{!hasBalance && (
|
|
95
95
|
<>
|
|
96
|
-
<p>
|
|
96
|
+
<p>
|
|
97
|
+
This example transaction sends a tiny amount of ETH from your wallet to itself.
|
|
98
|
+
</p>
|
|
97
99
|
<p>
|
|
98
100
|
Get some from{" "}
|
|
99
101
|
<a
|
|
@@ -30,10 +30,15 @@ export default function Header() {
|
|
|
30
30
|
return () => clearTimeout(timeout);
|
|
31
31
|
}, [isCopied]);
|
|
32
32
|
|
|
33
|
+
const isSmartAccountsEnabled = process.env.NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE === "evm-smart";
|
|
34
|
+
|
|
33
35
|
return (
|
|
34
36
|
<header>
|
|
35
37
|
<div className="header-inner">
|
|
36
|
-
<
|
|
38
|
+
<div className="title-container">
|
|
39
|
+
<h1 className="site-title">CDP Next.js StarterKit</h1>
|
|
40
|
+
{isSmartAccountsEnabled && <span className="smart-badge">SMART</span>}
|
|
41
|
+
</div>
|
|
37
42
|
<div className="user-info flex-row-container">
|
|
38
43
|
{evmAddress && (
|
|
39
44
|
<button
|
|
@@ -11,6 +11,8 @@ interface ProvidersProps {
|
|
|
11
11
|
|
|
12
12
|
const CDP_CONFIG: Config = {
|
|
13
13
|
projectId: process.env.NEXT_PUBLIC_CDP_PROJECT_ID ?? "",
|
|
14
|
+
createAccountOnLogin:
|
|
15
|
+
process.env.NEXT_PUBLIC_CDP_CREATE_ACCOUNT_TYPE === "evm-smart" ? "evm-smart" : "evm-eoa",
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
const APP_CONFIG: AppConfig = {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { useCurrentUser, useSendUserOperation } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { Button } from "@coinbase/cdp-react/components/ui/Button";
|
|
3
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
balance?: string;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This component demonstrates how to send a gasless transaction using Smart Accounts.
|
|
13
|
+
*
|
|
14
|
+
* @param {Props} props - The props for the SmartAccountTransaction component.
|
|
15
|
+
* @param {string} [props.balance] - The user's balance (not required for gasless transactions).
|
|
16
|
+
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
17
|
+
* @returns A component that displays a Smart Account transaction form and result.
|
|
18
|
+
*/
|
|
19
|
+
export default function SmartAccountTransaction(props: Props) {
|
|
20
|
+
const { balance, onSuccess } = props;
|
|
21
|
+
const { currentUser } = useCurrentUser();
|
|
22
|
+
const { sendUserOperation, data, error, status } = useSendUserOperation();
|
|
23
|
+
const [userOperationHash, setUserOperationHash] = useState("");
|
|
24
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
25
|
+
|
|
26
|
+
const smartAccount = currentUser?.evmSmartAccounts?.[0];
|
|
27
|
+
|
|
28
|
+
const hasBalance = useMemo(() => {
|
|
29
|
+
return balance && balance !== "0";
|
|
30
|
+
}, [balance]);
|
|
31
|
+
|
|
32
|
+
const handleSendUserOperation = async () => {
|
|
33
|
+
if (!smartAccount) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
setErrorMessage("");
|
|
37
|
+
setUserOperationHash("");
|
|
38
|
+
|
|
39
|
+
const result = await sendUserOperation({
|
|
40
|
+
evmSmartAccount: smartAccount,
|
|
41
|
+
network: "base-sepolia",
|
|
42
|
+
calls: [
|
|
43
|
+
{
|
|
44
|
+
to: smartAccount, // Send to yourself for testing
|
|
45
|
+
value: 1000000000000n, // 0.000001 ETH in wei
|
|
46
|
+
data: "0x", // Empty data for simple transfer
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
useCdpPaymaster: true, // Use the free CDP paymaster to cover the gas fees
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
setUserOperationHash(result.userOperationHash);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
setErrorMessage(err instanceof Error ? err.message : "Unknown error occurred");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const handleReset = () => {
|
|
59
|
+
setUserOperationHash("");
|
|
60
|
+
setErrorMessage("");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isLoading = status === "pending";
|
|
64
|
+
const isSuccess = status === "success" && data;
|
|
65
|
+
const hasError = error || errorMessage;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
{balance === undefined && (
|
|
70
|
+
<>
|
|
71
|
+
<h2 className="card-title">Send a gasless transaction</h2>
|
|
72
|
+
<LoadingSkeleton className="loading--text" />
|
|
73
|
+
<LoadingSkeleton className="loading--btn" />
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
{balance !== undefined && (
|
|
77
|
+
<>
|
|
78
|
+
{hasError && !isSuccess && (
|
|
79
|
+
<>
|
|
80
|
+
<h2 className="card-title">Oops</h2>
|
|
81
|
+
<p>{error?.message || errorMessage}</p>
|
|
82
|
+
<Button className="tx-button" onClick={handleReset} variant="secondary">
|
|
83
|
+
Reset and try again
|
|
84
|
+
</Button>
|
|
85
|
+
</>
|
|
86
|
+
)}
|
|
87
|
+
{!hasError && !isSuccess && !isLoading && (
|
|
88
|
+
<>
|
|
89
|
+
<h2 className="card-title">Send a gasless transaction</h2>
|
|
90
|
+
{hasBalance && smartAccount && (
|
|
91
|
+
<>
|
|
92
|
+
<p>Send 0.000001 ETH to yourself on Base Sepolia with no gas fees!</p>
|
|
93
|
+
<p className="smart-account-info">
|
|
94
|
+
✨ <strong>Smart Account Benefits:</strong> No gas fees, better UX, and enhanced
|
|
95
|
+
security
|
|
96
|
+
</p>
|
|
97
|
+
<Button
|
|
98
|
+
className="tx-button"
|
|
99
|
+
onClick={handleSendUserOperation}
|
|
100
|
+
disabled={isLoading}
|
|
101
|
+
>
|
|
102
|
+
Send Gasless Transaction
|
|
103
|
+
</Button>
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
{!hasBalance && (
|
|
107
|
+
<>
|
|
108
|
+
<p>
|
|
109
|
+
This example transaction sends a tiny amount of ETH from your wallet to itself.
|
|
110
|
+
</p>
|
|
111
|
+
<p>
|
|
112
|
+
Get some from{" "}
|
|
113
|
+
<a
|
|
114
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
115
|
+
target="_blank"
|
|
116
|
+
rel="noopener noreferrer"
|
|
117
|
+
>
|
|
118
|
+
Base Sepolia Faucet
|
|
119
|
+
</a>
|
|
120
|
+
</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
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
{!smartAccount && (
|
|
128
|
+
<>
|
|
129
|
+
<p>No Smart Account found. Please ensure you created a Smart Account.</p>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</>
|
|
133
|
+
)}
|
|
134
|
+
{isLoading && (
|
|
135
|
+
<>
|
|
136
|
+
<h2 className="card-title">Sending gasless transaction...</h2>
|
|
137
|
+
<p>Your transaction is being processed...</p>
|
|
138
|
+
{userOperationHash && (
|
|
139
|
+
<p>
|
|
140
|
+
User Operation Hash:{" "}
|
|
141
|
+
<code>
|
|
142
|
+
{userOperationHash.slice(0, 6)}...{userOperationHash.slice(-4)}
|
|
143
|
+
</code>
|
|
144
|
+
</p>
|
|
145
|
+
)}
|
|
146
|
+
<LoadingSkeleton className="loading--btn" />
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
{isSuccess && data && (
|
|
150
|
+
<>
|
|
151
|
+
<h2 className="card-title">Gasless transaction sent</h2>
|
|
152
|
+
<p>
|
|
153
|
+
Transaction hash:{" "}
|
|
154
|
+
<a
|
|
155
|
+
href={`https://sepolia.basescan.org/tx/${data.transactionHash}`}
|
|
156
|
+
target="_blank"
|
|
157
|
+
rel="noopener noreferrer"
|
|
158
|
+
>
|
|
159
|
+
{data.transactionHash?.slice(0, 6)}...{data.transactionHash?.slice(-4)}
|
|
160
|
+
</a>
|
|
161
|
+
</p>
|
|
162
|
+
<p className="success-message">
|
|
163
|
+
✅ <strong>Success!</strong> Your gasless transaction was completed with no fees.
|
|
164
|
+
</p>
|
|
165
|
+
<Button
|
|
166
|
+
variant="secondary"
|
|
167
|
+
className="tx-button"
|
|
168
|
+
onClick={() => {
|
|
169
|
+
handleReset();
|
|
170
|
+
onSuccess?.();
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
Send another gasless transaction
|
|
174
|
+
</Button>
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
@@ -13,14 +13,14 @@ interface Props {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* This component demonstrates how to send an EVM transaction using
|
|
16
|
+
* This component demonstrates how to send an EVM transaction using EOA (Externally Owned Accounts).
|
|
17
17
|
*
|
|
18
|
-
* @param {Props} props - The props for the
|
|
18
|
+
* @param {Props} props - The props for the EOATransaction component.
|
|
19
19
|
* @param {string} [props.balance] - The user's balance.
|
|
20
20
|
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
21
21
|
* @returns A component that displays a transaction form and a transaction hash.
|
|
22
22
|
*/
|
|
23
|
-
function
|
|
23
|
+
function EOATransaction(props: Props) {
|
|
24
24
|
const { balance, onSuccess } = props;
|
|
25
25
|
const { evmAddress } = useEvmAddress();
|
|
26
26
|
const [transactionHash, setTransactionHash] = useState("");
|
|
@@ -93,7 +93,9 @@ function Transaction(props: Props) {
|
|
|
93
93
|
)}
|
|
94
94
|
{!hasBalance && (
|
|
95
95
|
<>
|
|
96
|
-
<p>
|
|
96
|
+
<p>
|
|
97
|
+
This example transaction sends a tiny amount of ETH from your wallet to itself.
|
|
98
|
+
</p>
|
|
97
99
|
<p>
|
|
98
100
|
Get some from{" "}
|
|
99
101
|
<a
|
|
@@ -132,4 +134,4 @@ function Transaction(props: Props) {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export default
|
|
137
|
+
export default EOATransaction;
|
|
@@ -29,10 +29,15 @@ function Header() {
|
|
|
29
29
|
return () => clearTimeout(timeout);
|
|
30
30
|
}, [isCopied]);
|
|
31
31
|
|
|
32
|
+
const isSmartAccountsEnabled = import.meta.env.VITE_CDP_CREATE_ACCOUNT_TYPE === "evm-smart";
|
|
33
|
+
|
|
32
34
|
return (
|
|
33
35
|
<header>
|
|
34
36
|
<div className="header-inner">
|
|
35
|
-
<
|
|
37
|
+
<div className="title-container">
|
|
38
|
+
<h1 className="site-title">CDP React StarterKit</h1>
|
|
39
|
+
{isSmartAccountsEnabled && <span className="smart-badge">SMART</span>}
|
|
40
|
+
</div>
|
|
36
41
|
<div className="user-info flex-row-container">
|
|
37
42
|
{evmAddress && (
|
|
38
43
|
<button
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useCurrentUser, useSendUserOperation } from "@coinbase/cdp-hooks";
|
|
2
|
+
import { Button } from "@coinbase/cdp-react/components/ui/Button";
|
|
3
|
+
import { LoadingSkeleton } from "@coinbase/cdp-react/components/ui/LoadingSkeleton";
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
balance?: string;
|
|
8
|
+
onSuccess?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* This component demonstrates how to send a gasless transaction using Smart Accounts.
|
|
13
|
+
*
|
|
14
|
+
* @param {Props} props - The props for the SmartAccountTransaction component.
|
|
15
|
+
* @param {string} [props.balance] - The user's balance (not required for gasless transactions).
|
|
16
|
+
* @param {() => void} [props.onSuccess] - A function to call when the transaction is successful.
|
|
17
|
+
* @returns A component that displays a Smart Account transaction form and result.
|
|
18
|
+
*/
|
|
19
|
+
function SmartAccountTransaction(props: Props) {
|
|
20
|
+
const { balance, onSuccess } = props;
|
|
21
|
+
const { currentUser } = useCurrentUser();
|
|
22
|
+
const { sendUserOperation, data, error, status } = useSendUserOperation();
|
|
23
|
+
const [userOperationHash, setUserOperationHash] = useState("");
|
|
24
|
+
const [errorMessage, setErrorMessage] = useState("");
|
|
25
|
+
|
|
26
|
+
const smartAccount = currentUser?.evmSmartAccounts?.[0];
|
|
27
|
+
|
|
28
|
+
const hasBalance = useMemo(() => {
|
|
29
|
+
return balance && balance !== "0";
|
|
30
|
+
}, [balance]);
|
|
31
|
+
|
|
32
|
+
const handleSendUserOperation = async () => {
|
|
33
|
+
if (!smartAccount) return;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
setErrorMessage("");
|
|
37
|
+
setUserOperationHash("");
|
|
38
|
+
|
|
39
|
+
const result = await sendUserOperation({
|
|
40
|
+
evmSmartAccount: smartAccount,
|
|
41
|
+
network: "base-sepolia",
|
|
42
|
+
calls: [
|
|
43
|
+
{
|
|
44
|
+
to: smartAccount, // Send to yourself for testing
|
|
45
|
+
value: 1000000000000n, // 0.000001 ETH in wei
|
|
46
|
+
data: "0x", // Empty data for simple transfer
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
setUserOperationHash(result.userOperationHash);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
setErrorMessage(err instanceof Error ? err.message : "Unknown error occurred");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleReset = () => {
|
|
58
|
+
setUserOperationHash("");
|
|
59
|
+
setErrorMessage("");
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const isLoading = status === "pending";
|
|
63
|
+
const isSuccess = status === "success" && data;
|
|
64
|
+
const hasError = error || errorMessage;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
{balance === undefined && (
|
|
69
|
+
<>
|
|
70
|
+
<h2 className="card-title">Send a gasless transaction</h2>
|
|
71
|
+
<LoadingSkeleton className="loading--text" />
|
|
72
|
+
<LoadingSkeleton className="loading--btn" />
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
{balance !== undefined && (
|
|
76
|
+
<>
|
|
77
|
+
{hasError && !isSuccess && (
|
|
78
|
+
<>
|
|
79
|
+
<h2 className="card-title">Oops</h2>
|
|
80
|
+
<p>{error?.message || errorMessage}</p>
|
|
81
|
+
<Button className="tx-button" onClick={handleReset} variant="secondary">
|
|
82
|
+
Reset and try again
|
|
83
|
+
</Button>
|
|
84
|
+
</>
|
|
85
|
+
)}
|
|
86
|
+
{!hasError && !isSuccess && !isLoading && (
|
|
87
|
+
<>
|
|
88
|
+
<h2 className="card-title">Send a gasless transaction</h2>
|
|
89
|
+
{hasBalance && smartAccount && (
|
|
90
|
+
<>
|
|
91
|
+
<p>Send 0.000001 ETH to yourself on Base Sepolia with no gas fees!</p>
|
|
92
|
+
<p className="smart-account-info">
|
|
93
|
+
✨ <strong>Smart Account Benefits:</strong> No gas fees, better UX, and enhanced
|
|
94
|
+
security
|
|
95
|
+
</p>
|
|
96
|
+
<Button
|
|
97
|
+
className="tx-button"
|
|
98
|
+
onClick={handleSendUserOperation}
|
|
99
|
+
disabled={isLoading}
|
|
100
|
+
>
|
|
101
|
+
Send Gasless Transaction
|
|
102
|
+
</Button>
|
|
103
|
+
</>
|
|
104
|
+
)}
|
|
105
|
+
{!hasBalance && (
|
|
106
|
+
<>
|
|
107
|
+
<p>
|
|
108
|
+
This example transaction sends a tiny amount of ETH from your wallet to itself.
|
|
109
|
+
</p>
|
|
110
|
+
<p>
|
|
111
|
+
Get some from{" "}
|
|
112
|
+
<a
|
|
113
|
+
href="https://portal.cdp.coinbase.com/products/faucet"
|
|
114
|
+
target="_blank"
|
|
115
|
+
rel="noopener noreferrer"
|
|
116
|
+
>
|
|
117
|
+
Base Sepolia Faucet
|
|
118
|
+
</a>
|
|
119
|
+
</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
|
+
</>
|
|
125
|
+
)}
|
|
126
|
+
{!smartAccount && (
|
|
127
|
+
<>
|
|
128
|
+
<p>No Smart Account found. Please ensure you created a Smart Account.</p>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
{isLoading && (
|
|
134
|
+
<>
|
|
135
|
+
<h2 className="card-title">Sending gasless transaction...</h2>
|
|
136
|
+
<p>Your transaction is being processed...</p>
|
|
137
|
+
{userOperationHash && (
|
|
138
|
+
<p>
|
|
139
|
+
User Operation Hash:{" "}
|
|
140
|
+
<code>
|
|
141
|
+
{userOperationHash.slice(0, 6)}...{userOperationHash.slice(-4)}
|
|
142
|
+
</code>
|
|
143
|
+
</p>
|
|
144
|
+
)}
|
|
145
|
+
<LoadingSkeleton className="loading--btn" />
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
148
|
+
{isSuccess && data && (
|
|
149
|
+
<>
|
|
150
|
+
<h2 className="card-title">Gasless transaction sent</h2>
|
|
151
|
+
<p>
|
|
152
|
+
Transaction hash:{" "}
|
|
153
|
+
<a
|
|
154
|
+
href={`https://sepolia.basescan.org/tx/${data.transactionHash}`}
|
|
155
|
+
target="_blank"
|
|
156
|
+
rel="noopener noreferrer"
|
|
157
|
+
>
|
|
158
|
+
{data.transactionHash?.slice(0, 6)}...{data.transactionHash?.slice(-4)}
|
|
159
|
+
</a>
|
|
160
|
+
</p>
|
|
161
|
+
<p className="success-message">
|
|
162
|
+
✅ <strong>Success!</strong> Your gasless transaction was completed with no fees.
|
|
163
|
+
</p>
|
|
164
|
+
<Button
|
|
165
|
+
variant="secondary"
|
|
166
|
+
className="tx-button"
|
|
167
|
+
onClick={() => {
|
|
168
|
+
handleReset();
|
|
169
|
+
onSuccess?.();
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
Send another gasless transaction
|
|
173
|
+
</Button>
|
|
174
|
+
</>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default SmartAccountTransaction;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type Config } from "@coinbase/cdp-hooks";
|
|
2
2
|
import { type AppConfig } from "@coinbase/cdp-react";
|
|
3
3
|
|
|
4
|
-
export const CDP_CONFIG: Config = {
|
|
4
|
+
export const CDP_CONFIG: Config = {
|
|
5
|
+
projectId: import.meta.env.VITE_CDP_PROJECT_ID,
|
|
6
|
+
createAccountOnLogin:
|
|
7
|
+
import.meta.env.VITE_CDP_CREATE_ACCOUNT_TYPE === "evm-smart" ? "evm-smart" : "evm-eoa",
|
|
8
|
+
};
|
|
5
9
|
|
|
6
10
|
export const APP_CONFIG: AppConfig = {
|
|
7
11
|
name: "CDP React StarterKit",
|
|
@@ -180,6 +180,12 @@ header .wallet-address {
|
|
|
180
180
|
width: 100%;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
.title-container {
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 0.75rem;
|
|
187
|
+
}
|
|
188
|
+
|
|
183
189
|
.site-title {
|
|
184
190
|
font-size: 1.2rem;
|
|
185
191
|
font-weight: 400;
|
|
@@ -192,6 +198,38 @@ header .wallet-address {
|
|
|
192
198
|
display: none;
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
.smart-badge {
|
|
202
|
+
background-color: rgba(76, 175, 80, 0.15);
|
|
203
|
+
color: #4CAF50;
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
padding: 0.2rem 0.6rem;
|
|
206
|
+
font-size: 0.7rem;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
letter-spacing: 0.025em;
|
|
209
|
+
text-transform: uppercase;
|
|
210
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
211
|
+
display: inline-block;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.smart-account-info {
|
|
215
|
+
background-color: rgba(76, 175, 80, 0.1);
|
|
216
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
217
|
+
border-radius: 0.5rem;
|
|
218
|
+
padding: 0.75rem;
|
|
219
|
+
margin: 0.5rem 0;
|
|
220
|
+
font-size: 0.9rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.success-message {
|
|
224
|
+
background-color: rgba(76, 175, 80, 0.1);
|
|
225
|
+
border: 1px solid rgba(76, 175, 80, 0.3);
|
|
226
|
+
border-radius: 0.5rem;
|
|
227
|
+
padding: 0.75rem;
|
|
228
|
+
margin: 0.5rem 0;
|
|
229
|
+
font-size: 0.9rem;
|
|
230
|
+
color: var(--cdp-example-success-color, #4CAF50);
|
|
231
|
+
}
|
|
232
|
+
|
|
195
233
|
.card {
|
|
196
234
|
align-items: center;
|
|
197
235
|
background-color: var(--cdp-example-card-bg-color);
|