create-x402-conflux-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/finalize.d.ts +2 -0
  2. package/dist/finalize.js +45 -0
  3. package/dist/generator.d.ts +2 -0
  4. package/dist/generator.js +120 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +41 -0
  7. package/dist/patches.d.ts +13 -0
  8. package/dist/patches.js +104 -0
  9. package/dist/utils.d.ts +4 -0
  10. package/dist/utils.js +54 -0
  11. package/dist/wizard.d.ts +10 -0
  12. package/dist/wizard.js +83 -0
  13. package/package.json +27 -0
  14. package/templates/base/.env.example +30 -0
  15. package/templates/base/package.json +17 -0
  16. package/templates/base/packages/chain-config/package.json +24 -0
  17. package/templates/base/packages/chain-config/src/chains.ts +32 -0
  18. package/templates/base/packages/chain-config/src/constants.ts +24 -0
  19. package/templates/base/packages/chain-config/src/index.ts +3 -0
  20. package/templates/base/packages/chain-config/src/types.ts +17 -0
  21. package/templates/base/packages/chain-config/tsconfig.json +8 -0
  22. package/templates/base/packages/client/package.json +28 -0
  23. package/templates/base/packages/client/src/config.ts +19 -0
  24. package/templates/base/packages/client/src/index.ts +44 -0
  25. package/templates/base/packages/client/src/logger.ts +12 -0
  26. package/templates/base/packages/client/src/pay-and-fetch.ts +39 -0
  27. package/templates/base/packages/client/tsconfig.json +8 -0
  28. package/templates/base/packages/facilitator/package.json +29 -0
  29. package/templates/base/packages/facilitator/src/config.ts +37 -0
  30. package/templates/base/packages/facilitator/src/index.ts +202 -0
  31. package/templates/base/packages/facilitator/src/logger.ts +12 -0
  32. package/templates/base/packages/facilitator/tsconfig.json +8 -0
  33. package/templates/base/packages/server/package.json +30 -0
  34. package/templates/base/packages/server/src/app.ts +51 -0
  35. package/templates/base/packages/server/src/config.ts +37 -0
  36. package/templates/base/packages/server/src/index.ts +23 -0
  37. package/templates/base/packages/server/src/logger.ts +12 -0
  38. package/templates/base/packages/server/src/routes/config.ts +60 -0
  39. package/templates/base/packages/server/src/routes/health.ts +5 -0
  40. package/templates/base/packages/server/src/routes/index.ts +8 -0
  41. package/templates/base/packages/server/src/routes/sandbox.ts +12 -0
  42. package/templates/base/packages/server/tsconfig.json +8 -0
  43. package/templates/base/pnpm-workspace.yaml +2 -0
  44. package/templates/base/tsconfig.base.json +19 -0
  45. package/templates/identity/packages/attestor/package.json +28 -0
  46. package/templates/identity/packages/attestor/src/config.ts +22 -0
  47. package/templates/identity/packages/attestor/src/index.ts +121 -0
  48. package/templates/identity/packages/attestor/src/signer.ts +49 -0
  49. package/templates/identity/packages/attestor/src/types.ts +28 -0
  50. package/templates/identity/packages/attestor/src/verifier.ts +113 -0
  51. package/templates/identity/packages/attestor/tsconfig.json +18 -0
  52. package/templates/identity/packages/client/src/sign-request.ts +90 -0
  53. package/templates/identity/packages/contracts/contracts/IdentityRegistry.sol +105 -0
  54. package/templates/identity/packages/contracts/hardhat.config.ts +41 -0
  55. package/templates/identity/packages/contracts/package.json +22 -0
  56. package/templates/identity/packages/contracts/tsconfig.json +14 -0
  57. package/templates/identity/packages/identity-cli/package.json +26 -0
  58. package/templates/identity/packages/identity-cli/src/cli.ts +73 -0
  59. package/templates/identity/packages/identity-cli/tsconfig.json +17 -0
  60. package/templates/identity/packages/server/src/middleware/auth-check.ts +163 -0
  61. package/templates/identity/packages/server/src/middleware/nonce-store.ts +44 -0
