mycomesh-consumer 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycomesh-consumer",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "MycoMesh V11 Consumer: a custodied deposit, relay-blind encrypted requests to any Provider, and a local OpenAI-compatible endpoint.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/account.mjs CHANGED
@@ -46,9 +46,12 @@ export async function setup(network, { ownerPrivate, key, deposit, limit, forceF
46
46
  await faucet(network, owner);
47
47
  }
48
48
  const send = (to, data) => sendTransaction(network.rpc_urls, ownerPrivate, { to, data });
49
+ log("approving the deposit (transaction 1 of 3)...");
49
50
  await send(network.stablecoin, encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", deposit]]));
51
+ log("depositing (transaction 2 of 3)...");
50
52
  await send(network.settlement, encodeCall("deposit(uint256)", [["uint", deposit]]));
51
53
  if (!status.grant?.active || BigInt(status.grant.max_per_request) !== limit) {
54
+ log("authorizing this machine's payment key (transaction 3 of 3)...");
52
55
  await send(network.settlement, encodeCall("registerKey(address,uint256,uint64)", [["address", key], ["uint", limit], ["uint", 0]]));
53
56
  }
54
57
  return { owner, key, deposit: deposit.toString(), max_per_request: limit.toString() };
package/src/chain.mjs CHANGED
@@ -10,7 +10,9 @@ export async function rpcCall(urls, method, params, { timeoutMs = 15000 } = {})
10
10
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), signal: AbortSignal.timeout(timeoutMs),
11
11
  });
12
12
  const body = await response.json();
13
- if (body.error) throw Object.assign(new Error(`${method}: ${JSON.stringify(body.error).slice(0, 300)}`), { answered: true });
13
+ // An endpoint without the history asked for (pruned logs) is not an answer: try the next one.
14
+ const endpointOnly = /pruned|history unavailable|missing trie node|rate limit|too many requests/i.test(JSON.stringify(body.error ?? ""));
15
+ if (body.error) throw Object.assign(new Error(`${method}: ${JSON.stringify(body.error).slice(0, 300)}`), { answered: !endpointOnly });
14
16
  return body.result;
