@hpp-io/x402-mcp-bridge 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+
17
+ See https://www.apache.org/licenses/LICENSE-2.0 for the full text.
18
+
19
+ Copyright 2026 HPP / Noosphere contributors.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # @hpp-io/x402-mcp-bridge
2
+
3
+ > stdio MCP bridge that lets Claude Desktop / OpenClaw / other MCP hosts make
4
+ > autonomous **HPP USDC.e** payments via x402 — funds stay in a Safe, the
5
+ > bridge auto-tops up a delegate EOA within an on-chain spend cap
6
+ > (AllowanceModule).
7
+
8
+ ## What it is
9
+
10
+ The MCP host (Claude Desktop, OpenClaw, Cursor, …) doesn't know how to sign
11
+ EIP-3009 payments. This bridge runs as a stdio subprocess that the host
12
+ spawns, exposes itself as an MCP server to the host, and internally:
13
+
14
+ 1. Connects upstream to a remote x402 MCP server (SSE / StreamableHTTP).
15
+ 2. Forwards `listTools` / `callTool` between host and remote.
16
+ 3. When the remote returns 402, the bridge:
17
+ - Tops up the delegate EOA from the user's Safe via
18
+ `AllowanceModule.executeAllowanceTransfer` (chain-enforced daily cap).
19
+ - Signs EIP-3009 `transferWithAuthorization` with the delegate key.
20
+ - Retries the call.
21
+
22
+ The host sees a normal MCP server and never deals with payment.
23
+
24
+ ## Install / use
25
+
26
+ ```jsonc
27
+ // claude_desktop_config.json
28
+ {
29
+ "mcpServers": {
30
+ "hpp-x402": {
31
+ "command": "npx",
32
+ "args": ["-y", "@hpp-io/x402-mcp-bridge"],
33
+ "env": {
34
+ "DELEGATE_PRIVATE_KEY": "0x...",
35
+ "SAFE_ADDRESS": "0x...",
36
+ "ALLOWANCE_MODULE_ADDRESS": "0x...",
37
+ "USDCE_ADDRESS": "0x...",
38
+ "RESOURCE_SERVER_URL": "https://your-x402-server/mcp/sse",
39
+ "HPP_RPC_URL": "https://sepolia.hpp.io",
40
+ "HPP_NETWORK": "eip155:181228"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ Same config block works for OpenClaw and any other MCP host that supports
48
+ stdio MCP servers. See [`examples/`](./examples/) for ready-to-merge JSON
49
+ snippets and a [local development guide](./examples/local-dev.md) for using
50
+ the bridge before it's published to npm.
51
+
52
+ ## Pre-flight (one-time, on-chain)
53
+
54
+ Before the bridge can do anything, you need a Safe with USDC.e and an
55
+ AllowanceModule entry that authorises a delegate EOA to pull from it:
56
+
57
+ 1. Deploy / use an existing Safe at [`safe.hpp.io`](https://safe.hpp.io).
58
+ 2. Send USDC.e to the Safe.
59
+ 3. Enable the AllowanceModule on the Safe (one Safe transaction).
60
+ 4. `addDelegate(<delegate EOA>)` and
61
+ `setAllowance(<delegate>, USDC.e, <daily cap>, 1440 minutes, 0)` —
62
+ one Safe transaction each (or batched via Safe's MultiSend).
63
+ 5. Send a small amount of native ETH to the delegate EOA (it pays gas
64
+ for `executeAllowanceTransfer`).
65
+
66
+ Verified end-to-end on HPP Sepolia 2026-05-06 — the chain enforces the
67
+ daily cap, and `deleteAllowance` instantly blocks future top-ups.
68
+
69
+ ## Architecture
70
+
71
+ See [DESIGN.md in the parent task](../noosphere-evm/.claude/tasks/x402-mcp-wallet/design/DESIGN.md).
72
+
73
+ In short:
74
+
75
+ ```
76
+ host (Claude Desktop)
77
+ │ stdio (MCP)
78
+
79
+ [ this bridge ]
80
+ ─ Signer (delegate EOA)
81
+ ─ AutoTopup (Safe + AllowanceModule)
82
+ ─ remote MCP client (@x402/mcp wrapped)
83
+
84
+ │ SSE / StreamableHTTP (MCP + x402)
85
+
86
+ remote x402 MCP server
87
+ ```
88
+
89
+ ## Releasing to npm
90
+
91
+ Two paths, both via the GitHub Actions [Publish workflow](.github/workflows/publish.yml):
92
+
93
+ ```
94
+ # (1) Recommended: tag-driven release
95
+ git checkout main && git pull
96
+ npm version patch # or minor / major
97
+ git push --follow-tags # → workflow runs, publishes to npm
98
+
99
+ # (2) Ad-hoc: workflow_dispatch from GitHub UI
100
+ # Actions tab → "Publish to npm" → Run workflow
101
+ # Optional `dry_run` toggle for verification
102
+ ```
103
+
104
+ Required secret on the GitHub repo: `NPM_TOKEN` (npmjs.com → Profile →
105
+ Access Tokens → Generate "Automation" token scoped to `@hpp-io`).
106
+
107
+ The `prepublishOnly` script in package.json runs `typecheck && build`
108
+ locally too, so manual `npm publish` from a developer's machine still
109
+ guards against shipping broken builds.
110
+
111
+ ## Status
112
+
113
+ **v0.0.1 — PoC.** Working end-to-end against HPP Sepolia (smoke proves
114
+ both the "delegate has balance" and "delegate empty → autoTopup → pay"
115
+ paths). Not yet published to npm; install via local path or git clone.
116
+
117
+ Roadmap:
118
+
119
+ - v0.1: package on npm, full README with screenshots, OS keychain wrapper
120
+ for `DELEGATE_PRIVATE_KEY` (`keychain://hpp-x402-delegate`).
121
+ - v0.2: additional `Signer` implementations (HPP self-hosted server-wallet
122
+ API, ERC-4337 session keys).
123
+ - v0.3: structured guardrail layer (allowlist of `RESOURCE_SERVER_URL`,
124
+ per-call max payment cap) — mirrors Coinbase AgentKit's
125
+ `X402ActionProvider`.
126
+
127
+ ## License
128
+
129
+ Apache-2.0
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli/revoke.js").catch((err) => {
3
+ process.stderr.write(`hpp-x402-safe-revoke fatal: ${err?.message ?? err}\n`);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import("../dist/cli/setup.js").catch((err) => {
3
+ process.stderr.write(`hpp-x402-safe-setup fatal: ${err?.message ?? err}\n`);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry shim for `npx @hpp-io/x402-mcp-bridge` and Claude Desktop's
4
+ * "command": "npx" launch path. Imports the compiled ESM build.
5
+ */
6
+ import("../dist/index.js").then((m) => {
7
+ return m.runBridge();
8
+ }).catch((err) => {
9
+ process.stderr.write(`x402-mcp-bridge fatal: ${err?.message ?? err}\n`);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * AutoTopup — pulls USDC.e from the user's Safe to the delegate EOA via
3
+ * AllowanceModule.executeAllowanceTransfer.
4
+ *
5
+ * Triggered lazily before each x402 payment when the delegate's balance
6
+ * is insufficient. The chain enforces the daily cap; if a topup would
7
+ * exceed the allowance the call reverts with
8
+ *
9
+ * "newSpent > allowance.spent && newSpent <= allowance.amount"
10
+ *
11
+ * which the bridge maps to a clear "spend cap exceeded" error for the
12
+ * MCP host (and therefore the LLM and the user).
13
+ *
14
+ * Implementation note: Verified end-to-end on HPP Sepolia 2026-05-06,
15
+ * see ../noosphere-evm/.claude/tasks/x402-mcp-wallet/design/DEPLOYMENTS.md
16
+ */
17
+ import { type Address, type Hex } from "viem";
18
+ import type { Signer } from "./signer.js";
19
+ export interface AutoTopupOptions {
20
+ /** PoC-only: explicit pull amount (atomic). If unset, a dynamic value
21
+ * based on `requiredAtomic` × headroom is used. */
22
+ fixedAmountAtomic?: bigint;
23
+ /** Headroom multiplier for dynamic topup. Default 10. */
24
+ headroomX?: bigint;
25
+ }
26
+ export declare class AutoTopup {
27
+ private readonly signer;
28
+ private readonly safe;
29
+ private readonly allowanceModule;
30
+ private readonly token;
31
+ private readonly chainId;
32
+ private readonly rpcUrl;
33
+ private readonly opts;
34
+ private readonly publicClient;
35
+ private readonly walletClient;
36
+ constructor(signer: Signer, safe: Address, allowanceModule: Address, token: Address, chainId: number, rpcUrl: string, opts?: AutoTopupOptions);
37
+ /**
38
+ * Ensure the delegate holds at least `requiredAtomic` of `token`.
39
+ * No-op when balance already sufficient. Returns the topup tx hash
40
+ * (or null if no topup was needed).
41
+ */
42
+ ensure(requiredAtomic: bigint): Promise<Hex | null>;
43
+ /** Read delegate's current ERC-20 balance. */
44
+ private balanceOfDelegate;
45
+ /** allowance.amount - allowance.spent (after on-chain auto-reset, which
46
+ * AllowanceModule applies inside getTokenAllowance). */
47
+ private remainingAllowance;
48
+ /** Sign + send AllowanceModule.executeAllowanceTransfer. */
49
+ private execute;
50
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * AutoTopup — pulls USDC.e from the user's Safe to the delegate EOA via
3
+ * AllowanceModule.executeAllowanceTransfer.
4
+ *
5
+ * Triggered lazily before each x402 payment when the delegate's balance
6
+ * is insufficient. The chain enforces the daily cap; if a topup would
7
+ * exceed the allowance the call reverts with
8
+ *
9
+ * "newSpent > allowance.spent && newSpent <= allowance.amount"
10
+ *
11
+ * which the bridge maps to a clear "spend cap exceeded" error for the
12
+ * MCP host (and therefore the LLM and the user).
13
+ *
14
+ * Implementation note: Verified end-to-end on HPP Sepolia 2026-05-06,
15
+ * see ../noosphere-evm/.claude/tasks/x402-mcp-wallet/design/DEPLOYMENTS.md
16
+ */
17
+ import { createPublicClient, createWalletClient, http, defineChain, } from "viem";
18
+ import { RawEoaSigner } from "./signers/raw-eoa.js";
19
+ const ERC20_BALANCE_ABI = [
20
+ {
21
+ type: "function",
22
+ name: "balanceOf",
23
+ inputs: [{ name: "account", type: "address" }],
24
+ outputs: [{ name: "", type: "uint256" }],
25
+ stateMutability: "view",
26
+ },
27
+ ];
28
+ const ALLOWANCE_ABI = [
29
+ {
30
+ type: "function",
31
+ name: "getTokenAllowance",
32
+ inputs: [
33
+ { name: "safe", type: "address" },
34
+ { name: "delegate", type: "address" },
35
+ { name: "token", type: "address" },
36
+ ],
37
+ outputs: [{ name: "", type: "uint256[5]" }],
38
+ stateMutability: "view",
39
+ },
40
+ {
41
+ type: "function",
42
+ name: "executeAllowanceTransfer",
43
+ inputs: [
44
+ { name: "safe", type: "address" },
45
+ { name: "token", type: "address" },
46
+ { name: "to", type: "address" },
47
+ { name: "amount", type: "uint96" },
48
+ { name: "paymentToken", type: "address" },
49
+ { name: "payment", type: "uint96" },
50
+ { name: "delegate", type: "address" },
51
+ { name: "signature", type: "bytes" },
52
+ ],
53
+ outputs: [],
54
+ stateMutability: "nonpayable",
55
+ },
56
+ ];
57
+ export class AutoTopup {
58
+ signer;
59
+ safe;
60
+ allowanceModule;
61
+ token;
62
+ chainId;
63
+ rpcUrl;
64
+ opts;
65
+ publicClient;
66
+ walletClient;
67
+ constructor(signer, safe, allowanceModule, token, chainId, rpcUrl, opts = {}) {
68
+ this.signer = signer;
69
+ this.safe = safe;
70
+ this.allowanceModule = allowanceModule;
71
+ this.token = token;
72
+ this.chainId = chainId;
73
+ this.rpcUrl = rpcUrl;
74
+ this.opts = opts;
75
+ if (!(signer instanceof RawEoaSigner)) {
76
+ // The contract calls below require a viem-account-shaped signer.
77
+ // The Signer interface itself doesn't yet expose `sendTransaction`,
78
+ // so the autoTopup path currently requires RawEoaSigner. When new
79
+ // signer kinds land we'll widen this through a `tx.send` helper.
80
+ throw new Error("AutoTopup currently requires RawEoaSigner; other signer types pending");
81
+ }
82
+ const chain = defineChain({
83
+ id: chainId,
84
+ name: `chain-${chainId}`,
85
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
86
+ rpcUrls: { default: { http: [rpcUrl] } },
87
+ });
88
+ this.publicClient = createPublicClient({ chain, transport: http(rpcUrl) });
89
+ this.walletClient = createWalletClient({
90
+ chain,
91
+ transport: http(rpcUrl),
92
+ account: signer.viemAccount,
93
+ });
94
+ }
95
+ /**
96
+ * Ensure the delegate holds at least `requiredAtomic` of `token`.
97
+ * No-op when balance already sufficient. Returns the topup tx hash
98
+ * (or null if no topup was needed).
99
+ */
100
+ async ensure(requiredAtomic) {
101
+ const balance = await this.balanceOfDelegate();
102
+ if (balance >= requiredAtomic)
103
+ return null;
104
+ const need = requiredAtomic - balance;
105
+ const headroom = this.opts.headroomX ?? 10n;
106
+ const wanted = this.opts.fixedAmountAtomic ?? need * headroom > 0n
107
+ ? this.opts.fixedAmountAtomic ?? need * headroom
108
+ : need;
109
+ // Cap at remaining allowance to avoid the (otherwise inevitable) revert.
110
+ const remaining = await this.remainingAllowance();
111
+ const amount = wanted > remaining ? remaining : wanted;
112
+ if (amount < need) {
113
+ throw new Error(`spend cap exceeded — required ${requiredAtomic} but only ${balance + remaining} available before allowance reset`);
114
+ }
115
+ return this.execute(amount);
116
+ }
117
+ /** Read delegate's current ERC-20 balance. */
118
+ async balanceOfDelegate() {
119
+ return (await this.publicClient.readContract({
120
+ address: this.token,
121
+ abi: ERC20_BALANCE_ABI,
122
+ functionName: "balanceOf",
123
+ args: [this.signer.address],
124
+ }));
125
+ }
126
+ /** allowance.amount - allowance.spent (after on-chain auto-reset, which
127
+ * AllowanceModule applies inside getTokenAllowance). */
128
+ async remainingAllowance() {
129
+ const a = (await this.publicClient.readContract({
130
+ address: this.allowanceModule,
131
+ abi: ALLOWANCE_ABI,
132
+ functionName: "getTokenAllowance",
133
+ args: [this.safe, this.signer.address, this.token],
134
+ }));
135
+ if (a[0] === 0n)
136
+ return 0n;
137
+ return a[1] >= a[0] ? 0n : a[0] - a[1];
138
+ }
139
+ /** Sign + send AllowanceModule.executeAllowanceTransfer. */
140
+ async execute(amountAtomic) {
141
+ const a = (await this.publicClient.readContract({
142
+ address: this.allowanceModule,
143
+ abi: ALLOWANCE_ABI,
144
+ functionName: "getTokenAllowance",
145
+ args: [this.safe, this.signer.address, this.token],
146
+ }));
147
+ const nonce = Number(a[4]);
148
+ const signature = await this.signer.signTypedData({
149
+ domain: { chainId: this.chainId, verifyingContract: this.allowanceModule },
150
+ types: {
151
+ AllowanceTransfer: [
152
+ { name: "safe", type: "address" },
153
+ { name: "token", type: "address" },
154
+ { name: "to", type: "address" },
155
+ { name: "amount", type: "uint96" },
156
+ { name: "paymentToken", type: "address" },
157
+ { name: "payment", type: "uint96" },
158
+ { name: "nonce", type: "uint16" },
159
+ ],
160
+ },
161
+ primaryType: "AllowanceTransfer",
162
+ message: {
163
+ safe: this.safe,
164
+ token: this.token,
165
+ to: this.signer.address,
166
+ amount: amountAtomic,
167
+ paymentToken: "0x0000000000000000000000000000000000000000",
168
+ payment: 0n,
169
+ nonce,
170
+ },
171
+ });
172
+ const txHash = await this.walletClient.writeContract({
173
+ address: this.allowanceModule,
174
+ abi: ALLOWANCE_ABI,
175
+ functionName: "executeAllowanceTransfer",
176
+ args: [
177
+ this.safe,
178
+ this.token,
179
+ this.signer.address,
180
+ amountAtomic,
181
+ "0x0000000000000000000000000000000000000000",
182
+ 0n,
183
+ this.signer.address,
184
+ signature,
185
+ ],
186
+ chain: null,
187
+ account: this.signer.viemAccount,
188
+ });
189
+ await this.publicClient.waitForTransactionReceipt({ hash: txHash });
190
+ return txHash;
191
+ }
192
+ }
193
+ //# sourceMappingURL=autoTopup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoTopup.js","sourceRoot":"","sources":["../src/autoTopup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ export declare function configPath(): string;
2
+ export interface InstallEnv {
3
+ DELEGATE_PRIVATE_KEY: string;
4
+ SAFE_ADDRESS: string;
5
+ ALLOWANCE_MODULE_ADDRESS: string;
6
+ USDCE_ADDRESS: string;
7
+ RESOURCE_SERVER_URL: string;
8
+ HPP_RPC_URL: string;
9
+ HPP_NETWORK: string;
10
+ LOG_LEVEL?: string;
11
+ }
12
+ export interface InstallResult {
13
+ configPath: string;
14
+ backupPath: string | null;
15
+ changed: boolean;
16
+ preservedEntries: string[];
17
+ }
18
+ /**
19
+ * Merge an `hpp-x402` server entry into the host config and write it.
20
+ * `command`/`args` default to launching the published bridge via npx.
21
+ */
22
+ export declare function installClaudeDesktop(env: InstallEnv, opts?: {
23
+ force?: boolean;
24
+ bridgeAbsPath?: string;
25
+ }): InstallResult;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Merge an `hpp-x402` MCP server entry into the user's
3
+ * claude_desktop_config.json. Importable function (used by setup CLI
4
+ * with --install-claude) and a standalone bin entry.
5
+ *
6
+ * Preserves all other mcpServers entries; backs up the original.
7
+ */
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, } from "node:fs";
9
+ import { resolve, dirname } from "node:path";
10
+ import { homedir, platform } from "node:os";
11
+ const ENTRY_NAME = "hpp-x402";
12
+ export function configPath() {
13
+ const p = platform();
14
+ if (p === "darwin") {
15
+ return resolve(homedir(), "Library/Application Support/Claude/claude_desktop_config.json");
16
+ }
17
+ if (p === "win32") {
18
+ return resolve(process.env.APPDATA ?? "", "Claude/claude_desktop_config.json");
19
+ }
20
+ return resolve(homedir(), ".config/Claude/claude_desktop_config.json");
21
+ }
22
+ /**
23
+ * Merge an `hpp-x402` server entry into the host config and write it.
24
+ * `command`/`args` default to launching the published bridge via npx.
25
+ */
26
+ export function installClaudeDesktop(env, opts = {}) {
27
+ const path = configPath();
28
+ let existing = {};
29
+ if (existsSync(path)) {
30
+ existing = JSON.parse(readFileSync(path, "utf8"));
31
+ }
32
+ else {
33
+ mkdirSync(dirname(path), { recursive: true });
34
+ }
35
+ const command = opts.bridgeAbsPath ? "node" : "npx";
36
+ const args = opts.bridgeAbsPath
37
+ ? [opts.bridgeAbsPath]
38
+ : ["-y", "@hpp-io/x402-mcp-bridge"];
39
+ const entry = {
40
+ command,
41
+ args,
42
+ env: {
43
+ DELEGATE_PRIVATE_KEY: env.DELEGATE_PRIVATE_KEY,
44
+ SAFE_ADDRESS: env.SAFE_ADDRESS,
45
+ ALLOWANCE_MODULE_ADDRESS: env.ALLOWANCE_MODULE_ADDRESS,
46
+ USDCE_ADDRESS: env.USDCE_ADDRESS,
47
+ RESOURCE_SERVER_URL: env.RESOURCE_SERVER_URL,
48
+ HPP_RPC_URL: env.HPP_RPC_URL,
49
+ HPP_NETWORK: env.HPP_NETWORK,
50
+ LOG_LEVEL: env.LOG_LEVEL ?? "info",
51
+ },
52
+ };
53
+ const mcpServers = (existing.mcpServers ??= {});
54
+ const prior = mcpServers[ENTRY_NAME];
55
+ if (prior && JSON.stringify(prior) === JSON.stringify(entry)) {
56
+ return {
57
+ configPath: path,
58
+ backupPath: null,
59
+ changed: false,
60
+ preservedEntries: Object.keys(mcpServers).filter((k) => k !== ENTRY_NAME),
61
+ };
62
+ }
63
+ if (prior && !opts.force) {
64
+ throw new Error(`existing hpp-x402 entry differs; pass force:true (or --force in CLI) to overwrite`);
65
+ }
66
+ mcpServers[ENTRY_NAME] = entry;
67
+ const merged = { ...existing, mcpServers };
68
+ let backupPath = null;
69
+ if (existsSync(path)) {
70
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
71
+ backupPath = path + "." + ts + ".bak";
72
+ copyFileSync(path, backupPath);
73
+ }
74
+ writeFileSync(path, JSON.stringify(merged, null, 2) + "\n", "utf8");
75
+ return {
76
+ configPath: path,
77
+ backupPath,
78
+ changed: true,
79
+ preservedEntries: Object.keys(mcpServers).filter((k) => k !== ENTRY_NAME),
80
+ };
81
+ }
82
+ //# sourceMappingURL=install-claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-claude.js","sourceRoot":"","sources":["../../src/cli/install-claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,123 @@
1
+ import { buildClients, execSafeTx, encodeDeleteAllowance, } from "./safe.js";
2
+ const DEFAULTS = {
3
+ "eip155:181228": {
4
+ rpc: "https://sepolia.hpp.io",
5
+ usdc: "0x401eCb1D350407f13ba348573E5630B83638E30D",
6
+ module: "0x3CcE72483929e0517Dafc8fD192547B3B65f9b07",
7
+ chainId: 181228,
8
+ },
9
+ };
10
+ function parseArgs(argv) {
11
+ const out = {};
12
+ for (let i = 2; i < argv.length; i++) {
13
+ const a = argv[i];
14
+ if (!a.startsWith("--"))
15
+ continue;
16
+ const k = a.slice(2);
17
+ const next = argv[i + 1];
18
+ if (!next || next.startsWith("--")) {
19
+ out[k] = true;
20
+ }
21
+ else {
22
+ out[k] = next;
23
+ i++;
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ function requireStr(a, k) {
29
+ const v = a[k];
30
+ if (typeof v !== "string" || v.length === 0) {
31
+ console.error(`missing --${k}`);
32
+ process.exit(1);
33
+ }
34
+ return v;
35
+ }
36
+ const ERC20_ABI = [
37
+ {
38
+ type: "function",
39
+ name: "balanceOf",
40
+ inputs: [{ name: "account", type: "address" }],
41
+ outputs: [{ name: "", type: "uint256" }],
42
+ stateMutability: "view",
43
+ },
44
+ {
45
+ type: "function",
46
+ name: "transfer",
47
+ inputs: [
48
+ { name: "to", type: "address" },
49
+ { name: "amount", type: "uint256" },
50
+ ],
51
+ outputs: [{ name: "", type: "bool" }],
52
+ stateMutability: "nonpayable",
53
+ },
54
+ ];
55
+ async function main() {
56
+ const args = parseArgs(process.argv);
57
+ const network = args.network || "eip155:181228";
58
+ const def = DEFAULTS[network];
59
+ if (!def) {
60
+ console.error(`unsupported network: ${network}`);
61
+ process.exit(1);
62
+ }
63
+ const ownerPk = requireStr(args, "owner-pk");
64
+ const safe = requireStr(args, "safe");
65
+ const delegate = requireStr(args, "delegate");
66
+ const rpc = args.rpc || def.rpc;
67
+ const usdc = (args.usdc || def.usdc);
68
+ const module = (args.module || def.module);
69
+ const drain = args.drain === true;
70
+ const delegatePk = args["delegate-pk"];
71
+ if (drain && !delegatePk) {
72
+ console.error("--drain requires --delegate-pk to sign the transfer back");
73
+ process.exit(1);
74
+ }
75
+ const cfg = { rpcUrl: rpc, chainId: def.chainId };
76
+ console.log(`hpp-x402-safe-revoke`);
77
+ console.log(` Safe : ${safe}`);
78
+ console.log(` Delegate : ${delegate}`);
79
+ console.log(` Mode : ${drain ? "deleteAllowance + drain" : "deleteAllowance only"}`);
80
+ console.log("");
81
+ // ---- 1) deleteAllowance (Safe Tx, owner signs) ---------------------
82
+ console.log("[1/2] AllowanceModule.deleteAllowance…");
83
+ const deleteTx = await execSafeTx(cfg, ownerPk, safe, module, encodeDeleteAllowance(delegate, usdc));
84
+ console.log(` ✓ deleted (Safe tx ${deleteTx})`);
85
+ // ---- 2) (optional) drain delegate's remaining USDC.e ---------------
86
+ if (drain) {
87
+ console.log("[2/2] Draining delegate's USDC.e back to Safe…");
88
+ const { publicClient, walletClient } = buildClients(cfg, delegatePk);
89
+ const balance = (await publicClient.readContract({
90
+ address: usdc,
91
+ abi: ERC20_ABI,
92
+ functionName: "balanceOf",
93
+ args: [delegate],
94
+ }));
95
+ if (balance === 0n) {
96
+ console.log(" ✓ delegate already empty — nothing to drain");
97
+ }
98
+ else {
99
+ const tx = await walletClient.writeContract({
100
+ address: usdc,
101
+ abi: ERC20_ABI,
102
+ functionName: "transfer",
103
+ args: [safe, balance],
104
+ });
105
+ await publicClient.waitForTransactionReceipt({ hash: tx });
106
+ console.log(` ✓ drained ${Number(balance) / 1e6} USDC.e (tx ${tx})`);
107
+ }
108
+ }
109
+ else {
110
+ console.log("[2/2] Skipped drain (no --drain flag).");
111
+ }
112
+ console.log("\n🎉 revocation complete.");
113
+ console.log("\nThe delegate can no longer pull from the Safe via AllowanceModule.");
114
+ if (!drain) {
115
+ console.log("⚠ delegate may still hold leftover USDC.e it pulled earlier.");
116
+ console.log(" Run again with --drain --delegate-pk 0x... to recover that balance.");
117
+ }
118
+ }
119
+ main().catch((err) => {
120
+ console.error("\nERROR:", err?.message ?? err);
121
+ process.exit(1);
122
+ });
123
+ //# sourceMappingURL=revoke.js.map