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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "WebFetch(domain:raw.githubusercontent.com)",
6
+ "Bash(gh api:*)",
7
+ "Bash(npm view:*)"
8
+ ]
9
+ }
10
+ }
package/.env.example ADDED
@@ -0,0 +1,4 @@
1
+ # Base mainnet RPC URL for Anvil forking
2
+ # Free: https://mainnet.base.org
3
+ # Better: Alchemy or Infura Base mainnet RPC
4
+ BASE_RPC_URL=https://mainnet.base.org
@@ -0,0 +1,66 @@
1
+ name: Publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+
15
+ jobs:
16
+ publish:
17
+ name: Publish [npm]
18
+ runs-on: ubuntu-latest
19
+ environment: NPM
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: pnpm/action-setup@v4
24
+
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: "18"
28
+ registry-url: https://registry.npmjs.org
29
+ cache: pnpm
30
+
31
+ - name: Install dependencies
32
+ run: pnpm install --frozen-lockfile
33
+
34
+ - name: Extract and validate version from tag
35
+ id: info
36
+ run: |
37
+ TAG="${{ github.event.release.tag_name }}"
38
+
39
+ if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?)$ ]]; then
40
+ VERSION="${BASH_REMATCH[1]}"
41
+ else
42
+ echo "Error: Tag must be in format v0.0.0 (e.g., v1.0.0, v1.0.0-beta.1)"
43
+ echo "Received: $TAG"
44
+ exit 1
45
+ fi
46
+
47
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
48
+
49
+ if [[ "$VERSION" == *-* ]]; then
50
+ echo "dist_tag=next" >> $GITHUB_OUTPUT
51
+ else
52
+ echo "dist_tag=latest" >> $GITHUB_OUTPUT
53
+ fi
54
+
55
+ - name: Build
56
+ run: pnpm build
57
+
58
+ - name: Set version
59
+ run: |
60
+ VERSION="${{ steps.info.outputs.version }}"
61
+ sed -i 's/"version": "0.0.0"/"version": "'"$VERSION"'"/g' package.json
62
+
63
+ - name: Publish
64
+ run: npm publish --access public --tag ${{ steps.info.outputs.dist_tag }} --provenance
65
+ env:
66
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kevzzsk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # x402-fl
2
+
3
+ Local x402 facilitator for development and testing. Forks Base mainnet with Foundry Anvil and runs a local facilitator server. Fund any address with USDC via direct storage manipulation.
4
+
5
+ > **Warning**: This is for local development only. Do NOT use in production. It uses Anvil's well-known deterministic private keys, which are publicly known and have zero security.
6
+
7
+ ## What it does
8
+
9
+ `x402-fl` spins up a complete local x402 payment environment in one command:
10
+
11
+ 1. Forks Base mainnet using Anvil (so you get real USDC contract state)
12
+ 2. Starts a local facilitator server that can verify and settle x402 payments
13
+ 3. Provides a `fund` command to mint USDC to any address via Anvil storage manipulation
14
+
15
+ ## Prerequisites
16
+
17
+ - Node.js 18+
18
+ - [pnpm](https://pnpm.io/)
19
+ - [Foundry](https://www.getfoundry.sh/introduction/installation) (provides `anvil`)
20
+ - A Base mainnet RPC URL (e.g. `https://mainnet.base.org`, Alchemy, or Infura)
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ git clone https://github.com/anthropics/x402-fl.git
26
+ cd x402-fl
27
+ pnpm install
28
+ ```
29
+
30
+ Create a `.env` file:
31
+
32
+ ```bash
33
+ cp .env.example .env
34
+ # Edit .env and set BASE_RPC_URL
35
+ ```
36
+
37
+ Start the local environment:
38
+
39
+ ```bash
40
+ pnpm dev
41
+ ```
42
+
43
+ This starts Anvil on port 8545 and launches the facilitator on port 4022.
44
+
45
+ Fund an account with USDC:
46
+
47
+ ```bash
48
+ npx tsx src/cli.ts fund 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 100
49
+ ```
50
+
51
+ ## CLI Commands
52
+
53
+ ### `dev`
54
+
55
+ Start Anvil fork + facilitator server for local x402 development.
56
+
57
+ ```bash
58
+ pnpm dev [options]
59
+ ```
60
+
61
+ | Flag | Default | Description |
62
+ | ----------------------- | ---------------------- | --------------------- |
63
+ | `--port <number>` | `4022` | Facilitator HTTP port |
64
+ | `--anvil-port <number>` | `8545` | Anvil RPC port |
65
+ | `--rpc-url <url>` | `BASE_RPC_URL` env var | Base RPC URL to fork |
66
+
67
+ ### `fund`
68
+
69
+ Fund any address with USDC on the local Anvil fork.
70
+
71
+ ```bash
72
+ npx tsx src/cli.ts fund <address> <amount> [options]
73
+ ```
74
+
75
+ | Argument / Flag | Default | Description |
76
+ | ----------------------- | -------- | ------------------------------------------------- |
77
+ | `<address>` | required | 0x-prefixed Ethereum address to fund |
78
+ | `<amount>` | required | USDC amount (human-readable, e.g. `100` or `1.5`) |
79
+ | `--anvil-port <number>` | `8545` | Anvil RPC port |
80
+
81
+ ### `balance`
82
+
83
+ Check USDC balance for an address on the local Anvil fork.
84
+
85
+ ```bash
86
+ npx tsx src/cli.ts balance <address> [options]
87
+ ```
88
+
89
+ | Argument / Flag | Default | Description |
90
+ | ----------------------- | -------- | ---------------------------- |
91
+ | `<address>` | required | 0x-prefixed Ethereum address |
92
+ | `--anvil-port <number>` | `8545` | Anvil RPC port |
93
+
94
+ ## Test Accounts
95
+
96
+ The facilitator uses Anvil's default deterministic account (index 0). **Do not use these keys for anything real.**
97
+
98
+ | Role | Address |
99
+ | ----------- | -------------------------------------------- |
100
+ | Facilitator | `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266` |
101
+
102
+ Any address can be funded using `x402-fl fund`.
103
+
104
+ ## How it works
105
+
106
+ Anvil forks Base mainnet at the latest block, giving you a local copy of all on-chain state including the USDC contract. The `fund` command uses `anvil_setStorageAt` to directly write ERC-20 balance values into contract storage — it auto-detects the correct storage slot by probing common mapping layouts (slots 0–20). The facilitator server exposes `/verify`, `/settle`, `/supported`, and `/health` endpoints using the `@x402/core` and `@x402/evm` packages, operating against the local Anvil fork instead of real Base mainnet.
107
+
108
+ ## Roadmap
109
+
110
+ - [ ] Custom ERC-20 token support
111
+ - [ ] Custom Fork url
112
+ - [ ] Dockerised anvil node
113
+ - [ ] Testcontainers for facilitator + anvil for deterministic testing env
114
+
115
+ ## Issues
116
+
117
+ Found a bug or have a feature request? [Open an issue](https://github.com/anthropics/x402-fl/issues).
118
+
119
+ ## License
120
+
121
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "fs";
3
+ import { Command, InvalidArgumentError } from "commander";
4
+ import dotenv from "dotenv";
5
+ import { defaults } from "./lib/config.js";
6
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
7
+ dotenv.config();
8
+ function parsePort(value) {
9
+ const port = parseInt(value, 10);
10
+ if (isNaN(port) || port < 1 || port > 65535) {
11
+ throw new InvalidArgumentError("must be a number between 1 and 65535");
12
+ }
13
+ return port;
14
+ }
15
+ function parseAddress(value) {
16
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
17
+ throw new InvalidArgumentError("must be a valid 0x-prefixed Ethereum address (42 characters)");
18
+ }
19
+ return value;
20
+ }
21
+ function parseAmount(value) {
22
+ if (!/^\d+(\.\d+)?$/.test(value) || Number(value) <= 0) {
23
+ throw new InvalidArgumentError("must be a positive number (e.g. '100' or '1.5')");
24
+ }
25
+ return value;
26
+ }
27
+ const program = new Command();
28
+ program
29
+ .name("x402-fl")
30
+ .description("Local x402 facilitator dev tool\n\n" +
31
+ " Spin up a local Anvil fork and x402 facilitator server for development.\n" +
32
+ " Requires `anvil` (from Foundry) to be installed and available in PATH.\n\n" +
33
+ " Quick start:\n" +
34
+ " $ x402-fl dev --rpc-url https://mainnet.base.org\n" +
35
+ " $ x402-fl fund 0xYourAddress 100")
36
+ .version(pkg.version)
37
+ .showHelpAfterError(true)
38
+ .configureHelp({ showGlobalOptions: true });
39
+ program
40
+ .command("dev")
41
+ .description("Start Anvil fork + facilitator server for local x402 development")
42
+ .option("--port <number>", `facilitator server port (default: ${defaults.facilitatorPort})`, parsePort, defaults.facilitatorPort)
43
+ .option("--anvil-port <number>", `Anvil JSON-RPC port (default: ${defaults.anvilPort})`, parsePort, defaults.anvilPort)
44
+ .option("--rpc-url <url>", "Base mainnet RPC URL to fork (or set BASE_RPC_URL in .env)")
45
+ .addHelpText("after", `
46
+ Examples:
47
+ $ x402-fl dev --rpc-url https://mainnet.base.org
48
+ $ x402-fl dev --rpc-url $BASE_RPC_URL --port 5000 --anvil-port 9545
49
+
50
+ Environment variables:
51
+ BASE_RPC_URL Fallback RPC URL when --rpc-url is not provided`)
52
+ .action(async (opts) => {
53
+ const rpcUrl = opts.rpcUrl || process.env.BASE_RPC_URL;
54
+ if (!rpcUrl) {
55
+ program.error("missing required RPC URL\n\n" +
56
+ " Provide one of:\n" +
57
+ " --rpc-url <url> pass directly as a flag\n" +
58
+ " BASE_RPC_URL=<url> set in .env or environment\n\n" +
59
+ " Example:\n" +
60
+ " $ x402-fl dev --rpc-url https://mainnet.base.org");
61
+ }
62
+ const { devCommand } = await import("./commands/dev.js");
63
+ await devCommand({
64
+ port: opts.port,
65
+ anvilPort: opts.anvilPort,
66
+ rpcUrl,
67
+ });
68
+ });
69
+ program
70
+ .command("fund")
71
+ .description("Fund an address with USDC on local Anvil")
72
+ .argument("<address>", "0x-prefixed Ethereum address to fund", parseAddress)
73
+ .argument("<amount>", "USDC amount (e.g. '100' or '1.5')", parseAmount)
74
+ .option("--anvil-port <number>", `Anvil JSON-RPC port`, parsePort, defaults.anvilPort)
75
+ .addHelpText("after", `
76
+ Examples:
77
+ $ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 100
78
+ $ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1.5 --anvil-port 9545`)
79
+ .action(async (address, amount, opts) => {
80
+ const { fundCommand } = await import("./commands/fund.js");
81
+ await fundCommand({
82
+ address,
83
+ amount,
84
+ anvilPort: opts.anvilPort,
85
+ });
86
+ });
87
+ program
88
+ .command("balance")
89
+ .description("Get USDC balance for an address on local Anvil")
90
+ .argument("<address>", "0x-prefixed Ethereum address to check", parseAddress)
91
+ .option("--anvil-port <number>", `Anvil JSON-RPC port`, parsePort, defaults.anvilPort)
92
+ .addHelpText("after", `
93
+ Examples:
94
+ $ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
95
+ $ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --anvil-port 9545`)
96
+ .action(async (address, opts) => {
97
+ const { balanceCommand } = await import("./commands/balance.js");
98
+ await balanceCommand({
99
+ address,
100
+ anvilPort: opts.anvilPort,
101
+ });
102
+ });
103
+ program.parse();
@@ -0,0 +1,5 @@
1
+ export interface BalanceOptions {
2
+ address: `0x${string}`;
3
+ anvilPort: number;
4
+ }
5
+ export declare function balanceCommand(options: BalanceOptions): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { createPublicClient, fetchChainId } from "../lib/chain.js";
4
+ import { DEFAULT_DECIMALS, formatTokenAmount, USDC_ADDRESS, } from "../lib/config.js";
5
+ import { ERC20_ABI } from "../lib/abi.js";
6
+ export async function balanceCommand(options) {
7
+ const { address, anvilPort } = options;
8
+ const rpcUrl = `http://localhost:${anvilPort}`;
9
+ let chainId;
10
+ try {
11
+ chainId = await fetchChainId(rpcUrl);
12
+ }
13
+ catch {
14
+ console.error(`Error: Could not connect to Anvil at ${rpcUrl}.\n` +
15
+ `Make sure Anvil is running (e.g. "x402-fl dev" or "anvil --fork-url ...").`);
16
+ process.exit(1);
17
+ }
18
+ const client = createPublicClient(rpcUrl, chainId);
19
+ let decimals = DEFAULT_DECIMALS;
20
+ try {
21
+ decimals = await client.readContract({
22
+ address: USDC_ADDRESS,
23
+ abi: ERC20_ABI,
24
+ functionName: "decimals",
25
+ });
26
+ }
27
+ catch {
28
+ // Non-standard token without decimals(); fall back to default
29
+ }
30
+ const balance = await client.readContract({
31
+ address: USDC_ADDRESS,
32
+ abi: ERC20_ABI,
33
+ functionName: "balanceOf",
34
+ args: [address],
35
+ });
36
+ console.log(boxen([
37
+ `${chalk.dim("Token")} ${USDC_ADDRESS}`,
38
+ `${chalk.dim("Address")} ${address}`,
39
+ `${chalk.dim("Balance")} ${chalk.green.bold(formatTokenAmount(balance, decimals))} USDC`,
40
+ ].join("\n"), {
41
+ title: "USDC Balance",
42
+ padding: 1,
43
+ borderStyle: "round",
44
+ borderColor: "cyan",
45
+ }));
46
+ }
@@ -0,0 +1,6 @@
1
+ export interface DevOptions {
2
+ port: number;
3
+ anvilPort: number;
4
+ rpcUrl: string;
5
+ }
6
+ export declare function devCommand(options: DevOptions): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { startAnvil, waitForAnvil } from "../lib/anvil.js";
4
+ import { startFacilitator } from "../lib/facilitator.js";
5
+ import { fetchChainId } from "../lib/chain.js";
6
+ import { accounts, networkId, USDC_ADDRESS } from "../lib/config.js";
7
+ export async function devCommand(options) {
8
+ const localRpcUrl = `http://localhost:${options.anvilPort}`;
9
+ let anvilProc = null;
10
+ let server = null;
11
+ function cleanup() {
12
+ console.log("\nShutting down...");
13
+ let pending = 0;
14
+ const done = () => { if (--pending <= 0)
15
+ process.exit(0); };
16
+ if (server) {
17
+ pending++;
18
+ server.close(() => done());
19
+ server = null;
20
+ }
21
+ if (anvilProc) {
22
+ pending++;
23
+ anvilProc.on("exit", () => done());
24
+ anvilProc.kill("SIGTERM");
25
+ anvilProc = null;
26
+ }
27
+ if (pending === 0)
28
+ process.exit(0);
29
+ // Force exit after 3s if graceful shutdown hangs
30
+ setTimeout(() => process.exit(0), 3000).unref();
31
+ }
32
+ process.on("SIGINT", cleanup);
33
+ process.on("SIGTERM", cleanup);
34
+ // 0. Detect chain ID from fork RPC
35
+ console.log(chalk.dim(`Detecting chain ID from ${options.rpcUrl}...`));
36
+ const chainId = await fetchChainId(options.rpcUrl);
37
+ console.log(chalk.dim(`Detected chain ID: ${chainId}`));
38
+ // 1. Start Anvil
39
+ console.log(chalk.dim(`Starting Anvil (forking chain ${chainId}, port ${options.anvilPort})...`));
40
+ anvilProc = startAnvil({
41
+ forkUrl: options.rpcUrl,
42
+ port: options.anvilPort,
43
+ chainId,
44
+ });
45
+ anvilProc.on("exit", (code) => {
46
+ if (code !== null && code !== 0) {
47
+ console.error(chalk.red(`Anvil exited with code ${code}`));
48
+ process.exit(1);
49
+ }
50
+ });
51
+ // 2. Wait for Anvil to be ready
52
+ console.log(chalk.dim("Waiting for Anvil to be ready..."));
53
+ await waitForAnvil(localRpcUrl);
54
+ console.log(chalk.dim("Anvil is ready."));
55
+ // 3. Start facilitator
56
+ console.log(chalk.dim(`Starting facilitator on port ${options.port}...`));
57
+ server = await startFacilitator({
58
+ port: options.port,
59
+ rpcUrl: localRpcUrl,
60
+ chainId,
61
+ });
62
+ const network = networkId(chainId);
63
+ // 4. Print summary
64
+ console.log(boxen([
65
+ `${chalk.dim("Anvil RPC")} ${chalk.cyan(localRpcUrl)}`,
66
+ `${chalk.dim("Facilitator")} ${chalk.cyan(`http://localhost:${options.port}`)}`,
67
+ `${chalk.dim("Network")} ${network}`,
68
+ `${chalk.dim("Chain ID")} ${chainId}`,
69
+ "",
70
+ chalk.dim("Accounts"),
71
+ ` ${chalk.dim("Facilitator")} ${accounts.facilitator.address}`,
72
+ "",
73
+ `${chalk.dim("USDC")} ${USDC_ADDRESS}`,
74
+ "",
75
+ `${chalk.dim("Fund account with:")}`,
76
+ `$ ${chalk.white("x402-fl fund <address> <amount>")}`,
77
+ "",
78
+ chalk.dim("Press Ctrl+C to stop."),
79
+ ].join("\n"), {
80
+ title: "x402-fl dev",
81
+ padding: 1,
82
+ borderStyle: "round",
83
+ borderColor: "green",
84
+ }));
85
+ }
@@ -0,0 +1,9 @@
1
+ export interface FundOptions {
2
+ address: `0x${string}`;
3
+ amount: string;
4
+ anvilPort: number;
5
+ }
6
+ /**
7
+ * Fund any address with ERC-20 tokens by directly manipulating Anvil storage.
8
+ */
9
+ export declare function fundCommand(options: FundOptions): Promise<void>;
@@ -0,0 +1,131 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { createTestClient, http, publicActions, keccak256, encodePacked, pad, toHex, } from "viem";
4
+ import { createLocalChain, fetchChainId } from "../lib/chain.js";
5
+ import { DEFAULT_DECIMALS, formatTokenAmount, parseTokenAmount, USDC_ADDRESS } from "../lib/config.js";
6
+ import { ERC20_ABI } from "../lib/abi.js";
7
+ /**
8
+ * Compute the storage slot for `mapping(address => uint256)` at a given
9
+ * base slot index. Solidity stores mapping values at keccak256(key . slot).
10
+ */
11
+ function balanceSlot(address, baseSlot) {
12
+ return keccak256(encodePacked(["bytes32", "bytes32"], [pad(address, { size: 32 }), pad(toHex(baseSlot), { size: 32 })]));
13
+ }
14
+ /**
15
+ * Probe common mapping slots (0-20) to find the correct `balanceOf` storage
16
+ * slot for an arbitrary ERC-20 contract.
17
+ *
18
+ * Strategy: for each candidate base slot we write a known sentinel value into
19
+ * the computed mapping slot, then check whether `balanceOf` returns that
20
+ * sentinel. The original value is always restored before moving on.
21
+ */
22
+ async function findBalanceSlot(client, contractAddress, probeAddress) {
23
+ const sentinel = pad(toHex(31337n * 10n ** 18n), { size: 32 });
24
+ for (let slot = 0; slot <= 20; slot++) {
25
+ const storageKey = balanceSlot(probeAddress, slot);
26
+ // Save the original value so we can restore it.
27
+ const original = await client.getStorageAt({
28
+ address: contractAddress,
29
+ slot: storageKey,
30
+ });
31
+ // Write sentinel
32
+ await client.request({
33
+ method: "anvil_setStorageAt",
34
+ params: [contractAddress, storageKey, sentinel],
35
+ });
36
+ // Read balanceOf and see if it matches
37
+ const balance = await client.readContract({
38
+ address: contractAddress,
39
+ abi: ERC20_ABI,
40
+ functionName: "balanceOf",
41
+ args: [probeAddress],
42
+ });
43
+ // Restore original value regardless of outcome
44
+ await client.request({
45
+ method: "anvil_setStorageAt",
46
+ params: [contractAddress, storageKey, original ?? pad("0x0", { size: 32 })],
47
+ });
48
+ if (balance === 31337n * 10n ** 18n) {
49
+ return slot;
50
+ }
51
+ }
52
+ throw new Error(`Could not detect balanceOf storage slot for contract ${contractAddress}. ` +
53
+ `Probed slots 0-20 with no match.`);
54
+ }
55
+ /**
56
+ * Fund any address with ERC-20 tokens by directly manipulating Anvil storage.
57
+ */
58
+ export async function fundCommand(options) {
59
+ const { address, amount, anvilPort } = options;
60
+ const contractAddress = USDC_ADDRESS;
61
+ const rpcUrl = `http://localhost:${anvilPort}`;
62
+ // Detect chain ID from running Anvil instance
63
+ let chainId;
64
+ try {
65
+ chainId = await fetchChainId(rpcUrl);
66
+ }
67
+ catch {
68
+ console.error(`Error: Could not connect to Anvil at ${rpcUrl}.\n` +
69
+ `Make sure Anvil is running (e.g. "x402-fl dev" or "anvil --fork-url ...").`);
70
+ process.exit(1);
71
+ }
72
+ // Create a viem TestClient (Anvil mode) with public actions
73
+ const chain = createLocalChain(rpcUrl, chainId);
74
+ const client = createTestClient({
75
+ mode: "anvil",
76
+ chain,
77
+ transport: http(rpcUrl),
78
+ }).extend(publicActions);
79
+ // Read token decimals from ERC-20 metadata, default to 6
80
+ let decimals = DEFAULT_DECIMALS;
81
+ try {
82
+ decimals = await client.readContract({
83
+ address: contractAddress,
84
+ abi: ERC20_ABI,
85
+ functionName: "decimals",
86
+ });
87
+ }
88
+ catch {
89
+ // Non-standard token without decimals(); fall back to default
90
+ }
91
+ const formatAmount = (raw) => formatTokenAmount(raw, decimals);
92
+ // Read current balance
93
+ const before = await client.readContract({
94
+ address: contractAddress,
95
+ abi: ERC20_ABI,
96
+ functionName: "balanceOf",
97
+ args: [address],
98
+ });
99
+ // Detect the storage slot
100
+ const baseSlot = await findBalanceSlot(client, contractAddress, address);
101
+ const addAmount = parseTokenAmount(amount, decimals);
102
+ const newBalance = before + addAmount;
103
+ // Write the new balance to storage
104
+ const storageKey = balanceSlot(address, baseSlot);
105
+ const encodedBalance = pad(toHex(newBalance), { size: 32 });
106
+ await client.request({
107
+ method: "anvil_setStorageAt",
108
+ params: [contractAddress, storageKey, encodedBalance],
109
+ });
110
+ // Verify the balance was updated correctly
111
+ const after = await client.readContract({
112
+ address: contractAddress,
113
+ abi: ERC20_ABI,
114
+ functionName: "balanceOf",
115
+ args: [address],
116
+ });
117
+ if (after !== newBalance) {
118
+ console.error(`Warning: Balance mismatch after storage write.\n` +
119
+ ` Expected: ${formatAmount(newBalance)}\n` +
120
+ ` Got: ${formatAmount(after)}`);
121
+ }
122
+ // Print confirmation
123
+ console.log(boxen([
124
+ `${chalk.dim("Token")} ${contractAddress}`,
125
+ `${chalk.dim("Address")} ${address}`,
126
+ "",
127
+ `${chalk.dim("Before")} ${formatAmount(before)} USDC`,
128
+ `${chalk.dim("Added")} ${chalk.yellow("+" + formatAmount(addAmount))} USDC`,
129
+ `${chalk.dim("After")} ${chalk.green.bold(formatAmount(after))} USDC`,
130
+ ].join("\n"), { title: "Funded", padding: 1, borderStyle: "round", borderColor: "green" }));
131
+ }
@@ -0,0 +1,22 @@
1
+ export declare const ERC20_ABI: readonly [{
2
+ readonly inputs: readonly [{
3
+ readonly name: "account";
4
+ readonly type: "address";
5
+ }];
6
+ readonly name: "balanceOf";
7
+ readonly outputs: readonly [{
8
+ readonly name: "";
9
+ readonly type: "uint256";
10
+ }];
11
+ readonly stateMutability: "view";
12
+ readonly type: "function";
13
+ }, {
14
+ readonly inputs: readonly [];
15
+ readonly name: "decimals";
16
+ readonly outputs: readonly [{
17
+ readonly name: "";
18
+ readonly type: "uint8";
19
+ }];
20
+ readonly stateMutability: "view";
21
+ readonly type: "function";
22
+ }];
@@ -0,0 +1,16 @@
1
+ export const ERC20_ABI = [
2
+ {
3
+ inputs: [{ name: "account", type: "address" }],
4
+ name: "balanceOf",
5
+ outputs: [{ name: "", type: "uint256" }],
6
+ stateMutability: "view",
7
+ type: "function",
8
+ },
9
+ {
10
+ inputs: [],
11
+ name: "decimals",
12
+ outputs: [{ name: "", type: "uint8" }],
13
+ stateMutability: "view",
14
+ type: "function",
15
+ },
16
+ ];
@@ -0,0 +1,8 @@
1
+ import { type ChildProcess } from "child_process";
2
+ export interface AnvilOptions {
3
+ forkUrl: string;
4
+ port: number;
5
+ chainId: number;
6
+ }
7
+ export declare function startAnvil(options: AnvilOptions): ChildProcess;
8
+ export declare function waitForAnvil(rpcUrl: string, timeoutMs?: number): Promise<void>;