15
17
  } catch (error) {
16
18
  if (error.answered) throw error;
package/src/cli.mjs CHANGED
@@ -24,15 +24,15 @@ const USAGE = `Usage: mycomesh-consumer <command> [options]
24
24
  address print the payment key address
25
25
  wallet print the owner wallet address (a V3 keystore; MetaMask can import it)
26
26
  faucet testnet: get gas ETH and tUSDC for the owner wallet
27
- setup --deposit UNITS [--max-per-request UNITS]
27
+ setup [--deposit 20tUSDC] [--max-per-request 1tUSDC]
28
28
  deposit into the settlement contract and authorize the payment key
29
- (on testnets, tops the wallet up from the faucet first)
29
+ (on testnets, tops the wallet up from the faucet first; default 20 tUSDC)
30
30
  balance [--owner ADDR] show the custodied deposit
31
31
  rewards [claim] MYCO earned by the owner wallet (80% of each hour's emission goes to the
32
32
  Consumers who paid that hour's fees); \`claim\` mints it to the wallet
33
33
  withdraw [UNITS] request a withdrawal of the deposit; run again after the delay to receive it
34
- request "prompt" [--stream]
35
- send one request and print the verified answer
34
+ request "prompt" [--stream] [--json]
35
+ send one request and print the verified answer (JSON when piped or with --json)
36
36
  serve [--port 8110] run the local web console and OpenAI-compatible endpoint (default)
37
37
  tenant add NAME [--budget UNITS] [--max-per-request UNITS]
38
38
  multi-tenant accounts: a payment key and API key per tenant, capped on-chain
@@ -41,10 +41,23 @@ const USAGE = `Usage: mycomesh-consumer <command> [options]
41
41
  reveal a recorded request and response to a Provider-AI jury
42
42
  (within 24 hours; the reporter bond is returned if fraud is confirmed)
43
43
 
44
+ Amounts: a bare integer is base units (1 tUSDC = 1000000); 20tUSDC or 20.5 means tUSDC.
44
45
  Common: --network FILE (default: the bundled MycoMesh V11 Sepolia manifest), --data-dir DIR, --model ID,
45
46
  --max-fee UNITS, --provider SIGNER, --owner-key-file F (raw key or keystore instead of the local wallet).
46
47
  The wallet password comes from MYCOMESH_WALLET_PASSWORD or a terminal prompt.`;
47
48
 
49
+ // Amounts: a bare integer is base units (1e-6 tUSDC), as before; "20tUSDC", "20 usdc" or "20.5" are tUSDC.
50
+ export function parseAmount(value, name) {
51
+ const text = String(value).trim();
52
+ const match = /^([0-9]+(?:\.[0-9]{1,6})?)\s*(t?usdc)?$/i.exec(text);
53
+ if (!match) throw new Error(`${name}: write base units (20000000) or tUSDC (20tUSDC, 20.5)`);
54
+ if (!match[2] && !match[1].includes(".")) return BigInt(match[1]);
55
+ const [whole, fraction = ""] = match[1].split(".");
56
+ return BigInt(whole) * 1_000_000n + BigInt(fraction.padEnd(6, "0"));
57
+ }
58
+ export const tusdc = (units) => `${(Number(units) / 1e6).toLocaleString("en-US", { maximumFractionDigits: 6 })} tUSDC`;
59
+ const human = () => Boolean(process.stdout.isTTY) && !process.env.MYCOMESH_JSON;
60
+
48
61
  function dataDir(value) {
49
62
  const dir = value || process.env.MYCOMESH_CONSUMER_DATA_DIR || join(homedir(), ".mycomesh", "v11");
50
63
  mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -77,7 +90,7 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
77
90
  port: { type: "string", default: "8110" }, host: { type: "string", default: "127.0.0.1" },
78
91
  "max-output-tokens": { type: "string", default: "4096" }, help: { type: "boolean" },
79
92
  reason: { type: "string", default: "unrelated_response" }, faucet: { type: "boolean" }, provider: { type: "string" },
80
- statement: { type: "string", default: "" }, stream: { type: "boolean" }, budget: { type: "string" },
93
+ statement: { type: "string", default: "" }, stream: { type: "boolean" }, json: { type: "boolean" }, budget: { type: "string" },
81
94
  "no-browser": { type: "boolean" },
82
95
  },
83
96
  });
@@ -93,6 +106,11 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
93
106
  if (!values["owner-key-file"]) {
94
107
  stdout.write(`owner wallet ${walletAddress(await createWallet(dir))} (${join(dir, "owner-wallet.json")})\n`);
95
108
  }
109
+ if (human()) {
110
+ stdout.write("\nThe first address is this machine's payment key: it signs each request, holds no money, needs no password.\n"
111
+ + "The owner wallet holds your tUSDC and deposit; it is password-protected and signs deposits, withdrawals and claims.\n"
112
+ + "Next: mycomesh-consumer setup (gets testnet funds and deposits 20 tUSDC; about a minute)\n");
113
+ }
96
114
  return 0;
97
115
  }
98
116
  if (command === "wallet") {
@@ -107,14 +125,15 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
107
125
  }
108
126
  if (command === "setup") {
109
127
  const keyPrivate = paymentKey(dir);
110
- if (!values.deposit) throw new Error("--deposit UNITS is required");
111
128
  if (!values["owner-key-file"]) await createWallet(dir);
112
129
  const result = await setup(network, {
113
- ownerPrivate: await ownerKey(values["owner-key-file"], dir), key: addressOf(keyPrivate), deposit: BigInt(values.deposit),
114
- limit: BigInt(values["max-per-request"] || values["max-fee"]), forceFaucet: values.faucet,
130
+ ownerPrivate: await ownerKey(values["owner-key-file"], dir), key: addressOf(keyPrivate),
131
+ deposit: parseAmount(values.deposit ?? "20tUSDC", "--deposit"),
132
+ limit: parseAmount(values["max-per-request"] || values["max-fee"], "--max-per-request"), forceFaucet: values.faucet,
115
133
  log: (line) => stdout.write(`${line}\n`),
116
134
  });
117
135
  stdout.write(`deposited ${result.deposit} and authorized ${result.key} up to ${result.max_per_request} per request\n`);
136
+ if (human()) stdout.write(`Ready: ${tusdc(result.deposit)} deposited. Try: mycomesh-consumer request "hello", or mycomesh-consumer serve\n`);
118
137
  return 0;
119
138
  }
120
139
  if (command === "withdraw") {
@@ -128,7 +147,7 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
128
147
  stdout.write(`withdrew ${status.withdrawal.amount}\n`);
129
148
  return 0;
130
149
  }
131
- const amount = BigInt(rest[0] || status.deposit);
150
+ const amount = rest[0] ? parseAmount(rest[0], "amount") : BigInt(status.deposit);
132
151
  await requestWithdrawal(network, owner, amount);
133
152
  stdout.write(`requested a withdrawal of ${amount}; run \`withdraw\` again after the delay\n`);
