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.
- package/dist/finalize.d.ts +2 -0
- package/dist/finalize.js +45 -0
- package/dist/generator.d.ts +2 -0
- package/dist/generator.js +120 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/patches.d.ts +13 -0
- package/dist/patches.js +104 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +54 -0
- package/dist/wizard.d.ts +10 -0
- package/dist/wizard.js +83 -0
- package/package.json +27 -0
- package/templates/base/.env.example +30 -0
- package/templates/base/package.json +17 -0
- package/templates/base/packages/chain-config/package.json +24 -0
- package/templates/base/packages/chain-config/src/chains.ts +32 -0
- package/templates/base/packages/chain-config/src/constants.ts +24 -0
- package/templates/base/packages/chain-config/src/index.ts +3 -0
- package/templates/base/packages/chain-config/src/types.ts +17 -0
- package/templates/base/packages/chain-config/tsconfig.json +8 -0
- package/templates/base/packages/client/package.json +28 -0
- package/templates/base/packages/client/src/config.ts +19 -0
- package/templates/base/packages/client/src/index.ts +44 -0
- package/templates/base/packages/client/src/logger.ts +12 -0
- package/templates/base/packages/client/src/pay-and-fetch.ts +39 -0
- package/templates/base/packages/client/tsconfig.json +8 -0
- package/templates/base/packages/facilitator/package.json +29 -0
- package/templates/base/packages/facilitator/src/config.ts +37 -0
- package/templates/base/packages/facilitator/src/index.ts +202 -0
- package/templates/base/packages/facilitator/src/logger.ts +12 -0
- package/templates/base/packages/facilitator/tsconfig.json +8 -0
- package/templates/base/packages/server/package.json +30 -0
- package/templates/base/packages/server/src/app.ts +51 -0
- package/templates/base/packages/server/src/config.ts +37 -0
- package/templates/base/packages/server/src/index.ts +23 -0
- package/templates/base/packages/server/src/logger.ts +12 -0
- package/templates/base/packages/server/src/routes/config.ts +60 -0
- package/templates/base/packages/server/src/routes/health.ts +5 -0
- package/templates/base/packages/server/src/routes/index.ts +8 -0
- package/templates/base/packages/server/src/routes/sandbox.ts +12 -0
- package/templates/base/packages/server/tsconfig.json +8 -0
- package/templates/base/pnpm-workspace.yaml +2 -0
- package/templates/base/tsconfig.base.json +19 -0
- package/templates/identity/packages/attestor/package.json +28 -0
- package/templates/identity/packages/attestor/src/config.ts +22 -0
- package/templates/identity/packages/attestor/src/index.ts +121 -0
- package/templates/identity/packages/attestor/src/signer.ts +49 -0
- package/templates/identity/packages/attestor/src/types.ts +28 -0
- package/templates/identity/packages/attestor/src/verifier.ts +113 -0
- package/templates/identity/packages/attestor/tsconfig.json +18 -0
- package/templates/identity/packages/client/src/sign-request.ts +90 -0
- package/templates/identity/packages/contracts/contracts/IdentityRegistry.sol +105 -0
- package/templates/identity/packages/contracts/hardhat.config.ts +41 -0
- package/templates/identity/packages/contracts/package.json +22 -0
- package/templates/identity/packages/contracts/tsconfig.json +14 -0
- package/templates/identity/packages/identity-cli/package.json +26 -0
- package/templates/identity/packages/identity-cli/src/cli.ts +73 -0
- package/templates/identity/packages/identity-cli/tsconfig.json +17 -0
- package/templates/identity/packages/server/src/middleware/auth-check.ts +163 -0
- package/templates/identity/packages/server/src/middleware/nonce-store.ts +44 -0
package/dist/finalize.js
ADDED
|
@@ -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,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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|
package/dist/patches.js
ADDED
|
@@ -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
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/wizard.d.ts
ADDED
|
@@ -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
|