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,200 @@
1
+ import { Command } from "commander";
2
+ import { ServiceAgreementClient, SessionManager } from "@arc402/sdk";
3
+ import { ethers } from "ethers";
4
+ import { getUsdcAddress, loadConfig } from "../config";
5
+ import { requireSigner } from "../client";
6
+ import { hashFile, hashString } from "../utils/hash";
7
+ import { parseDuration } from "../utils/time";
8
+ import { printSenderInfo, executeContractWriteViaWallet } from "../wallet-router";
9
+ import { SERVICE_AGREEMENT_ABI } from "../abis";
10
+
11
+ const sessionManager = new SessionManager();
12
+
13
+ export function registerHireCommand(program: Command): void {
14
+ program
15
+ .command("hire")
16
+ .description("Create the on-chain commitment after off-chain negotiation")
17
+ .requiredOption("--agent <address>")
18
+ .requiredOption("--task <description>")
19
+ .requiredOption("--service-type <type>")
20
+ .option("--max <amount>", "Max price in wei (e.g. 1000000000000000) or ETH (e.g. 0.001eth) or USDC (e.g. 1USDC). Required unless --session is provided.")
21
+ .option("--deadline <duration>", "Deadline as duration (1h, 30m, 7d) or absolute ISO date (2026-04-01). Required unless --session is provided.")
22
+ .option("--token <token>", "eth or usdc", "eth")
23
+ .option("--deliverable-spec <filepath>")
24
+ .option("--session <sessionId>", "Load agreed price and deadline from a completed negotiation session")
25
+ .option("--json")
26
+ .action(async (opts) => {
27
+ const config = loadConfig();
28
+ if (!config.serviceAgreementAddress) throw new Error("serviceAgreementAddress missing in config");
29
+ const { signer, address } = await requireSigner(config);
30
+ const client = new ServiceAgreementClient(config.serviceAgreementAddress, signer);
31
+
32
+ let maxAmount: string;
33
+ let deadlineArg: string;
34
+ let transcriptHash: string | undefined;
35
+
36
+ if (opts.session) {
37
+ const session = sessionManager.load(opts.session);
38
+ if (session.state !== "ACCEPTED") throw new Error(`Session ${opts.session} is not in ACCEPTED state (state: ${session.state})`);
39
+ if (!session.agreedPrice || !session.agreedDeadline) throw new Error(`Session ${opts.session} is missing agreedPrice or agreedDeadline`);
40
+ maxAmount = session.agreedPrice;
41
+ deadlineArg = session.agreedDeadline;
42
+ transcriptHash = session.transcriptHash;
43
+ } else {
44
+ if (!opts.max) throw new Error("--max is required when --session is not provided. Examples: 0.001eth, 1000000000000000 (wei), 1USDC");
45
+ if (!opts.deadline) throw new Error("--deadline is required when --session is not provided. Examples: 1h, 30m, 7d, 2026-04-01");
46
+ maxAmount = opts.max;
47
+ deadlineArg = opts.deadline;
48
+ }
49
+
50
+ // Normalise --max: strip trailing 'eth' or 'USDC' suffix and convert to correct unit
51
+ const useUsdc = String(opts.token).toLowerCase() === "usdc";
52
+ const ethSuffix = /^(\d+(?:\.\d+)?)eth$/i.exec(maxAmount);
53
+ const usdcSuffix = /^(\d+(?:\.\d+)?)usdc$/i.exec(maxAmount);
54
+ if (ethSuffix) maxAmount = String(BigInt(Math.round(parseFloat(ethSuffix[1]) * 1e18)));
55
+ else if (usdcSuffix) maxAmount = usdcSuffix[1]; // keep decimal for USDC path
56
+ const token = useUsdc ? getUsdcAddress(config) : ethers.ZeroAddress;
57
+ let price: bigint;
58
+ try {
59
+ price = useUsdc ? BigInt(Math.round(Number(maxAmount) * 1_000_000)) : BigInt(maxAmount);
60
+ } catch {
61
+ throw new Error(`Invalid --max value "${opts.max}". Use wei (1000000000000000), ETH (0.001eth), or USDC (1USDC)`);
62
+ }
63
+ if (price <= 0n) throw new Error(`--max must be greater than zero`);
64
+
65
+ // Pre-flight: check client !== provider (J2-03)
66
+ if (address.toLowerCase() === opts.agent.toLowerCase()) {
67
+ console.error("Cannot hire yourself: client and provider addresses are the same.");
68
+ process.exit(1);
69
+ }
70
+
71
+ // Pre-flight: check provider is registered in AgentRegistry (J2-02)
72
+ const agentRegistryAddress = config.agentRegistryV2Address ?? config.agentRegistryAddress;
73
+ if (agentRegistryAddress) {
74
+ const arProvider = new ethers.JsonRpcProvider(config.rpcUrl);
75
+ const arCheck = new ethers.Contract(
76
+ agentRegistryAddress,
77
+ ["function isRegistered(address wallet) external view returns (bool)"],
78
+ arProvider,
79
+ );
80
+ let isRegistered = true;
81
+ try {
82
+ isRegistered = await arCheck.isRegistered(opts.agent);
83
+ } catch { /* assume registered if read fails */ }
84
+ if (!isRegistered) {
85
+ console.error(`Provider ${opts.agent} is not registered in AgentRegistry.`);
86
+ console.error(`Verify the agent address is correct, or check the registry at ${agentRegistryAddress}.`);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ // Pre-flight: check token is allowed on this ServiceAgreement (J2-01)
92
+ if (useUsdc) {
93
+ const saProvider = new ethers.JsonRpcProvider(config.rpcUrl);
94
+ const saCheck = new ethers.Contract(
95
+ config.serviceAgreementAddress,
96
+ ["function allowedTokens(address) external view returns (bool)"],
97
+ saProvider,
98
+ );
99
+ let isAllowed = false;
100
+ try {
101
+ isAllowed = await saCheck.allowedTokens(token);
102
+ } catch { isAllowed = true; /* assume allowed if read fails */ }
103
+ if (!isAllowed) {
104
+ console.error(`Token ${token} is not allowed on this ServiceAgreement.`);
105
+ console.error(`Only the SA owner can allowlist tokens via:`);
106
+ console.error(` cast send ${config.serviceAgreementAddress} "allowToken(address)" ${token}`);
107
+ console.error(`For ETH payments, use --token eth`);
108
+ process.exit(1);
109
+ }
110
+ }
111
+
112
+ // Use spec hash as deliverables hash; if transcript exists, incorporate it
113
+ const baseHash = opts.deliverableSpec ? hashFile(opts.deliverableSpec) : hashString(opts.task);
114
+ const deliverablesHash = transcriptHash
115
+ ? (ethers.keccak256(ethers.toUtf8Bytes(baseHash + transcriptHash)) as `0x${string}`)
116
+ : baseHash;
117
+
118
+ // Parse deadline: if it looks like an ISO date, convert to seconds from now
119
+ let deadlineSeconds: number;
120
+ const isoMatch = deadlineArg.match(/^\d{4}-\d{2}-\d{2}/);
121
+ if (isoMatch) {
122
+ const target = Math.floor(new Date(deadlineArg).getTime() / 1000);
123
+ deadlineSeconds = target - Math.floor(Date.now() / 1000);
124
+ if (deadlineSeconds <= 0) throw new Error(`Deadline ${deadlineArg} is in the past`);
125
+ } else {
126
+ deadlineSeconds = parseDuration(deadlineArg);
127
+ }
128
+
129
+ printSenderInfo(config);
130
+
131
+ let agreementId: bigint;
132
+
133
+ if (config.walletContractAddress) {
134
+ // Smart wallet path — wallet handles per-tx USDC approval via maxApprovalAmount
135
+ const tx = await executeContractWriteViaWallet(
136
+ config.walletContractAddress,
137
+ signer,
138
+ config.serviceAgreementAddress,
139
+ SERVICE_AGREEMENT_ABI,
140
+ "propose",
141
+ [opts.agent, opts.serviceType, opts.task, price, token, deadlineSeconds, deliverablesHash],
142
+ useUsdc ? 0n : price, // ETH value forwarded to SA; 0 for USDC agreements
143
+ useUsdc ? token : ethers.ZeroAddress, // approvalToken for USDC
144
+ useUsdc ? price : 0n, // maxApprovalAmount for USDC
145
+ );
146
+ const receipt = await tx.wait();
147
+ const saInterface = new ethers.Interface(SERVICE_AGREEMENT_ABI);
148
+ let found = false;
149
+ for (const log of receipt!.logs) {
150
+ if (log.address.toLowerCase() === config.serviceAgreementAddress.toLowerCase()) {
151
+ try {
152
+ const parsed = saInterface.parseLog(log);
153
+ if (parsed?.name === "AgreementProposed") {
154
+ agreementId = parsed.args[0] as bigint;
155
+ found = true;
156
+ break;
157
+ }
158
+ } catch { /* skip unparseable logs */ }
159
+ }
160
+ }
161
+ if (!found) throw new Error("AgreementProposed event not found in transaction receipt");
162
+ } else {
163
+ // EOA path — existing behaviour
164
+ if (useUsdc) {
165
+ const usdc = new ethers.Contract(
166
+ token,
167
+ ["function approve(address spender,uint256 amount) external returns (bool)", "function allowance(address owner,address spender) external view returns (uint256)"],
168
+ signer
169
+ );
170
+ const allowance = await usdc.allowance(address, config.serviceAgreementAddress);
171
+ if (allowance < price) await (await usdc.approve(config.serviceAgreementAddress, price)).wait();
172
+ }
173
+
174
+ const result = await client.propose({
175
+ provider: opts.agent,
176
+ serviceType: opts.serviceType,
177
+ description: opts.task,
178
+ price,
179
+ token,
180
+ deadline: deadlineSeconds,
181
+ deliverablesHash,
182
+ });
183
+ agreementId = result.agreementId;
184
+ }
185
+
186
+ if (opts.session) {
187
+ sessionManager.setOnChainId(opts.session, agreementId!.toString());
188
+ }
189
+
190
+ if (opts.json) {
191
+ const output: Record<string, unknown> = { agreementId: agreementId!.toString(), deliverablesHash };
192
+ if (transcriptHash) output.transcriptHash = transcriptHash;
193
+ if (opts.session) output.sessionId = opts.session;
194
+ return console.log(JSON.stringify(output, null, 2));
195
+ }
196
+
197
+ console.log(`agreementId=${agreementId!} deliverablesHash=${deliverablesHash}`);
198
+ if (transcriptHash) console.log(`transcriptHash=${transcriptHash}`);
199
+ });
200
+ }
@@ -0,0 +1,175 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import { loadConfig } from "../config";
4
+ import { getClient, requireSigner } from "../client";
5
+
6
+ const MIGRATION_REGISTRY_ABI = [
7
+ "function registerMigration(address oldWallet, address newWallet) external",
8
+ "function resolveActiveWallet(address wallet) external view returns (address)",
9
+ "function getLineage(address wallet) external view returns (address[])",
10
+ "function migratedTo(address wallet) external view returns (address)",
11
+ "function migratedFrom(address wallet) external view returns (address)",
12
+ "event MigrationRegistered(address indexed oldWallet, address indexed newWallet, address indexed owner, uint256 migratedAt, uint256 scoreAtMigration, uint256 appliedDecay)",
13
+ ] as const;
14
+
15
+ export function registerMigrateCommands(program: Command): void {
16
+ const migrate = program
17
+ .command("migrate")
18
+ .description("Wallet migration — register, query status, or print lineage history")
19
+ .argument("[oldWallet]", "old wallet address (required for registration)")
20
+ .argument("[newWallet]", "new wallet address (required for registration)")
21
+ .option("--json")
22
+ .action(async (oldWallet, newWallet, opts) => {
23
+ if (!oldWallet || !newWallet) {
24
+ console.error("Usage: arc402 migrate <oldWallet> <newWallet>");
25
+ console.error("Both wallets must share the same registered owner address.");
26
+ process.exit(1);
27
+ }
28
+
29
+ const config = loadConfig();
30
+ if (!config.migrationRegistryAddress) {
31
+ console.error("migrationRegistryAddress not configured. Run `arc402 config set migrationRegistryAddress <address>`.");
32
+ process.exit(1);
33
+ }
34
+
35
+ const { signer } = await requireSigner(config);
36
+ const contract = new ethers.Contract(config.migrationRegistryAddress, MIGRATION_REGISTRY_ABI, signer);
37
+
38
+ const tx = await contract.registerMigration(oldWallet, newWallet);
39
+ const receipt = await tx.wait();
40
+
41
+ const payload = {
42
+ oldWallet,
43
+ newWallet,
44
+ txHash: receipt.hash,
45
+ };
46
+ if (opts.json) return console.log(JSON.stringify(payload, null, 2));
47
+ console.log(`migration registered`);
48
+ console.log(` old: ${oldWallet}`);
49
+ console.log(` new: ${newWallet}`);
50
+ console.log(` note: 10% trust score decay applied on migration`);
51
+ console.log(` tx: ${receipt.hash}`);
52
+ });
53
+
54
+ // ─── migrate status <address> ──────────────────────────────────────────────
55
+
56
+ migrate
57
+ .command("status <address>")
58
+ .description("Show whether a wallet is in a migration lineage and its current active wallet")
59
+ .option("--json")
60
+ .action(async (address, opts) => {
61
+ const config = loadConfig();
62
+ if (!config.migrationRegistryAddress) {
63
+ console.error("migrationRegistryAddress not configured. Run `arc402 config set migrationRegistryAddress <address>`.");
64
+ process.exit(1);
65
+ }
66
+
67
+ const { provider } = await getClient(config);
68
+ const contract = new ethers.Contract(config.migrationRegistryAddress, MIGRATION_REGISTRY_ABI, provider);
69
+
70
+ const [activeWallet, migratedTo, migratedFrom] = await Promise.all([
71
+ contract.resolveActiveWallet(address),
72
+ contract.migratedTo(address),
73
+ contract.migratedFrom(address),
74
+ ]);
75
+
76
+ const isCurrent = activeWallet.toLowerCase() === address.toLowerCase();
77
+ const hasMigrated = migratedTo !== ethers.ZeroAddress;
78
+ const wasSource = migratedFrom !== ethers.ZeroAddress;
79
+
80
+ const payload = {
81
+ address,
82
+ activeWallet,
83
+ isCurrent,
84
+ migratedTo: hasMigrated ? migratedTo : null,
85
+ migratedFrom: wasSource ? migratedFrom : null,
86
+ };
87
+ if (opts.json) return console.log(JSON.stringify(payload, null, 2));
88
+ console.log(`address=${address}`);
89
+ console.log(` active wallet: ${activeWallet}`);
90
+ if (isCurrent) {
91
+ console.log(` status: current (no further migration)`);
92
+ } else {
93
+ console.log(` status: migrated — score queries resolve to ${activeWallet}`);
94
+ }
95
+ if (hasMigrated) console.log(` migrated to: ${migratedTo}`);
96
+ if (wasSource) console.log(` migrated from: ${migratedFrom}`);
97
+ });
98
+
99
+ // ─── migrate lineage <address> ────────────────────────────────────────────
100
+
101
+ migrate
102
+ .command("lineage <address>")
103
+ .description("Print full migration lineage history with timestamps")
104
+ .option("--json")
105
+ .action(async (address, opts) => {
106
+ const config = loadConfig();
107
+ if (!config.migrationRegistryAddress) {
108
+ console.error("migrationRegistryAddress not configured. Run `arc402 config set migrationRegistryAddress <address>`.");
109
+ process.exit(1);
110
+ }
111
+
112
+ const { provider } = await getClient(config);
113
+ const contract = new ethers.Contract(config.migrationRegistryAddress, MIGRATION_REGISTRY_ABI, provider);
114
+
115
+ const lineage: string[] = await contract.getLineage(address);
116
+
117
+ if (lineage.length === 0) {
118
+ const payload = { address, lineage: [], migrations: 0 };
119
+ if (opts.json) return console.log(JSON.stringify(payload, null, 2));
120
+ console.log(`address=${address}`);
121
+ console.log(` no migration history`);
122
+ return;
123
+ }
124
+
125
+ // Fetch MigrationRegistered events for timestamps and decay info
126
+ interface MigrationEntry { step: number; from: string; to: string; timestamp: string | null; scoreAtMigration: string | null; decayBps: string | null }
127
+ const entries: MigrationEntry[] = [];
128
+
129
+ for (let i = 0; i < lineage.length - 1; i++) {
130
+ const from = lineage[i];
131
+ const to = lineage[i + 1];
132
+ let timestamp: string | null = null;
133
+ let scoreAtMigration: string | null = null;
134
+ let decayBps: string | null = null;
135
+
136
+ try {
137
+ const filter = contract.filters.MigrationRegistered(from, to);
138
+ const events = await contract.queryFilter(filter);
139
+ if (events.length > 0) {
140
+ const ev = events[0] as ethers.EventLog;
141
+ const block = await provider.getBlock(ev.blockNumber);
142
+ timestamp = block ? new Date(block.timestamp * 1000).toISOString() : null;
143
+ scoreAtMigration = ev.args[3]?.toString() ?? null;
144
+ decayBps = ev.args[4]?.toString() ?? null;
145
+ }
146
+ } catch {
147
+ // event query not critical — continue without timestamps
148
+ }
149
+
150
+ entries.push({ step: i + 1, from, to, timestamp, scoreAtMigration, decayBps });
151
+ }
152
+
153
+ const payload = {
154
+ address,
155
+ lineage,
156
+ migrations: entries.length,
157
+ history: entries,
158
+ };
159
+ if (opts.json) return console.log(JSON.stringify(payload, null, 2));
160
+
161
+ console.log(`address=${address}`);
162
+ console.log(` lineage depth: ${lineage.length} wallet${lineage.length !== 1 ? "s" : ""}`);
163
+ console.log(` migrations: ${entries.length}`);
164
+ console.log();
165
+ lineage.forEach((addr, i) => {
166
+ const label = i === 0 ? " (origin)" : i === lineage.length - 1 ? " (current)" : "";
167
+ console.log(` [${i}] ${addr}${label}`);
168
+ if (i < entries.length) {
169
+ const e = entries[i];
170
+ if (e.timestamp) console.log(` migrated: ${e.timestamp}`);
171
+ if (e.scoreAtMigration) console.log(` score at migration: ${e.scoreAtMigration} (decay: ${Number(e.decayBps) / 100}%)`);
172
+ }
173
+ });
174
+ });
175
+ }
@@ -0,0 +1,270 @@
1
+ import { Command } from "commander";
2
+ import { ethers } from "ethers";
3
+ import {
4
+ createSignedProposal,
5
+ createSignedCounter,
6
+ createSignedAccept,
7
+ createSignedReject,
8
+ NegotiationGuard,
9
+ SessionManager,
10
+ } from "@arc402/sdk";
11
+ import { loadConfig } from "../config";
12
+ import { requireSigner, getClient } from "../client";
13
+ import { hashFile, hashString } from "../utils/hash";
14
+
15
+ const sessionManager = new SessionManager();
16
+
17
+ export function registerNegotiateCommands(program: Command): void {
18
+ const negotiate = program
19
+ .command("negotiate")
20
+ .description(
21
+ "Signed agent-to-agent negotiation. Every message is authenticated, " +
22
+ "sessions are tracked locally, transcripts are hashed and committed on-chain. " +
23
+ "This is the secure communication layer — not just payload generation."
24
+ );
25
+
26
+ // Session management — nest list/show under a single 'session' subcommand
27
+ const session = negotiate
28
+ .command("session")
29
+ .description("Manage local negotiation sessions");
30
+
31
+ session
32
+ .command("list")
33
+ .description("List all negotiation sessions")
34
+ .option("--json", "Machine-parseable output")
35
+ .action((opts) => {
36
+ const sessions = sessionManager.list();
37
+ if (opts.json) {
38
+ console.log(JSON.stringify(sessions));
39
+ } else {
40
+ sessions.forEach(s => {
41
+ console.log(`${s.sessionId.slice(0, 10)}... ${s.state.padEnd(10)} ${s.initiator} ↔ ${s.responder} msgs:${s.messages.length}`);
42
+ });
43
+ }
44
+ });
45
+
46
+ session
47
+ .command("show <sessionId>")
48
+ .description("Show full session including message history and transcript hash")
49
+ .option("--json", "Machine-parseable output")
50
+ .action((sessionId, opts) => {
51
+ const sess = sessionManager.load(sessionId);
52
+ if (opts.json) {
53
+ console.log(JSON.stringify(sess));
54
+ } else {
55
+ console.log(`Session: ${sess.sessionId}`);
56
+ console.log(`State: ${sess.state}`);
57
+ console.log(`Parties: ${sess.initiator} ↔ ${sess.responder}`);
58
+ console.log(`Messages: ${sess.messages.length}`);
59
+ if (sess.transcriptHash) console.log(`Transcript hash: ${sess.transcriptHash}`);
60
+ if (sess.onChainAgreementId) console.log(`On-chain agreement: ${sess.onChainAgreementId}`);
61
+ sess.messages.forEach((m, i) => console.log(` ${i + 1}. ${m.type} from ${m.from.slice(0, 8)}...`));
62
+ }
63
+ });
64
+
65
+ // Signed propose (starts a new session)
66
+ negotiate
67
+ .command("propose")
68
+ .description("Send a signed PROPOSE. Creates a new negotiation session.")
69
+ .requiredOption("--to <address>")
70
+ .requiredOption("--service-type <type>")
71
+ .requiredOption("--price <amountWei>")
72
+ .option("--token <token>", "Token address", ethers.ZeroAddress)
73
+ .requiredOption("--deadline <iso>")
74
+ .requiredOption("--spec <text>")
75
+ .option("--spec-file <path>")
76
+ .option("--expires-in <seconds>", "Proposal TTL in seconds", "3600")
77
+ .option("--json", "Machine-parseable output")
78
+ .action(async (opts) => {
79
+ const config = loadConfig();
80
+ const { signer } = await requireSigner(config);
81
+ const myAddress = await signer.getAddress();
82
+ const specHash = opts.specFile ? hashFile(opts.specFile) : hashString(opts.spec);
83
+ const now = Math.floor(Date.now() / 1000);
84
+
85
+ const session = sessionManager.createSession(myAddress, opts.to);
86
+
87
+ const proposal = await createSignedProposal({
88
+ from: myAddress,
89
+ to: opts.to,
90
+ serviceType: opts.serviceType,
91
+ price: opts.price,
92
+ token: opts.token,
93
+ deadline: opts.deadline,
94
+ spec: opts.spec,
95
+ specHash,
96
+ expiresAt: now + parseInt(opts.expiresIn),
97
+ protocolVersion: "1.0.0",
98
+ } as Parameters<typeof createSignedProposal>[0], signer);
99
+
100
+ sessionManager.addMessage(session.sessionId, proposal);
101
+
102
+ if (opts.json) {
103
+ console.log(JSON.stringify({ sessionId: session.sessionId, message: proposal }));
104
+ } else {
105
+ console.log(`Session started: ${session.sessionId}`);
106
+ console.log(`Signed PROPOSE:`);
107
+ console.log(JSON.stringify(proposal, null, 2));
108
+ }
109
+ });
110
+
111
+ // Counter
112
+ negotiate
113
+ .command("counter <sessionId>")
114
+ .description("Send a signed COUNTER within an existing session.")
115
+ .requiredOption("--justification <text>")
116
+ .option("--price <amountWei>")
117
+ .option("--deadline <iso>")
118
+ .option("--json", "Machine-parseable output")
119
+ .action(async (sessionId, opts) => {
120
+ const config = loadConfig();
121
+ const { signer } = await requireSigner(config);
122
+ const myAddress = await signer.getAddress();
123
+ const session = sessionManager.load(sessionId);
124
+ const lastMessage = session.messages[session.messages.length - 1];
125
+ const refNonce = "nonce" in lastMessage ? lastMessage.nonce : (lastMessage as any).refNonce;
126
+
127
+ const counter = await createSignedCounter({
128
+ from: myAddress,
129
+ to: lastMessage.from === myAddress ? lastMessage.to : lastMessage.from,
130
+ refNonce,
131
+ justification: opts.justification,
132
+ price: opts.price,
133
+ deadline: opts.deadline,
134
+ }, signer);
135
+
136
+ sessionManager.addMessage(sessionId, counter);
137
+
138
+ if (opts.json) {
139
+ console.log(JSON.stringify(counter));
140
+ } else {
141
+ console.log(`Signed COUNTER added to session ${sessionId.slice(0, 10)}...`);
142
+ console.log(JSON.stringify(counter, null, 2));
143
+ }
144
+ });
145
+
146
+ // Accept — closes session, computes transcript hash
147
+ negotiate
148
+ .command("accept <sessionId>")
149
+ .description("Accept terms. Closes the session and computes transcript hash.")
150
+ .requiredOption("--price <amountWei>")
151
+ .requiredOption("--deadline <iso>")
152
+ .option("--record", "Commit transcript hash on-chain alongside propose()")
153
+ .option("--json", "Machine-parseable output")
154
+ .action(async (sessionId, opts) => {
155
+ const config = loadConfig();
156
+ const { signer } = await requireSigner(config);
157
+ const myAddress = await signer.getAddress();
158
+ const session = sessionManager.load(sessionId);
159
+ const lastMessage = session.messages[session.messages.length - 1];
160
+ const refNonce = "nonce" in lastMessage ? lastMessage.nonce : (lastMessage as any).refNonce;
161
+
162
+ const accept = await createSignedAccept({
163
+ from: myAddress,
164
+ to: lastMessage.from === myAddress ? lastMessage.to : lastMessage.from,
165
+ refNonce,
166
+ agreedPrice: opts.price,
167
+ agreedDeadline: opts.deadline,
168
+ }, signer);
169
+
170
+ sessionManager.addMessage(sessionId, accept);
171
+ const updatedSession = sessionManager.load(sessionId);
172
+
173
+ if (opts.json) {
174
+ console.log(JSON.stringify({
175
+ sessionId,
176
+ transcriptHash: updatedSession.transcriptHash,
177
+ message: accept,
178
+ }));
179
+ } else {
180
+ console.log(`✓ Session ${sessionId.slice(0, 10)}... ACCEPTED`);
181
+ console.log(`✓ Transcript hash: ${updatedSession.transcriptHash}`);
182
+ if (opts.record) {
183
+ console.log(`\nTranscript hash is ready to commit on-chain.`);
184
+ console.log(`Run: arc402 hire --session ${sessionId} to propose() and record the transcript hash.`);
185
+ }
186
+ }
187
+ });
188
+
189
+ // Reject
190
+ negotiate
191
+ .command("reject <sessionId>")
192
+ .description("Reject and close session.")
193
+ .requiredOption("--reason <text>")
194
+ .option("--json", "Machine-parseable output")
195
+ .action(async (sessionId, opts) => {
196
+ const config = loadConfig();
197
+ const { signer } = await requireSigner(config);
198
+ const myAddress = await signer.getAddress();
199
+ const session = sessionManager.load(sessionId);
200
+ const lastMessage = session.messages[session.messages.length - 1];
201
+ const refNonce = "nonce" in lastMessage ? lastMessage.nonce : (lastMessage as any).refNonce;
202
+
203
+ const reject = await createSignedReject({
204
+ from: myAddress,
205
+ to: lastMessage.from === myAddress ? lastMessage.to : lastMessage.from,
206
+ reason: opts.reason,
207
+ refNonce,
208
+ }, signer);
209
+
210
+ sessionManager.addMessage(sessionId, reject);
211
+
212
+ if (opts.json) {
213
+ console.log(JSON.stringify(reject));
214
+ } else {
215
+ console.log(`✗ Session ${sessionId.slice(0, 10)}... REJECTED`);
216
+ console.log(`Reason: ${opts.reason}`);
217
+ }
218
+ });
219
+
220
+ // Verify an incoming message
221
+ negotiate
222
+ .command("verify")
223
+ .description("Verify an incoming signed negotiation message against AgentRegistry.")
224
+ .requiredOption("--message <json>", "Raw JSON string or @file.json")
225
+ .option("--json", "Machine-parseable output")
226
+ .action(async (opts) => {
227
+ const config = loadConfig();
228
+ if (!config.agentRegistryAddress) throw new Error("agentRegistryAddress not in config");
229
+ const { provider } = await getClient(config);
230
+
231
+ const guard = new NegotiationGuard({ agentRegistryAddress: config.agentRegistryAddress, runner: provider });
232
+
233
+ const rawJson = opts.message.startsWith("@")
234
+ ? require("fs").readFileSync(opts.message.slice(1), "utf8")
235
+ : opts.message;
236
+
237
+ const result = await guard.verify(rawJson);
238
+
239
+ if (opts.json) {
240
+ console.log(JSON.stringify(result));
241
+ } else if (result.valid) {
242
+ console.log(`✓ Valid — signer: ${result.recoveredSigner}`);
243
+ } else {
244
+ console.error(`✗ Invalid — ${result.error}`);
245
+ process.exit(1);
246
+ }
247
+ });
248
+
249
+ // Transcript subcommand
250
+ const transcript = negotiate.command("transcript").description("Transcript management for closed negotiation sessions");
251
+
252
+ transcript
253
+ .command("show <sessionId>")
254
+ .description("Show the transcript hash for a completed session")
255
+ .option("--json", "Machine-parseable output")
256
+ .action((sessionId, opts) => {
257
+ const session = sessionManager.load(sessionId);
258
+ if (!session.transcriptHash) {
259
+ console.error("Session not yet closed — no transcript hash");
260
+ process.exit(1);
261
+ }
262
+ if (opts.json) {
263
+ console.log(JSON.stringify({ sessionId, transcriptHash: session.transcriptHash, messageCount: session.messages.length }));
264
+ } else {
265
+ console.log(`Transcript hash: ${session.transcriptHash}`);
266
+ console.log(`Messages: ${session.messages.length}`);
267
+ console.log(`State: ${session.state}`);
268
+ }
269
+ });
270
+ }