134
153
  return 0;
@@ -136,7 +155,8 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
136
155
  if (command === "balance") {
137
156
  const owner = values.owner || ownerAddress(values["owner-key-file"], dir);
138
157
  if (!owner) throw new Error("--owner, --owner-key-file or a local wallet (`init`) is required");
139
- stdout.write(`${await availableBalance(network, owner)}\n`);
158
+ const balance = await availableBalance(network, owner);
159
+ stdout.write(human() ? `${tusdc(balance)} deposited (${balance} base units)\n` : `${balance}\n`);
140
160
  return 0;
141
161
  }
142
162
  if (command === "rewards") {
@@ -164,18 +184,19 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
164
184
  const owner = await ownerKey(values["owner-key-file"], dir);
165
185
  if (action === "add") {
166
186
  const created = await addTenant(network, dir, owner, name, {
167
- maxPerRequest: BigInt(values["max-per-request"] || values["max-fee"]), budget: BigInt(values.budget || 0) });
187
+ maxPerRequest: parseAmount(values["max-per-request"] || values["max-fee"], "--max-per-request"),
188
+ budget: parseAmount(values.budget || 0, "--budget") });
168
189
  stdout.write(`${JSON.stringify(created)}\nThe API key is shown once; give it to the tenant (Authorization: Bearer ...).\n`);
169
190
  return 0;
170
191
  }
171
192
  const tenant = loadTenants(dir)[name];
172
193
  if (!tenant) throw new Error(`no tenant ${name}`);
173
- if (action === "budget") { await setBudget(network, owner, tenant.key, BigInt(amount)); stdout.write(`budget of ${name} set to ${amount}\n`); return 0; }
194
+ if (action === "budget") { await setBudget(network, owner, tenant.key, parseAmount(amount, "budget")); stdout.write(`budget of ${name} set to ${amount}\n`); return 0; }
174
195
  if (action === "revoke") { await revokeTenant(network, dir, owner, name); stdout.write(`revoked ${name}\n`); return 0; }
175
196
  throw new Error("tenant add | list | budget | revoke");
176
197
  }
177
198
  const keyPrivate = paymentKey(dir, command === "serve");
