arc402-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/README.md +245 -0
  2. package/dist/abis.d.ts +19 -0
  3. package/dist/abis.d.ts.map +1 -0
  4. package/dist/abis.js +177 -0
  5. package/dist/abis.js.map +1 -0
  6. package/dist/bundler.d.ts +65 -0
  7. package/dist/bundler.d.ts.map +1 -0
  8. package/dist/bundler.js +181 -0
  9. package/dist/bundler.js.map +1 -0
  10. package/dist/client.d.ts +14 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +24 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/coinbase-smart-wallet.d.ts +28 -0
  15. package/dist/coinbase-smart-wallet.d.ts.map +1 -0
  16. package/dist/coinbase-smart-wallet.js +38 -0
  17. package/dist/coinbase-smart-wallet.js.map +1 -0
  18. package/dist/commands/accept.d.ts +3 -0
  19. package/dist/commands/accept.d.ts.map +1 -0
  20. package/dist/commands/accept.js +26 -0
  21. package/dist/commands/accept.js.map +1 -0
  22. package/dist/commands/agent-handshake.d.ts +3 -0
  23. package/dist/commands/agent-handshake.d.ts.map +1 -0
  24. package/dist/commands/agent-handshake.js +61 -0
  25. package/dist/commands/agent-handshake.js.map +1 -0
  26. package/dist/commands/agent.d.ts +3 -0
  27. package/dist/commands/agent.d.ts.map +1 -0
  28. package/dist/commands/agent.js +417 -0
  29. package/dist/commands/agent.js.map +1 -0
  30. package/dist/commands/agreements.d.ts +3 -0
  31. package/dist/commands/agreements.d.ts.map +1 -0
  32. package/dist/commands/agreements.js +344 -0
  33. package/dist/commands/agreements.js.map +1 -0
  34. package/dist/commands/arbitrator.d.ts +3 -0
  35. package/dist/commands/arbitrator.d.ts.map +1 -0
  36. package/dist/commands/arbitrator.js +157 -0
  37. package/dist/commands/arbitrator.js.map +1 -0
  38. package/dist/commands/arena-handshake.d.ts +3 -0
  39. package/dist/commands/arena-handshake.d.ts.map +1 -0
  40. package/dist/commands/arena-handshake.js +187 -0
  41. package/dist/commands/arena-handshake.js.map +1 -0
  42. package/dist/commands/cancel.d.ts +3 -0
  43. package/dist/commands/cancel.d.ts.map +1 -0
  44. package/dist/commands/cancel.js +30 -0
  45. package/dist/commands/cancel.js.map +1 -0
  46. package/dist/commands/channel.d.ts +3 -0
  47. package/dist/commands/channel.d.ts.map +1 -0
  48. package/dist/commands/channel.js +238 -0
  49. package/dist/commands/channel.js.map +1 -0
  50. package/dist/commands/coldstart.d.ts +3 -0
  51. package/dist/commands/coldstart.d.ts.map +1 -0
  52. package/dist/commands/coldstart.js +148 -0
  53. package/dist/commands/coldstart.js.map +1 -0
  54. package/dist/commands/config.d.ts +3 -0
  55. package/dist/commands/config.d.ts.map +1 -0
  56. package/dist/commands/config.js +40 -0
  57. package/dist/commands/config.js.map +1 -0
  58. package/dist/commands/contract-interaction.d.ts +3 -0
  59. package/dist/commands/contract-interaction.d.ts.map +1 -0
  60. package/dist/commands/contract-interaction.js +165 -0
  61. package/dist/commands/contract-interaction.js.map +1 -0
  62. package/dist/commands/daemon.d.ts +3 -0
  63. package/dist/commands/daemon.d.ts.map +1 -0
  64. package/dist/commands/daemon.js +891 -0
  65. package/dist/commands/daemon.js.map +1 -0
  66. package/dist/commands/deliver.d.ts +3 -0
  67. package/dist/commands/deliver.d.ts.map +1 -0
  68. package/dist/commands/deliver.js +156 -0
  69. package/dist/commands/deliver.js.map +1 -0
  70. package/dist/commands/discover.d.ts +3 -0
  71. package/dist/commands/discover.d.ts.map +1 -0
  72. package/dist/commands/discover.js +224 -0
  73. package/dist/commands/discover.js.map +1 -0
  74. package/dist/commands/dispute.d.ts +3 -0
  75. package/dist/commands/dispute.d.ts.map +1 -0
  76. package/dist/commands/dispute.js +348 -0
  77. package/dist/commands/dispute.js.map +1 -0
  78. package/dist/commands/endpoint.d.ts +3 -0
  79. package/dist/commands/endpoint.d.ts.map +1 -0
  80. package/dist/commands/endpoint.js +604 -0
  81. package/dist/commands/endpoint.js.map +1 -0
  82. package/dist/commands/hire.d.ts +3 -0
  83. package/dist/commands/hire.d.ts.map +1 -0
  84. package/dist/commands/hire.js +189 -0
  85. package/dist/commands/hire.js.map +1 -0
  86. package/dist/commands/migrate.d.ts +3 -0
  87. package/dist/commands/migrate.d.ts.map +1 -0
  88. package/dist/commands/migrate.js +163 -0
  89. package/dist/commands/migrate.js.map +1 -0
  90. package/dist/commands/negotiate.d.ts +3 -0
  91. package/dist/commands/negotiate.d.ts.map +1 -0
  92. package/dist/commands/negotiate.js +247 -0
  93. package/dist/commands/negotiate.js.map +1 -0
  94. package/dist/commands/openshell.d.ts +3 -0
  95. package/dist/commands/openshell.d.ts.map +1 -0
  96. package/dist/commands/openshell.js +952 -0
  97. package/dist/commands/openshell.js.map +1 -0
  98. package/dist/commands/owner.d.ts +3 -0
  99. package/dist/commands/owner.d.ts.map +1 -0
  100. package/dist/commands/owner.js +32 -0
  101. package/dist/commands/owner.js.map +1 -0
  102. package/dist/commands/policy.d.ts +4 -0
  103. package/dist/commands/policy.d.ts.map +1 -0
  104. package/dist/commands/policy.js +248 -0
  105. package/dist/commands/policy.js.map +1 -0
  106. package/dist/commands/relay.d.ts +3 -0
  107. package/dist/commands/relay.d.ts.map +1 -0
  108. package/dist/commands/relay.js +279 -0
  109. package/dist/commands/relay.js.map +1 -0
  110. package/dist/commands/remediate.d.ts +3 -0
  111. package/dist/commands/remediate.d.ts.map +1 -0
  112. package/dist/commands/remediate.js +42 -0
  113. package/dist/commands/remediate.js.map +1 -0
  114. package/dist/commands/reputation.d.ts +4 -0
  115. package/dist/commands/reputation.d.ts.map +1 -0
  116. package/dist/commands/reputation.js +72 -0
  117. package/dist/commands/reputation.js.map +1 -0
  118. package/dist/commands/setup.d.ts +3 -0
  119. package/dist/commands/setup.d.ts.map +1 -0
  120. package/dist/commands/setup.js +332 -0
  121. package/dist/commands/setup.js.map +1 -0
  122. package/dist/commands/trust.d.ts +3 -0
  123. package/dist/commands/trust.d.ts.map +1 -0
  124. package/dist/commands/trust.js +23 -0
  125. package/dist/commands/trust.js.map +1 -0
  126. package/dist/commands/verify.d.ts +3 -0
  127. package/dist/commands/verify.d.ts.map +1 -0
  128. package/dist/commands/verify.js +88 -0
  129. package/dist/commands/verify.js.map +1 -0
  130. package/dist/commands/wallet.d.ts +3 -0
  131. package/dist/commands/wallet.d.ts.map +1 -0
  132. package/dist/commands/wallet.js +2520 -0
  133. package/dist/commands/wallet.js.map +1 -0
  134. package/dist/commands/watchtower.d.ts +3 -0
  135. package/dist/commands/watchtower.d.ts.map +1 -0
  136. package/dist/commands/watchtower.js +238 -0
  137. package/dist/commands/watchtower.js.map +1 -0
  138. package/dist/commands/workroom.d.ts +3 -0
  139. package/dist/commands/workroom.d.ts.map +1 -0
  140. package/dist/commands/workroom.js +855 -0
  141. package/dist/commands/workroom.js.map +1 -0
  142. package/dist/config.d.ts +62 -0
  143. package/dist/config.d.ts.map +1 -0
  144. package/dist/config.js +141 -0
  145. package/dist/config.js.map +1 -0
  146. package/dist/daemon/config.d.ts +74 -0
  147. package/dist/daemon/config.d.ts.map +1 -0
  148. package/dist/daemon/config.js +271 -0
  149. package/dist/daemon/config.js.map +1 -0
  150. package/dist/daemon/hire-listener.d.ts +31 -0
  151. package/dist/daemon/hire-listener.d.ts.map +1 -0
  152. package/dist/daemon/hire-listener.js +207 -0
  153. package/dist/daemon/hire-listener.js.map +1 -0
  154. package/dist/daemon/index.d.ts +29 -0
  155. package/dist/daemon/index.d.ts.map +1 -0
  156. package/dist/daemon/index.js +535 -0
  157. package/dist/daemon/index.js.map +1 -0
  158. package/dist/daemon/job-lifecycle.d.ts +62 -0
  159. package/dist/daemon/job-lifecycle.d.ts.map +1 -0
  160. package/dist/daemon/job-lifecycle.js +201 -0
  161. package/dist/daemon/job-lifecycle.js.map +1 -0
  162. package/dist/daemon/notify.d.ts +22 -0
  163. package/dist/daemon/notify.d.ts.map +1 -0
  164. package/dist/daemon/notify.js +148 -0
  165. package/dist/daemon/notify.js.map +1 -0
  166. package/dist/daemon/token-metering.d.ts +42 -0
  167. package/dist/daemon/token-metering.d.ts.map +1 -0
  168. package/dist/daemon/token-metering.js +178 -0
  169. package/dist/daemon/token-metering.js.map +1 -0
  170. package/dist/daemon/userops.d.ts +21 -0
  171. package/dist/daemon/userops.d.ts.map +1 -0
  172. package/dist/daemon/userops.js +88 -0
  173. package/dist/daemon/userops.js.map +1 -0
  174. package/dist/daemon/wallet-monitor.d.ts +16 -0
  175. package/dist/daemon/wallet-monitor.d.ts.map +1 -0
  176. package/dist/daemon/wallet-monitor.js +57 -0
  177. package/dist/daemon/wallet-monitor.js.map +1 -0
  178. package/dist/drain-v4.d.ts +2 -0
  179. package/dist/drain-v4.d.ts.map +1 -0
  180. package/dist/drain-v4.js +167 -0
  181. package/dist/drain-v4.js.map +1 -0
  182. package/dist/endpoint-config.d.ts +36 -0
  183. package/dist/endpoint-config.d.ts.map +1 -0
  184. package/dist/endpoint-config.js +96 -0
  185. package/dist/endpoint-config.js.map +1 -0
  186. package/dist/index.d.ts +3 -0
  187. package/dist/index.d.ts.map +1 -0
  188. package/dist/index.js +79 -0
  189. package/dist/index.js.map +1 -0
  190. package/dist/openshell-runtime.d.ts +55 -0
  191. package/dist/openshell-runtime.d.ts.map +1 -0
  192. package/dist/openshell-runtime.js +268 -0
  193. package/dist/openshell-runtime.js.map +1 -0
  194. package/dist/signing.d.ts +2 -0
  195. package/dist/signing.d.ts.map +1 -0
  196. package/dist/signing.js +23 -0
  197. package/dist/signing.js.map +1 -0
  198. package/dist/telegram-notify.d.ts +23 -0
  199. package/dist/telegram-notify.d.ts.map +1 -0
  200. package/dist/telegram-notify.js +106 -0
  201. package/dist/telegram-notify.js.map +1 -0
  202. package/dist/ui/banner.d.ts +7 -0
  203. package/dist/ui/banner.d.ts.map +1 -0
  204. package/dist/ui/banner.js +37 -0
  205. package/dist/ui/banner.js.map +1 -0
  206. package/dist/ui/colors.d.ts +14 -0
  207. package/dist/ui/colors.d.ts.map +1 -0
  208. package/dist/ui/colors.js +29 -0
  209. package/dist/ui/colors.js.map +1 -0
  210. package/dist/ui/format.d.ts +26 -0
  211. package/dist/ui/format.d.ts.map +1 -0
  212. package/dist/ui/format.js +77 -0
  213. package/dist/ui/format.js.map +1 -0
  214. package/dist/ui/spinner.d.ts +8 -0
  215. package/dist/ui/spinner.d.ts.map +1 -0
  216. package/dist/ui/spinner.js +43 -0
  217. package/dist/ui/spinner.js.map +1 -0
  218. package/dist/utils/format.d.ts +10 -0
  219. package/dist/utils/format.d.ts.map +1 -0
  220. package/dist/utils/format.js +61 -0
  221. package/dist/utils/format.js.map +1 -0
  222. package/dist/utils/hash.d.ts +3 -0
  223. package/dist/utils/hash.d.ts.map +1 -0
  224. package/dist/utils/hash.js +43 -0
  225. package/dist/utils/hash.js.map +1 -0
  226. package/dist/utils/time.d.ts +3 -0
  227. package/dist/utils/time.d.ts.map +1 -0
  228. package/dist/utils/time.js +21 -0
  229. package/dist/utils/time.js.map +1 -0
  230. package/dist/wallet-router.d.ts +25 -0
  231. package/dist/wallet-router.d.ts.map +1 -0
  232. package/dist/wallet-router.js +153 -0
  233. package/dist/wallet-router.js.map +1 -0
  234. package/dist/walletconnect-session.d.ts +12 -0
  235. package/dist/walletconnect-session.d.ts.map +1 -0
  236. package/dist/walletconnect-session.js +26 -0
  237. package/dist/walletconnect-session.js.map +1 -0
  238. package/dist/walletconnect.d.ts +46 -0
  239. package/dist/walletconnect.d.ts.map +1 -0
  240. package/dist/walletconnect.js +267 -0
  241. package/dist/walletconnect.js.map +1 -0
  242. package/package.json +38 -0
  243. package/scripts/authorize-machine-key.ts +43 -0
  244. package/scripts/drain-wallet.ts +149 -0
  245. package/scripts/execute-spend-only.ts +81 -0
  246. package/scripts/register-agent-userop.ts +186 -0
  247. package/src/abis.ts +187 -0
  248. package/src/bundler.ts +235 -0
  249. package/src/client.ts +34 -0
  250. package/src/coinbase-smart-wallet.ts +51 -0
  251. package/src/commands/accept.ts +25 -0
  252. package/src/commands/agent-handshake.ts +67 -0
  253. package/src/commands/agent.ts +458 -0
  254. package/src/commands/agreements.ts +324 -0
  255. package/src/commands/arbitrator.ts +129 -0
  256. package/src/commands/arena-handshake.ts +217 -0
  257. package/src/commands/cancel.ts +26 -0
  258. package/src/commands/channel.ts +208 -0
  259. package/src/commands/coldstart.ts +156 -0
  260. package/src/commands/config.ts +35 -0
  261. package/src/commands/contract-interaction.ts +166 -0
  262. package/src/commands/daemon.ts +971 -0
  263. package/src/commands/deliver.ts +116 -0
  264. package/src/commands/discover.ts +295 -0
  265. package/src/commands/dispute.ts +373 -0
  266. package/src/commands/endpoint.ts +619 -0
  267. package/src/commands/hire.ts +200 -0
  268. package/src/commands/migrate.ts +175 -0
  269. package/src/commands/negotiate.ts +270 -0
  270. package/src/commands/openshell.ts +1053 -0
  271. package/src/commands/owner.ts +30 -0
  272. package/src/commands/policy.ts +252 -0
  273. package/src/commands/relay.ts +272 -0
  274. package/src/commands/remediate.ts +22 -0
  275. package/src/commands/reputation.ts +71 -0
  276. package/src/commands/setup.ts +343 -0
  277. package/src/commands/trust.ts +15 -0
  278. package/src/commands/verify.ts +88 -0
  279. package/src/commands/wallet.ts +2892 -0
  280. package/src/commands/watchtower.ts +232 -0
  281. package/src/commands/workroom.ts +889 -0
  282. package/src/config.ts +153 -0
  283. package/src/daemon/config.ts +308 -0
  284. package/src/daemon/hire-listener.ts +226 -0
  285. package/src/daemon/index.ts +609 -0
  286. package/src/daemon/job-lifecycle.ts +215 -0
  287. package/src/daemon/notify.ts +157 -0
  288. package/src/daemon/token-metering.ts +183 -0
  289. package/src/daemon/userops.ts +119 -0
  290. package/src/daemon/wallet-monitor.ts +90 -0
  291. package/src/drain-v4.ts +159 -0
  292. package/src/endpoint-config.ts +83 -0
  293. package/src/index.ts +75 -0
  294. package/src/openshell-runtime.ts +277 -0
  295. package/src/signing.ts +28 -0
  296. package/src/telegram-notify.ts +88 -0
  297. package/src/ui/banner.ts +41 -0
  298. package/src/ui/colors.ts +30 -0
  299. package/src/ui/format.ts +77 -0
  300. package/src/ui/spinner.ts +46 -0
  301. package/src/utils/format.ts +48 -0
  302. package/src/utils/hash.ts +5 -0
  303. package/src/utils/time.ts +15 -0
  304. package/src/wallet-router.ts +178 -0
  305. package/src/walletconnect-session.ts +27 -0
  306. package/src/walletconnect.ts +294 -0
  307. package/test/time.test.js +11 -0
  308. package/tsconfig.json +19 -0