@@ -0,0 +1,2 @@
1
+ import type { WizardAnswers } from './wizard.js';
2
+ export declare function finalize(targetDir: string, answers: WizardAnswers): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import { execSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { spinner, outro, note } from '@clack/prompts';
4
+ import pc from 'picocolors';
5
+ export async function finalize(targetDir, answers) {
6
+ const s = spinner();
7
+ const projectName = path.basename(targetDir);
8
+ // 1. Git init
9
+ s.start('Initializing git repository...');
10
+ try {
11
+ execSync('git init', { cwd: targetDir, stdio: 'ignore' });
12
+ s.stop('Git repository initialized');
13
+ }
14
+ catch {
15
+ s.stop('Skipped git init');
16
+ }
17
+ // 2. Install dependencies
18
+ const pm = answers.packageManager;
19
+ s.start(`Installing dependencies with ${pm}...`);
20
+ try {
21
+ const installCmd = pm === 'yarn' ? 'yarn install' : `${pm} install`;
22
+ execSync(installCmd, { cwd: targetDir, stdio: 'ignore', timeout: 120_000 });
23
+ s.stop('Dependencies installed');
24
+ }
25
+ catch {
26
+ s.stop(`Could not install dependencies — run ${pc.cyan(`${pm} install`)} manually`);
27
+ }
28
+ // 3. Next steps
29
+ const steps = [
30
+ `cd ${projectName}`,
31
+ 'cp .env.example .env # Fill in your private keys',
32
+ `${pm === 'npm' ? 'npm run' : pm} build # Build all packages`,
33
+ `${pm === 'npm' ? 'npm run' : pm} dev:facilitator # Start facilitator (terminal 1)`,
34
+ `${pm === 'npm' ? 'npm run' : pm} dev:server # Start server (terminal 2)`,
35
+ `${pm === 'npm' ? 'npm run' : pm} start:client # Run client (terminal 3)`,
36
+ ];
37
+ if (answers.includeIdentity) {
38
+ steps.push('');
39
+ steps.push('Identity setup:');
40
+ steps.push(` ${pm === 'npm' ? 'npm run' : pm} test:contracts # Run contract tests`);
41
+ steps.push(` ${pm === 'npm' ? 'npm run' : pm} deploy:contracts # Deploy to Conflux eSpace`);
42
+ }
43
+ note(steps.join('\n'), 'Next steps');
44
+ outro(pc.green(`Project created at ./${projectName}`));
45
+ }
@@ -0,0 +1,2 @@
1
+ import type { WizardAnswers } from './wizard.js';
2
+ export declare function generate(answers: WizardAnswers): Promise<string>;
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { spinner } from '@clack/prompts';
5
+ import { copyDir, substituteInDir } from './utils.js';
6
+ import { patchServerConfig, patchServerApp, patchClientConfig, patchClientPayAndFetch, patchEnvExample, patchRootPackageJson, patchPnpmWorkspace, } from './patches.js';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ function getTemplatesDir() {
9
+ // In development (src/), templates are at ../templates
10
+ // In dist (dist/), templates are at ../templates
11
+ const dir = path.resolve(__dirname, '..', 'templates');
12
+ if (!fs.existsSync(dir)) {
13
+ throw new Error(`Templates directory not found at ${dir}`);
14
+ }
15
+ return dir;
16
+ }
17
+ export async function generate(answers) {
18
+ const s = spinner();
19
+ s.start('Generating project...');
20
+ const targetDir = path.resolve(process.cwd(), answers.projectName);
21
+ if (fs.existsSync(targetDir)) {
22
+ const entries = fs.readdirSync(targetDir);
23
+ if (entries.length > 0) {
24
+ throw new Error(`Directory ${answers.projectName} already exists and is not empty`);
25
+ }
26
+ }
27
+ const templatesDir = getTemplatesDir();
28
+ const baseDir = path.join(templatesDir, 'base');
29
+ const identityDir = path.join(templatesDir, 'identity');
30
+ // 1. Copy base templates
31
+ copyDir(baseDir, targetDir);
32
+ // 2. Copy identity overlay if needed
33
+ if (answers.includeIdentity) {
34
+ copyDir(identityDir, targetDir);
35
+ }
36
+ // 3. Apply patches when identity is included, otherwise strip markers
37
+ if (answers.includeIdentity) {
38
+ applyPatches(targetDir);
39
+ }
40
+ else {
41
+ stripIdentityMarkers(targetDir);
42
+ }
43
+ // 4. Variable substitution
44
+ const vars = {
45
+ PROJECT_NAME: answers.projectName,
46
+ CHAIN_ID: String(answers.chainId),
47
+ RPC_URL: answers.rpcUrl,
48
+ NETWORK_CAIP2: `eip155:${answers.chainId}`,
49
+ AUTH_MODE: answers.authMode,
50
+ };
51
+ substituteInDir(targetDir, vars);
52
+ s.stop('Project generated!');
53
+ return targetDir;
54
+ }
55
+ function stripIdentityMarkers(targetDir) {
56
+ const filesToClean = [
57
+ 'packages/server/src/config.ts',
58
+ 'packages/server/src/app.ts',
59
+ 'packages/client/src/config.ts',
60
+ 'packages/client/src/pay-and-fetch.ts',
61
+ '.env.example',
62
+ 'package.json',
63
+ ];
64
+ for (const file of filesToClean) {
65
+ const filePath = path.join(targetDir, file);
66
+ if (fs.existsSync(filePath)) {
67
+ let content = fs.readFileSync(filePath, 'utf-8');
68
+ // Remove lines containing identity markers
69
+ content = content
70
+ .split('\n')
71
+ .filter((line) => !line.includes('{{IDENTITY_'))
72
+ .join('\n');
73
+ // Fix trailing commas in JSON (e.g. after removing last property)
74
+ if (file.endsWith('.json')) {
75
+ content = content.replace(/,(\s*[}\]])/g, '$1');
76
+ }
77
+ fs.writeFileSync(filePath, content);
78
+ }
79
+ }
80
+ }
81
+ function applyPatches(targetDir) {
82
+ const patches = [
83
+ {
84
+ file: 'packages/server/src/config.ts',
85
+ patch: patchServerConfig,
86
+ },
87
+ {
88
+ file: 'packages/server/src/app.ts',
89
+ patch: patchServerApp,
90
+ },
91
+ {
92
+ file: 'packages/client/src/config.ts',
93
+ patch: patchClientConfig,
94
+ },
95
+ {
96
+ file: 'packages/client/src/pay-and-fetch.ts',
97
+ patch: patchClientPayAndFetch,
98
+ },
99
+ {
100
+ file: '.env.example',
101
+ patch: patchEnvExample,
102
+ },
103
+ {
104
+ file: 'package.json',
105
+ patch: patchRootPackageJson,
106
+ },
107
+ {
108
+ file: 'pnpm-workspace.yaml',
109
+ patch: patchPnpmWorkspace,
110
+ },
111
+ ];
112
+ for (const { file, patch } of patches) {
113
+ const filePath = path.join(targetDir, file);
114
+ if (fs.existsSync(filePath)) {
115
+ const content = fs.readFileSync(filePath, 'utf-8');
116
+ const patched = patch(content);
117
+ fs.writeFileSync(filePath, patched);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import { intro, outro, log } from '@clack/prompts';
3
+ import pc from 'picocolors';
4
+ import { runWizard } from './wizard.js';
5
+ import { generate } from './generator.js';
6
+ import { finalize } from './finalize.js';
7
+ import { createRequire } from 'node:module';
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require('../package.json');
10
+ const args = process.argv.slice(2);
11
+ if (args.includes('--version') || args.includes('-v')) {
12
+ console.log(pkg.version);
13
+ process.exit(0);
14
+ }
15
+ if (args.includes('--help') || args.includes('-h')) {
16
+ console.log(`
17
+ ${pc.bold('create-x402-app')} v${pkg.version}
18
+
19
+ Scaffold a Conflux x402 payment project.
20
+
21
+ ${pc.dim('Usage:')}
22
+ npx create-x402-app
23
+ npx create-x402-app --version
24
+ npx create-x402-app --help
25
+ `);
26
+ process.exit(0);
27
+ }
28
+ async function main() {
29
+ intro(pc.bgCyan(pc.black(' create-x402-app ')));
30
+ const answers = await runWizard();
31
+ if (!answers) {
32
+ outro(pc.dim('Setup cancelled.'));
33
+ process.exit(0);
34
+ }
35
+ const targetDir = await generate(answers);
36
+ await finalize(targetDir, answers);
37
+ }
38
+ main().catch((err) => {
39
+ log.error(err instanceof Error ? err.message : String(err));
40
+ process.exit(1);
41
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Patch functions that modify base template files when identity overlay is active.
3
+ * Each function receives the base file content and returns modified content.
4
+ * Base templates contain marker comments (e.g. // {{IDENTITY_IMPORTS}}) that
5
+ * patches replace with actual code.
6
+ */
7
+ export declare function patchServerConfig(content: string): string;
8
+ export declare function patchServerApp(content: string): string;
9
+ export declare function patchClientConfig(content: string): string;
10
+ export declare function patchClientPayAndFetch(content: string): string;
11
+ export declare function patchEnvExample(content: string): string;
12
+ export declare function patchRootPackageJson(content: string): string;
13
+ export declare function patchPnpmWorkspace(_content: string): string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Patch functions that modify base template files when identity overlay is active.
3
+ * Each function receives the base file content and returns modified content.
4
+ * Base templates contain marker comments (e.g. // {{IDENTITY_IMPORTS}}) that
5
+ * patches replace with actual code.
6
+ */
7
+ export function patchServerConfig(content) {
8
+ return content
9
+ .replace('// {{IDENTITY_CONFIG_FIELDS}}', ` // Auth gate
10
+ identityRegistryAddress: z.string().startsWith('0x').optional(),
11
+ rpcUrl: z.string().url().default('{{RPC_URL}}'),
12
+ chainId: z.coerce.number().default({{CHAIN_ID}}),`)
13
+ .replace('// {{IDENTITY_ENV_PARSE}}', ` identityRegistryAddress: process.env.IDENTITY_REGISTRY_ADDRESS,
14
+ rpcUrl: process.env.RPC_URL,
15
+ chainId: process.env.CHAIN_ID,`)
16
+ .replace('// {{IDENTITY_VALIDATION}}', ` if (config.authMode === 'domain_gate' && !config.identityRegistryAddress) {
17
+ throw new Error('IDENTITY_REGISTRY_ADDRESS is required when AUTH_MODE=domain_gate')
18
+ }`);
19
+ }
20
+ export function patchServerApp(content) {
21
+ return content
22
+ .replace('// {{IDENTITY_IMPORTS}}', `import { createPublicClient, http } from 'viem'
23
+ import { confluxESpace } from '@{{PROJECT_NAME}}/chain-config'
24
+ import { createAuthCheckMiddleware } from './middleware/auth-check.js'`)
25
+ .replace(' // {{IDENTITY_MIDDLEWARE}}', ` // Setup auth gate middleware (before x402)
26
+ if (config.authMode !== 'none') {
27
+ const publicClient = createPublicClient({
28
+ chain: confluxESpace,
29
+ transport: http(config.rpcUrl),
30
+ })
31
+
32
+ app.use(createAuthCheckMiddleware({ config, publicClient }))
33
+ logger.info(
34
+ { authMode: config.authMode, registryAddress: config.identityRegistryAddress },
35
+ 'auth gate enabled',
36
+ )
37
+ } else {
38
+ logger.info('auth gate disabled (AUTH_MODE=none)')
39
+ }
40
+ `);
41
+ }
42
+ export function patchClientConfig(content) {
43
+ return content
44
+ .replace('// {{IDENTITY_CONFIG_FIELDS}}', ` authEnabled: z
45
+ .string()
46
+ .transform((v) => v === 'true')
47
+ .default('false'),
48
+ chainId: z.coerce.number().default({{CHAIN_ID}}),`)
49
+ .replace('// {{IDENTITY_ENV_PARSE}}', ` authEnabled: process.env.AUTH_ENABLED,
50
+ chainId: process.env.CHAIN_ID,`);
51
+ }
52
+ export function patchClientPayAndFetch(content) {
53
+ return content
54
+ .replace('// {{IDENTITY_IMPORTS}}', `import { signRequest } from './sign-request.js'`)
55
+ .replace(' // {{IDENTITY_FETCH_WRAP}}', ` // Wrap fetch to add auth headers when enabled
56
+ const fetchWithPay = config.authEnabled
57
+ ? async (url: string | URL | Request, init?: RequestInit) => {
58
+ const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.toString() : url.url
59
+ const method = init?.method ?? 'GET'
60
+ const body = init?.body ? String(init.body) : undefined
61
+
62
+ const authHeaders = await signRequest(viemClient, {
63
+ method,
64
+ url: urlStr,
65
+ body,
66
+ chainId: config.chainId,
67
+ })
68
+
69
+ const mergedInit: RequestInit = {
70
+ ...init,
71
+ headers: {
72
+ ...Object.fromEntries(new Headers(init?.headers).entries()),
73
+ ...authHeaders,
74
+ },
75
+ }
76
+
77
+ return baseFetchWithPay(url, mergedInit)
78
+ }
79
+ : baseFetchWithPay`);
80
+ }
81
+ export function patchEnvExample(content) {
82
+ return content.replace('# {{IDENTITY_ENV_VARS}}', `# ===== Client Auth =====
83
+ AUTH_ENABLED=false # true = sign requests (required when AUTH_MODE=domain_gate)
84
+
85
+ # ===== Identity Gating Contracts =====
86
+ ZK_VERIFIER_ADDRESS=0x... # ZKVerifier contract address (from deployment)
87
+
88
+ # ===== Attestor Service =====
89
+ ATTESTOR_PRIVATE_KEY=0x... # Attestor private key for signing attestations
90
+ ATTESTOR_URL=http://localhost:3003
91
+ PORT=3003
92
+ DEFAULT_EXPIRY_SECONDS=2592000
93
+ MOCK_MODE=false`);
94
+ }
95
+ export function patchRootPackageJson(content) {
96
+ // Insert identity-related scripts into the root package.json
97
+ return content.replace('"// {{IDENTITY_SCRIPTS}}": ""', `"dev:attestor": "pnpm --filter @{{PROJECT_NAME}}/attestor dev",
98
+ "test:contracts": "pnpm --filter @{{PROJECT_NAME}}/contracts test",
99
+ "deploy:contracts": "pnpm --filter @{{PROJECT_NAME}}/contracts deploy"`);
100
+ }
101
+ export function patchPnpmWorkspace(_content) {
102
+ // The base already has packages/* which covers identity packages
103
+ return _content;
104
+ }
@@ -0,0 +1,4 @@
1
+ export declare function isTextFile(filePath: string): boolean;
2
+ export declare function substituteVars(content: string, vars: Record<string, string>): string;
3
+ export declare function copyDir(src: string, dest: string): void;
4
+ export declare function substituteInDir(dir: string, vars: Record<string, string>): void;
package/dist/utils.js ADDED
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const TEXT_EXTENSIONS = new Set([
4
+ '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt',
5
+ '.yaml', '.yml', '.toml', '.env', '.gitignore', '.sol',
6
+ '.css', '.html', '.sh', '.mjs', '.cjs',
7
+ ]);
8
+ export function isTextFile(filePath) {
9
+ const ext = path.extname(filePath);
10
+ if (TEXT_EXTENSIONS.has(ext))
11
+ return true;
12
+ // Files without extensions (like .gitignore, .env.example) are text
13
+ const basename = path.basename(filePath);
14
+ if (basename.startsWith('.') || basename.includes('.example'))
15
+ return true;
16
+ return false;
17
+ }
18
+ export function substituteVars(content, vars) {
19
+ let result = content;
20
+ for (const [key, value] of Object.entries(vars)) {
21
+ result = result.replaceAll(`{{${key}}}`, value);
22
+ }
23
+ return result;
24
+ }
25
+ export function copyDir(src, dest) {
26
+ fs.mkdirSync(dest, { recursive: true });
27
+ const entries = fs.readdirSync(src, { withFileTypes: true });
28
+ for (const entry of entries) {
29
+ const srcPath = path.join(src, entry.name);
30
+ const destPath = path.join(dest, entry.name);
31
+ if (entry.isDirectory()) {
32
+ copyDir(srcPath, destPath);
33
+ }
34
+ else {
35
+ fs.copyFileSync(srcPath, destPath);
36
+ }
37
+ }
38
+ }
39
+ export function substituteInDir(dir, vars) {
40
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ for (const entry of entries) {
42
+ const fullPath = path.join(dir, entry.name);
43
+ if (entry.isDirectory()) {
44
+ substituteInDir(fullPath, vars);
45
+ }
46
+ else if (isTextFile(fullPath)) {
47
+ const content = fs.readFileSync(fullPath, 'utf-8');
48
+ const substituted = substituteVars(content, vars);
49
+ if (substituted !== content) {
50
+ fs.writeFileSync(fullPath, substituted);
51
+ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,10 @@
1
+ export interface WizardAnswers {
2
+ projectName: string;
3
+ network: 'mainnet' | 'testnet';
4
+ chainId: number;
5
+ rpcUrl: string;
6
+ authMode: 'none' | 'domain_gate';
7
+ includeIdentity: boolean;
8
+ packageManager: 'pnpm' | 'npm' | 'yarn';
9
+ }
10
+ export declare function runWizard(): Promise<WizardAnswers | undefined>;
package/dist/wizard.js ADDED
@@ -0,0 +1,83 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ const VALID_DIR_RE = /^[a-zA-Z0-9_@][a-zA-Z0-9._@/-]*$/;
4
+ export async function runWizard() {
5
+ const answers = await p.group({
6
+ projectName: () => p.text({
7
+ message: 'Project name',
8
+ placeholder: 'my-x402-app',
9
+ defaultValue: 'my-x402-app',
10
+ validate(value) {
11
+ if (!value)
12
+ return 'Project name is required';
13
+ if (!VALID_DIR_RE.test(value))
14
+ return 'Invalid directory name';
15
+ },
16
+ }),
17
+ network: () => p.select({
18
+ message: 'Which network?',
19
+ options: [
20
+ {
21
+ value: 'testnet',
22
+ label: `Conflux eSpace Testnet ${pc.dim('(chainId: 71)')}`,
23
+ hint: 'recommended for development',
24
+ },
25
+ {
26
+ value: 'mainnet',
27
+ label: `Conflux eSpace Mainnet ${pc.dim('(chainId: 1030)')}`,
28
+ },
29
+ ],
30
+ }),
31
+ authMode: () => p.select({
32
+ message: 'Authentication mode',
33
+ options: [
34
+ {
35
+ value: 'none',
36
+ label: 'None',
37
+ hint: 'pure x402 payments, no identity gate',
38
+ },
39
+ {
40
+ value: 'domain_gate',
41
+ label: 'Domain gate',
42
+ hint: 'wallet signature + on-chain identity registry',
43
+ },
44
+ ],
45
+ }),
46
+ includeIdentity: ({ results }) => {
47
+ if (results.authMode !== 'domain_gate') {
48
+ return Promise.resolve(false);
49
+ }
50
+ return p.confirm({
51
+ message: 'Include identity contracts, attestor & CLI packages?',
52
+ initialValue: true,
53
+ });
54
+ },
55
+ packageManager: () => p.select({
56
+ message: 'Package manager',
57
+ options: [
58
+ { value: 'pnpm', label: 'pnpm', hint: 'recommended' },
59
+ { value: 'npm', label: 'npm' },
60
+ { value: 'yarn', label: 'yarn' },
61
+ ],
62
+ }),
63
+ }, {
64
+ onCancel() {
65
+ return undefined;
66
+ },
67
+ });
68
+ if (p.isCancel(answers))
69
+ return undefined;
70
+ const chainId = answers.network === 'mainnet' ? 1030 : 71;
71
+ const rpcUrl = answers.network === 'mainnet'
72
+ ? 'https://evm.confluxrpc.com'
73
+ : 'https://evmtestnet.confluxrpc.com';
74
+ return {
75
+ projectName: answers.projectName,
76
+ network: answers.network,
77
+ chainId,
78
+ rpcUrl,
79
+ authMode: answers.authMode,
80
+ includeIdentity: answers.includeIdentity,
81
+ packageManager: answers.packageManager,
82
+ };
83
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "create-x402-conflux-app",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Scaffold CLI for Conflux x402 payment projects",
6
+ "bin": {
7
+ "create-x402-app": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsx src/index.ts",
16
+ "prepublishOnly": "npm run build && npm pack --dry-run"
17
+ },
18
+ "dependencies": {
19
+ "@clack/prompts": "^0.9.1",
20
+ "picocolors": "^1.1.1"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.6.0",
24
+ "tsx": "^4.19.0",
25
+ "@types/node": "^20.0.0"
26
+ }
27
+ }
@@ -0,0 +1,30 @@
1
+ # ===== Facilitator =====
2
+ FACILITATOR_PRIVATE_KEY=0x... # Facilitator wallet private key (needs CFX for gas)
3
+ FACILITATOR_PORT=4022
4
+
5
+ # ===== Settlement =====
6
+ VERIFY_ONLY_MODE=true # true = signature check only, false = real settlement
7
+
8
+ # ===== Safety Controls =====
9
+ MAX_PER_TRANSACTION=1.0 # Per-tx limit (USD)
10
+ MAX_DAILY_TOTAL=10.0 # Daily total limit (USD)
11
+ GAS_BUFFER_PERCENT=50 # Gas estimate buffer % (Conflux needs ~50%)
12
+
13
+ # ===== Server =====
14
+ SERVER_PORT=4021
15
+ FACILITATOR_URL=http://localhost:4022
16
+ EVM_ADDRESS=0x... # payTo address (receives USDT0)
17
+ PAYMENT_ENABLED=true
18
+
19
+ # ===== Server Auth Gate =====
20
+ AUTH_MODE={{AUTH_MODE}}
21
+ IDENTITY_REGISTRY_ADDRESS=0x... # Required when AUTH_MODE=domain_gate
22
+
23
+ # ===== Service-to-Service Auth =====
24
+ FACILITATOR_API_KEY= # Leave empty to disable
25
+
26
+ # ===== Client =====
27
+ CLIENT_PRIVATE_KEY=0x... # Payer wallet private key (needs USDT0 balance)
28
+ SERVER_URL=http://localhost:4021
29
+
30
+ # {{IDENTITY_ENV_VARS}}
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Conflux eSpace x402 payment project",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "scripts": {
10
+ "build": "pnpm -r build",
11
+ "dev:facilitator": "pnpm --filter @{{PROJECT_NAME}}/facilitator dev",
12
+ "dev:server": "pnpm --filter @{{PROJECT_NAME}}/server dev",
13
+ "start:client": "pnpm --filter @{{PROJECT_NAME}}/client start",
14
+ "test": "pnpm -r test",
15
+ "// {{IDENTITY_SCRIPTS}}": ""
16
+ }
17
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@{{PROJECT_NAME}}/chain-config",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsc --watch"
17
+ },
18
+ "dependencies": {
19
+ "viem": "^2.21.0"
20
+ },
21
+ "devDependencies": {
22
+ "typescript": "^5.6.0"
23
+ }
24
+ }
@@ -0,0 +1,32 @@
1
+ import { defineChain } from 'viem'
2
+
3
+ export const confluxESpace = defineChain({
4
+ id: 1030,
5
+ name: 'Conflux eSpace',
6
+ nativeCurrency: { name: 'CFX', symbol: 'CFX', decimals: 18 },
7
+ rpcUrls: {
8
+ default: { http: ['https://evm.confluxrpc.com'] },
9
+ },
10
+ blockExplorers: {
11
+ default: {
12
+ name: 'ConfluxScan',
13
+ url: 'https://evm.confluxscan.io',
14
+ },
15
+ },
16
+ })
17
+
18
+ export const confluxESpaceTestnet = defineChain({
19
+ id: 71,
20
+ name: 'Conflux eSpace Testnet',
21
+ nativeCurrency: { name: 'CFX', symbol: 'CFX', decimals: 18 },
22
+ rpcUrls: {
23
+ default: { http: ['https://evmtestnet.confluxrpc.com'] },
24
+ },
25
+ blockExplorers: {
26
+ default: {
27
+ name: 'ConfluxScan Testnet',
28
+ url: 'https://evmtestnet.confluxscan.io',
29
+ },
30
+ },
31
+ testnet: true,
32
+ })
@@ -0,0 +1,24 @@
1
+ import type { ChainConfig, TokenConfig } from './types.js'
2
+
3
+ export const USDT0_MAINNET: TokenConfig = {
4
+ address: '0xaf37E8B6C9ED7f6318979f56Fc287d76c30847ff',
5
+ decimals: 6,
6
+ eip712: {
7
+ name: 'USDT0',
8
+ version: '1',
9
+ },
10
+ } as const
11
+
12
+ export const CONFLUX_ESPACE_MAINNET: ChainConfig = {
13
+ caip2Id: 'eip155:1030',
14
+ chainId: 1030,
15
+ rpcUrl: 'https://evm.confluxrpc.com',
16
+ token: USDT0_MAINNET,
17
+ } as const
18
+
19
+ export const CONFLUX_ESPACE_TESTNET: ChainConfig = {
20
+ caip2Id: 'eip155:71',
21
+ chainId: 71,
22
+ rpcUrl: 'https://evmtestnet.confluxrpc.com',
23
+ token: USDT0_MAINNET,
24
+ } as const
@@ -0,0 +1,3 @@
1
+ export { confluxESpace, confluxESpaceTestnet } from './chains.js'
2
+ export { CONFLUX_ESPACE_MAINNET, CONFLUX_ESPACE_TESTNET, USDT0_MAINNET } from './constants.js'
3
+ export type { Caip2Network, ChainConfig, TokenConfig } from './types.js'