178
- const consumer = new Consumer({ network, keyPrivate, maxFee: Number(values["max-fee"]), journalDir: dir });
199
+ const consumer = new Consumer({ network, keyPrivate, maxFee: Number(parseAmount(values["max-fee"], "--max-fee")), journalDir: dir });
179
200
  if (command === "request") {
180
201
  const { response, receipt } = await consumer.request({
181
202
  endpoint: "responses", model: values.model, content: rest.join(" "), maxOutputTokens: Number(values["max-output-tokens"]),
@@ -183,6 +204,12 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
183
204
  });
184
205
  if (values.stream) process.stderr.write("\n");
185
206
  const output = response.output;
207
+ if (human() && !values.json) {
208
+ if (!values.stream) stdout.write(`${output.output_text ?? JSON.stringify(output)}\n`);
209
+ stdout.write(`\n— ${tusdc(receipt.receipt.actual_fee)} · Provider ${receipt.authorization.provider_signer.slice(0, 10)}… · `
210
+ + "verified against the Provider's signed receipt (--json for details)\n");
211
+ return 0;
212
+ }
186
213
  stdout.write(`${JSON.stringify({ output_text: output.output_text ?? output, fee: receipt.receipt.actual_fee,
187
214
  provider: receipt.authorization.provider_signer, request_id: receipt.authorization.request_id,
188
215
  settlement_key: settlementKey(receipt.authorization.key, receipt.authorization.request_id) })}\n`);
package/src/console.mjs CHANGED
@@ -11,6 +11,9 @@ import { networkPrices } from "./pricing.mjs";
11
11
  import { claimRewards, rewardsSummary } from "./rewards.mjs";
12
12
 
13
13
  const PAGE = new URL("./web/console.html", import.meta.url);
14
+ // Only actions that sign with the owner wallet ask for its password; after one, the wallet stays unlocked in
15
+ // this process for a while so the next signature is a confirmation, not another password.
16
+ export const UNLOCK_MS = 15 * 60_000;
14
17
 
15
18
  /**
16
19
  * Only this machine's own pages may call the local node: reject other Host
@@ -26,7 +29,18 @@ export function localRequestAllowed(req, port) {
26
29
  export function consoleRoutes({ consumer, dataDir }) {
27
30
  const network = consumer.network;
28
31
  const owner = () => ownerAddress(undefined, dataDir);
29
- const withWallet = (body) => unlockWallet(dataDir, body.password);
32
+ let unlocked = null; // { key, until }
33
+ const isUnlocked = () => Boolean(unlocked && Date.now() < unlocked.until);
34
+ const withWallet = (body) => {
35
+ if (body.password) {
36
+ unlocked = { key: unlockWallet(dataDir, body.password), until: Date.now() + UNLOCK_MS };
37
+ } else if (!isUnlocked()) {
38
+ unlocked = null;
39
+ throw Object.assign(new Error("wallet is locked"), { status: 401, code: "locked" });
40
+ }
41
+ unlocked.until = Date.now() + UNLOCK_MS; // each signature keeps it open a little longer
42
+ return unlocked.key;
43
+ };
30
44
  const units = (value, name) => {
31
45
  if (!/^[0-9]+$/.test(String(value ?? ""))) throw Object.assign(new Error(`${name} must be a whole number of base units`), { status: 400 });
32
46
  return BigInt(value);
@@ -37,7 +51,7 @@ export function consoleRoutes({ consumer, dataDir }) {
37
51
  const address = owner();
38
52
  return {
39
53
  network: { id: network.network_id, chain_id: network.chain_id, settlement: network.settlement, faucet: Boolean(network.faucet_url) },
40
- payment_key: consumer.key, max_fee: String(consumer.maxFee),
54
+ payment_key: consumer.key, max_fee: String(consumer.maxFee), unlocked_until: isUnlocked() ? unlocked.until : null,
41
55
  wallet: address ? await walletStatus(network, address, consumer.key) : null,
42
56
  };
43
57
  },
@@ -71,6 +85,8 @@ export function consoleRoutes({ consumer, dataDir }) {
71
85
  return { claimed: await claimRewards(network, ownerPrivate, addressOf(ownerPrivate)) };
72
86
  },
73
87
  "POST /api/wallet": async (body) => ({ owner: walletAddress(await createWallet(dataDir, body.password)) }),
88
+ "POST /api/unlock": async (body) => { withWallet({ password: body.password }); return { unlocked_until: unlocked.until }; },
89
+ "POST /api/lock": async () => { unlocked = null; return { locked: true }; },
74
90
  "POST /api/faucet": async () => {
75
91
  const address = owner();
76
92
  if (!address) throw Object.assign(new Error("create the wallet first"), { status: 400 });
package/src/consumer.mjs CHANGED
@@ -597,7 +597,7 @@ export function serveConsumer(consumer, { host = "127.0.0.1", port = 8110, apiKe
597
597
  if (res.headersSent) { writer?.fail(error.message); return res.end(); }
598
598
  if (error.message === "wrong wallet password") return send(401, { error: error.message });
599
599
  if (error.status && error.status < 500 && !error.dispatched && routes[`${req.method} ${new URL(req.url, "http://x").pathname}`]) {
600
- return send(error.status, { error: error.message });
600
+ return send(error.status, { error: error.message, ...(error.code ? { code: error.code } : {}) });
601
601
  }
602
602
  const status = error.code === "outcome_unknown" ? 504 : (error.status && error.status < 500 ? error.status : 502);
603
603
  if (dialect === "anthropic") return send(status, anthropicError(error.message, status));
@@ -50,6 +50,7 @@
50
50
  <span class="badge" id="net">连接中…</span>
51
51
  <span class="badge" id="relays">Relay —</span>
52
52
  <span class="muted mono" id="paykey" style="margin-left:auto"></span>
53
+ <button class="ghost" id="lock" style="display:none;padding:4px 10px" title="钱包已解锁:15 分钟内签名不用再输密码">🔓 锁定钱包</button>
53
54
  </header>
54
55
  <nav>
55
56
  <button data-tab="chat" class="active">对话</button>
@@ -142,7 +143,9 @@
142
143
 
143
144
  <dialog id="dialog"><form method="dialog" id="dialog-form">
144
145
  <h3 id="dialog-title" style="margin-top:0"></h3><div id="dialog-body"></div>
145
- <label>钱包密码</label><input type="password" id="dialog-pw" required>
146
+ <div id="dialog-pw-box"><label>钱包密码</label><input type="password" id="dialog-pw" required>
147
+ <p class="muted" style="margin:6px 0 0">输入一次后,15 分钟内再签名不用重复输入。</p></div>
148
+ <p class="muted" id="dialog-unlocked" style="display:none">钱包已解锁,确认后用它签名。</p>
146
149
  <div class="row" style="justify-content:flex-end"><button class="ghost" value="cancel" formnovalidate>取消</button><button class="primary" value="ok">确认</button></div>
147
150
  </form></dialog>
148
151
  <div id="toast"></div>
@@ -163,16 +166,41 @@ function toast(text, bad = false) {
163
166
  async function api(path, body) {
164
167
  const response = await fetch(path, body === undefined ? {} : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
165
168
  const data = await response.json().catch(() => ({}));
166
- if (!response.ok) throw new Error(typeof data.error === "string" ? data.error : data.error?.message || `HTTP ${response.status}`);
169
+ if (!response.ok) {
170
+ const message = typeof data.error === "string" ? data.error : data.error?.message || `HTTP ${response.status}`;
171
+ throw Object.assign(new Error(message === "wrong wallet password" ? "钱包密码不对" : message), { code: data.code });
172
+ }
167
173
  return data;
168
174
  }
169
- function ask(title, bodyHtml = "") {
175
+ // Only actions signed by the owner wallet come here. While the wallet is unlocked (15 minutes after the last
176
+ // signature) this is a plain confirmation; otherwise it asks for the password once.
177
+ const unlocked = () => Boolean(status?.unlocked_until && status.unlocked_until > Date.now());
178
+ function ask(title, bodyHtml = "", { password = !unlocked() } = {}) {
170
179
  return new Promise((resolve) => {
171
180
  $("dialog-title").textContent = title; $("dialog-body").innerHTML = bodyHtml; $("dialog-pw").value = "";
172
- $("dialog").onclose = () => resolve($("dialog").returnValue === "ok" ? { password: $("dialog-pw").value, form: $("dialog-body") } : null);
181
+ $("dialog-pw-box").style.display = password ? "" : "none"; $("dialog-pw").required = password;
182
+ $("dialog-unlocked").style.display = password ? "none" : "";
183
+ $("dialog").onclose = () => resolve($("dialog").returnValue === "ok"
184
+ ? { password: password ? $("dialog-pw").value : undefined, form: $("dialog-body") } : null);
173
185
  $("dialog").showModal();
174
186
  });
175
187
  }
188
+ /** Ask, sign, and when the unlock ran out meanwhile, ask once more with the password. */
189
+ async function signed(button, title, bodyHtml, call) {
190
+ let answer = await ask(title, bodyHtml); if (!answer) return;
191
+ await busy(button, async () => {
192
+ try { await call(answer); } catch (error) {
193
+ if (error.code !== "locked") throw error;
194
+ status.unlocked_until = null;
195
+ answer = await ask(title, bodyHtml, { password: true }); if (!answer) return;
196
+ await call(answer);
197
+ }
198
+ status = { ...status, unlocked_until: Date.now() + 15 * 60_000 }; showLock();
199
+ });
200
+ }
201
+ function showLock() {
202
+ $("lock").style.display = unlocked() ? "" : "none";
203
+ }
176
204
  async function busy(button, work) {
177
205
  button.disabled = true; const label = button.textContent; button.textContent = "处理中…";
178
206
  try { await work(); } catch (error) { toast(error.message, true); } finally { button.disabled = false; button.textContent = label; }
@@ -190,7 +218,7 @@ let status = null;
190
218
  async function refreshWallet() {
191
219
  status = await api("/api/status");
192
220
  $("net").textContent = `${status.network.id} · 链 ${status.network.chain_id}`; $("net").className = "badge ok";
193
- $("paykey").textContent = `付款 key ${short(status.payment_key)}`;
221
+ $("paykey").textContent = `付款 key ${short(status.payment_key)}`; showLock();
194
222
  $("no-wallet").style.display = status.wallet ? "none" : "block"; $("has-wallet").style.display = status.wallet ? "block" : "none";
195
223
  $("faucet-btn").style.display = status.network.faucet ? "" : "none";
196
224
  const w = status.wallet; if (!w) return;
@@ -241,60 +269,46 @@ async function refreshTenants() {
241
269
  const cell = row.lastElementChild;
242
270
  for (const [label, action] of [["调整预算", "budget"], ["撤销", "revoke"]]) {
243
271
  const button = document.createElement("button"); button.className = "ghost"; button.textContent = label; button.style.marginRight = "6px";
244
- button.onclick = async () => {
245
- const answer = await ask(`${label}:${t.name}`, action === "budget" ? `<label>新的总预算(tUSDC)</label><input id="new-budget" value="${fmt(t.budget)}">` : "");
246
- if (!answer) return;
247
- await busy(button, async () => {
272
+ button.onclick = () => signed(button, `${label}:${t.name}`,
273
+ action === "budget" ? `<label>新的总预算(tUSDC)</label><input id="new-budget" value="${fmt(t.budget)}">` : "", async (answer) => {
248
274
  await api(`/api/tenants/${action}`, { password: answer.password, name: t.name,
249
275
  ...(action === "budget" ? { budget: toUnits(answer.form.querySelector("#new-budget").value) } : {}) });
250
276
  toast("已更新"); await refreshTenants();
251
277
  });
252
- };
253
278
  cell.append(button);
254
279
  }
255
280
  }
256
281
  $("tenants").append(row);
257
282
  }
258
283
  }
259
- $("t-add").onclick = async () => {
260
- const answer = await ask(`添加租户 ${$("t-name").value}`); if (!answer) return;
261
- await busy($("t-add"), async () => {
262
- const created = await api("/api/tenants", { password: answer.password, name: $("t-name").value.trim(),
263
- budget: toUnits($("t-budget").value), max_per_request: toUnits($("t-limit").value) });
264
- $("t-created").style.display = "block";
265
- $("t-created").textContent = `租户 ${created.name} 的 API key(只显示这一次):${created.api_key}`;
266
- await refreshTenants();
267
- });
268
- };
284
+ $("t-add").onclick = () => signed($("t-add"), `添加租户 ${$("t-name").value}`, "", async (answer) => {
285
+ const created = await api("/api/tenants", { password: answer.password, name: $("t-name").value.trim(),
286
+ budget: toUnits($("t-budget").value), max_per_request: toUnits($("t-limit").value) });
287
+ $("t-created").style.display = "block";
288
+ $("t-created").textContent = `租户 ${created.name} 的 API key(只显示这一次):${created.api_key}`;
289
+ await refreshTenants();
290
+ });
269
291
  $("create-wallet").onclick = () => busy($("create-wallet"), async () => {
270
292
  if ($("new-pw").value !== $("new-pw2").value) throw new Error("两次输入的密码不一致");
271
293
  await api("/api/wallet", { password: $("new-pw").value }); toast("钱包已创建"); await refreshWallet();
272
294
  });
295
+ $("lock").onclick = async () => { await api("/api/lock", {}); status.unlocked_until = null; showLock(); toast("钱包已锁定"); };
273
296
  $("faucet-btn").onclick = () => busy($("faucet-btn"), async () => { await api("/api/faucet", {}); toast("已领取测试 ETH 和 tUSDC"); await refreshWallet(); });
274
- $("deposit-btn").onclick = async () => {
275
- const answer = await ask(`存入 ${$("dep-amount").value} tUSDC`, `<p class="muted">单次请求上限 ${$("dep-limit").value} tUSDC。需要几笔链上交易,约 1 分钟。</p>`);
276
- if (!answer) return;
277
- await busy($("deposit-btn"), async () => {
297
+ $("deposit-btn").onclick = () => signed($("deposit-btn"), `存入 ${$("dep-amount").value} tUSDC`,
298
+ `<p class="muted">单次请求上限 ${$("dep-limit").value} tUSDC。需要几笔链上交易,约 1 分钟。</p>`, async (answer) => {
278
299
  await api("/api/setup", { password: answer.password, deposit: toUnits($("dep-amount").value), max_per_request: toUnits($("dep-limit").value) });
279
300
  toast("押金已存入"); await refreshWallet();
280
301
  });
281
- };
282
- $("myco-claim").onclick = async () => {
283
- const answer = await ask("领取 MYCO"); if (!answer) return;
284
- await busy($("myco-claim"), async () => {
285
- const { claimed } = await api("/api/rewards/claim", { password: answer.password });
286
- const hours = Object.values(claimed).reduce((a, b) => a + b, 0);
287
- toast(hours ? `已领取 ${hours} 个小时的 MYCO` : "暂时没有可领取的 MYCO"); await refreshRewards();
288
- });
289
- };
290
- $("withdraw-btn").onclick = async () => {
291
- const answer = await ask("取回押金"); if (!answer) return;
292
- await busy($("withdraw-btn"), async () => {
293
- const amount = $("wd-amount").value.trim();
294
- const result = await api("/api/withdraw", { password: answer.password, ...(amount ? { amount: toUnits(amount) } : {}) });
295
- toast(result.withdrawn ? `已取回 ${fmt(result.withdrawn)} tUSDC` : `已申请取回 ${fmt(result.requested)} tUSDC`); await refreshWallet();
296
- });
297
- };
302
+ $("myco-claim").onclick = () => signed($("myco-claim"), "领取 MYCO", "", async (answer) => {
303
+ const { claimed } = await api("/api/rewards/claim", { password: answer.password });
304
+ const hours = Object.values(claimed).reduce((a, b) => a + b, 0);
305
+ toast(hours ? `已领取 ${hours} 个小时的 MYCO` : "暂时没有可领取的 MYCO"); await refreshRewards();
306
+ });
307
+ $("withdraw-btn").onclick = () => signed($("withdraw-btn"), "取回押金", "", async (answer) => {
308
+ const amount = $("wd-amount").value.trim();
309
+ const result = await api("/api/withdraw", { password: answer.password, ...(amount ? { amount: toUnits(amount) } : {}) });
310
+ toast(result.withdrawn ? `已取回 ${fmt(result.withdrawn)} tUSDC` : `已申请取回 ${fmt(result.requested)} tUSDC`); await refreshWallet();
311
+ });
298
312
 
299
313
  // ---------------- chat ----------------
300
314
  let conversation = [];
@@ -349,15 +363,11 @@ async function refreshHistory() {
349
363
  const action = document.createElement("td");
350
364
  if (entry.status === "pending" && entry.release_at > now) {
351
365
  const button = document.createElement("button"); button.className = "ghost"; button.textContent = "争议";
352
- button.onclick = async () => {
353
- const answer = await ask("发起争议", `<p class="muted">会公开这次的问题和回答给随机抽选的 Provider-AI 陪审团,并锁定 1 tUSDC 保证金;确认欺诈后退款并返还保证金。</p>
354
- <label>说明</label><textarea id="statement" placeholder="例如:回答与问题完全无关"></textarea>`);
355
- if (!answer) return;
356
- await busy(button, async () => {
366
+ button.onclick = () => signed(button, "发起争议", `<p class="muted">会公开这次的问题和回答给随机抽选的 Provider-AI 陪审团,并锁定 1 tUSDC 保证金;确认欺诈后退款并返还保证金。</p>
367
+ <label>说明</label><textarea id="statement" placeholder="例如:回答与问题完全无关"></textarea>`, async (answer) => {
357
368
  const result = await api("/api/dispute", { password: answer.password, settlement_key: entry.settlement_key, statement: answer.form.querySelector("#statement").value });
358
369
  toast(`争议已提交,${result.relays_accepted.length} 个 Relay 接收了证据`); await refreshHistory();
359
370
  });
360
- };
361
371
  action.append(button);
362
372
  }
363
373
  row.append(action); $("history").append(row);