@@ -0,0 +1,2520 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerWalletCommands = registerWalletCommands;
7
+ const sdk_1 = require("@arc402/sdk");
8
+ const ethers_1 = require("ethers");
9
+ const prompts_1 = __importDefault(require("prompts"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const child_process_1 = require("child_process");
14
+ const config_1 = require("../config");
15
+ const client_1 = require("../client");
16
+ const format_1 = require("../utils/format");
17
+ const abis_1 = require("../abis");
18
+ const config_2 = require("../config");
19
+ const walletconnect_1 = require("../walletconnect");
20
+ const bundler_1 = require("../bundler");
21
+ const walletconnect_session_1 = require("../walletconnect-session");
22
+ const wallet_router_1 = require("../wallet-router");
23
+ const coinbase_smart_wallet_1 = require("../coinbase-smart-wallet");
24
+ const telegram_notify_1 = require("../telegram-notify");
25
+ const POLICY_ENGINE_DEFAULT = "0x44102e70c2A366632d98Fe40d892a2501fC7fFF2";
26
+ function parseAmount(raw) {
27
+ const lower = raw.toLowerCase();
28
+ if (lower.endsWith("eth")) {
29
+ return ethers_1.ethers.parseEther(lower.slice(0, -3).trim());
30
+ }
31
+ return BigInt(raw);
32
+ }
33
+ // Standard onboarding categories required for a newly deployed wallet.
34
+ const ONBOARDING_CATEGORIES = [
35
+ { name: "general", amountEth: "0.001" },
36
+ { name: "compute", amountEth: "0.05" },
37
+ { name: "research", amountEth: "0.05" },
38
+ { name: "protocol", amountEth: "0.1" },
39
+ ];
40
+ /**
41
+ * P0: Mandatory post-deploy onboarding ceremony.
42
+ * Registers wallet on PolicyEngine, enables DeFi access, and sets required spend limits.
43
+ * Uses a sendTx callback so it works with any signing method (WalletConnect, private key, etc.).
44
+ * Skips registerWallet/enableDeFiAccess if they were already done by the wallet constructor.
45
+ */
46
+ async function runWalletOnboardingCeremony(walletAddress, ownerAddress, config, provider, sendTx) {
47
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
48
+ const executeIface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_EXECUTE_ABI);
49
+ const govIface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_GOVERNANCE_ABI);
50
+ const limitsIface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_LIMITS_ABI);
51
+ const policyGov = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_GOVERNANCE_ABI, provider);
52
+ // Check what's already done (constructor may have done registerWallet + enableDefiAccess)
53
+ let alreadyRegistered = false;
54
+ let alreadyDefiEnabled = false;
55
+ try {
56
+ const registeredOwner = await policyGov.walletOwners(walletAddress);
57
+ alreadyRegistered = registeredOwner !== ethers_1.ethers.ZeroAddress;
58
+ }
59
+ catch { /* older PolicyEngine without this getter — assume not registered */ }
60
+ try {
61
+ alreadyDefiEnabled = await policyGov.defiAccessEnabled(walletAddress);
62
+ }
63
+ catch { /* assume not enabled */ }
64
+ console.log("\n── Onboarding ceremony ────────────────────────────────────────");
65
+ console.log(` PolicyEngine: ${policyAddress}`);
66
+ console.log(` Wallet: ${walletAddress}`);
67
+ // Step 1: registerWallet (if not already done)
68
+ if (!alreadyRegistered) {
69
+ const registerCalldata = govIface.encodeFunctionData("registerWallet", [walletAddress, ownerAddress]);
70
+ await sendTx({
71
+ to: walletAddress,
72
+ data: executeIface.encodeFunctionData("executeContractCall", [{
73
+ target: policyAddress,
74
+ data: registerCalldata,
75
+ value: 0n,
76
+ minReturnValue: 0n,
77
+ maxApprovalAmount: 0n,
78
+ approvalToken: ethers_1.ethers.ZeroAddress,
79
+ }]),
80
+ value: "0x0",
81
+ }, "registerWallet on PolicyEngine");
82
+ }
83
+ else {
84
+ console.log(" ✓ registerWallet — already done by constructor");
85
+ }
86
+ // Step 2: enableDefiAccess (if not already done)
87
+ if (!alreadyDefiEnabled) {
88
+ await sendTx({
89
+ to: policyAddress,
90
+ data: govIface.encodeFunctionData("enableDefiAccess", [walletAddress]),
91
+ value: "0x0",
92
+ }, "enableDefiAccess on PolicyEngine");
93
+ }
94
+ else {
95
+ console.log(" ✓ enableDefiAccess — already done by constructor");
96
+ }
97
+ // Steps 3–6: category limits (always set — idempotent)
98
+ for (const { name, amountEth } of ONBOARDING_CATEGORIES) {
99
+ await sendTx({
100
+ to: policyAddress,
101
+ data: limitsIface.encodeFunctionData("setCategoryLimitFor", [
102
+ walletAddress,
103
+ name,
104
+ ethers_1.ethers.parseEther(amountEth),
105
+ ]),
106
+ value: "0x0",
107
+ }, `setCategoryLimitFor: ${name} → ${amountEth} ETH`);
108
+ }
109
+ console.log("── Onboarding complete ─────────────────────────────────────────");
110
+ console.log("💡 Tip: For production security, also configure:");
111
+ console.log(" arc402 wallet set-velocity-limit <eth> — wallet-level hourly ETH cap");
112
+ console.log(" arc402 wallet policy set-daily-limit --category general --amount <eth> — daily per-category cap");
113
+ }
114
+ function printOpenShellHint() {
115
+ const r = (0, child_process_1.spawnSync)("which", ["openshell"], { encoding: "utf-8" });
116
+ if (r.status === 0 && r.stdout.trim()) {
117
+ console.log("\nOpenShell detected. Run: arc402 openshell init");
118
+ }
119
+ else {
120
+ console.log("\nOptional: install OpenShell for sandboxed execution: arc402 openshell install");
121
+ }
122
+ }
123
+ function registerWalletCommands(program) {
124
+ const wallet = program.command("wallet").description("Wallet utilities");
125
+ // ─── status ────────────────────────────────────────────────────────────────
126
+ wallet.command("status").description("Show address, balances, contract wallet, guardian, and frozen status").option("--json").action(async (opts) => {
127
+ const config = (0, config_1.loadConfig)();
128
+ const { provider, address } = await (0, client_1.getClient)(config);
129
+ if (!address)
130
+ throw new Error("No wallet configured");
131
+ const usdcAddress = (0, config_1.getUsdcAddress)(config);
132
+ const usdc = new ethers_1.ethers.Contract(usdcAddress, ["function balanceOf(address owner) external view returns (uint256)"], provider);
133
+ const trust = new sdk_1.TrustClient(config.trustRegistryAddress, provider);
134
+ const [ethBalance, usdcBalance, score] = await Promise.all([
135
+ provider.getBalance(address),
136
+ usdc.balanceOf(address),
137
+ trust.getScore(address),
138
+ ]);
139
+ // Query contract wallet for frozen/guardian state if deployed
140
+ let contractFrozen = null;
141
+ let contractGuardian = null;
142
+ if (config.walletContractAddress) {
143
+ try {
144
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_GUARDIAN_ABI, provider);
145
+ [contractFrozen, contractGuardian] = await Promise.all([
146
+ walletContract.frozen(),
147
+ walletContract.guardian(),
148
+ ]);
149
+ }
150
+ catch { /* contract may not be deployed yet */ }
151
+ }
152
+ const payload = {
153
+ address,
154
+ network: config.network,
155
+ ethBalance: ethers_1.ethers.formatEther(ethBalance),
156
+ usdcBalance: (Number(usdcBalance) / 1e6).toFixed(2),
157
+ trustScore: score.score,
158
+ trustTier: (0, format_1.getTrustTier)(score.score),
159
+ walletContractAddress: config.walletContractAddress ?? null,
160
+ frozen: contractFrozen,
161
+ guardian: contractGuardian,
162
+ guardianAddress: config.guardianAddress ?? null,
163
+ };
164
+ if (opts.json) {
165
+ console.log(JSON.stringify(payload, null, 2));
166
+ }
167
+ else {
168
+ console.log(`${payload.address}\nETH=${payload.ethBalance}\nUSDC=${payload.usdcBalance}\nTrust=${payload.trustScore} ${payload.trustTier}`);
169
+ if (payload.walletContractAddress)
170
+ console.log(`Contract=${payload.walletContractAddress}`);
171
+ if (contractFrozen !== null)
172
+ console.log(`Frozen=${contractFrozen}`);
173
+ if (contractGuardian && contractGuardian !== ethers_1.ethers.ZeroAddress)
174
+ console.log(`Guardian=${contractGuardian}`);
175
+ }
176
+ });
177
+ // ─── wc-reset ──────────────────────────────────────────────────────────────
178
+ //
179
+ // Clears the saved WalletConnect session from config AND wipes the WC SDK
180
+ // storage file (~/.arc402/wc-storage.json). Use when MetaMask killed the
181
+ // session on its end and the CLI is stuck trying to resume a dead connection.
182
+ // Next wallet command will trigger a fresh QR pairing flow.
183
+ wallet.command("wc-reset")
184
+ .description("Clear stale WalletConnect session — forces a fresh QR pairing on next connection")
185
+ .option("--json")
186
+ .action(async (opts) => {
187
+ const config = (0, config_1.loadConfig)();
188
+ const hadSession = !!config.wcSession;
189
+ // 1. Clear from config
190
+ (0, walletconnect_session_1.clearWCSession)(config);
191
+ // 2. Wipe WC SDK storage (may be a file or a directory depending on SDK version)
192
+ const wcStoragePath = path_1.default.join(os_1.default.homedir(), ".arc402", "wc-storage.json");
193
+ let storageWiped = false;
194
+ try {
195
+ if (fs_1.default.existsSync(wcStoragePath)) {
196
+ fs_1.default.rmSync(wcStoragePath, { recursive: true, force: true });
197
+ storageWiped = true;
198
+ }
199
+ }
200
+ catch (e) {
201
+ const msg = e instanceof Error ? e.message : String(e);
202
+ if (opts.json) {
203
+ console.log(JSON.stringify({ ok: false, error: `Could not delete ${wcStoragePath}: ${msg}` }));
204
+ }
205
+ else {
206
+ console.warn(`⚠ Could not delete ${wcStoragePath}: ${msg}`);
207
+ console.warn(" You may need to delete it manually.");
208
+ }
209
+ return;
210
+ }
211
+ if (opts.json) {
212
+ console.log(JSON.stringify({ ok: true, hadSession, storageWiped }));
213
+ }
214
+ else {
215
+ console.log("✓ WalletConnect session cleared");
216
+ if (storageWiped)
217
+ console.log(` Storage wiped: ${wcStoragePath}`);
218
+ else
219
+ console.log(" (No storage file found — already clean)");
220
+ console.log("\nNext: run any wallet command and scan the fresh QR code.");
221
+ }
222
+ });
223
+ // ─── new ───────────────────────────────────────────────────────────────────
224
+ wallet.command("new")
225
+ .description("Generate a fresh keypair and save to config")
226
+ .option("--network <network>", "Network (base-mainnet or base-sepolia)", "base-sepolia")
227
+ .action(async (opts) => {
228
+ const network = opts.network;
229
+ const defaults = config_1.NETWORK_DEFAULTS[network];
230
+ if (!defaults) {
231
+ console.error(`Unknown network: ${network}. Use base-mainnet or base-sepolia.`);
232
+ process.exit(1);
233
+ }
234
+ const generated = ethers_1.ethers.Wallet.createRandom();
235
+ const config = {
236
+ network,
237
+ rpcUrl: defaults.rpcUrl,
238
+ privateKey: generated.privateKey,
239
+ trustRegistryAddress: defaults.trustRegistryAddress,
240
+ walletFactoryAddress: defaults.walletFactoryAddress,
241
+ };
242
+ (0, config_1.saveConfig)(config);
243
+ console.log(`Address: ${generated.address}`);
244
+ console.log(`Config saved to ${(0, config_1.getConfigPath)()}`);
245
+ console.log(`Next: fund your wallet with ETH, then run: arc402 wallet deploy`);
246
+ });
247
+ // ─── import ────────────────────────────────────────────────────────────────
248
+ wallet.command("import <privateKey>")
249
+ .description("Import an existing private key")
250
+ .option("--network <network>", "Network (base-mainnet or base-sepolia)", "base-sepolia")
251
+ .action(async (privateKey, opts) => {
252
+ const network = opts.network;
253
+ const defaults = config_1.NETWORK_DEFAULTS[network];
254
+ if (!defaults) {
255
+ console.error(`Unknown network: ${network}. Use base-mainnet or base-sepolia.`);
256
+ process.exit(1);
257
+ }
258
+ let imported;
259
+ try {
260
+ imported = new ethers_1.ethers.Wallet(privateKey);
261
+ }
262
+ catch {
263
+ console.error("Invalid private key. Must be a 0x-prefixed hex string.");
264
+ process.exit(1);
265
+ }
266
+ const config = {
267
+ network,
268
+ rpcUrl: defaults.rpcUrl,
269
+ privateKey: imported.privateKey,
270
+ trustRegistryAddress: defaults.trustRegistryAddress,
271
+ walletFactoryAddress: defaults.walletFactoryAddress,
272
+ };
273
+ (0, config_1.saveConfig)(config);
274
+ console.log(`Address: ${imported.address}`);
275
+ console.log(`Config saved to ${(0, config_1.getConfigPath)()}`);
276
+ console.warn(`WARN: Store your private key safely — anyone with it controls your wallet`);
277
+ });
278
+ // ─── fund ──────────────────────────────────────────────────────────────────
279
+ wallet.command("fund")
280
+ .description("Show how to get ETH onto your wallet")
281
+ .action(async () => {
282
+ const config = (0, config_1.loadConfig)();
283
+ const { provider, address } = await (0, client_1.getClient)(config);
284
+ if (!address)
285
+ throw new Error("No wallet configured");
286
+ const ethBalance = await provider.getBalance(address);
287
+ console.log(`\nYour wallet address:\n ${address}`);
288
+ console.log(`\nCurrent balance: ${ethers_1.ethers.formatEther(ethBalance)} ETH`);
289
+ console.log(`\nFunding options:`);
290
+ console.log(` Bridge (Base mainnet): https://bridge.base.org`);
291
+ console.log(` Coinbase: If you use Coinbase, you can withdraw directly to Base mainnet`);
292
+ if (config.network === "base-sepolia") {
293
+ console.log(` Testnet faucet: https://www.alchemy.com/faucets/base-sepolia`);
294
+ }
295
+ });
296
+ // ─── balance ───────────────────────────────────────────────────────────────
297
+ wallet.command("balance")
298
+ .description("Check ETH balance on Base")
299
+ .option("--json")
300
+ .action(async (opts) => {
301
+ const config = (0, config_1.loadConfig)();
302
+ const { provider, address } = await (0, client_1.getClient)(config);
303
+ if (!address)
304
+ throw new Error("No wallet configured");
305
+ const ethBalance = await provider.getBalance(address);
306
+ const formatted = ethers_1.ethers.formatEther(ethBalance);
307
+ if (opts.json) {
308
+ console.log(JSON.stringify({ address, balance: formatted, balanceWei: ethBalance.toString() }));
309
+ }
310
+ else {
311
+ console.log(`Balance: ${formatted} ETH`);
312
+ }
313
+ });
314
+ // ─── list ──────────────────────────────────────────────────────────────────
315
+ wallet.command("list")
316
+ .description("List all ARC402Wallet contracts owned by the configured master key")
317
+ .option("--owner <address>", "Master key address to query (defaults to config.ownerAddress)")
318
+ .option("--json")
319
+ .action(async (opts) => {
320
+ const config = (0, config_1.loadConfig)();
321
+ const factoryAddress = config.walletFactoryAddress ?? config_1.NETWORK_DEFAULTS[config.network]?.walletFactoryAddress;
322
+ if (!factoryAddress) {
323
+ console.error("walletFactoryAddress not found in config or NETWORK_DEFAULTS.");
324
+ process.exit(1);
325
+ }
326
+ const ownerAddress = opts.owner ?? config.ownerAddress;
327
+ if (!ownerAddress) {
328
+ console.error("No owner address. Pass --owner <address> or set ownerAddress in config (run `arc402 wallet deploy` first).");
329
+ process.exit(1);
330
+ }
331
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
332
+ const factory = new ethers_1.ethers.Contract(factoryAddress, abis_1.WALLET_FACTORY_ABI, provider);
333
+ const wallets = await factory.getWallets(ownerAddress);
334
+ const results = await Promise.all(wallets.map(async (addr) => {
335
+ const walletContract = new ethers_1.ethers.Contract(addr, abis_1.ARC402_WALLET_GUARDIAN_ABI, provider);
336
+ const trustContract = new ethers_1.ethers.Contract(config.trustRegistryAddress, abis_1.TRUST_REGISTRY_ABI, provider);
337
+ const [frozen, score] = await Promise.all([
338
+ walletContract.frozen().catch(() => null),
339
+ trustContract.getScore(addr).catch(() => BigInt(0)),
340
+ ]);
341
+ return { address: addr, frozen: frozen, score: Number(score) };
342
+ }));
343
+ if (opts.json) {
344
+ console.log(JSON.stringify(results.map((w, i) => ({
345
+ index: i + 1,
346
+ address: w.address,
347
+ active: w.address.toLowerCase() === config.walletContractAddress?.toLowerCase(),
348
+ trustScore: w.score,
349
+ frozen: w.frozen,
350
+ })), null, 2));
351
+ }
352
+ else {
353
+ const short = (addr) => `${addr.slice(0, 6)}...${addr.slice(-5)}`;
354
+ console.log(`\nARC-402 Wallets owned by ${short(ownerAddress)}\n`);
355
+ results.forEach((w, i) => {
356
+ const active = w.address.toLowerCase() === config.walletContractAddress?.toLowerCase();
357
+ const activeTag = active ? " [active]" : " ";
358
+ console.log(` #${i + 1} ${w.address}${activeTag} Trust: ${w.score} Frozen: ${w.frozen}`);
359
+ });
360
+ console.log(`\n ${results.length} wallet${results.length === 1 ? "" : "s"} total`);
361
+ }
362
+ });
363
+ // ─── use ───────────────────────────────────────────────────────────────────
364
+ wallet.command("use <address>")
365
+ .description("Switch the active wallet contract address in config")
366
+ .action(async (address) => {
367
+ let checksumAddress;
368
+ try {
369
+ checksumAddress = ethers_1.ethers.getAddress(address);
370
+ }
371
+ catch {
372
+ console.error(`Invalid address: ${address}`);
373
+ process.exit(1);
374
+ }
375
+ const config = (0, config_1.loadConfig)();
376
+ const factoryAddress = config.walletFactoryAddress ?? config_1.NETWORK_DEFAULTS[config.network]?.walletFactoryAddress;
377
+ if (factoryAddress && config.ownerAddress) {
378
+ try {
379
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
380
+ const factory = new ethers_1.ethers.Contract(factoryAddress, abis_1.WALLET_FACTORY_ABI, provider);
381
+ const wallets = await factory.getWallets(config.ownerAddress);
382
+ const found = wallets.some((w) => w.toLowerCase() === checksumAddress.toLowerCase());
383
+ if (!found) {
384
+ console.warn(`WARN: ${checksumAddress} was not found in WalletFactory wallets for owner ${config.ownerAddress}`);
385
+ console.warn(" Proceeding anyway — use 'arc402 wallet list' to see known wallets.");
386
+ }
387
+ }
388
+ catch { /* allow override if factory call fails */ }
389
+ }
390
+ config.walletContractAddress = checksumAddress;
391
+ (0, config_1.saveConfig)(config);
392
+ console.log(`Active wallet set to ${checksumAddress}`);
393
+ });
394
+ // ─── deploy ────────────────────────────────────────────────────────────────
395
+ wallet.command("deploy")
396
+ .description("Deploy ARC402Wallet contract via WalletFactory (phone wallet signs via WalletConnect)")
397
+ .option("--smart-wallet", "Connect via Base Smart Wallet (Coinbase Wallet SDK) instead of WalletConnect")
398
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only (for Ledger Live, Trezor Suite, etc.)")
399
+ .option("--sponsored", "Use CDP paymaster for gas sponsorship (requires paymasterUrl + cdpKeyName + CDP_PRIVATE_KEY env)")
400
+ .action(async (opts) => {
401
+ const config = (0, config_1.loadConfig)();
402
+ const factoryAddress = config.walletFactoryAddress ?? config_1.NETWORK_DEFAULTS[config.network]?.walletFactoryAddress;
403
+ if (!factoryAddress) {
404
+ console.error("walletFactoryAddress not found in config or NETWORK_DEFAULTS. Add walletFactoryAddress to your config.");
405
+ process.exit(1);
406
+ }
407
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
408
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
409
+ const factoryInterface = new ethers_1.ethers.Interface(abis_1.WALLET_FACTORY_ABI);
410
+ if (opts.sponsored) {
411
+ // ── Sponsored deploy via CDP paymaster + ERC-4337 bundler ─────────────
412
+ // Note: WalletFactoryV3/V4 use msg.sender as wallet owner. In ERC-4337
413
+ // context msg.sender = EntryPoint. A factory upgrade with explicit owner
414
+ // param is needed for fully correct sponsored deployment. Until then,
415
+ // this path is available for testing and future-proofing.
416
+ const paymasterUrl = config.paymasterUrl ?? config_1.NETWORK_DEFAULTS[config.network]?.paymasterUrl;
417
+ const cdpKeyName = config.cdpKeyName ?? process.env.CDP_KEY_NAME;
418
+ const cdpPrivateKey = config.cdpPrivateKey ?? process.env.CDP_PRIVATE_KEY;
419
+ if (!paymasterUrl) {
420
+ console.error("paymasterUrl not configured. Add it to config or set NEXT_PUBLIC_PAYMASTER_URL.");
421
+ process.exit(1);
422
+ }
423
+ const { signer, address: ownerAddress } = await (0, client_1.requireSigner)(config);
424
+ const bundlerUrl = process.env.BUNDLER_URL ?? "https://api.pimlico.io/v2/base/rpc";
425
+ const pm = new bundler_1.PaymasterClient(paymasterUrl, cdpKeyName, cdpPrivateKey);
426
+ const bundler = new bundler_1.BundlerClient(bundlerUrl, bundler_1.DEFAULT_ENTRY_POINT, chainId);
427
+ console.log(`Sponsoring deploy via ${paymasterUrl}...`);
428
+ const factoryIface = new ethers_1.ethers.Interface(abis_1.WALLET_FACTORY_ABI);
429
+ const factoryData = factoryIface.encodeFunctionData("createWallet", [bundler_1.DEFAULT_ENTRY_POINT]);
430
+ // Predict counterfactual sender address using EntryPoint.getSenderAddress
431
+ const entryPoint = new ethers_1.ethers.Contract(bundler_1.DEFAULT_ENTRY_POINT, ["function getSenderAddress(bytes calldata initCode) external"], provider);
432
+ const initCodePacked = ethers_1.ethers.concat([factoryAddress, factoryData]);
433
+ let senderAddress;
434
+ try {
435
+ // getSenderAddress always reverts with SenderAddressResult(address)
436
+ await entryPoint.getSenderAddress(initCodePacked);
437
+ throw new Error("getSenderAddress did not revert as expected");
438
+ }
439
+ catch (e) {
440
+ const msg = e instanceof Error ? e.message : String(e);
441
+ const match = msg.match(/0x6ca7b806([0-9a-fA-F]{64})/);
442
+ if (!match) {
443
+ console.error("Could not predict wallet address:", msg);
444
+ process.exit(1);
445
+ }
446
+ senderAddress = ethers_1.ethers.getAddress("0x" + match[1].slice(24));
447
+ }
448
+ console.log(`Predicted wallet address: ${senderAddress}`);
449
+ const userOp = await pm.sponsorUserOperation({
450
+ sender: senderAddress,
451
+ nonce: "0x0",
452
+ callData: "0x",
453
+ factory: factoryAddress,
454
+ factoryData,
455
+ callGasLimit: ethers_1.ethers.toBeHex(300000),
456
+ verificationGasLimit: ethers_1.ethers.toBeHex(400000),
457
+ preVerificationGas: ethers_1.ethers.toBeHex(60000),
458
+ maxFeePerGas: ethers_1.ethers.toBeHex((await provider.getFeeData()).maxFeePerGas ?? BigInt(1000000000)),
459
+ maxPriorityFeePerGas: ethers_1.ethers.toBeHex((await provider.getFeeData()).maxPriorityFeePerGas ?? BigInt(100000000)),
460
+ signature: "0x",
461
+ }, bundler_1.DEFAULT_ENTRY_POINT);
462
+ // Sign UserOp with owner key
463
+ const userOpHash = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["address", "uint256", "bytes32", "bytes32", "bytes32", "uint256", "bytes32", "bytes32"], [
464
+ userOp.sender, BigInt(userOp.nonce),
465
+ ethers_1.ethers.keccak256(userOp.factory ? ethers_1.ethers.concat([userOp.factory, userOp.factoryData ?? "0x"]) : "0x"),
466
+ ethers_1.ethers.keccak256(userOp.callData),
467
+ ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode(["uint256", "uint256", "uint256", "uint256", "uint256", "address", "bytes"], [userOp.verificationGasLimit, userOp.callGasLimit, userOp.preVerificationGas, userOp.maxFeePerGas, userOp.maxPriorityFeePerGas, userOp.paymaster ?? ethers_1.ethers.ZeroAddress, userOp.paymasterData ?? "0x"])),
468
+ BigInt(chainId), bundler_1.DEFAULT_ENTRY_POINT, ethers_1.ethers.ZeroHash,
469
+ ]));
470
+ userOp.signature = await signer.signMessage(ethers_1.ethers.getBytes(userOpHash));
471
+ const userOpHash2 = await bundler.sendUserOperation(userOp);
472
+ console.log(`UserOp submitted: ${userOpHash2}`);
473
+ console.log("Waiting for confirmation...");
474
+ const receipt = await bundler.getUserOperationReceipt(userOpHash2);
475
+ if (!receipt.success) {
476
+ console.error("UserOperation failed on-chain.");
477
+ process.exit(1);
478
+ }
479
+ config.walletContractAddress = senderAddress;
480
+ config.ownerAddress = ownerAddress;
481
+ (0, config_1.saveConfig)(config);
482
+ console.log(`\n✓ ARC402Wallet deployed (sponsored) at: ${senderAddress}`);
483
+ console.log("Gas sponsorship active — initial setup ops are free");
484
+ console.log(`Owner: ${ownerAddress}`);
485
+ console.log(`\n⚠ IMPORTANT: Onboarding ceremony was not run on this wallet.`);
486
+ console.log(` Category spend limits have NOT been configured. All executeSpend and`);
487
+ console.log(` executeTokenSpend calls will fail with "PolicyEngine: category not configured"`);
488
+ console.log(` until you run governance setup manually via WalletConnect:`);
489
+ console.log(`\n arc402 wallet governance setup`);
490
+ console.log(`\n This must be done before making any spend from this wallet.`);
491
+ console.log(`\nNext: arc402 wallet set-passkey <x> <y> --sponsored`);
492
+ printOpenShellHint();
493
+ }
494
+ else if (opts.smartWallet) {
495
+ const { txHash, account } = await (0, coinbase_smart_wallet_1.requestCoinbaseSmartWalletSignature)(chainId, (ownerAccount) => ({
496
+ to: factoryAddress,
497
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
498
+ value: "0x0",
499
+ }), "Approve ARC402Wallet deployment — you will be set as owner");
500
+ console.log(`\nTransaction submitted: ${txHash}`);
501
+ console.log("Waiting for confirmation...");
502
+ const receipt = await provider.waitForTransaction(txHash);
503
+ if (!receipt) {
504
+ console.error("Transaction not confirmed. Check on-chain.");
505
+ process.exit(1);
506
+ }
507
+ let walletAddress = null;
508
+ const factoryContract = new ethers_1.ethers.Contract(factoryAddress, abis_1.WALLET_FACTORY_ABI, provider);
509
+ for (const log of receipt.logs) {
510
+ try {
511
+ const parsed = factoryContract.interface.parseLog(log);
512
+ if (parsed?.name === "WalletCreated") {
513
+ walletAddress = parsed.args.walletAddress;
514
+ break;
515
+ }
516
+ }
517
+ catch { /* skip unparseable logs */ }
518
+ }
519
+ if (!walletAddress) {
520
+ console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
521
+ process.exit(1);
522
+ }
523
+ config.walletContractAddress = walletAddress;
524
+ config.ownerAddress = account;
525
+ (0, config_1.saveConfig)(config);
526
+ console.log(`ARC402Wallet deployed at: ${walletAddress}`);
527
+ console.log(`Owner: ${account} (your Base Smart Wallet)`);
528
+ console.log(`Your wallet contract is ready for policy enforcement`);
529
+ console.log(`\nNext: run 'arc402 wallet set-guardian' to configure the emergency guardian key.`);
530
+ printOpenShellHint();
531
+ }
532
+ else if (config.walletConnectProjectId) {
533
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
534
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
535
+ : undefined;
536
+ // ── Step 1: Connect ────────────────────────────────────────────────────
537
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: "Approve ARC402Wallet deployment — you will be set as owner", hardware: !!opts.hardware });
538
+ const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
539
+ const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
540
+ console.log(`\n✓ Connected: ${shortAddr} on ${networkName}`);
541
+ if (telegramOpts) {
542
+ // Send "connected" message with a deploy confirmation button.
543
+ // TODO: wire up full callback_data round-trip when a persistent bot process is available.
544
+ await (0, telegram_notify_1.sendTelegramMessage)({
545
+ botToken: telegramOpts.botToken,
546
+ chatId: telegramOpts.chatId,
547
+ threadId: telegramOpts.threadId,
548
+ text: `✓ Wallet connected: ${shortAddr} — tap to deploy:`,
549
+ buttons: [[{ text: "🚀 Deploy ARC-402 Wallet", callback_data: "arc402_deploy_confirm" }]],
550
+ });
551
+ }
552
+ // ── Step 2: Confirm & Deploy ───────────────────────────────────────────
553
+ // WalletConnect approval already confirmed intent — sending automatically
554
+ console.log("Deploying...");
555
+ const txHash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, {
556
+ to: factoryAddress,
557
+ data: factoryInterface.encodeFunctionData("createWallet", ["0x0000000071727De22E5E9d8BAf0edAc6f37da032"]),
558
+ value: "0x0",
559
+ });
560
+ console.log(`\nTransaction submitted: ${txHash}`);
561
+ console.log("Waiting for confirmation...");
562
+ const receipt = await provider.waitForTransaction(txHash);
563
+ if (!receipt) {
564
+ console.error("Transaction not confirmed. Check on-chain.");
565
+ process.exit(1);
566
+ }
567
+ let walletAddress = null;
568
+ const factoryContract = new ethers_1.ethers.Contract(factoryAddress, abis_1.WALLET_FACTORY_ABI, provider);
569
+ for (const log of receipt.logs) {
570
+ try {
571
+ const parsed = factoryContract.interface.parseLog(log);
572
+ if (parsed?.name === "WalletCreated") {
573
+ walletAddress = parsed.args.walletAddress;
574
+ break;
575
+ }
576
+ }
577
+ catch { /* skip unparseable logs */ }
578
+ }
579
+ if (!walletAddress) {
580
+ console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
581
+ process.exit(1);
582
+ }
583
+ config.walletContractAddress = walletAddress;
584
+ config.ownerAddress = account;
585
+ (0, config_1.saveConfig)(config);
586
+ console.log(`\n✓ ARC402Wallet deployed at: ${walletAddress}`);
587
+ console.log(`Owner: ${account} (your phone wallet)`);
588
+ // ── Mandatory onboarding ceremony (same WalletConnect session) ────────
589
+ console.log("\nStarting mandatory onboarding ceremony in this WalletConnect session...");
590
+ await runWalletOnboardingCeremony(walletAddress, account, config, provider, async (call, description) => {
591
+ console.log(` Sending: ${description}`);
592
+ const hash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, call);
593
+ await provider.waitForTransaction(hash, 1);
594
+ console.log(` ✓ ${description}: ${hash}`);
595
+ return hash;
596
+ });
597
+ console.log(`Your wallet contract is ready for policy enforcement`);
598
+ const paymasterUrl2 = config.paymasterUrl ?? config_1.NETWORK_DEFAULTS[config.network]?.paymasterUrl;
599
+ const deployedBalance = await provider.getBalance(walletAddress);
600
+ if (paymasterUrl2 && deployedBalance < BigInt(1000000000000000)) {
601
+ console.log("Gas sponsorship active — initial setup ops are free");
602
+ }
603
+ console.log(`\nNext: run 'arc402 wallet set-guardian' to configure the emergency guardian key.`);
604
+ printOpenShellHint();
605
+ }
606
+ else {
607
+ console.warn("⚠ WalletConnect not configured. Using stored private key (insecure).");
608
+ console.warn(" Run `arc402 config set walletConnectProjectId <id>` to enable phone wallet signing.");
609
+ const { signer, address } = await (0, client_1.requireSigner)(config);
610
+ const factory = new ethers_1.ethers.Contract(factoryAddress, abis_1.WALLET_FACTORY_ABI, signer);
611
+ console.log(`Deploying ARC402Wallet via factory at ${factoryAddress}...`);
612
+ const tx = await factory.createWallet("0x0000000071727De22E5E9d8BAf0edAc6f37da032");
613
+ const receipt = await tx.wait();
614
+ let walletAddress = null;
615
+ for (const log of receipt.logs) {
616
+ try {
617
+ const parsed = factory.interface.parseLog(log);
618
+ if (parsed?.name === "WalletCreated") {
619
+ walletAddress = parsed.args.walletAddress;
620
+ break;
621
+ }
622
+ }
623
+ catch { /* skip unparseable logs */ }
624
+ }
625
+ if (!walletAddress) {
626
+ console.error("Could not find WalletCreated event in receipt. Check the transaction on-chain.");
627
+ process.exit(1);
628
+ }
629
+ // Generate guardian key (separate from hot key) and call setGuardian
630
+ const guardianWallet = ethers_1.ethers.Wallet.createRandom();
631
+ config.walletContractAddress = walletAddress;
632
+ config.guardianPrivateKey = guardianWallet.privateKey;
633
+ config.guardianAddress = guardianWallet.address;
634
+ (0, config_1.saveConfig)(config);
635
+ // Call setGuardian on the deployed wallet
636
+ const walletContract = new ethers_1.ethers.Contract(walletAddress, abis_1.ARC402_WALLET_GUARDIAN_ABI, signer);
637
+ const setGuardianTx = await walletContract.setGuardian(guardianWallet.address);
638
+ await setGuardianTx.wait();
639
+ // ── Mandatory onboarding ceremony (private key path) ──────────────────
640
+ console.log("\nRunning mandatory onboarding ceremony...");
641
+ const provider2 = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
642
+ await runWalletOnboardingCeremony(walletAddress, address, config, provider2, async (call, description) => {
643
+ console.log(` Sending: ${description}`);
644
+ const tx2 = await signer.sendTransaction({ to: call.to, data: call.data, value: call.value === "0x0" ? 0n : BigInt(call.value) });
645
+ await tx2.wait(1);
646
+ console.log(` ✓ ${description}: ${tx2.hash}`);
647
+ return tx2.hash;
648
+ });
649
+ console.log(`ARC402Wallet deployed at: ${walletAddress}`);
650
+ console.log(`Guardian key generated: ${guardianWallet.address}`);
651
+ console.log(`Guardian private key saved to config (keep it safe — used for emergency freeze only)`);
652
+ console.log(`Your wallet contract is ready for policy enforcement`);
653
+ printOpenShellHint();
654
+ }
655
+ });
656
+ // ─── send ──────────────────────────────────────────────────────────────────
657
+ wallet.command("send <address> <amount>")
658
+ .description("Send ETH from configured wallet (amount: '0.001eth' or wei)")
659
+ .option("--json")
660
+ .action(async (to, amountRaw, opts) => {
661
+ const config = (0, config_1.loadConfig)();
662
+ const { signer } = await (0, client_1.requireSigner)(config);
663
+ const value = parseAmount(amountRaw);
664
+ const tx = await signer.sendTransaction({ to, value });
665
+ if (opts.json) {
666
+ console.log(JSON.stringify({ txHash: tx.hash, to, amount: ethers_1.ethers.formatEther(value) }));
667
+ }
668
+ else {
669
+ console.log(`Tx hash: ${tx.hash}`);
670
+ }
671
+ });
672
+ // ─── policy ────────────────────────────────────────────────────────────────
673
+ const walletPolicy = wallet.command("policy").description("View and set spending policy on ARC402Wallet");
674
+ walletPolicy.command("show")
675
+ .description("Show per-tx and daily spending limits for a category")
676
+ .requiredOption("--category <cat>", "Category name (e.g. code.review)")
677
+ .action(async (opts) => {
678
+ const config = (0, config_1.loadConfig)();
679
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
680
+ const walletAddr = config.walletContractAddress;
681
+ if (!walletAddr) {
682
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
683
+ process.exit(1);
684
+ }
685
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
686
+ const contract = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_LIMITS_ABI, provider);
687
+ const [perTxLimit, dailyLimit] = await Promise.all([
688
+ contract.categoryLimits(walletAddr, opts.category),
689
+ contract.dailyCategoryLimit(walletAddr, opts.category),
690
+ ]);
691
+ console.log(`Category: ${opts.category}`);
692
+ console.log(`Per-tx: ${perTxLimit === 0n ? "(not set)" : ethers_1.ethers.formatEther(perTxLimit) + " ETH"}`);
693
+ console.log(`Daily: ${dailyLimit === 0n ? "(not set)" : ethers_1.ethers.formatEther(dailyLimit) + " ETH"}`);
694
+ if (dailyLimit > 0n) {
695
+ console.log(`\nNote: Daily limits use two 12-hour buckets (current + previous window).`);
696
+ console.log(` The effective limit applies across a rolling 12-24 hour period, not a strict calendar day.`);
697
+ }
698
+ });
699
+ walletPolicy.command("set-limit")
700
+ .description("Set a spending limit for a category (phone wallet signs via WalletConnect)")
701
+ .requiredOption("--category <cat>", "Category name (e.g. code.review)")
702
+ .requiredOption("--amount <eth>", "Limit in ETH (e.g. 0.1)")
703
+ .action(async (opts) => {
704
+ const config = (0, config_1.loadConfig)();
705
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
706
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
707
+ const amount = ethers_1.ethers.parseEther(opts.amount);
708
+ const policyInterface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_LIMITS_ABI);
709
+ if (config.walletConnectProjectId) {
710
+ const walletAddr = config.walletContractAddress;
711
+ if (!walletAddr) {
712
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
713
+ process.exit(1);
714
+ }
715
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
716
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, (account) => ({
717
+ to: policyAddress,
718
+ data: policyInterface.encodeFunctionData("setCategoryLimitFor", [walletAddr, opts.category, amount]),
719
+ value: "0x0",
720
+ }), `Approve spend limit: ${opts.category} → ${opts.amount} ETH`, config.telegramBotToken && config.telegramChatId ? {
721
+ botToken: config.telegramBotToken,
722
+ chatId: config.telegramChatId,
723
+ threadId: config.telegramThreadId,
724
+ } : undefined, config);
725
+ console.log(`\nTransaction submitted: ${txHash}`);
726
+ await provider.waitForTransaction(txHash);
727
+ console.log(`Spend limit for ${opts.category} set to ${opts.amount} ETH`);
728
+ }
729
+ else {
730
+ console.warn("⚠ WalletConnect not configured. Using stored private key (insecure).");
731
+ console.warn(" Run `arc402 config set walletConnectProjectId <id>` to enable phone wallet signing.");
732
+ const { signer, address } = await (0, client_1.requireSigner)(config);
733
+ const contract = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_LIMITS_ABI, signer);
734
+ await (await contract.setCategoryLimitFor(address, opts.category, amount)).wait();
735
+ console.log(`Spend limit for ${opts.category} set to ${opts.amount} ETH`);
736
+ }
737
+ });
738
+ // ─── policy set-daily-limit (J8-01) ──────────────────────────────────────
739
+ //
740
+ // Sets the daily (rolling 12/24h window) category limit on PolicyEngine.
741
+ // Note: the limit uses two 12-hour buckets — the effective maximum across
742
+ // any 24h window is up to 2× the configured value at bucket boundaries.
743
+ walletPolicy.command("set-daily-limit")
744
+ .description("Set a daily category spending limit (phone wallet signs via WalletConnect). Note: uses 12-hour rolling buckets — see below.")
745
+ .requiredOption("--category <cat>", "Category name (e.g. compute)")
746
+ .requiredOption("--amount <eth>", "Daily limit in ETH (e.g. 0.5)")
747
+ .action(async (opts) => {
748
+ const config = (0, config_1.loadConfig)();
749
+ console.log(`\nNote: ARC-402 has two independent velocity limit layers:`);
750
+ console.log(` 1. Wallet-level (arc402 wallet set-velocity-limit): ETH cap per rolling hour, enforced by ARC402Wallet contract. Breach auto-freezes wallet.`);
751
+ console.log(` 2. PolicyEngine-level (arc402 wallet policy set-daily-limit): Per-category daily cap, enforced by PolicyEngine. Breach returns a soft error without freezing.`);
752
+ console.log(` Both must be configured for full protection.\n`);
753
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
754
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
755
+ const walletAddr = config.walletContractAddress;
756
+ if (!walletAddr) {
757
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
758
+ process.exit(1);
759
+ }
760
+ const amount = ethers_1.ethers.parseEther(opts.amount);
761
+ console.log(`\nNote: Daily limits use two 12-hour buckets (current + previous window).`);
762
+ console.log(` The effective limit applies across a rolling 12-24 hour period, not a strict calendar day.`);
763
+ console.log(` Setting daily limit for category "${opts.category}" to ${opts.amount} ETH.\n`);
764
+ const policyInterface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_LIMITS_ABI);
765
+ if (config.walletConnectProjectId) {
766
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
767
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
768
+ to: policyAddress,
769
+ data: policyInterface.encodeFunctionData("setDailyLimitFor", [walletAddr, opts.category, amount]),
770
+ value: "0x0",
771
+ }), `Approve daily limit: ${opts.category} → ${opts.amount} ETH`, config.telegramBotToken && config.telegramChatId ? {
772
+ botToken: config.telegramBotToken,
773
+ chatId: config.telegramChatId,
774
+ threadId: config.telegramThreadId,
775
+ } : undefined, config);
776
+ await provider.waitForTransaction(txHash);
777
+ console.log(`Daily limit for ${opts.category} set to ${opts.amount} ETH (12/24h rolling window)`);
778
+ }
779
+ else {
780
+ console.warn("⚠ WalletConnect not configured. Using stored private key (insecure).");
781
+ const { signer, address } = await (0, client_1.requireSigner)(config);
782
+ const contract = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_LIMITS_ABI, signer);
783
+ await (await contract.setDailyLimitFor(address, opts.category, amount)).wait();
784
+ console.log(`Daily limit for ${opts.category} set to ${opts.amount} ETH (12/24h rolling window)`);
785
+ }
786
+ });
787
+ walletPolicy.command("set <policyId>")
788
+ .description("Set the active policy ID on ARC402Wallet (phone wallet signs via WalletConnect)")
789
+ .action(async (policyId) => {
790
+ const config = (0, config_1.loadConfig)();
791
+ if (!config.walletContractAddress) {
792
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
793
+ process.exit(1);
794
+ }
795
+ if (!config.walletConnectProjectId) {
796
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
797
+ process.exit(1);
798
+ }
799
+ // Normalise policyId to bytes32 hex
800
+ let policyIdHex;
801
+ try {
802
+ policyIdHex = ethers_1.ethers.zeroPadValue(ethers_1.ethers.hexlify(policyId.startsWith("0x") ? policyId : ethers_1.ethers.toUtf8Bytes(policyId)), 32);
803
+ }
804
+ catch {
805
+ console.error("Invalid policyId — must be a hex string (0x…) or UTF-8 label.");
806
+ process.exit(1);
807
+ }
808
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
809
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
810
+ const ownerInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_OWNER_ABI);
811
+ let currentPolicy = "(unknown)";
812
+ try {
813
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_OWNER_ABI, provider);
814
+ currentPolicy = await walletContract.activePolicyId();
815
+ }
816
+ catch { /* contract may not be deployed yet */ }
817
+ console.log(`\nWallet: ${config.walletContractAddress}`);
818
+ console.log(`Current policy: ${currentPolicy}`);
819
+ console.log(`New policy: ${policyIdHex}`);
820
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
821
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
822
+ : undefined;
823
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
824
+ to: config.walletContractAddress,
825
+ data: ownerInterface.encodeFunctionData("updatePolicy", [policyIdHex]),
826
+ value: "0x0",
827
+ }), `Approve: update policy to ${policyIdHex}`, telegramOpts, config);
828
+ await provider.waitForTransaction(txHash);
829
+ console.log(`\n✓ Active policy updated`);
830
+ console.log(` Tx: ${txHash}`);
831
+ console.log(` Policy: ${policyIdHex}`);
832
+ });
833
+ // ─── freeze (guardian key — emergency wallet freeze) ──────────────────────
834
+ //
835
+ // Uses the guardian private key from config to call ARC402Wallet.freeze() or
836
+ // ARC402Wallet.freezeAndDrain() directly on the wallet contract.
837
+ // No human approval needed — designed for immediate AI-initiated emergency response.
838
+ wallet.command("freeze")
839
+ .description("Emergency freeze via guardian key. Use immediately if suspicious activity is detected. Owner must unfreeze.")
840
+ .option("--drain", "Also drain all ETH to owner address (use when machine compromise is suspected)")
841
+ .option("--json")
842
+ .action(async (opts) => {
843
+ const config = (0, config_1.loadConfig)();
844
+ if (!config.walletContractAddress) {
845
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
846
+ process.exit(1);
847
+ }
848
+ if (!config.guardianPrivateKey) {
849
+ console.error("guardianPrivateKey not set in config. Guardian key was generated during `arc402 wallet deploy`.");
850
+ process.exit(1);
851
+ }
852
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
853
+ const guardianSigner = new ethers_1.ethers.Wallet(config.guardianPrivateKey, provider);
854
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_GUARDIAN_ABI, guardianSigner);
855
+ let tx;
856
+ if (opts.drain) {
857
+ console.log("Triggering freeze-and-drain via guardian key...");
858
+ tx = await walletContract.freezeAndDrain();
859
+ }
860
+ else {
861
+ console.log("Triggering emergency freeze via guardian key...");
862
+ tx = await walletContract.freeze();
863
+ }
864
+ const receipt = await tx.wait();
865
+ if (opts.json) {
866
+ console.log(JSON.stringify({ txHash: receipt.hash, walletAddress: config.walletContractAddress, drained: !!opts.drain }));
867
+ }
868
+ else {
869
+ console.log(`Wallet ${config.walletContractAddress} is now FROZEN`);
870
+ if (opts.drain)
871
+ console.log("All ETH drained to owner.");
872
+ console.log(`Tx: ${receipt.hash}`);
873
+ console.log(`\nOwner must unfreeze: arc402 wallet unfreeze`);
874
+ }
875
+ });
876
+ // ─── unfreeze (owner key — requires WalletConnect) ────────────────────────
877
+ //
878
+ // Deliberately uses WalletConnect (phone wallet) so unfreezing requires owner
879
+ // approval from the phone. Guardian can freeze fast; only owner can unfreeze.
880
+ wallet.command("unfreeze")
881
+ .description("Unfreeze wallet contract via owner phone wallet (WalletConnect). Only the owner can unfreeze — guardian cannot.")
882
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only")
883
+ .option("--json")
884
+ .action(async (opts) => {
885
+ const config = (0, config_1.loadConfig)();
886
+ if (!config.walletContractAddress) {
887
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
888
+ process.exit(1);
889
+ }
890
+ if (!config.walletConnectProjectId) {
891
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
892
+ process.exit(1);
893
+ }
894
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
895
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
896
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_GUARDIAN_ABI);
897
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
898
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
899
+ : undefined;
900
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: "Approve: unfreeze ARC402Wallet", hardware: !!opts.hardware });
901
+ const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
902
+ const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
903
+ console.log(`\n✓ Connected: ${shortAddr} on ${networkName}`);
904
+ console.log(`\nWallet to unfreeze: ${config.walletContractAddress}`);
905
+ // WalletConnect approval already confirmed intent — sending automatically
906
+ console.log("Sending transaction...");
907
+ const txHash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, {
908
+ to: config.walletContractAddress,
909
+ data: walletInterface.encodeFunctionData("unfreeze", []),
910
+ value: "0x0",
911
+ });
912
+ await provider.waitForTransaction(txHash);
913
+ if (opts.json) {
914
+ console.log(JSON.stringify({ txHash, walletAddress: config.walletContractAddress }));
915
+ }
916
+ else {
917
+ console.log(`\n✓ Wallet ${config.walletContractAddress} unfrozen`);
918
+ console.log(` Tx: ${txHash}`);
919
+ }
920
+ });
921
+ // ─── set-guardian ──────────────────────────────────────────────────────────
922
+ //
923
+ // Generates a guardian key locally, then registers it on-chain via the owner's
924
+ // phone wallet (WalletConnect). Guardian changes require owner approval.
925
+ wallet.command("set-guardian")
926
+ .description("Generate a new guardian key and register it on the wallet contract (phone wallet signs via WalletConnect)")
927
+ .option("--guardian-key <key>", "Use an existing private key as the guardian (optional)")
928
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only")
929
+ .action(async (opts) => {
930
+ const config = (0, config_1.loadConfig)();
931
+ if (!config.walletContractAddress) {
932
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
933
+ process.exit(1);
934
+ }
935
+ if (!config.walletConnectProjectId) {
936
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
937
+ process.exit(1);
938
+ }
939
+ let guardianWallet;
940
+ if (opts.guardianKey) {
941
+ try {
942
+ guardianWallet = new ethers_1.ethers.Wallet(opts.guardianKey);
943
+ }
944
+ catch {
945
+ console.error("Invalid guardian key. Must be a 0x-prefixed hex string.");
946
+ process.exit(1);
947
+ }
948
+ }
949
+ else {
950
+ guardianWallet = new ethers_1.ethers.Wallet(ethers_1.ethers.Wallet.createRandom().privateKey);
951
+ console.log("Generated new guardian key.");
952
+ }
953
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
954
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
955
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_GUARDIAN_ABI);
956
+ console.log(`\nGuardian address: ${guardianWallet.address}`);
957
+ console.log(`Wallet contract: ${config.walletContractAddress}`);
958
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
959
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
960
+ : undefined;
961
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: `Approve: set guardian to ${guardianWallet.address}`, hardware: !!opts.hardware });
962
+ const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
963
+ const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
964
+ console.log(`\n✓ Connected: ${shortAddr} on ${networkName}`);
965
+ // WalletConnect approval already confirmed intent — sending automatically
966
+ console.log("Sending transaction...");
967
+ const txHash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, {
968
+ to: config.walletContractAddress,
969
+ data: walletInterface.encodeFunctionData("setGuardian", [guardianWallet.address]),
970
+ value: "0x0",
971
+ });
972
+ await provider.waitForTransaction(txHash);
973
+ config.guardianPrivateKey = guardianWallet.privateKey;
974
+ config.guardianAddress = guardianWallet.address;
975
+ (0, config_1.saveConfig)(config);
976
+ console.log(`\n✓ Guardian set to: ${guardianWallet.address}`);
977
+ console.log(` Tx: ${txHash}`);
978
+ console.log(` Guardian private key saved to config.`);
979
+ console.log(` WARN: The guardian key can freeze your wallet. Store it separately from your hot key.`);
980
+ });
981
+ // ─── policy-engine freeze / unfreeze (legacy — for PolicyEngine-level freeze) ──
982
+ wallet.command("freeze-policy <walletAddress>")
983
+ .description("Freeze PolicyEngine spend for a wallet address (authorized freeze agents only)")
984
+ .action(async (walletAddress) => {
985
+ const config = (0, config_1.loadConfig)();
986
+ if (!config.policyEngineAddress)
987
+ throw new Error("policyEngineAddress missing in config");
988
+ const { signer } = await (0, client_1.requireSigner)(config);
989
+ const client = new sdk_1.PolicyClient(config.policyEngineAddress, signer);
990
+ await client.freezeSpend(walletAddress);
991
+ console.log(`wallet ${walletAddress} spend frozen (PolicyEngine)`);
992
+ });
993
+ wallet.command("unfreeze-policy <walletAddress>")
994
+ .description("Unfreeze PolicyEngine spend for a wallet. Only callable by the wallet or its registered owner.")
995
+ .action(async (walletAddress) => {
996
+ const config = (0, config_1.loadConfig)();
997
+ if (!config.policyEngineAddress)
998
+ throw new Error("policyEngineAddress missing in config");
999
+ const { signer } = await (0, client_1.requireSigner)(config);
1000
+ const client = new sdk_1.PolicyClient(config.policyEngineAddress, signer);
1001
+ await client.unfreeze(walletAddress);
1002
+ console.log(`wallet ${walletAddress} spend unfrozen (PolicyEngine)`);
1003
+ });
1004
+ // ─── upgrade-registry ──────────────────────────────────────────────────────
1005
+ wallet.command("upgrade-registry <newRegistryAddress>")
1006
+ .description("Propose a registry upgrade on the ARC402Wallet (2-day timelock, phone wallet signs via WalletConnect)")
1007
+ .option("--dry-run", "Show calldata without connecting to wallet")
1008
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only")
1009
+ .action(async (newRegistryAddress, opts) => {
1010
+ const config = (0, config_1.loadConfig)();
1011
+ if (!config.walletContractAddress) {
1012
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1013
+ process.exit(1);
1014
+ }
1015
+ if (!config.walletConnectProjectId && !opts.dryRun) {
1016
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1017
+ process.exit(1);
1018
+ }
1019
+ let checksumAddress;
1020
+ try {
1021
+ checksumAddress = ethers_1.ethers.getAddress(newRegistryAddress);
1022
+ }
1023
+ catch {
1024
+ console.error(`Invalid address: ${newRegistryAddress}`);
1025
+ process.exit(1);
1026
+ }
1027
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1028
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1029
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_REGISTRY_ABI);
1030
+ let currentRegistry = "(unknown)";
1031
+ try {
1032
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_REGISTRY_ABI, provider);
1033
+ currentRegistry = await walletContract.registry();
1034
+ }
1035
+ catch { /* contract may not expose registry() */ }
1036
+ // Box: 54-char inner width (║ + 54 + ║ = 56 total)
1037
+ const fromPad = currentRegistry.padEnd(42);
1038
+ console.log(`\n╔══════════════════════════════════════════════════════╗`);
1039
+ console.log(`║ ARC402Wallet Registry Upgrade ║`);
1040
+ console.log(`╟──────────────────────────────────────────────────────╢`);
1041
+ console.log(`║ Wallet: ${config.walletContractAddress}║`);
1042
+ console.log(`║ From: ${fromPad}║`);
1043
+ console.log(`║ To: ${checksumAddress}║`);
1044
+ console.log(`║ Timelock: 2 days (cancelable) ║`);
1045
+ console.log(`║ Action: proposeRegistryUpdate() ║`);
1046
+ console.log(`╚══════════════════════════════════════════════════════╝\n`);
1047
+ const calldata = walletInterface.encodeFunctionData("proposeRegistryUpdate", [checksumAddress]);
1048
+ if (opts.dryRun) {
1049
+ console.log("Calldata (dry-run):");
1050
+ console.log(` To: ${config.walletContractAddress}`);
1051
+ console.log(` Data: ${calldata}`);
1052
+ console.log(` Value: 0x0`);
1053
+ return;
1054
+ }
1055
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1056
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1057
+ : undefined;
1058
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: "Approve registry upgrade proposal on ARC402Wallet", hardware: !!opts.hardware });
1059
+ const networkName = chainId === 8453 ? "Base" : "Base Sepolia";
1060
+ const shortAddr = `${account.slice(0, 6)}...${account.slice(-5)}`;
1061
+ console.log(`\n✓ Connected: ${shortAddr} on ${networkName}`);
1062
+ // WalletConnect approval already confirmed intent — sending automatically
1063
+ console.log("Sending transaction...");
1064
+ const txHash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, {
1065
+ to: config.walletContractAddress,
1066
+ data: calldata,
1067
+ value: "0x0",
1068
+ });
1069
+ const unlockAt = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000);
1070
+ console.log(`\n✓ Registry upgrade proposed`);
1071
+ console.log(` Tx: ${txHash}`);
1072
+ console.log(` Unlock at: ${unlockAt.toISOString()} (approximately)`);
1073
+ console.log(`\nNext steps:`);
1074
+ console.log(` Wait 2 days, then run:`);
1075
+ console.log(` arc402 wallet execute-registry-upgrade`);
1076
+ console.log(`\nTo cancel before execution:`);
1077
+ console.log(` arc402 wallet cancel-registry-upgrade`);
1078
+ });
1079
+ // ─── execute-registry-upgrade ──────────────────────────────────────────────
1080
+ wallet.command("execute-registry-upgrade")
1081
+ .description("Execute a pending registry upgrade after the 2-day timelock (phone wallet signs via WalletConnect)")
1082
+ .action(async () => {
1083
+ const config = (0, config_1.loadConfig)();
1084
+ if (!config.walletContractAddress) {
1085
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1086
+ process.exit(1);
1087
+ }
1088
+ if (!config.walletConnectProjectId) {
1089
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1090
+ process.exit(1);
1091
+ }
1092
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1093
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1094
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_REGISTRY_ABI, provider);
1095
+ let pendingRegistry;
1096
+ let unlockAt;
1097
+ try {
1098
+ [pendingRegistry, unlockAt] = await Promise.all([
1099
+ walletContract.pendingRegistry(),
1100
+ walletContract.registryUpdateUnlockAt(),
1101
+ ]);
1102
+ }
1103
+ catch (e) {
1104
+ console.error("Failed to read pending registry from contract:", e);
1105
+ process.exit(1);
1106
+ }
1107
+ if (pendingRegistry === ethers_1.ethers.ZeroAddress) {
1108
+ console.log("No pending registry upgrade.");
1109
+ return;
1110
+ }
1111
+ const nowSeconds = BigInt(Math.floor(Date.now() / 1000));
1112
+ if (unlockAt > nowSeconds) {
1113
+ const remaining = Number(unlockAt - nowSeconds);
1114
+ const hours = Math.floor(remaining / 3600);
1115
+ const minutes = Math.floor((remaining % 3600) / 60);
1116
+ console.log(`Timelock not yet elapsed.`);
1117
+ console.log(`Pending registry: ${pendingRegistry}`);
1118
+ console.log(`Unlocks in: ${hours}h ${minutes}m`);
1119
+ console.log(`Unlock at: ${new Date(Number(unlockAt) * 1000).toISOString()}`);
1120
+ return;
1121
+ }
1122
+ console.log(`Pending registry: ${pendingRegistry}`);
1123
+ console.log("Timelock elapsed — proceeding with executeRegistryUpdate()");
1124
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1125
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1126
+ : undefined;
1127
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_REGISTRY_ABI);
1128
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1129
+ to: config.walletContractAddress,
1130
+ data: walletInterface.encodeFunctionData("executeRegistryUpdate", []),
1131
+ value: "0x0",
1132
+ }), "Approve registry upgrade execution on ARC402Wallet", telegramOpts, config);
1133
+ // Wait for tx to confirm, then read back the active registry (J6-02)
1134
+ await provider.waitForTransaction(txHash);
1135
+ let confirmedRegistry = pendingRegistry;
1136
+ try {
1137
+ confirmedRegistry = await walletContract.registry();
1138
+ }
1139
+ catch { /* use pendingRegistry as fallback */ }
1140
+ console.log(`\n✓ Registry upgrade executed`);
1141
+ console.log(` Tx: ${txHash}`);
1142
+ console.log(` New registry: ${confirmedRegistry}`);
1143
+ if (confirmedRegistry.toLowerCase() === pendingRegistry.toLowerCase()) {
1144
+ console.log(` Registry updated successfully — addresses now resolve through new registry.`);
1145
+ }
1146
+ else {
1147
+ console.warn(` WARN: Confirmed registry (${confirmedRegistry}) differs from expected (${pendingRegistry}). Check the transaction.`);
1148
+ }
1149
+ console.log(`\nVerify contracts resolve correctly with \`arc402 wallet status\``);
1150
+ });
1151
+ // ─── whitelist-contract ────────────────────────────────────────────────────
1152
+ //
1153
+ // Adds a contract to the per-wallet DeFi whitelist on PolicyEngine so that
1154
+ // executeContractCall can target it. Called directly by the owner (MetaMask)
1155
+ // on PolicyEngine — does NOT route through the wallet contract.
1156
+ wallet.command("whitelist-contract <target>")
1157
+ .description("Whitelist a contract address on PolicyEngine so this wallet can call it via executeContractCall (phone wallet signs via WalletConnect)")
1158
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only")
1159
+ .option("--json")
1160
+ .action(async (target, opts) => {
1161
+ const config = (0, config_1.loadConfig)();
1162
+ if (!config.walletContractAddress) {
1163
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1164
+ process.exit(1);
1165
+ }
1166
+ if (!config.walletConnectProjectId) {
1167
+ console.error("walletConnectProjectId not set in config.");
1168
+ process.exit(1);
1169
+ }
1170
+ let checksumTarget;
1171
+ try {
1172
+ checksumTarget = ethers_1.ethers.getAddress(target);
1173
+ }
1174
+ catch {
1175
+ console.error(`Invalid address: ${target}`);
1176
+ process.exit(1);
1177
+ }
1178
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
1179
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1180
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1181
+ // Check if already whitelisted
1182
+ const peAbi = [
1183
+ "function whitelistContract(address wallet, address target) external",
1184
+ "function isContractWhitelisted(address wallet, address target) external view returns (bool)",
1185
+ ];
1186
+ const pe = new ethers_1.ethers.Contract(policyAddress, peAbi, provider);
1187
+ let alreadyWhitelisted = false;
1188
+ try {
1189
+ alreadyWhitelisted = await pe.isContractWhitelisted(config.walletContractAddress, checksumTarget);
1190
+ }
1191
+ catch { /* ignore */ }
1192
+ if (alreadyWhitelisted) {
1193
+ console.log(`✓ ${checksumTarget} is already whitelisted for ${config.walletContractAddress}`);
1194
+ process.exit(0);
1195
+ }
1196
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1197
+ console.log(`PolicyEngine: ${policyAddress}`);
1198
+ console.log(`Whitelisting: ${checksumTarget}`);
1199
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1200
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1201
+ : undefined;
1202
+ const policyIface = new ethers_1.ethers.Interface(peAbi);
1203
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1204
+ to: policyAddress,
1205
+ data: policyIface.encodeFunctionData("whitelistContract", [
1206
+ config.walletContractAddress,
1207
+ checksumTarget,
1208
+ ]),
1209
+ value: "0x0",
1210
+ }), `Approve: whitelist ${checksumTarget} on PolicyEngine for your wallet`, telegramOpts, config);
1211
+ await provider.waitForTransaction(txHash);
1212
+ if (opts.json) {
1213
+ console.log(JSON.stringify({ ok: true, txHash, wallet: config.walletContractAddress, target: checksumTarget }));
1214
+ }
1215
+ else {
1216
+ console.log(`\n✓ Contract whitelisted`);
1217
+ console.log(` Tx: ${txHash}`);
1218
+ console.log(` Wallet: ${config.walletContractAddress}`);
1219
+ console.log(` Target: ${checksumTarget}`);
1220
+ }
1221
+ });
1222
+ // ─── set-interceptor ───────────────────────────────────────────────────────
1223
+ wallet.command("set-interceptor <address>")
1224
+ .description("Set the authorized X402 interceptor address on ARC402Wallet (phone wallet signs via WalletConnect)")
1225
+ .action(async (interceptorAddress) => {
1226
+ const config = (0, config_1.loadConfig)();
1227
+ if (!config.walletContractAddress) {
1228
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1229
+ process.exit(1);
1230
+ }
1231
+ if (!config.walletConnectProjectId) {
1232
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1233
+ process.exit(1);
1234
+ }
1235
+ let checksumAddress;
1236
+ try {
1237
+ checksumAddress = ethers_1.ethers.getAddress(interceptorAddress);
1238
+ }
1239
+ catch {
1240
+ console.error(`Invalid address: ${interceptorAddress}`);
1241
+ process.exit(1);
1242
+ }
1243
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1244
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1245
+ const ownerInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_OWNER_ABI);
1246
+ let currentInterceptor = "(unknown)";
1247
+ try {
1248
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_OWNER_ABI, provider);
1249
+ currentInterceptor = await walletContract.authorizedInterceptor();
1250
+ }
1251
+ catch { /* contract may not be deployed yet */ }
1252
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1253
+ console.log(`Current interceptor: ${currentInterceptor}`);
1254
+ console.log(`New interceptor: ${checksumAddress}`);
1255
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1256
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1257
+ : undefined;
1258
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1259
+ to: config.walletContractAddress,
1260
+ data: ownerInterface.encodeFunctionData("setAuthorizedInterceptor", [checksumAddress]),
1261
+ value: "0x0",
1262
+ }), `Approve: set X402 interceptor to ${checksumAddress}`, telegramOpts, config);
1263
+ await provider.waitForTransaction(txHash);
1264
+ console.log(`\n✓ X402 interceptor updated`);
1265
+ console.log(` Tx: ${txHash}`);
1266
+ console.log(` Interceptor: ${checksumAddress}`);
1267
+ });
1268
+ // ─── set-velocity-limit ────────────────────────────────────────────────────
1269
+ wallet.command("set-velocity-limit <limit>")
1270
+ .description("Set the per-rolling-window ETH velocity limit on ARC402Wallet (limit in ETH, phone wallet signs via WalletConnect)")
1271
+ .action(async (limitEth) => {
1272
+ const config = (0, config_1.loadConfig)();
1273
+ console.log(`\nNote: ARC-402 has two independent velocity limit layers:`);
1274
+ console.log(` 1. Wallet-level (arc402 wallet set-velocity-limit): ETH cap per rolling hour, enforced by ARC402Wallet contract. Breach auto-freezes wallet.`);
1275
+ console.log(` 2. PolicyEngine-level (arc402 wallet policy set-daily-limit): Per-category daily cap, enforced by PolicyEngine. Breach returns a soft error without freezing.`);
1276
+ console.log(` Both must be configured for full protection.\n`);
1277
+ if (!config.walletContractAddress) {
1278
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1279
+ process.exit(1);
1280
+ }
1281
+ if (!config.walletConnectProjectId) {
1282
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1283
+ process.exit(1);
1284
+ }
1285
+ let limitWei;
1286
+ try {
1287
+ limitWei = ethers_1.ethers.parseEther(limitEth);
1288
+ }
1289
+ catch {
1290
+ console.error(`Invalid limit: ${limitEth}. Provide a value in ETH (e.g. 0.5)`);
1291
+ process.exit(1);
1292
+ }
1293
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1294
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1295
+ const ownerInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_OWNER_ABI);
1296
+ let currentLimit = "(unknown)";
1297
+ try {
1298
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_OWNER_ABI, provider);
1299
+ const raw = await walletContract.velocityLimit();
1300
+ currentLimit = raw === 0n ? "disabled" : `${ethers_1.ethers.formatEther(raw)} ETH`;
1301
+ }
1302
+ catch { /* contract may not be deployed yet */ }
1303
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1304
+ console.log(`Current limit: ${currentLimit}`);
1305
+ console.log(`New limit: ${limitEth} ETH (max ETH per rolling window)`);
1306
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1307
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1308
+ : undefined;
1309
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1310
+ to: config.walletContractAddress,
1311
+ data: ownerInterface.encodeFunctionData("setVelocityLimit", [limitWei]),
1312
+ value: "0x0",
1313
+ }), `Approve: set velocity limit to ${limitEth} ETH`, telegramOpts, config);
1314
+ await provider.waitForTransaction(txHash);
1315
+ console.log(`\n✓ Velocity limit updated`);
1316
+ console.log(` Tx: ${txHash}`);
1317
+ console.log(` New limit: ${limitEth} ETH per rolling window`);
1318
+ });
1319
+ // ─── register-policy ───────────────────────────────────────────────────────
1320
+ //
1321
+ // Calls registerWallet(walletAddress, ownerAddress) on PolicyEngine via
1322
+ // executeContractCall on the ARC402Wallet. PolicyEngine requires msg.sender == wallet,
1323
+ // so this must go through the wallet contract — not called directly by the owner key.
1324
+ wallet.command("register-policy")
1325
+ .description("Register this wallet on PolicyEngine (required before spend limits can be set)")
1326
+ .option("--hardware", "Hardware wallet mode: show raw wc: URI only")
1327
+ .action(async (opts) => {
1328
+ const config = (0, config_1.loadConfig)();
1329
+ if (!config.walletContractAddress) {
1330
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1331
+ process.exit(1);
1332
+ }
1333
+ if (!config.walletConnectProjectId) {
1334
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1335
+ process.exit(1);
1336
+ }
1337
+ const ownerAddress = config.ownerAddress;
1338
+ if (!ownerAddress) {
1339
+ console.error("ownerAddress not set in config. Run `arc402 wallet deploy` first.");
1340
+ process.exit(1);
1341
+ }
1342
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
1343
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1344
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1345
+ // Encode registerWallet(wallet, owner) calldata — called on PolicyEngine
1346
+ const policyInterface = new ethers_1.ethers.Interface([
1347
+ "function registerWallet(address wallet, address owner) external",
1348
+ ]);
1349
+ const registerCalldata = policyInterface.encodeFunctionData("registerWallet", [
1350
+ config.walletContractAddress,
1351
+ ownerAddress,
1352
+ ]);
1353
+ const executeInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_EXECUTE_ABI);
1354
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1355
+ console.log(`PolicyEngine: ${policyAddress}`);
1356
+ console.log(`Owner: ${ownerAddress}`);
1357
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1358
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1359
+ : undefined;
1360
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1361
+ to: config.walletContractAddress,
1362
+ data: executeInterface.encodeFunctionData("executeContractCall", [{
1363
+ target: policyAddress,
1364
+ data: registerCalldata,
1365
+ value: 0n,
1366
+ minReturnValue: 0n,
1367
+ maxApprovalAmount: 0n,
1368
+ approvalToken: ethers_1.ethers.ZeroAddress,
1369
+ }]),
1370
+ value: "0x0",
1371
+ }), `Approve: register wallet on PolicyEngine`, telegramOpts, config);
1372
+ await provider.waitForTransaction(txHash);
1373
+ console.log(`\n✓ Wallet registered on PolicyEngine`);
1374
+ console.log(` Tx: ${txHash}`);
1375
+ console.log(`\nNext: run 'arc402 wallet policy set-limit' to configure spending limits.`);
1376
+ });
1377
+ // ─── cancel-registry-upgrade ───────────────────────────────────────────────
1378
+ wallet.command("cancel-registry-upgrade")
1379
+ .description("Cancel a pending registry upgrade before it executes (phone wallet signs via WalletConnect)")
1380
+ .action(async () => {
1381
+ const config = (0, config_1.loadConfig)();
1382
+ if (!config.walletContractAddress) {
1383
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1384
+ process.exit(1);
1385
+ }
1386
+ if (!config.walletConnectProjectId) {
1387
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1388
+ process.exit(1);
1389
+ }
1390
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1391
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1392
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_REGISTRY_ABI, provider);
1393
+ let pendingRegistry;
1394
+ let unlockAtCancel = 0n;
1395
+ try {
1396
+ [pendingRegistry, unlockAtCancel] = await Promise.all([
1397
+ walletContract.pendingRegistry(),
1398
+ walletContract.registryUpdateUnlockAt().catch(() => 0n),
1399
+ ]);
1400
+ }
1401
+ catch (e) {
1402
+ console.error("Failed to read pending registry from contract:", e);
1403
+ process.exit(1);
1404
+ }
1405
+ if (pendingRegistry === ethers_1.ethers.ZeroAddress) {
1406
+ console.log("No pending registry upgrade to cancel.");
1407
+ return;
1408
+ }
1409
+ const nowSecondsCancel = BigInt(Math.floor(Date.now() / 1000));
1410
+ const unlockDateCancel = unlockAtCancel > 0n
1411
+ ? new Date(Number(unlockAtCancel) * 1000).toISOString()
1412
+ : "(unknown)";
1413
+ const timelockStatus = unlockAtCancel > nowSecondsCancel
1414
+ ? `ACTIVE — executes at ${unlockDateCancel}`
1415
+ : `ELAPSED at ${unlockDateCancel} — execution window open`;
1416
+ console.log(`\nPending registry upgrade:`);
1417
+ console.log(` Pending address: ${pendingRegistry}`);
1418
+ console.log(` Timelock: ${timelockStatus}`);
1419
+ console.log(`\nCancelling pending registry upgrade to: ${pendingRegistry}`);
1420
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1421
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1422
+ : undefined;
1423
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_REGISTRY_ABI);
1424
+ const { txHash } = await (0, walletconnect_1.requestPhoneWalletSignature)(config.walletConnectProjectId, chainId, () => ({
1425
+ to: config.walletContractAddress,
1426
+ data: walletInterface.encodeFunctionData("cancelRegistryUpdate", []),
1427
+ value: "0x0",
1428
+ }), "Approve registry upgrade cancellation on ARC402Wallet", telegramOpts, config);
1429
+ console.log(`\n✓ Registry upgrade cancelled`);
1430
+ console.log(` Tx: ${txHash}`);
1431
+ });
1432
+ // ─── governance setup ──────────────────────────────────────────────────────
1433
+ //
1434
+ // Interactive wizard that collects velocity limit, guardian key, and category
1435
+ // limits in one session, then batches all transactions through a single
1436
+ // WalletConnect session (wallet_sendCalls if supported, else sequential).
1437
+ const governance = wallet.command("governance").description("Wallet governance management");
1438
+ governance.command("setup")
1439
+ .description("Interactive governance setup — velocity limit, guardian key, and spending limits in one WalletConnect session")
1440
+ .action(async () => {
1441
+ const config = (0, config_1.loadConfig)();
1442
+ if (!config.walletContractAddress) {
1443
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1444
+ process.exit(1);
1445
+ }
1446
+ if (!config.walletConnectProjectId) {
1447
+ console.error("walletConnectProjectId not set in config. Run `arc402 config set walletConnectProjectId <id>`.");
1448
+ process.exit(1);
1449
+ }
1450
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
1451
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1452
+ // ── Step 1: velocity limit ────────────────────────────────────────────
1453
+ const { velocityEth } = await (0, prompts_1.default)({
1454
+ type: "text",
1455
+ name: "velocityEth",
1456
+ message: "Velocity limit (max ETH per rolling window)",
1457
+ initial: "0.05",
1458
+ validate: (v) => {
1459
+ try {
1460
+ ethers_1.ethers.parseEther(v);
1461
+ return true;
1462
+ }
1463
+ catch {
1464
+ return "Enter a valid ETH amount (e.g. 0.05)";
1465
+ }
1466
+ },
1467
+ });
1468
+ if (velocityEth === undefined) {
1469
+ console.log("Aborted.");
1470
+ return;
1471
+ }
1472
+ // ── Step 2: guardian key ──────────────────────────────────────────────
1473
+ const { wantGuardian } = await (0, prompts_1.default)({
1474
+ type: "confirm",
1475
+ name: "wantGuardian",
1476
+ message: "Set guardian key?",
1477
+ initial: true,
1478
+ });
1479
+ if (wantGuardian === undefined) {
1480
+ console.log("Aborted.");
1481
+ return;
1482
+ }
1483
+ let guardianWallet = null;
1484
+ if (wantGuardian) {
1485
+ guardianWallet = new ethers_1.ethers.Wallet(ethers_1.ethers.Wallet.createRandom().privateKey);
1486
+ console.log(` Generated guardian address: ${guardianWallet.address}`);
1487
+ }
1488
+ const categories = [];
1489
+ const defaultCategories = [
1490
+ { label: "general", default: "0.02" },
1491
+ { label: "research", default: "0.05" },
1492
+ { label: "compute", default: "0.10" },
1493
+ ];
1494
+ console.log("\nSpending categories — press Enter to skip any:");
1495
+ for (const { label, default: def } of defaultCategories) {
1496
+ const { amountRaw } = await (0, prompts_1.default)({
1497
+ type: "text",
1498
+ name: "amountRaw",
1499
+ message: ` ${label} limit in ETH`,
1500
+ initial: def,
1501
+ });
1502
+ if (amountRaw === undefined) {
1503
+ console.log("Aborted.");
1504
+ return;
1505
+ }
1506
+ if (amountRaw.trim() !== "") {
1507
+ categories.push({ category: label, amountEth: amountRaw.trim() });
1508
+ }
1509
+ }
1510
+ // Custom categories loop
1511
+ while (true) {
1512
+ const { customName } = await (0, prompts_1.default)({
1513
+ type: "text",
1514
+ name: "customName",
1515
+ message: " Add custom category? [name or Enter to skip]",
1516
+ initial: "",
1517
+ });
1518
+ if (customName === undefined || customName.trim() === "")
1519
+ break;
1520
+ const { customAmount } = await (0, prompts_1.default)({
1521
+ type: "text",
1522
+ name: "customAmount",
1523
+ message: ` ${customName.trim()} limit in ETH`,
1524
+ initial: "0.05",
1525
+ validate: (v) => {
1526
+ try {
1527
+ ethers_1.ethers.parseEther(v);
1528
+ return true;
1529
+ }
1530
+ catch {
1531
+ return "Enter a valid ETH amount";
1532
+ }
1533
+ },
1534
+ });
1535
+ if (customAmount === undefined) {
1536
+ console.log("Aborted.");
1537
+ return;
1538
+ }
1539
+ categories.push({ category: customName.trim(), amountEth: customAmount.trim() });
1540
+ }
1541
+ // ── Step 4: summary ───────────────────────────────────────────────────
1542
+ console.log("\n─────────────────────────────────────────────────────");
1543
+ console.log("Changes to be made:");
1544
+ console.log(` Wallet: ${config.walletContractAddress}`);
1545
+ console.log(` Velocity limit: ${velocityEth} ETH per rolling window`);
1546
+ if (guardianWallet) {
1547
+ console.log(` Guardian key: ${guardianWallet.address} (new — private key will be saved to config)`);
1548
+ }
1549
+ if (categories.length > 0) {
1550
+ console.log(" Spending limits:");
1551
+ for (const { category, amountEth } of categories) {
1552
+ console.log(` ${category.padEnd(12)} ${amountEth} ETH`);
1553
+ }
1554
+ }
1555
+ console.log(` Transactions: ${1 + (guardianWallet ? 1 : 0) + categories.length} + onboarding (registerWallet, enableDefiAccess) total`);
1556
+ console.log("─────────────────────────────────────────────────────");
1557
+ // ── Step 5: confirm ───────────────────────────────────────────────────
1558
+ const { confirmed } = await (0, prompts_1.default)({
1559
+ type: "confirm",
1560
+ name: "confirmed",
1561
+ message: "Confirm and sign with your wallet?",
1562
+ initial: true,
1563
+ });
1564
+ if (!confirmed) {
1565
+ console.log("Aborted.");
1566
+ return;
1567
+ }
1568
+ // ── Step 6: connect WalletConnect once, send all transactions ─────────
1569
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1570
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1571
+ : undefined;
1572
+ console.log("\nConnecting wallet...");
1573
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: "Approve governance setup transactions on ARC402Wallet" });
1574
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1575
+ const ownerInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_OWNER_ABI);
1576
+ const guardianInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_GUARDIAN_ABI);
1577
+ const policyInterface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_LIMITS_ABI);
1578
+ const executeInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_EXECUTE_ABI);
1579
+ const govInterface = new ethers_1.ethers.Interface(abis_1.POLICY_ENGINE_GOVERNANCE_ABI);
1580
+ const policyGovContract = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_GOVERNANCE_ABI, provider);
1581
+ const calls = [];
1582
+ // ── P0: mandatory onboarding calls (registerWallet + enableDefiAccess) ──
1583
+ // Check what the constructor already did to avoid double-registration reverts
1584
+ let govAlreadyRegistered = false;
1585
+ let govAlreadyDefiEnabled = false;
1586
+ try {
1587
+ const registeredOwner = await policyGovContract.walletOwners(config.walletContractAddress);
1588
+ govAlreadyRegistered = registeredOwner !== ethers_1.ethers.ZeroAddress;
1589
+ }
1590
+ catch { /* assume not registered */ }
1591
+ try {
1592
+ govAlreadyDefiEnabled = await policyGovContract.defiAccessEnabled(config.walletContractAddress);
1593
+ }
1594
+ catch { /* assume not enabled */ }
1595
+ if (!govAlreadyRegistered) {
1596
+ const registerCalldata = govInterface.encodeFunctionData("registerWallet", [config.walletContractAddress, account]);
1597
+ calls.push({
1598
+ to: config.walletContractAddress,
1599
+ data: executeInterface.encodeFunctionData("executeContractCall", [{
1600
+ target: policyAddress,
1601
+ data: registerCalldata,
1602
+ value: 0n,
1603
+ minReturnValue: 0n,
1604
+ maxApprovalAmount: 0n,
1605
+ approvalToken: ethers_1.ethers.ZeroAddress,
1606
+ }]),
1607
+ value: "0x0",
1608
+ });
1609
+ }
1610
+ if (!govAlreadyDefiEnabled) {
1611
+ calls.push({
1612
+ to: policyAddress,
1613
+ data: govInterface.encodeFunctionData("enableDefiAccess", [config.walletContractAddress]),
1614
+ value: "0x0",
1615
+ });
1616
+ }
1617
+ // velocity limit
1618
+ calls.push({
1619
+ to: config.walletContractAddress,
1620
+ data: ownerInterface.encodeFunctionData("setVelocityLimit", [ethers_1.ethers.parseEther(velocityEth)]),
1621
+ value: "0x0",
1622
+ });
1623
+ // guardian
1624
+ if (guardianWallet) {
1625
+ calls.push({
1626
+ to: config.walletContractAddress,
1627
+ data: guardianInterface.encodeFunctionData("setGuardian", [guardianWallet.address]),
1628
+ value: "0x0",
1629
+ });
1630
+ }
1631
+ // category limits — called directly on PolicyEngine by owner key
1632
+ for (const { category, amountEth } of categories) {
1633
+ calls.push({
1634
+ to: policyAddress,
1635
+ data: policyInterface.encodeFunctionData("setCategoryLimitFor", [
1636
+ config.walletContractAddress,
1637
+ category,
1638
+ ethers_1.ethers.parseEther(amountEth),
1639
+ ]),
1640
+ value: "0x0",
1641
+ });
1642
+ }
1643
+ // Try wallet_sendCalls (EIP-5792) first, fall back to sequential eth_sendTransaction
1644
+ let txHashes = [];
1645
+ let usedBatch = false;
1646
+ try {
1647
+ const batchResult = await client.request({
1648
+ topic: session.topic,
1649
+ chainId: `eip155:${chainId}`,
1650
+ request: {
1651
+ method: "wallet_sendCalls",
1652
+ params: [{
1653
+ version: "1.0",
1654
+ chainId: `0x${chainId.toString(16)}`,
1655
+ from: account,
1656
+ calls: calls.map(c => ({ to: c.to, data: c.data, value: c.value })),
1657
+ }],
1658
+ },
1659
+ });
1660
+ txHashes = [typeof batchResult === "string" ? batchResult : batchResult.id];
1661
+ usedBatch = true;
1662
+ }
1663
+ catch {
1664
+ // wallet_sendCalls not supported — send sequentially
1665
+ console.log(" (wallet_sendCalls not supported — sending sequentially)");
1666
+ for (let i = 0; i < calls.length; i++) {
1667
+ console.log(` Sending transaction ${i + 1}/${calls.length}...`);
1668
+ const txHash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, calls[i]);
1669
+ txHashes.push(txHash);
1670
+ }
1671
+ }
1672
+ // Persist guardian key if generated
1673
+ if (guardianWallet) {
1674
+ config.guardianPrivateKey = guardianWallet.privateKey;
1675
+ config.guardianAddress = guardianWallet.address;
1676
+ (0, config_1.saveConfig)(config);
1677
+ }
1678
+ console.log(`\n✓ Governance setup complete`);
1679
+ if (usedBatch) {
1680
+ console.log(` Batch tx: ${txHashes[0]}`);
1681
+ }
1682
+ else {
1683
+ txHashes.forEach((h, i) => console.log(` Tx ${i + 1}: ${h}`));
1684
+ }
1685
+ if (guardianWallet) {
1686
+ console.log(` Guardian key saved to config — address: ${guardianWallet.address}`);
1687
+ console.log(` WARN: Store the guardian private key separately from your hot key.`);
1688
+ }
1689
+ console.log(`\nVerify with: arc402 wallet status && arc402 wallet policy show`);
1690
+ });
1691
+ // ─── authorize-machine-key ─────────────────────────────────────────────────
1692
+ wallet.command("authorize-machine-key <key>")
1693
+ .description("Authorize a machine key (hot key) on your ARC402Wallet (phone wallet signs via WalletConnect)")
1694
+ .action(async (keyAddress) => {
1695
+ const config = (0, config_1.loadConfig)();
1696
+ if (!config.walletContractAddress) {
1697
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1698
+ process.exit(1);
1699
+ }
1700
+ if (!config.walletConnectProjectId) {
1701
+ console.error("walletConnectProjectId not set in config.");
1702
+ process.exit(1);
1703
+ }
1704
+ let checksumKey;
1705
+ try {
1706
+ checksumKey = ethers_1.ethers.getAddress(keyAddress);
1707
+ }
1708
+ catch {
1709
+ console.error(`Invalid address: ${keyAddress}`);
1710
+ process.exit(1);
1711
+ }
1712
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1713
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1714
+ const machineKeyAbi = ["function authorizeMachineKey(address key) external", "function authorizedMachineKeys(address) external view returns (bool)"];
1715
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, machineKeyAbi, provider);
1716
+ let alreadyAuthorized = false;
1717
+ try {
1718
+ alreadyAuthorized = await walletContract.authorizedMachineKeys(checksumKey);
1719
+ }
1720
+ catch { /* ignore */ }
1721
+ if (alreadyAuthorized) {
1722
+ console.log(`\n✓ ${checksumKey} is already authorized as a machine key on ${config.walletContractAddress}`);
1723
+ process.exit(0);
1724
+ }
1725
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1726
+ console.log(`Machine key: ${checksumKey}`);
1727
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1728
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1729
+ : undefined;
1730
+ const walletInterface = new ethers_1.ethers.Interface(machineKeyAbi);
1731
+ const txData = {
1732
+ to: config.walletContractAddress,
1733
+ data: walletInterface.encodeFunctionData("authorizeMachineKey", [checksumKey]),
1734
+ value: "0x0",
1735
+ };
1736
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, {
1737
+ telegramOpts,
1738
+ prompt: `Authorize machine key ${checksumKey} on ARC402Wallet — allows autonomous protocol ops`,
1739
+ });
1740
+ console.log(`\n✓ Connected: ${account}`);
1741
+ console.log("Sending authorizeMachineKey transaction...");
1742
+ const hash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, txData);
1743
+ console.log(`\nTransaction submitted: ${hash}`);
1744
+ console.log("Waiting for confirmation...");
1745
+ const receipt = await provider.waitForTransaction(hash, 1, 60000);
1746
+ if (!receipt || receipt.status !== 1) {
1747
+ console.error("Transaction failed.");
1748
+ process.exit(1);
1749
+ }
1750
+ const confirmed = await walletContract.authorizedMachineKeys(checksumKey);
1751
+ console.log(`\n✓ Machine key authorized: ${confirmed ? "YES" : "NO"}`);
1752
+ console.log(` Wallet: ${config.walletContractAddress}`);
1753
+ console.log(` Machine key: ${checksumKey}`);
1754
+ console.log(` Tx: ${hash}`);
1755
+ await client.disconnect({ topic: session.topic, reason: { code: 6000, message: "done" } });
1756
+ process.exit(0);
1757
+ });
1758
+ // ─── revoke-machine-key (J1-04 / J5-01) ───────────────────────────────────
1759
+ //
1760
+ // Revokes an authorized machine key via owner WalletConnect approval.
1761
+ // Pre-checks that the key IS currently authorized before sending.
1762
+ wallet.command("revoke-machine-key <address>")
1763
+ .description("Revoke an authorized machine key on ARC402Wallet (phone wallet signs via WalletConnect)")
1764
+ .action(async (keyAddress) => {
1765
+ const config = (0, config_1.loadConfig)();
1766
+ if (!config.walletContractAddress) {
1767
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1768
+ process.exit(1);
1769
+ }
1770
+ if (!config.walletConnectProjectId) {
1771
+ console.error("walletConnectProjectId not set in config.");
1772
+ process.exit(1);
1773
+ }
1774
+ let checksumKey;
1775
+ try {
1776
+ checksumKey = ethers_1.ethers.getAddress(keyAddress);
1777
+ }
1778
+ catch {
1779
+ console.error(`Invalid address: ${keyAddress}`);
1780
+ process.exit(1);
1781
+ }
1782
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
1783
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1784
+ const walletContract = new ethers_1.ethers.Contract(config.walletContractAddress, abis_1.ARC402_WALLET_MACHINE_KEY_ABI, provider);
1785
+ // Pre-check: verify the key IS currently authorized
1786
+ let isAuthorized = false;
1787
+ try {
1788
+ isAuthorized = await walletContract.authorizedMachineKeys(checksumKey);
1789
+ }
1790
+ catch { /* ignore — attempt revoke anyway */ }
1791
+ if (!isAuthorized) {
1792
+ console.error(`Machine key ${checksumKey} is NOT currently authorized on ${config.walletContractAddress}.`);
1793
+ console.error(`Run \`arc402 wallet list-machine-keys\` to see authorized keys.`);
1794
+ process.exit(1);
1795
+ }
1796
+ console.log(`\nWallet: ${config.walletContractAddress}`);
1797
+ console.log(`Revoking: ${checksumKey}`);
1798
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
1799
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
1800
+ : undefined;
1801
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_MACHINE_KEY_ABI);
1802
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, { telegramOpts, prompt: `Revoke machine key ${checksumKey} on ARC402Wallet` });
1803
+ console.log(`\n✓ Connected: ${account}`);
1804
+ console.log("Sending revokeMachineKey transaction...");
1805
+ const hash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, {
1806
+ to: config.walletContractAddress,
1807
+ data: walletInterface.encodeFunctionData("revokeMachineKey", [checksumKey]),
1808
+ value: "0x0",
1809
+ });
1810
+ console.log(`\nTransaction submitted: ${hash}`);
1811
+ console.log("Waiting for confirmation...");
1812
+ const receipt = await provider.waitForTransaction(hash, 1, 60000);
1813
+ if (!receipt || receipt.status !== 1) {
1814
+ console.error("Transaction failed.");
1815
+ process.exit(1);
1816
+ }
1817
+ const stillAuthorized = await walletContract.authorizedMachineKeys(checksumKey);
1818
+ console.log(`\n✓ Machine key revoked: ${stillAuthorized ? "NO (still authorized — check tx)" : "YES"}`);
1819
+ console.log(` Wallet: ${config.walletContractAddress}`);
1820
+ console.log(` Machine key: ${checksumKey}`);
1821
+ console.log(` Tx: ${hash}`);
1822
+ await client.disconnect({ topic: session.topic, reason: { code: 6000, message: "done" } });
1823
+ process.exit(0);
1824
+ });
1825
+ // ─── list-machine-keys (J5-02) ─────────────────────────────────────────────
1826
+ //
1827
+ // Lists authorized machine keys by scanning MachineKeyAuthorized/MachineKeyRevoked
1828
+ // events. Falls back to checking the configured machine key if no events found.
1829
+ wallet.command("list-machine-keys")
1830
+ .description("List authorized machine keys by scanning contract events")
1831
+ .option("--json")
1832
+ .action(async (opts) => {
1833
+ const config = (0, config_1.loadConfig)();
1834
+ const walletAddr = config.walletContractAddress;
1835
+ if (!walletAddr) {
1836
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1837
+ process.exit(1);
1838
+ }
1839
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1840
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_MACHINE_KEY_ABI, provider);
1841
+ // Scan for MachineKeyAuthorized and MachineKeyRevoked events
1842
+ const authorizedTopic = ethers_1.ethers.id("MachineKeyAuthorized(address)");
1843
+ const revokedTopic = ethers_1.ethers.id("MachineKeyRevoked(address)");
1844
+ const authorizedKeys = new Set();
1845
+ const revokedKeys = new Set();
1846
+ try {
1847
+ const [authLogs, revokeLogs] = await Promise.all([
1848
+ provider.getLogs({ address: walletAddr, topics: [authorizedTopic], fromBlock: 0 }),
1849
+ provider.getLogs({ address: walletAddr, topics: [revokedTopic], fromBlock: 0 }),
1850
+ ]);
1851
+ for (const log of authLogs) {
1852
+ const key = ethers_1.ethers.getAddress("0x" + log.topics[1].slice(26));
1853
+ authorizedKeys.add(key);
1854
+ }
1855
+ for (const log of revokeLogs) {
1856
+ const key = ethers_1.ethers.getAddress("0x" + log.topics[1].slice(26));
1857
+ revokedKeys.add(key);
1858
+ }
1859
+ }
1860
+ catch { /* event scan failed — fall back to config key */ }
1861
+ // Build active key list: authorized but not revoked
1862
+ const activeFromEvents = [...authorizedKeys].filter((k) => !revokedKeys.has(k));
1863
+ // Also check configured machine key
1864
+ const configMachineKey = config.privateKey ? new ethers_1.ethers.Wallet(config.privateKey).address : null;
1865
+ // Verify each candidate against chain
1866
+ const candidates = new Set(activeFromEvents);
1867
+ if (configMachineKey)
1868
+ candidates.add(configMachineKey);
1869
+ const results = [];
1870
+ for (const addr of candidates) {
1871
+ let authorized = false;
1872
+ try {
1873
+ authorized = await walletContract.authorizedMachineKeys(addr);
1874
+ }
1875
+ catch { /* ignore */ }
1876
+ results.push({ address: addr, authorized });
1877
+ }
1878
+ if (opts.json) {
1879
+ console.log(JSON.stringify({ walletAddress: walletAddr, machineKeys: results }, null, 2));
1880
+ }
1881
+ else {
1882
+ console.log(`\nMachine keys for wallet: ${walletAddr}\n`);
1883
+ if (results.length === 0) {
1884
+ console.log(" No machine keys found.");
1885
+ }
1886
+ else {
1887
+ for (const r of results) {
1888
+ const status = r.authorized ? "AUTHORIZED" : "not authorized";
1889
+ const tag = r.address === configMachineKey ? " [configured]" : "";
1890
+ console.log(` ${r.address} ${status}${tag}`);
1891
+ }
1892
+ }
1893
+ console.log(`\nTo authorize: arc402 wallet authorize-machine-key <address>`);
1894
+ console.log(`To revoke: arc402 wallet revoke-machine-key <address>`);
1895
+ }
1896
+ });
1897
+ // ─── open-context (J1-03) ──────────────────────────────────────────────────
1898
+ //
1899
+ // Standalone command for opening a spend context via machine key.
1900
+ // Note: each context allows only one spend — a new context must be opened per payment.
1901
+ wallet.command("open-context")
1902
+ .description("Open a spend context on the wallet via machine key (each context allows only one spend)")
1903
+ .option("--task-type <type>", "Task type string for the context", "general")
1904
+ .option("--json")
1905
+ .action(async (opts) => {
1906
+ const config = (0, config_1.loadConfig)();
1907
+ (0, config_2.warnIfPublicRpc)(config);
1908
+ const walletAddr = config.walletContractAddress;
1909
+ if (!walletAddr) {
1910
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1911
+ process.exit(1);
1912
+ }
1913
+ if (!config.privateKey) {
1914
+ console.error("privateKey not set in config — machine key required for open-context.");
1915
+ process.exit(1);
1916
+ }
1917
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1918
+ const machineKey = new ethers_1.ethers.Wallet(config.privateKey, provider);
1919
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, machineKey);
1920
+ // Check context isn't already open
1921
+ const isOpen = await walletContract.contextOpen();
1922
+ if (isOpen) {
1923
+ console.error("A context is already open on this wallet.");
1924
+ console.error("Close it first: arc402 wallet close-context");
1925
+ process.exit(1);
1926
+ }
1927
+ const contextId = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
1928
+ console.log(`Opening context (taskType: ${opts.taskType})...`);
1929
+ const tx = await walletContract.openContext(contextId, opts.taskType);
1930
+ const receipt = await tx.wait(1);
1931
+ if (opts.json) {
1932
+ console.log(JSON.stringify({ walletAddress: walletAddr, contextId, taskType: opts.taskType, txHash: receipt?.hash }));
1933
+ }
1934
+ else {
1935
+ console.log(`✓ Context opened`);
1936
+ console.log(` contextId: ${contextId}`);
1937
+ console.log(` taskType: ${opts.taskType}`);
1938
+ console.log(` Tx: ${receipt?.hash}`);
1939
+ console.log(`\nNote: Each context allows only one spend. Call \`arc402 wallet attest\` then \`arc402 wallet drain\` (or executeSpend directly).`);
1940
+ }
1941
+ });
1942
+ // ─── attest (J1-03) ────────────────────────────────────────────────────────
1943
+ //
1944
+ // Standalone command for creating an attestation via machine key.
1945
+ // Returns the attestationId for use in executeSpend / drain.
1946
+ wallet.command("attest")
1947
+ .description("Create an attestation via machine key directly on wallet, returns attestationId")
1948
+ .requiredOption("--recipient <addr>", "Recipient address")
1949
+ .requiredOption("--amount <eth>", "Amount in ETH")
1950
+ .requiredOption("--category <cat>", "Spend category (used as action)")
1951
+ .option("--token <addr>", "Token contract address (default: ETH / zero address)")
1952
+ .option("--ttl <seconds>", "Attestation TTL in seconds (default: 600)", "600")
1953
+ .option("--json")
1954
+ .action(async (opts) => {
1955
+ const config = (0, config_1.loadConfig)();
1956
+ (0, config_2.warnIfPublicRpc)(config);
1957
+ const walletAddr = config.walletContractAddress;
1958
+ if (!walletAddr) {
1959
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
1960
+ process.exit(1);
1961
+ }
1962
+ if (!config.privateKey) {
1963
+ console.error("privateKey not set in config — machine key required for attest.");
1964
+ process.exit(1);
1965
+ }
1966
+ let checksumRecipient;
1967
+ try {
1968
+ checksumRecipient = ethers_1.ethers.getAddress(opts.recipient);
1969
+ }
1970
+ catch {
1971
+ console.error(`Invalid recipient address: ${opts.recipient}`);
1972
+ process.exit(1);
1973
+ }
1974
+ const tokenAddress = opts.token ? ethers_1.ethers.getAddress(opts.token) : ethers_1.ethers.ZeroAddress;
1975
+ const amount = ethers_1.ethers.parseEther(opts.amount);
1976
+ const expiresAt = Math.floor(Date.now() / 1000) + parseInt(opts.ttl, 10);
1977
+ const attestationId = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
1978
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
1979
+ const machineKey = new ethers_1.ethers.Wallet(config.privateKey, provider);
1980
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, machineKey);
1981
+ console.log(`Creating attestation...`);
1982
+ const tx = await walletContract.attest(attestationId, opts.category, `cli attest: ${opts.category} to ${checksumRecipient}`, checksumRecipient, amount, tokenAddress, expiresAt);
1983
+ const receipt = await tx.wait(1);
1984
+ if (opts.json) {
1985
+ console.log(JSON.stringify({
1986
+ walletAddress: walletAddr,
1987
+ attestationId,
1988
+ recipient: checksumRecipient,
1989
+ amount: opts.amount,
1990
+ token: tokenAddress,
1991
+ category: opts.category,
1992
+ expiresAt,
1993
+ txHash: receipt?.hash,
1994
+ }));
1995
+ }
1996
+ else {
1997
+ console.log(`✓ Attestation created`);
1998
+ console.log(` attestationId: ${attestationId}`);
1999
+ console.log(` recipient: ${checksumRecipient}`);
2000
+ console.log(` amount: ${opts.amount} ETH`);
2001
+ console.log(` token: ${tokenAddress === ethers_1.ethers.ZeroAddress ? "ETH" : tokenAddress}`);
2002
+ console.log(` expiresAt: ${new Date(expiresAt * 1000).toISOString()}`);
2003
+ console.log(` Tx: ${receipt?.hash}`);
2004
+ console.log(`\nUse this attestationId in \`arc402 wallet drain\` or your spend flow.`);
2005
+ }
2006
+ });
2007
+ // ─── velocity-status (J8-03) ───────────────────────────────────────────────
2008
+ //
2009
+ // Read-only: shows wallet-level velocity limit, window start, cumulative spend, and remaining.
2010
+ wallet.command("velocity-status")
2011
+ .description("Show wallet-level velocity limit, current window spend, and remaining budget")
2012
+ .option("--json")
2013
+ .action(async (opts) => {
2014
+ const config = (0, config_1.loadConfig)();
2015
+ const walletAddr = config.walletContractAddress;
2016
+ if (!walletAddr) {
2017
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2018
+ process.exit(1);
2019
+ }
2020
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2021
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_OWNER_ABI, provider);
2022
+ let velocityLimit = 0n;
2023
+ let velocityWindowStart = 0n;
2024
+ let cumulativeSpend = 0n;
2025
+ try {
2026
+ [velocityLimit, velocityWindowStart, cumulativeSpend] = await Promise.all([
2027
+ walletContract.velocityLimit(),
2028
+ walletContract.velocityWindowStart(),
2029
+ walletContract.cumulativeSpend(),
2030
+ ]);
2031
+ }
2032
+ catch (e) {
2033
+ console.error("Failed to read velocity data from wallet:", e instanceof Error ? e.message : String(e));
2034
+ process.exit(1);
2035
+ }
2036
+ const remaining = velocityLimit === 0n ? null : (velocityLimit > cumulativeSpend ? velocityLimit - cumulativeSpend : 0n);
2037
+ const windowStartDate = velocityWindowStart === 0n ? null : new Date(Number(velocityWindowStart) * 1000);
2038
+ if (opts.json) {
2039
+ console.log(JSON.stringify({
2040
+ walletAddress: walletAddr,
2041
+ velocityLimit: ethers_1.ethers.formatEther(velocityLimit),
2042
+ velocityLimitEnabled: velocityLimit > 0n,
2043
+ velocityWindowStart: windowStartDate?.toISOString() ?? null,
2044
+ cumulativeSpend: ethers_1.ethers.formatEther(cumulativeSpend),
2045
+ remaining: remaining !== null ? ethers_1.ethers.formatEther(remaining) : null,
2046
+ }, null, 2));
2047
+ }
2048
+ else {
2049
+ console.log(`\nWallet velocity status: ${walletAddr}\n`);
2050
+ if (velocityLimit === 0n) {
2051
+ console.log(` Velocity limit: disabled (set with \`arc402 wallet set-velocity-limit <eth>\`)`);
2052
+ }
2053
+ else {
2054
+ console.log(` Limit: ${ethers_1.ethers.formatEther(velocityLimit)} ETH per rolling window`);
2055
+ console.log(` Window start: ${windowStartDate?.toISOString() ?? "(no window yet)"}`);
2056
+ console.log(` Spent: ${ethers_1.ethers.formatEther(cumulativeSpend)} ETH`);
2057
+ console.log(` Remaining: ${remaining !== null ? ethers_1.ethers.formatEther(remaining) + " ETH" : "N/A"}`);
2058
+ }
2059
+ }
2060
+ });
2061
+ // ─── check-context ─────────────────────────────────────────────────────────
2062
+ //
2063
+ // P1 guardrail: inspect on-chain context state before attempting openContext.
2064
+ wallet.command("check-context")
2065
+ .description("Check whether the wallet's spend context is currently open (uses Alchemy RPC)")
2066
+ .option("--json")
2067
+ .action(async (opts) => {
2068
+ const config = (0, config_1.loadConfig)();
2069
+ (0, config_2.warnIfPublicRpc)(config);
2070
+ const walletAddr = config.walletContractAddress;
2071
+ if (!walletAddr) {
2072
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2073
+ process.exit(1);
2074
+ }
2075
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2076
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, provider);
2077
+ const isOpen = await walletContract.contextOpen();
2078
+ if (opts.json) {
2079
+ console.log(JSON.stringify({ walletAddress: walletAddr, contextOpen: isOpen }));
2080
+ }
2081
+ else {
2082
+ console.log(`Wallet: ${walletAddr}`);
2083
+ console.log(`contextOpen: ${isOpen ? "OPEN — close before opening a new context" : "closed"}`);
2084
+ }
2085
+ });
2086
+ // ─── close-context ─────────────────────────────────────────────────────────
2087
+ //
2088
+ // P1 guardrail: force-close a stale context that was left open by a failed operation.
2089
+ // Uses the machine key (config.privateKey) — onlyOwnerOrMachineKey.
2090
+ wallet.command("close-context")
2091
+ .description("Force-close a stale open context on the wallet (machine key signs — onlyOwnerOrMachineKey)")
2092
+ .option("--json")
2093
+ .action(async (opts) => {
2094
+ const config = (0, config_1.loadConfig)();
2095
+ (0, config_2.warnIfPublicRpc)(config);
2096
+ const walletAddr = config.walletContractAddress;
2097
+ if (!walletAddr) {
2098
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2099
+ process.exit(1);
2100
+ }
2101
+ if (!config.privateKey) {
2102
+ console.error("privateKey not set in config — machine key required for close-context.");
2103
+ process.exit(1);
2104
+ }
2105
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2106
+ const machineKey = new ethers_1.ethers.Wallet(config.privateKey, provider);
2107
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, machineKey);
2108
+ const isOpen = await walletContract.contextOpen();
2109
+ if (!isOpen) {
2110
+ if (opts.json) {
2111
+ console.log(JSON.stringify({ walletAddress: walletAddr, contextOpen: false, action: "nothing — already closed" }));
2112
+ }
2113
+ else {
2114
+ console.log("Context is already closed — nothing to do.");
2115
+ }
2116
+ return;
2117
+ }
2118
+ console.log("Closing stale context...");
2119
+ const tx = await walletContract.closeContext();
2120
+ const receipt = await tx.wait(2);
2121
+ if (opts.json) {
2122
+ console.log(JSON.stringify({ walletAddress: walletAddr, txHash: receipt?.hash, contextOpen: false }));
2123
+ }
2124
+ else {
2125
+ console.log(`✓ Context closed`);
2126
+ console.log(` Tx: ${receipt?.hash}`);
2127
+ console.log(` Wallet: ${walletAddr}`);
2128
+ }
2129
+ });
2130
+ // ─── drain ─────────────────────────────────────────────────────────────────
2131
+ //
2132
+ // P1 + BUG-DRAIN-06: full autonomous drain via machine key.
2133
+ // Flow: check context → close if stale → openContext → attest (direct) → executeSpend → closeContext
2134
+ // All transactions signed by machine key (onlyOwnerOrMachineKey). No WalletConnect needed.
2135
+ wallet.command("drain")
2136
+ .description("Drain ETH from wallet contract to recipient via machine key (openContext → attest → executeSpend → closeContext). Note: each context allows exactly one spend — a new context is opened per call.")
2137
+ .argument("[recipient]", "Recipient address (defaults to config.ownerAddress)")
2138
+ .option("--amount <eth>", "Amount to drain in ETH (default: all minus 0.00005 ETH gas reserve)")
2139
+ .option("--category <cat>", "Spend category (default: general)", "general")
2140
+ .option("--json")
2141
+ .action(async (recipientArg, opts) => {
2142
+ const config = (0, config_1.loadConfig)();
2143
+ (0, config_2.warnIfPublicRpc)(config);
2144
+ const walletAddr = config.walletContractAddress;
2145
+ if (!walletAddr) {
2146
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2147
+ process.exit(1);
2148
+ }
2149
+ if (!config.privateKey) {
2150
+ console.error("privateKey not set in config — machine key required for drain.");
2151
+ process.exit(1);
2152
+ }
2153
+ const recipient = recipientArg ?? config.ownerAddress;
2154
+ if (!recipient) {
2155
+ console.error("No recipient address. Pass a recipient argument or set ownerAddress in config.");
2156
+ process.exit(1);
2157
+ }
2158
+ let checksumRecipient;
2159
+ try {
2160
+ checksumRecipient = ethers_1.ethers.getAddress(recipient);
2161
+ }
2162
+ catch {
2163
+ console.error(`Invalid recipient address: ${recipient}`);
2164
+ process.exit(1);
2165
+ }
2166
+ const GAS_RESERVE = ethers_1.ethers.parseEther("0.00005");
2167
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2168
+ const machineKey = new ethers_1.ethers.Wallet(config.privateKey, provider);
2169
+ const walletContract = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, machineKey);
2170
+ // ── Pre-flight checks ──────────────────────────────────────────────────
2171
+ const balance = await provider.getBalance(walletAddr);
2172
+ console.log(`Wallet balance: ${ethers_1.ethers.formatEther(balance)} ETH`);
2173
+ if (balance <= GAS_RESERVE) {
2174
+ console.error(`Insufficient balance: ${ethers_1.ethers.formatEther(balance)} ETH — need more than ${ethers_1.ethers.formatEther(GAS_RESERVE)} ETH reserve`);
2175
+ process.exit(1);
2176
+ }
2177
+ // Check category is configured on PolicyEngine
2178
+ const policyAddress = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
2179
+ const policyContract = new ethers_1.ethers.Contract(policyAddress, abis_1.POLICY_ENGINE_LIMITS_ABI, provider);
2180
+ const categoryLimit = await policyContract.categoryLimits(walletAddr, opts.category);
2181
+ if (categoryLimit === 0n) {
2182
+ console.error(`Category "${opts.category}" is not configured on PolicyEngine for this wallet.`);
2183
+ console.error(`Fix: arc402 wallet policy set-limit --category ${opts.category} --amount <eth>`);
2184
+ process.exit(1);
2185
+ }
2186
+ // Verify machine key is authorized
2187
+ const machineKeyAbi = ["function authorizedMachineKeys(address) external view returns (bool)"];
2188
+ const walletCheck = new ethers_1.ethers.Contract(walletAddr, machineKeyAbi, provider);
2189
+ let isAuthorized = false;
2190
+ try {
2191
+ isAuthorized = await walletCheck.authorizedMachineKeys(machineKey.address);
2192
+ }
2193
+ catch { /* older wallet — assume authorized */
2194
+ isAuthorized = true;
2195
+ }
2196
+ if (!isAuthorized) {
2197
+ console.error(`Machine key ${machineKey.address} is not authorized on wallet ${walletAddr}`);
2198
+ console.error(`Fix: arc402 wallet authorize-machine-key ${machineKey.address}`);
2199
+ process.exit(1);
2200
+ }
2201
+ // Compute drain amount
2202
+ let drainAmount;
2203
+ if (opts.amount) {
2204
+ drainAmount = ethers_1.ethers.parseEther(opts.amount);
2205
+ }
2206
+ else {
2207
+ drainAmount = balance - GAS_RESERVE;
2208
+ }
2209
+ if (drainAmount > categoryLimit) {
2210
+ console.warn(`WARN: drainAmount (${ethers_1.ethers.formatEther(drainAmount)} ETH) exceeds category limit (${ethers_1.ethers.formatEther(categoryLimit)} ETH)`);
2211
+ console.warn(` Capping at category limit.`);
2212
+ drainAmount = categoryLimit;
2213
+ }
2214
+ console.log(`\nDrain plan:`);
2215
+ console.log(` Wallet: ${walletAddr}`);
2216
+ console.log(` Recipient: ${checksumRecipient}`);
2217
+ console.log(` Amount: ${ethers_1.ethers.formatEther(drainAmount)} ETH`);
2218
+ console.log(` Category: ${opts.category}`);
2219
+ console.log(` MachineKey: ${machineKey.address}`);
2220
+ console.log(`\nNote: Each context allows exactly one spend. A new context is opened for each drain call.\n`);
2221
+ // ── Step 1: context cleanup ────────────────────────────────────────────
2222
+ const isOpen = await walletContract.contextOpen();
2223
+ if (isOpen) {
2224
+ console.log("Stale context found — closing it first...");
2225
+ const closeTx = await walletContract.closeContext();
2226
+ await closeTx.wait(2);
2227
+ console.log(` ✓ Closed: ${closeTx.hash}`);
2228
+ }
2229
+ // ── Step 2: openContext ────────────────────────────────────────────────
2230
+ const contextId = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(`drain-${Date.now()}`));
2231
+ console.log("Opening context...");
2232
+ const openTx = await walletContract.openContext(contextId, "drain");
2233
+ const openReceipt = await openTx.wait(1);
2234
+ console.log(` ✓ openContext: ${openReceipt?.hash}`);
2235
+ // ── Step 3: attest (direct on wallet — onlyOwnerOrMachineKey, NOT via executeContractCall)
2236
+ const attestationId = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2237
+ const expiry = Math.floor(Date.now() / 1000) + 600; // 10 min TTL
2238
+ console.log("Creating attestation (direct on wallet)...");
2239
+ const attestTx = await walletContract.attest(attestationId, "spend", `drain to ${checksumRecipient}`, checksumRecipient, drainAmount, ethers_1.ethers.ZeroAddress, expiry);
2240
+ const attestReceipt = await attestTx.wait(1);
2241
+ console.log(` ✓ attest: ${attestReceipt?.hash}`);
2242
+ // ── Step 4: executeSpend ───────────────────────────────────────────────
2243
+ console.log("Executing spend...");
2244
+ let spendReceiptHash;
2245
+ try {
2246
+ const spendTx = await walletContract.executeSpend(checksumRecipient, drainAmount, opts.category, attestationId);
2247
+ const spendReceipt = await spendTx.wait(1);
2248
+ spendReceiptHash = spendReceipt?.hash;
2249
+ }
2250
+ catch (e) {
2251
+ (0, wallet_router_1.handleWalletError)(e);
2252
+ }
2253
+ console.log(` ✓ executeSpend: ${spendReceiptHash}`);
2254
+ // ── Step 5: closeContext ───────────────────────────────────────────────
2255
+ console.log("Closing context...");
2256
+ const closeTx2 = await walletContract.closeContext();
2257
+ const closeReceipt = await closeTx2.wait(1);
2258
+ console.log(` ✓ closeContext: ${closeReceipt?.hash}`);
2259
+ const newBalance = await provider.getBalance(walletAddr);
2260
+ if (opts.json) {
2261
+ console.log(JSON.stringify({
2262
+ ok: true,
2263
+ walletAddress: walletAddr,
2264
+ recipient: checksumRecipient,
2265
+ amount: ethers_1.ethers.formatEther(drainAmount),
2266
+ category: opts.category,
2267
+ txHashes: {
2268
+ openContext: openReceipt?.hash,
2269
+ attest: attestReceipt?.hash,
2270
+ executeSpend: spendReceiptHash,
2271
+ closeContext: closeReceipt?.hash,
2272
+ },
2273
+ remainingBalance: ethers_1.ethers.formatEther(newBalance),
2274
+ }));
2275
+ }
2276
+ else {
2277
+ console.log(`\n✓ Drain complete`);
2278
+ console.log(` Sent: ${ethers_1.ethers.formatEther(drainAmount)} ETH → ${checksumRecipient}`);
2279
+ console.log(` Remaining: ${ethers_1.ethers.formatEther(newBalance)} ETH`);
2280
+ }
2281
+ });
2282
+ // ─── drain-token ───────────────────────────────────────────────────────────
2283
+ //
2284
+ // ERC-20 token drain via machine key (J1-07).
2285
+ // Flow: check context → close if stale → openContext → attest (with token address)
2286
+ // → executeTokenSpend → closeContext
2287
+ // Note: Each context can only be used for one spend. A new context must be opened
2288
+ // for each payment.
2289
+ wallet.command("drain-token")
2290
+ .description("Drain ERC-20 tokens from wallet contract to recipient via machine key (openContext → attest → executeTokenSpend → closeContext). Note: each context allows exactly one spend.")
2291
+ .argument("<recipient>", "Recipient address")
2292
+ .argument("<amount>", "Token amount in human units (e.g. 1.5 for 1.5 USDC)")
2293
+ .requiredOption("--token <address>", "ERC-20 token contract address (or 'usdc' for configured USDC address)")
2294
+ .option("--category <cat>", "Spend category (default: general)", "general")
2295
+ .option("--decimals <n>", "Token decimals override (default: auto-detect from contract)", "auto")
2296
+ .option("--json")
2297
+ .action(async (recipientArg, amountArg, opts) => {
2298
+ const config = (0, config_1.loadConfig)();
2299
+ (0, config_2.warnIfPublicRpc)(config);
2300
+ const walletAddr = config.walletContractAddress;
2301
+ if (!walletAddr) {
2302
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2303
+ process.exit(1);
2304
+ }
2305
+ if (!config.privateKey) {
2306
+ console.error("privateKey not set in config — machine key required for drain-token.");
2307
+ process.exit(1);
2308
+ }
2309
+ // Resolve token address
2310
+ let tokenAddress;
2311
+ if (opts.token.toLowerCase() === "usdc") {
2312
+ tokenAddress = (0, config_1.getUsdcAddress)(config);
2313
+ }
2314
+ else {
2315
+ try {
2316
+ tokenAddress = ethers_1.ethers.getAddress(opts.token);
2317
+ }
2318
+ catch {
2319
+ console.error(`Invalid token address: ${opts.token}`);
2320
+ process.exit(1);
2321
+ }
2322
+ }
2323
+ let checksumRecipient;
2324
+ try {
2325
+ checksumRecipient = ethers_1.ethers.getAddress(recipientArg);
2326
+ }
2327
+ catch {
2328
+ console.error(`Invalid recipient address: ${recipientArg}`);
2329
+ process.exit(1);
2330
+ }
2331
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2332
+ const machineKey = new ethers_1.ethers.Wallet(config.privateKey, provider);
2333
+ // Determine token decimals
2334
+ const erc20Abi = [
2335
+ "function decimals() external view returns (uint8)",
2336
+ "function balanceOf(address owner) external view returns (uint256)",
2337
+ ];
2338
+ const erc20 = new ethers_1.ethers.Contract(tokenAddress, erc20Abi, provider);
2339
+ let decimals;
2340
+ if (opts.decimals !== "auto") {
2341
+ decimals = parseInt(opts.decimals, 10);
2342
+ }
2343
+ else {
2344
+ try {
2345
+ decimals = Number(await erc20.decimals());
2346
+ }
2347
+ catch {
2348
+ decimals = 18;
2349
+ }
2350
+ }
2351
+ let tokenAmount;
2352
+ try {
2353
+ tokenAmount = ethers_1.ethers.parseUnits(amountArg, decimals);
2354
+ }
2355
+ catch {
2356
+ console.error(`Invalid amount: ${amountArg}. Provide a decimal value (e.g. 1.5).`);
2357
+ process.exit(1);
2358
+ }
2359
+ // Check token balance
2360
+ const tokenBalance = await erc20.balanceOf(walletAddr);
2361
+ if (tokenBalance < tokenAmount) {
2362
+ console.error(`Insufficient token balance: ${ethers_1.ethers.formatUnits(tokenBalance, decimals)} < ${amountArg}`);
2363
+ process.exit(1);
2364
+ }
2365
+ // Check category is configured on PolicyEngine
2366
+ const policyAddressT = config.policyEngineAddress ?? POLICY_ENGINE_DEFAULT;
2367
+ const policyContractT = new ethers_1.ethers.Contract(policyAddressT, abis_1.POLICY_ENGINE_LIMITS_ABI, provider);
2368
+ const categoryLimitT = await policyContractT.categoryLimits(walletAddr, opts.category);
2369
+ if (categoryLimitT === 0n) {
2370
+ console.error(`Category "${opts.category}" is not configured on PolicyEngine for this wallet.`);
2371
+ console.error(`Fix: arc402 wallet policy set-limit --category ${opts.category} --amount <eth>`);
2372
+ process.exit(1);
2373
+ }
2374
+ // Verify machine key is authorized
2375
+ const mkAbi = ["function authorizedMachineKeys(address) external view returns (bool)"];
2376
+ const walletCheckT = new ethers_1.ethers.Contract(walletAddr, mkAbi, provider);
2377
+ let isAuthorizedT = false;
2378
+ try {
2379
+ isAuthorizedT = await walletCheckT.authorizedMachineKeys(machineKey.address);
2380
+ }
2381
+ catch {
2382
+ isAuthorizedT = true;
2383
+ }
2384
+ if (!isAuthorizedT) {
2385
+ console.error(`Machine key ${machineKey.address} is not authorized on wallet ${walletAddr}`);
2386
+ console.error(`Fix: arc402 wallet authorize-machine-key ${machineKey.address}`);
2387
+ process.exit(1);
2388
+ }
2389
+ const walletContractT = new ethers_1.ethers.Contract(walletAddr, abis_1.ARC402_WALLET_PROTOCOL_ABI, machineKey);
2390
+ console.log(`\nDrain token plan:`);
2391
+ console.log(` Wallet: ${walletAddr}`);
2392
+ console.log(` Recipient: ${checksumRecipient}`);
2393
+ console.log(` Amount: ${amountArg} (${tokenAmount.toString()} units)`);
2394
+ console.log(` Token: ${tokenAddress}`);
2395
+ console.log(` Category: ${opts.category}`);
2396
+ console.log(` MachineKey: ${machineKey.address}`);
2397
+ console.log(`\nNote: Each context allows exactly one spend. A new context is opened for each drain-token call.\n`);
2398
+ // ── Step 1: context cleanup ──────────────────────────────────────────────
2399
+ const isOpenT = await walletContractT.contextOpen();
2400
+ if (isOpenT) {
2401
+ console.log("Stale context found — closing it first...");
2402
+ const closeTxT = await walletContractT.closeContext();
2403
+ await closeTxT.wait(2);
2404
+ console.log(` ✓ Closed: ${closeTxT.hash}`);
2405
+ }
2406
+ // ── Step 2: openContext ──────────────────────────────────────────────────
2407
+ const contextIdT = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(`drain-token-${Date.now()}`));
2408
+ console.log("Opening context...");
2409
+ const openTxT = await walletContractT.openContext(contextIdT, "drain");
2410
+ const openReceiptT = await openTxT.wait(1);
2411
+ console.log(` ✓ openContext: ${openReceiptT?.hash}`);
2412
+ // ── Step 3: attest with token address ────────────────────────────────────
2413
+ const attestationIdT = ethers_1.ethers.hexlify(ethers_1.ethers.randomBytes(32));
2414
+ const expiryT = Math.floor(Date.now() / 1000) + 600; // 10 min TTL
2415
+ console.log("Creating attestation (with token address)...");
2416
+ const attestTxT = await walletContractT.attest(attestationIdT, "spend", `token drain to ${checksumRecipient}`, checksumRecipient, tokenAmount, tokenAddress, expiryT);
2417
+ const attestReceiptT = await attestTxT.wait(1);
2418
+ console.log(` ✓ attest: ${attestReceiptT?.hash}`);
2419
+ // ── Step 4: executeTokenSpend ────────────────────────────────────────────
2420
+ console.log("Executing token spend...");
2421
+ const spendTxT = await walletContractT.executeTokenSpend(checksumRecipient, tokenAmount, tokenAddress, opts.category, attestationIdT);
2422
+ const spendReceiptT = await spendTxT.wait(1);
2423
+ console.log(` ✓ executeTokenSpend: ${spendReceiptT?.hash}`);
2424
+ // ── Step 5: closeContext ─────────────────────────────────────────────────
2425
+ console.log("Closing context...");
2426
+ const closeTxT2 = await walletContractT.closeContext();
2427
+ const closeReceiptT = await closeTxT2.wait(1);
2428
+ console.log(` ✓ closeContext: ${closeReceiptT?.hash}`);
2429
+ const newTokenBalance = await erc20.balanceOf(walletAddr);
2430
+ if (opts.json) {
2431
+ console.log(JSON.stringify({
2432
+ ok: true,
2433
+ walletAddress: walletAddr,
2434
+ recipient: checksumRecipient,
2435
+ amount: amountArg,
2436
+ token: tokenAddress,
2437
+ category: opts.category,
2438
+ txHashes: {
2439
+ openContext: openReceiptT?.hash,
2440
+ attest: attestReceiptT?.hash,
2441
+ executeTokenSpend: spendReceiptT?.hash,
2442
+ closeContext: closeReceiptT?.hash,
2443
+ },
2444
+ remainingTokenBalance: ethers_1.ethers.formatUnits(newTokenBalance, decimals),
2445
+ }));
2446
+ }
2447
+ else {
2448
+ console.log(`\n✓ Token drain complete`);
2449
+ console.log(` Sent: ${amountArg} → ${checksumRecipient}`);
2450
+ console.log(` Token: ${tokenAddress}`);
2451
+ console.log(` Remaining: ${ethers_1.ethers.formatUnits(newTokenBalance, decimals)}`);
2452
+ }
2453
+ });
2454
+ // ─── set-passkey ───────────────────────────────────────────────────────────
2455
+ //
2456
+ // Called after registering a Face ID on app.arc402.xyz/onboard (Step 2).
2457
+ // Takes the P256 public key coordinates extracted from the WebAuthn credential
2458
+ // and writes them on-chain via ARC402Wallet.setPasskey(bytes32, bytes32).
2459
+ // After this call, governance UserOps must carry a P256 signature (Face ID).
2460
+ wallet.command("set-passkey <pubKeyX> <pubKeyY>")
2461
+ .description("Activate passkey (Face ID) on ARC402Wallet — takes P256 x/y coords from passkey setup (phone wallet signs via WalletConnect)")
2462
+ .action(async (pubKeyX, pubKeyY) => {
2463
+ const config = (0, config_1.loadConfig)();
2464
+ if (!config.walletContractAddress) {
2465
+ console.error("walletContractAddress not set in config. Run `arc402 wallet deploy` first.");
2466
+ process.exit(1);
2467
+ }
2468
+ if (!config.walletConnectProjectId) {
2469
+ console.error("walletConnectProjectId not set in config.");
2470
+ process.exit(1);
2471
+ }
2472
+ // Validate hex bytes32 format
2473
+ const isBytes32Hex = (v) => /^0x[0-9a-fA-F]{64}$/.test(v);
2474
+ if (!isBytes32Hex(pubKeyX)) {
2475
+ console.error(`Invalid pubKeyX: expected 0x-prefixed 32-byte hex, got: ${pubKeyX}`);
2476
+ process.exit(1);
2477
+ }
2478
+ if (!isBytes32Hex(pubKeyY)) {
2479
+ console.error(`Invalid pubKeyY: expected 0x-prefixed 32-byte hex, got: ${pubKeyY}`);
2480
+ process.exit(1);
2481
+ }
2482
+ const chainId = config.network === "base-mainnet" ? 8453 : 84532;
2483
+ const provider = new ethers_1.ethers.JsonRpcProvider(config.rpcUrl);
2484
+ const walletInterface = new ethers_1.ethers.Interface(abis_1.ARC402_WALLET_PASSKEY_ABI);
2485
+ console.log(`\nWallet: ${config.walletContractAddress}`);
2486
+ console.log(`pubKeyX: ${pubKeyX}`);
2487
+ console.log(`pubKeyY: ${pubKeyY}`);
2488
+ const telegramOpts = config.telegramBotToken && config.telegramChatId
2489
+ ? { botToken: config.telegramBotToken, chatId: config.telegramChatId, threadId: config.telegramThreadId }
2490
+ : undefined;
2491
+ const txData = {
2492
+ to: config.walletContractAddress,
2493
+ data: walletInterface.encodeFunctionData("setPasskey", [pubKeyX, pubKeyY]),
2494
+ value: "0x0",
2495
+ };
2496
+ const { client, session, account } = await (0, walletconnect_1.connectPhoneWallet)(config.walletConnectProjectId, chainId, config, {
2497
+ telegramOpts,
2498
+ prompt: `Activate passkey (Face ID) on ARC402Wallet — enables P256 governance signing`,
2499
+ });
2500
+ console.log(`\n✓ Connected: ${account}`);
2501
+ console.log("Sending setPasskey transaction...");
2502
+ const hash = await (0, walletconnect_1.sendTransactionWithSession)(client, session, account, chainId, txData);
2503
+ console.log(`\nTransaction submitted: ${hash}`);
2504
+ console.log("Waiting for confirmation...");
2505
+ const receipt = await provider.waitForTransaction(hash, 1, 60000);
2506
+ if (!receipt || receipt.status !== 1) {
2507
+ console.error("Transaction failed.");
2508
+ process.exit(1);
2509
+ }
2510
+ console.log(`\n✓ Passkey activated on ARC402Wallet`);
2511
+ console.log(` Wallet: ${config.walletContractAddress}`);
2512
+ console.log(` pubKeyX: ${pubKeyX}`);
2513
+ console.log(` pubKeyY: ${pubKeyY}`);
2514
+ console.log(` Tx: ${hash}`);
2515
+ console.log(`\nGovernance ops now require Face ID instead of MetaMask.`);
2516
+ await client.disconnect({ topic: session.topic, reason: { code: 6000, message: "done" } });
2517
+ process.exit(0);
2518
+ });
2519
+ }
2520
+ //# sourceMappingURL=wallet.js.map