x402-fl 0.0.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.
Potentially problematic release.
This version of x402-fl might be problematic. Click here for more details.
- package/.claude/settings.local.json +10 -0
- package/.env.example +4 -0
- package/.github/workflows/publish.yml +66 -0
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +103 -0
- package/dist/commands/balance.d.ts +5 -0
- package/dist/commands/balance.js +46 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +85 -0
- package/dist/commands/fund.d.ts +9 -0
- package/dist/commands/fund.js +131 -0
- package/dist/lib/abi.d.ts +22 -0
- package/dist/lib/abi.js +16 -0
- package/dist/lib/anvil.d.ts +8 -0
- package/dist/lib/anvil.js +51 -0
- package/dist/lib/chain.d.ts +11603 -0
- package/dist/lib/chain.js +30 -0
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.js +31 -0
- package/dist/lib/facilitator.d.ts +7 -0
- package/dist/lib/facilitator.js +93 -0
- package/package.json +41 -0
- package/src/cli.ts +162 -0
- package/src/commands/balance.ts +66 -0
- package/src/commands/dev.ts +112 -0
- package/src/commands/fund.ts +183 -0
- package/src/lib/abi.ts +16 -0
- package/src/lib/anvil.ts +66 -0
- package/src/lib/chain.ts +40 -0
- package/src/lib/config.ts +40 -0
- package/src/lib/facilitator.ts +129 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createPublicClient as viemCreatePublicClient, createWalletClient as viemCreateWalletClient, defineChain, http, publicActions, } from "viem";
|
|
2
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3
|
+
export async function fetchChainId(rpcUrl) {
|
|
4
|
+
const client = viemCreatePublicClient({ transport: http(rpcUrl) });
|
|
5
|
+
return client.getChainId();
|
|
6
|
+
}
|
|
7
|
+
export function createLocalChain(rpcUrl, chainId) {
|
|
8
|
+
return defineChain({
|
|
9
|
+
id: chainId,
|
|
10
|
+
name: "Local Anvil Fork",
|
|
11
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
12
|
+
rpcUrls: {
|
|
13
|
+
default: { http: [rpcUrl] },
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function createPublicClient(rpcUrl, chainId) {
|
|
18
|
+
return viemCreatePublicClient({
|
|
19
|
+
chain: createLocalChain(rpcUrl, chainId),
|
|
20
|
+
transport: http(rpcUrl),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createWalletClient(privateKey, rpcUrl, chainId) {
|
|
24
|
+
const account = privateKeyToAccount(privateKey);
|
|
25
|
+
return viemCreateWalletClient({
|
|
26
|
+
account,
|
|
27
|
+
chain: createLocalChain(rpcUrl, chainId),
|
|
28
|
+
transport: http(rpcUrl),
|
|
29
|
+
}).extend(publicActions);
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const accounts: {
|
|
2
|
+
facilitator: {
|
|
3
|
+
address: `0x${string}`;
|
|
4
|
+
privateKey: `0x${string}`;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
export declare const defaults: {
|
|
8
|
+
readonly anvilPort: 8545;
|
|
9
|
+
readonly facilitatorPort: 4022;
|
|
10
|
+
};
|
|
11
|
+
export declare function networkId(chainId: number): `eip155:${number}`;
|
|
12
|
+
export declare const USDC_ADDRESS: `0x${string}`;
|
|
13
|
+
export declare const DEFAULT_DECIMALS = 6;
|
|
14
|
+
export declare function parseTokenAmount(amount: string, decimals?: number): bigint;
|
|
15
|
+
export declare function formatTokenAmount(raw: bigint, decimals?: number): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { mnemonicToAccount } from "viem/accounts";
|
|
2
|
+
// Anvil default mnemonic — first account (index 0) is the facilitator
|
|
3
|
+
const ANVIL_MNEMONIC = "test test test test test test test test test test test junk";
|
|
4
|
+
const facilitatorAccount = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 });
|
|
5
|
+
export const accounts = {
|
|
6
|
+
facilitator: {
|
|
7
|
+
address: facilitatorAccount.address,
|
|
8
|
+
privateKey: `0x${Buffer.from(facilitatorAccount.getHdKey().privateKey).toString("hex")}`,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export const defaults = {
|
|
12
|
+
anvilPort: 8545,
|
|
13
|
+
facilitatorPort: 4022,
|
|
14
|
+
};
|
|
15
|
+
export function networkId(chainId) {
|
|
16
|
+
return `eip155:${chainId}`;
|
|
17
|
+
}
|
|
18
|
+
// Base mainnet USDC
|
|
19
|
+
export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
20
|
+
export const DEFAULT_DECIMALS = 6;
|
|
21
|
+
export function parseTokenAmount(amount, decimals = DEFAULT_DECIMALS) {
|
|
22
|
+
const [whole, frac = ""] = amount.split(".");
|
|
23
|
+
const padded = frac.padEnd(decimals, "0").slice(0, decimals);
|
|
24
|
+
return BigInt(whole + padded);
|
|
25
|
+
}
|
|
26
|
+
export function formatTokenAmount(raw, decimals = DEFAULT_DECIMALS) {
|
|
27
|
+
const str = raw.toString().padStart(decimals + 1, "0");
|
|
28
|
+
const whole = str.slice(0, -decimals);
|
|
29
|
+
const frac = str.slice(-decimals).replace(/0+$/, "") || "0";
|
|
30
|
+
return `${whole}.${frac}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { x402Facilitator } from "@x402/core/facilitator";
|
|
3
|
+
import { toFacilitatorEvmSigner } from "@x402/evm";
|
|
4
|
+
import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
|
|
5
|
+
import { createWalletClient } from "./chain.js";
|
|
6
|
+
import { accounts, networkId } from "./config.js";
|
|
7
|
+
export function startFacilitator(options) {
|
|
8
|
+
const viemClient = createWalletClient(accounts.facilitator.privateKey, options.rpcUrl, options.chainId);
|
|
9
|
+
const evmSigner = toFacilitatorEvmSigner({
|
|
10
|
+
address: accounts.facilitator.address,
|
|
11
|
+
readContract: (args) => viemClient.readContract(args),
|
|
12
|
+
verifyTypedData: (args) => viemClient.verifyTypedData(args),
|
|
13
|
+
writeContract: (args) => viemClient.writeContract(args),
|
|
14
|
+
sendTransaction: (args) => viemClient.sendTransaction(args),
|
|
15
|
+
waitForTransactionReceipt: (args) => viemClient.waitForTransactionReceipt(args),
|
|
16
|
+
getCode: (args) => viemClient.getCode(args),
|
|
17
|
+
});
|
|
18
|
+
const facilitator = new x402Facilitator();
|
|
19
|
+
const network = networkId(options.chainId);
|
|
20
|
+
facilitator.register(network, new ExactEvmScheme(evmSigner));
|
|
21
|
+
const app = express();
|
|
22
|
+
app.use(express.json());
|
|
23
|
+
app.use((req, res, next) => {
|
|
24
|
+
const start = Date.now();
|
|
25
|
+
res.on("finish", () => {
|
|
26
|
+
console.log(`[facilitator] ${req.method} ${req.path} ${res.statusCode} (${Date.now() - start}ms)`);
|
|
27
|
+
});
|
|
28
|
+
next();
|
|
29
|
+
});
|
|
30
|
+
app.post("/verify", async (req, res) => {
|
|
31
|
+
try {
|
|
32
|
+
const { paymentPayload, paymentRequirements } = req.body;
|
|
33
|
+
if (!paymentPayload || !paymentRequirements) {
|
|
34
|
+
res
|
|
35
|
+
.status(400)
|
|
36
|
+
.json({ error: "Missing paymentPayload or paymentRequirements" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const response = await facilitator.verify(paymentPayload, paymentRequirements);
|
|
40
|
+
res.json(response);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error("Verify error:", error);
|
|
44
|
+
res.status(500).json({
|
|
45
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
app.post("/settle", async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const { paymentPayload, paymentRequirements } = req.body;
|
|
52
|
+
if (!paymentPayload || !paymentRequirements) {
|
|
53
|
+
res
|
|
54
|
+
.status(400)
|
|
55
|
+
.json({ error: "Missing paymentPayload or paymentRequirements" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const response = await facilitator.settle(paymentPayload, paymentRequirements);
|
|
59
|
+
res.json(response);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error("Settle error:", error);
|
|
63
|
+
res.status(500).json({
|
|
64
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
app.get("/supported", (_req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const response = facilitator.getSupported();
|
|
71
|
+
res.json(response);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error("Supported error:", error);
|
|
75
|
+
res.status(500).json({
|
|
76
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
app.get("/health", (_req, res) => {
|
|
81
|
+
res.json({
|
|
82
|
+
status: "ok",
|
|
83
|
+
network,
|
|
84
|
+
facilitator: accounts.facilitator.address,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const server = app.listen(options.port, () => {
|
|
89
|
+
resolve(server);
|
|
90
|
+
});
|
|
91
|
+
server.on("error", reject);
|
|
92
|
+
});
|
|
93
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x402-fl",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Local x402 facilitator dev tool, spin up Anvil + facilitator for local x402 development",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"x402-fl": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/cli.ts dev"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"x402",
|
|
15
|
+
"facilitator",
|
|
16
|
+
"local",
|
|
17
|
+
"development",
|
|
18
|
+
"anvil"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@x402/core": "^2.5.0",
|
|
23
|
+
"@x402/evm": "^2.5.0",
|
|
24
|
+
"boxen": "^8.0.1",
|
|
25
|
+
"chalk": "^5.6.2",
|
|
26
|
+
"commander": "^12.1.0",
|
|
27
|
+
"dotenv": "^16.4.7",
|
|
28
|
+
"express": "^4.21.2",
|
|
29
|
+
"viem": "^2.39.3"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/express": "^5.0.0",
|
|
33
|
+
"@types/node": "^22.13.4",
|
|
34
|
+
"tsx": "^4.19.2",
|
|
35
|
+
"typescript": "^5.7.3"
|
|
36
|
+
},
|
|
37
|
+
"packageManager": "pnpm@10.28.2",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
import { defaults } from "./lib/config.js";
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
9
|
+
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
function parsePort(value: string): number {
|
|
13
|
+
const port = parseInt(value, 10);
|
|
14
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
15
|
+
throw new InvalidArgumentError("must be a number between 1 and 65535");
|
|
16
|
+
}
|
|
17
|
+
return port;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseAddress(value: string): `0x${string}` {
|
|
21
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
|
|
22
|
+
throw new InvalidArgumentError(
|
|
23
|
+
"must be a valid 0x-prefixed Ethereum address (42 characters)",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return value as `0x${string}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseAmount(value: string): string {
|
|
30
|
+
if (!/^\d+(\.\d+)?$/.test(value) || Number(value) <= 0) {
|
|
31
|
+
throw new InvalidArgumentError(
|
|
32
|
+
"must be a positive number (e.g. '100' or '1.5')",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const program = new Command();
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.name("x402-fl")
|
|
42
|
+
.description(
|
|
43
|
+
"Local x402 facilitator dev tool\n\n" +
|
|
44
|
+
" Spin up a local Anvil fork and x402 facilitator server for development.\n" +
|
|
45
|
+
" Requires `anvil` (from Foundry) to be installed and available in PATH.\n\n" +
|
|
46
|
+
" Quick start:\n" +
|
|
47
|
+
" $ x402-fl dev --rpc-url https://mainnet.base.org\n" +
|
|
48
|
+
" $ x402-fl fund 0xYourAddress 100",
|
|
49
|
+
)
|
|
50
|
+
.version(pkg.version)
|
|
51
|
+
.showHelpAfterError(true)
|
|
52
|
+
.configureHelp({ showGlobalOptions: true });
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command("dev")
|
|
56
|
+
.description(
|
|
57
|
+
"Start Anvil fork + facilitator server for local x402 development",
|
|
58
|
+
)
|
|
59
|
+
.option(
|
|
60
|
+
"--port <number>",
|
|
61
|
+
`facilitator server port (default: ${defaults.facilitatorPort})`,
|
|
62
|
+
parsePort,
|
|
63
|
+
defaults.facilitatorPort,
|
|
64
|
+
)
|
|
65
|
+
.option(
|
|
66
|
+
"--anvil-port <number>",
|
|
67
|
+
`Anvil JSON-RPC port (default: ${defaults.anvilPort})`,
|
|
68
|
+
parsePort,
|
|
69
|
+
defaults.anvilPort,
|
|
70
|
+
)
|
|
71
|
+
.option(
|
|
72
|
+
"--rpc-url <url>",
|
|
73
|
+
"Base mainnet RPC URL to fork (or set BASE_RPC_URL in .env)",
|
|
74
|
+
)
|
|
75
|
+
.addHelpText(
|
|
76
|
+
"after",
|
|
77
|
+
`
|
|
78
|
+
Examples:
|
|
79
|
+
$ x402-fl dev --rpc-url https://mainnet.base.org
|
|
80
|
+
$ x402-fl dev --rpc-url $BASE_RPC_URL --port 5000 --anvil-port 9545
|
|
81
|
+
|
|
82
|
+
Environment variables:
|
|
83
|
+
BASE_RPC_URL Fallback RPC URL when --rpc-url is not provided`,
|
|
84
|
+
)
|
|
85
|
+
.action(async (opts) => {
|
|
86
|
+
const rpcUrl = opts.rpcUrl || process.env.BASE_RPC_URL;
|
|
87
|
+
if (!rpcUrl) {
|
|
88
|
+
program.error(
|
|
89
|
+
"missing required RPC URL\n\n" +
|
|
90
|
+
" Provide one of:\n" +
|
|
91
|
+
" --rpc-url <url> pass directly as a flag\n" +
|
|
92
|
+
" BASE_RPC_URL=<url> set in .env or environment\n\n" +
|
|
93
|
+
" Example:\n" +
|
|
94
|
+
" $ x402-fl dev --rpc-url https://mainnet.base.org",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { devCommand } = await import("./commands/dev.js");
|
|
99
|
+
await devCommand({
|
|
100
|
+
port: opts.port,
|
|
101
|
+
anvilPort: opts.anvilPort,
|
|
102
|
+
rpcUrl,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
program
|
|
107
|
+
.command("fund")
|
|
108
|
+
.description("Fund an address with USDC on local Anvil")
|
|
109
|
+
.argument("<address>", "0x-prefixed Ethereum address to fund", parseAddress)
|
|
110
|
+
.argument(
|
|
111
|
+
"<amount>",
|
|
112
|
+
"USDC amount (e.g. '100' or '1.5')",
|
|
113
|
+
parseAmount,
|
|
114
|
+
)
|
|
115
|
+
.option(
|
|
116
|
+
"--anvil-port <number>",
|
|
117
|
+
`Anvil JSON-RPC port`,
|
|
118
|
+
parsePort,
|
|
119
|
+
defaults.anvilPort,
|
|
120
|
+
)
|
|
121
|
+
.addHelpText(
|
|
122
|
+
"after",
|
|
123
|
+
`
|
|
124
|
+
Examples:
|
|
125
|
+
$ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 100
|
|
126
|
+
$ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1.5 --anvil-port 9545`,
|
|
127
|
+
)
|
|
128
|
+
.action(async (address: `0x${string}`, amount: string, opts) => {
|
|
129
|
+
const { fundCommand } = await import("./commands/fund.js");
|
|
130
|
+
await fundCommand({
|
|
131
|
+
address,
|
|
132
|
+
amount,
|
|
133
|
+
anvilPort: opts.anvilPort,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
program
|
|
138
|
+
.command("balance")
|
|
139
|
+
.description("Get USDC balance for an address on local Anvil")
|
|
140
|
+
.argument("<address>", "0x-prefixed Ethereum address to check", parseAddress)
|
|
141
|
+
.option(
|
|
142
|
+
"--anvil-port <number>",
|
|
143
|
+
`Anvil JSON-RPC port`,
|
|
144
|
+
parsePort,
|
|
145
|
+
defaults.anvilPort,
|
|
146
|
+
)
|
|
147
|
+
.addHelpText(
|
|
148
|
+
"after",
|
|
149
|
+
`
|
|
150
|
+
Examples:
|
|
151
|
+
$ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
|
152
|
+
$ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --anvil-port 9545`,
|
|
153
|
+
)
|
|
154
|
+
.action(async (address: `0x${string}`, opts) => {
|
|
155
|
+
const { balanceCommand } = await import("./commands/balance.js");
|
|
156
|
+
await balanceCommand({
|
|
157
|
+
address,
|
|
158
|
+
anvilPort: opts.anvilPort,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
program.parse();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import boxen from "boxen";
|
|
3
|
+
import { createPublicClient, fetchChainId } from "../lib/chain.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_DECIMALS,
|
|
6
|
+
formatTokenAmount,
|
|
7
|
+
USDC_ADDRESS,
|
|
8
|
+
} from "../lib/config.js";
|
|
9
|
+
import { ERC20_ABI } from "../lib/abi.js";
|
|
10
|
+
|
|
11
|
+
export interface BalanceOptions {
|
|
12
|
+
address: `0x${string}`;
|
|
13
|
+
anvilPort: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function balanceCommand(options: BalanceOptions): Promise<void> {
|
|
17
|
+
const { address, anvilPort } = options;
|
|
18
|
+
const rpcUrl = `http://localhost:${anvilPort}`;
|
|
19
|
+
|
|
20
|
+
let chainId: number;
|
|
21
|
+
try {
|
|
22
|
+
chainId = await fetchChainId(rpcUrl);
|
|
23
|
+
} catch {
|
|
24
|
+
console.error(
|
|
25
|
+
`Error: Could not connect to Anvil at ${rpcUrl}.\n` +
|
|
26
|
+
`Make sure Anvil is running (e.g. "x402-fl dev" or "anvil --fork-url ...").`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const client = createPublicClient(rpcUrl, chainId);
|
|
32
|
+
|
|
33
|
+
let decimals = DEFAULT_DECIMALS;
|
|
34
|
+
try {
|
|
35
|
+
decimals = await client.readContract({
|
|
36
|
+
address: USDC_ADDRESS,
|
|
37
|
+
abi: ERC20_ABI,
|
|
38
|
+
functionName: "decimals",
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
// Non-standard token without decimals(); fall back to default
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const balance = await client.readContract({
|
|
45
|
+
address: USDC_ADDRESS,
|
|
46
|
+
abi: ERC20_ABI,
|
|
47
|
+
functionName: "balanceOf",
|
|
48
|
+
args: [address],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(
|
|
52
|
+
boxen(
|
|
53
|
+
[
|
|
54
|
+
`${chalk.dim("Token")} ${USDC_ADDRESS}`,
|
|
55
|
+
`${chalk.dim("Address")} ${address}`,
|
|
56
|
+
`${chalk.dim("Balance")} ${chalk.green.bold(formatTokenAmount(balance, decimals))} USDC`,
|
|
57
|
+
].join("\n"),
|
|
58
|
+
{
|
|
59
|
+
title: "USDC Balance",
|
|
60
|
+
padding: 1,
|
|
61
|
+
borderStyle: "round",
|
|
62
|
+
borderColor: "cyan",
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import boxen from "boxen";
|
|
3
|
+
import type { ChildProcess } from "child_process";
|
|
4
|
+
import type { Server } from "http";
|
|
5
|
+
import { startAnvil, waitForAnvil } from "../lib/anvil.js";
|
|
6
|
+
import { startFacilitator } from "../lib/facilitator.js";
|
|
7
|
+
import { fetchChainId } from "../lib/chain.js";
|
|
8
|
+
import { accounts, defaults, networkId, USDC_ADDRESS } from "../lib/config.js";
|
|
9
|
+
|
|
10
|
+
export interface DevOptions {
|
|
11
|
+
port: number;
|
|
12
|
+
anvilPort: number;
|
|
13
|
+
rpcUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function devCommand(options: DevOptions): Promise<void> {
|
|
17
|
+
const localRpcUrl = `http://localhost:${options.anvilPort}`;
|
|
18
|
+
let anvilProc: ChildProcess | null = null;
|
|
19
|
+
let server: Server | null = null;
|
|
20
|
+
|
|
21
|
+
function cleanup() {
|
|
22
|
+
console.log("\nShutting down...");
|
|
23
|
+
let pending = 0;
|
|
24
|
+
const done = () => { if (--pending <= 0) process.exit(0); };
|
|
25
|
+
|
|
26
|
+
if (server) {
|
|
27
|
+
pending++;
|
|
28
|
+
server.close(() => done());
|
|
29
|
+
server = null;
|
|
30
|
+
}
|
|
31
|
+
if (anvilProc) {
|
|
32
|
+
pending++;
|
|
33
|
+
anvilProc.on("exit", () => done());
|
|
34
|
+
anvilProc.kill("SIGTERM");
|
|
35
|
+
anvilProc = null;
|
|
36
|
+
}
|
|
37
|
+
if (pending === 0) process.exit(0);
|
|
38
|
+
|
|
39
|
+
// Force exit after 3s if graceful shutdown hangs
|
|
40
|
+
setTimeout(() => process.exit(0), 3000).unref();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.on("SIGINT", cleanup);
|
|
44
|
+
process.on("SIGTERM", cleanup);
|
|
45
|
+
|
|
46
|
+
// 0. Detect chain ID from fork RPC
|
|
47
|
+
console.log(chalk.dim(`Detecting chain ID from ${options.rpcUrl}...`));
|
|
48
|
+
const chainId = await fetchChainId(options.rpcUrl);
|
|
49
|
+
console.log(chalk.dim(`Detected chain ID: ${chainId}`));
|
|
50
|
+
|
|
51
|
+
// 1. Start Anvil
|
|
52
|
+
console.log(
|
|
53
|
+
chalk.dim(
|
|
54
|
+
`Starting Anvil (forking chain ${chainId}, port ${options.anvilPort})...`,
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
anvilProc = startAnvil({
|
|
58
|
+
forkUrl: options.rpcUrl,
|
|
59
|
+
port: options.anvilPort,
|
|
60
|
+
chainId,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
anvilProc.on("exit", (code) => {
|
|
64
|
+
if (code !== null && code !== 0) {
|
|
65
|
+
console.error(chalk.red(`Anvil exited with code ${code}`));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 2. Wait for Anvil to be ready
|
|
71
|
+
console.log(chalk.dim("Waiting for Anvil to be ready..."));
|
|
72
|
+
await waitForAnvil(localRpcUrl);
|
|
73
|
+
console.log(chalk.dim("Anvil is ready."));
|
|
74
|
+
|
|
75
|
+
// 3. Start facilitator
|
|
76
|
+
console.log(chalk.dim(`Starting facilitator on port ${options.port}...`));
|
|
77
|
+
server = await startFacilitator({
|
|
78
|
+
port: options.port,
|
|
79
|
+
rpcUrl: localRpcUrl,
|
|
80
|
+
chainId,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const network = networkId(chainId);
|
|
84
|
+
|
|
85
|
+
// 4. Print summary
|
|
86
|
+
console.log(
|
|
87
|
+
boxen(
|
|
88
|
+
[
|
|
89
|
+
`${chalk.dim("Anvil RPC")} ${chalk.cyan(localRpcUrl)}`,
|
|
90
|
+
`${chalk.dim("Facilitator")} ${chalk.cyan(`http://localhost:${options.port}`)}`,
|
|
91
|
+
`${chalk.dim("Network")} ${network}`,
|
|
92
|
+
`${chalk.dim("Chain ID")} ${chainId}`,
|
|
93
|
+
"",
|
|
94
|
+
chalk.dim("Accounts"),
|
|
95
|
+
` ${chalk.dim("Facilitator")} ${accounts.facilitator.address}`,
|
|
96
|
+
"",
|
|
97
|
+
`${chalk.dim("USDC")} ${USDC_ADDRESS}`,
|
|
98
|
+
"",
|
|
99
|
+
`${chalk.dim("Fund account with:")}`,
|
|
100
|
+
`$ ${chalk.white("x402-fl fund <address> <amount>")}`,
|
|
101
|
+
"",
|
|
102
|
+
chalk.dim("Press Ctrl+C to stop."),
|
|
103
|
+
].join("\n"),
|
|
104
|
+
{
|
|
105
|
+
title: "x402-fl dev",
|
|
106
|
+
padding: 1,
|
|
107
|
+
borderStyle: "round",
|
|
108
|
+
borderColor: "green",
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
}
|