mycomesh-consumer 0.2.1 → 0.4.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.
- package/README.md +19 -13
- package/networks/mycomesh-v11-sepolia.json +2 -0
- package/package.json +2 -1
- package/src/account.mjs +93 -0
- package/src/cli.mjs +75 -53
- package/src/console.mjs +86 -0
- package/src/consumer.mjs +148 -14
- package/src/disputes.mjs +29 -1
- package/src/protocol.mjs +23 -0
- package/src/sse.mjs +116 -29
- package/src/wallet.mjs +114 -0
- package/src/web/console.html +282 -0
package/README.md
CHANGED
|
@@ -8,26 +8,32 @@ Requires Node.js 20 or newer. No Docker or Python.
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install --global mycomesh-consumer
|
|
11
|
-
mycomesh-consumer init
|
|
12
|
-
mycomesh-consumer setup --
|
|
13
|
-
mycomesh-consumer request "What is MycoMesh?"
|
|
14
|
-
mycomesh-consumer serve
|
|
11
|
+
mycomesh-consumer init # payment key + password-protected owner wallet
|
|
12
|
+
mycomesh-consumer setup --deposit 20000000
|
|
13
|
+
mycomesh-consumer request --stream "What is MycoMesh?"
|
|
14
|
+
mycomesh-consumer serve # http://127.0.0.1:8110/v1 (responses, chat/completions, models; live streaming)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
- `init` creates the payment key in `~/.mycomesh/v11
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
- `init` creates the payment key and an owner wallet in `~/.mycomesh/v11`. The wallet is an
|
|
18
|
+
Ethereum V3 keystore (MetaMask can import it) protected by a password from
|
|
19
|
+
`MYCOMESH_WALLET_PASSWORD` or a terminal prompt; `--owner-key-file` uses your own key instead.
|
|
20
|
+
- `setup --deposit UNITS` deposits tUSDC and authorizes the payment key up to `--max-per-request`
|
|
21
|
+
units per request. On the testnet it first tops the wallet up from the faucet (gas ETH and tUSDC);
|
|
22
|
+
`faucet` does that on its own.
|
|
23
|
+
- `balance` shows the deposit.
|
|
22
24
|
- `request "..." [--model gpt-5.5] [--provider SIGNER]` prints the verified answer,
|
|
23
25
|
the fee and the settlement key.
|
|
24
|
-
- `serve` runs
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
- `serve` runs the local web console at `http://127.0.0.1:8110/` (chat, wallet, deposit and
|
|
27
|
+
withdrawal, request history and disputes, network status) and an OpenAI-compatible endpoint for
|
|
28
|
+
Codex and other clients; set `MYCOMESH_CONSUMER_API_KEY` to require a bearer token. Only pages from
|
|
29
|
+
this machine can call it: other websites and DNS-rebinding hosts are refused.
|
|
30
|
+
- `withdraw [UNITS]` requests a withdrawal; run it again after the delay to receive the funds.
|
|
31
|
+
- `dispute <settlement-key|last> --statement "..."`
|
|
27
32
|
reveals a recorded request and response to a randomly drawn Provider-AI jury
|
|
28
33
|
within the 24-hour dispute window. The 1 tUSDC reporter bond is returned and a
|
|
29
34
|
bounty paid when fraud is confirmed.
|
|
30
35
|
|
|
31
36
|
Each response is decrypted locally and checked against the Provider-signed
|
|
32
|
-
receipt before it is shown.
|
|
37
|
+
receipt before it is shown; streamed text is checked against it too. Relays come from the bundled
|
|
38
|
+
manifest and the on-chain Relay directory. The bundled network manifest is
|
|
33
39
|
`networks/mycomesh-v11-sepolia.json`; use `--network FILE` for another.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"settlement": "0xd46b7efb1e650e83cdeb4a36fdfef33499e54842",
|
|
6
6
|
"stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
|
|
7
7
|
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
|
+
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
8
9
|
"deployment_block": 11809185,
|
|
9
10
|
"rpc_urls": [
|
|
10
11
|
"https://ethereum-sepolia-rpc.publicnode.com",
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
"https://sepolia.gateway.tenderly.co"
|
|
13
14
|
],
|
|
14
15
|
"tls_ca_file": "mycomesh-testnet-ca.crt",
|
|
16
|
+
"faucet_url": "https://136.0.3.126:10443",
|
|
15
17
|
"relays": [
|
|
16
18
|
{
|
|
17
19
|
"url": "https://136.0.3.126:10443",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mycomesh-consumer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"bin/mycomesh-consumer.mjs",
|
|
18
18
|
"src/*.mjs",
|
|
19
|
+
"src/web/*.html",
|
|
19
20
|
"networks/mycomesh-v11-sepolia.json",
|
|
20
21
|
"networks/mycomesh-testnet-ca.crt",
|
|
21
22
|
"README.md"
|
package/src/account.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Consumer account operations shared by the CLI and the local web console.
|
|
2
|
+
import { addressOf, encodeCall } from "./eip712.mjs";
|
|
3
|
+
import { rpcCall, sendTransaction } from "./chain.mjs";
|
|
4
|
+
import { directoryRelays, httpJson } from "./consumer.mjs";
|
|
5
|
+
import { buildEvidence, evidenceHash, openDisputeCall, reportId } from "./disputes.mjs";
|
|
6
|
+
|
|
7
|
+
export const STATUSES = ["none", "pending", "disputed", "released", "confirmed", "dismissed", "timed_out", "jury_unavailable", "voided"];
|
|
8
|
+
|
|
9
|
+
async function words(network, to, signature, pairs = []) {
|
|
10
|
+
const raw = (await rpcCall(network.rpc_urls, "eth_call", [{ to, data: encodeCall(signature, pairs) }, "latest"])).slice(2);
|
|
11
|
+
return Array.from({ length: raw.length / 64 }, (_, i) => BigInt(`0x${raw.slice(i * 64, i * 64 + 64)}`));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Balances of the owner wallet and the payment key's grant; amounts are strings of base units. */
|
|
15
|
+
export async function walletStatus(network, owner, key) {
|
|
16
|
+
const [eth, [tokens], [deposit], withdrawal, grant, params] = await Promise.all([
|
|
17
|
+
rpcCall(network.rpc_urls, "eth_getBalance", [owner, "latest"]),
|
|
18
|
+
words(network, network.stablecoin, "balanceOf(address)", [["address", owner]]),
|
|
19
|
+
words(network, network.settlement, "availableBalance(address)", [["address", owner]]),
|
|
20
|
+
words(network, network.settlement, "withdrawals(address)", [["address", owner]]),
|
|
21
|
+
key ? words(network, network.settlement, "keyGrants(address)", [["address", key]]) : Promise.resolve(null),
|
|
22
|
+
words(network, network.settlement, "params()"),
|
|
23
|
+
]);
|
|
24
|
+
return {
|
|
25
|
+
owner, key, eth_wei: BigInt(eth).toString(), wallet_tokens: tokens.toString(), deposit: deposit.toString(),
|
|
26
|
+
withdrawal: { amount: withdrawal[0].toString(), available_at: Number(withdrawal[1]) },
|
|
27
|
+
grant: grant && { active: Boolean(grant[3]) && `0x${grant[0].toString(16).padStart(40, "0")}` === owner.toLowerCase(),
|
|
28
|
+
max_per_request: grant[1].toString() },
|
|
29
|
+
dispute_window: Number(params[0]), reporter_bond: params[3].toString(), faucet: Boolean(network.faucet_url),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function faucet(network, address) {
|
|
34
|
+
if (!network.faucet_url) throw new Error("this network has no faucet");
|
|
35
|
+
const reply = await httpJson(`${network.faucet_url}/v11/faucet`, { method: "POST", body: { address }, ca: network.tls_ca, timeoutMs: 400_000 });
|
|
36
|
+
if (reply.status !== 200) throw new Error(`faucet: ${reply.body?.error || reply.status}`);
|
|
37
|
+
return reply.body;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Deposit and grant the payment key; on testnets, top the wallet up from the faucet first. */
|
|
41
|
+
export async function setup(network, { ownerPrivate, key, deposit, limit, forceFaucet = false, log = () => {} }) {
|
|
42
|
+
const owner = addressOf(ownerPrivate);
|
|
43
|
+
const status = await walletStatus(network, owner, key);
|
|
44
|
+
if (network.faucet_url && (forceFaucet || BigInt(status.eth_wei) < 2_000_000_000_000_000n || BigInt(status.wallet_tokens) < deposit)) {
|
|
45
|
+
log(`funding ${owner} from the testnet faucet...`);
|
|
46
|
+
await faucet(network, owner);
|
|
47
|
+
}
|
|
48
|
+
const send = (to, data) => sendTransaction(network.rpc_urls, ownerPrivate, { to, data });
|
|
49
|
+
await send(network.stablecoin, encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", deposit]]));
|
|
50
|
+
await send(network.settlement, encodeCall("deposit(uint256)", [["uint", deposit]]));
|
|
51
|
+
if (!status.grant?.active || BigInt(status.grant.max_per_request) !== limit) {
|
|
52
|
+
await send(network.settlement, encodeCall("registerKey(address,uint256,uint64)", [["address", key], ["uint", limit], ["uint", 0]]));
|
|
53
|
+
}
|
|
54
|
+
return { owner, key, deposit: deposit.toString(), max_per_request: limit.toString() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Withdrawals take two steps: request (funds stop backing requests), then withdraw after the delay. */
|
|
58
|
+
export async function requestWithdrawal(network, ownerPrivate, amount) {
|
|
59
|
+
await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.settlement,
|
|
60
|
+
data: encodeCall("requestWithdrawal(uint256)", [["uint", amount]]) });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function completeWithdrawal(network, ownerPrivate) {
|
|
64
|
+
await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.settlement, data: encodeCall("withdraw()", []) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function settlementStatus(network, key) {
|
|
68
|
+
const fields = await words(network, network.settlement, "settlementInfo(bytes32)", [["bytes32", key]]);
|
|
69
|
+
return { status: STATUSES[Number(fields[14])] || "unknown", release_at: Number(fields[13]), fee: fields[10].toString() };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Commit the evidence on-chain (with the reporter bond), then hand it to every Relay's dispute desk. */
|
|
73
|
+
export async function dispute(network, ownerPrivate, record, { reasonCode, statement }) {
|
|
74
|
+
const evidence = buildEvidence(record, { reasonCode, statement });
|
|
75
|
+
const digest = evidenceHash(evidence);
|
|
76
|
+
const [bond] = (await words(network, network.settlement, "params()")).slice(3, 4);
|
|
77
|
+
if (bond > 0n) {
|
|
78
|
+
await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.stablecoin,
|
|
79
|
+
data: encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", bond]]) });
|
|
80
|
+
}
|
|
81
|
+
await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.settlement, data: openDisputeCall(record.settlement_key, digest) });
|
|
82
|
+
let relays = network.relays;
|
|
83
|
+
try { relays = [...relays, ...await directoryRelays(network)]; } catch {}
|
|
84
|
+
const accepted = [];
|
|
85
|
+
for (const relay of relays) {
|
|
86
|
+
try {
|
|
87
|
+
const reply = await httpJson(`${relay.url}/v11/evidence`, { method: "POST", body: evidence, ca: network.tls_ca, timeoutMs: 30_000 });
|
|
88
|
+
if (reply.status === 200) accepted.push(relay.url);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
return { settlement_key: record.settlement_key, evidence_hash: digest,
|
|
92
|
+
report_id: reportId(record.settlement_key, addressOf(ownerPrivate), digest), relays_accepted: accepted };
|
|
93
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
// mycomesh-consumer (V11): init, setup, balance, request, serve.
|
|
1
|
+
// mycomesh-consumer (V11): init, wallet, faucet, setup, balance, request, serve, dispute.
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { addressOf,
|
|
9
|
-
import { availableBalance
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { addressOf, settlementKey } from "./eip712.mjs";
|
|
9
|
+
import { availableBalance } from "./chain.mjs";
|
|
10
|
+
import { completeWithdrawal, dispute, faucet, requestWithdrawal, setup, walletStatus } from "./account.mjs";
|
|
11
|
+
import { Consumer, loadNetwork, serveConsumer } from "./consumer.mjs";
|
|
12
|
+
import { createWallet, ownerAddress, ownerKey, walletAddress } from "./wallet.mjs";
|
|
13
|
+
import { loadRequest } from "./disputes.mjs";
|
|
12
14
|
|
|
13
15
|
const DEFAULT_NETWORK = join(dirname(fileURLToPath(import.meta.url)), "../networks/mycomesh-v11-sepolia.json");
|
|
14
16
|
|
|
15
17
|
const USAGE = `Usage: mycomesh-consumer <command> [options]
|
|
16
18
|
|
|
17
|
-
init create the local payment key
|
|
19
|
+
init create the local payment key and the password-protected owner wallet
|
|
18
20
|
address print the payment key address
|
|
19
|
-
|
|
21
|
+
wallet print the owner wallet address (a V3 keystore; MetaMask can import it)
|
|
22
|
+
faucet testnet: get gas ETH and tUSDC for the owner wallet
|
|
23
|
+
setup --deposit UNITS [--max-per-request UNITS]
|
|
20
24
|
deposit into the settlement contract and authorize the payment key
|
|
21
|
-
(
|
|
25
|
+
(on testnets, tops the wallet up from the faucet first)
|
|
22
26
|
balance [--owner ADDR] show the custodied deposit
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
withdraw [UNITS] request a withdrawal of the deposit; run again after the delay to receive it
|
|
28
|
+
request "prompt" [--stream]
|
|
29
|
+
send one request and print the verified answer
|
|
30
|
+
serve [--port 8110] run the local web console and OpenAI-compatible endpoint (default)
|
|
31
|
+
dispute <settlement-key|last> [--reason CODE] [--statement TEXT]
|
|
26
32
|
reveal a recorded request and response to a Provider-AI jury
|
|
27
33
|
(within 24 hours; the reporter bond is returned if fraud is confirmed)
|
|
28
34
|
|
|
29
|
-
Common: --network FILE (default: the bundled MycoMesh V11 Sepolia manifest), --data-dir DIR, --model ID,
|
|
35
|
+
Common: --network FILE (default: the bundled MycoMesh V11 Sepolia manifest), --data-dir DIR, --model ID,
|
|
36
|
+
--max-fee UNITS, --provider SIGNER, --owner-key-file F (raw key or keystore instead of the local wallet).
|
|
37
|
+
The wallet password comes from MYCOMESH_WALLET_PASSWORD or a terminal prompt.`;
|
|
30
38
|
|
|
31
39
|
function dataDir(value) {
|
|
32
40
|
const dir = value || process.env.MYCOMESH_CONSUMER_DATA_DIR || join(homedir(), ".mycomesh", "v11");
|
|
@@ -59,71 +67,83 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
|
|
|
59
67
|
model: { type: "string", default: "gpt-5.5" }, "max-fee": { type: "string", default: "1000000" },
|
|
60
68
|
port: { type: "string", default: "8110" }, host: { type: "string", default: "127.0.0.1" },
|
|
61
69
|
"max-output-tokens": { type: "string", default: "4096" }, help: { type: "boolean" },
|
|
62
|
-
reason: { type: "string", default: "unrelated_response" }, faucet: { type: "
|
|
70
|
+
reason: { type: "string", default: "unrelated_response" }, faucet: { type: "boolean" }, provider: { type: "string" },
|
|
71
|
+
statement: { type: "string", default: "" }, stream: { type: "boolean" },
|
|
63
72
|
},
|
|
64
73
|
});
|
|
65
74
|
const [command = "serve", ...rest] = positionals;
|
|
66
75
|
if (values.help || command === "help") { stdout.write(`${USAGE}\n`); return 0; }
|
|
67
76
|
const dir = dataDir(values["data-dir"]);
|
|
68
|
-
if (command === "
|
|
69
|
-
stdout.write(`${addressOf(paymentKey(dir
|
|
77
|
+
if (command === "address") {
|
|
78
|
+
stdout.write(`${addressOf(paymentKey(dir))}\n`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (command === "init") {
|
|
82
|
+
stdout.write(`${addressOf(paymentKey(dir, true))}\n`);
|
|
83
|
+
if (!values["owner-key-file"]) {
|
|
84
|
+
stdout.write(`owner wallet ${walletAddress(await createWallet(dir))} (${join(dir, "owner-wallet.json")})\n`);
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (command === "wallet") {
|
|
89
|
+
stdout.write(`${values["owner-key-file"] ? ownerAddress(values["owner-key-file"], dir) : walletAddress(await createWallet(dir))}\n`);
|
|
70
90
|
return 0;
|
|
71
91
|
}
|
|
72
92
|
const network = loadNetwork(values.network || process.env.MYCOMESH_NETWORK || DEFAULT_NETWORK);
|
|
93
|
+
if (command === "faucet") {
|
|
94
|
+
const address = values.owner || ownerAddress(values["owner-key-file"], dir) || walletAddress(await createWallet(dir));
|
|
95
|
+
stdout.write(`${JSON.stringify(await faucet(network, address))}\n`);
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
73
98
|
if (command === "setup") {
|
|
74
99
|
const keyPrivate = paymentKey(dir);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
if (!values.deposit) throw new Error("--deposit UNITS is required");
|
|
101
|
+
if (!values["owner-key-file"]) await createWallet(dir);
|
|
102
|
+
const result = await setup(network, {
|
|
103
|
+
ownerPrivate: await ownerKey(values["owner-key-file"], dir), key: addressOf(keyPrivate), deposit: BigInt(values.deposit),
|
|
104
|
+
limit: BigInt(values["max-per-request"] || values["max-fee"]), forceFaucet: values.faucet,
|
|
105
|
+
log: (line) => stdout.write(`${line}\n`),
|
|
106
|
+
});
|
|
107
|
+
stdout.write(`deposited ${result.deposit} and authorized ${result.key} up to ${result.max_per_request} per request\n`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
if (command === "withdraw") {
|
|
111
|
+
const owner = await ownerKey(values["owner-key-file"], dir);
|
|
112
|
+
const status = await walletStatus(network, addressOf(owner), null);
|
|
113
|
+
if (BigInt(status.withdrawal.amount) > 0n) {
|
|
114
|
+
if (status.withdrawal.available_at > Math.floor(Date.now() / 1000)) {
|
|
115
|
+
throw new Error(`the withdrawal of ${status.withdrawal.amount} unlocks at ${new Date(status.withdrawal.available_at * 1000).toISOString()}`);
|
|
116
|
+
}
|
|
117
|
+
await completeWithdrawal(network, owner);
|
|
118
|
+
stdout.write(`withdrew ${status.withdrawal.amount}\n`);
|
|
119
|
+
return 0;
|
|
81
120
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await sendTransaction(network.rpc_urls, owner, { to: network.settlement,
|
|
86
|
-
data: encodeCall("registerKey(address,uint256,uint64)", [["address", addressOf(keyPrivate)], ["uint", limit], ["uint", 0]]) });
|
|
87
|
-
stdout.write(`deposited ${deposit} and authorized ${addressOf(keyPrivate)} up to ${limit} per request\n`);
|
|
121
|
+
const amount = BigInt(rest[0] || status.deposit);
|
|
122
|
+
await requestWithdrawal(network, owner, amount);
|
|
123
|
+
stdout.write(`requested a withdrawal of ${amount}; run \`withdraw\` again after the delay\n`);
|
|
88
124
|
return 0;
|
|
89
125
|
}
|
|
90
126
|
if (command === "balance") {
|
|
91
|
-
const owner = values.owner || (values["owner-key-file"]
|
|
92
|
-
if (!owner) throw new Error("--owner
|
|
127
|
+
const owner = values.owner || ownerAddress(values["owner-key-file"], dir);
|
|
128
|
+
if (!owner) throw new Error("--owner, --owner-key-file or a local wallet (`init`) is required");
|
|
93
129
|
stdout.write(`${await availableBalance(network, owner)}\n`);
|
|
94
130
|
return 0;
|
|
95
131
|
}
|
|
96
132
|
if (command === "dispute") {
|
|
97
|
-
const owner =
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const params = await rpcCall(network.rpc_urls, "eth_call", [{ to: network.settlement, data: encodeCall("params()", []) }, "latest"]);
|
|
103
|
-
const bond = BigInt(`0x${params.slice(2 + 64 * 3, 2 + 64 * 4)}`);
|
|
104
|
-
if (bond > 0n) {
|
|
105
|
-
await sendTransaction(network.rpc_urls, owner, { to: network.stablecoin,
|
|
106
|
-
data: encodeCall("approve(address,uint256)", [["address", network.settlement], ["uint", bond]]) });
|
|
107
|
-
}
|
|
108
|
-
await sendTransaction(network.rpc_urls, owner, { to: network.settlement, data: openDisputeCall(record.settlement_key, digest) });
|
|
109
|
-
const accepted = [];
|
|
110
|
-
for (const relay of network.relays) {
|
|
111
|
-
try {
|
|
112
|
-
const reply = await httpJson(`${relay.url}/v11/evidence`, { method: "POST", body: evidence, ca: network.tls_ca, timeoutMs: 30_000 });
|
|
113
|
-
if (reply.status === 200) accepted.push(relay.url);
|
|
114
|
-
} catch {}
|
|
115
|
-
}
|
|
116
|
-
stdout.write(`${JSON.stringify({ settlement_key: record.settlement_key, evidence_hash: digest,
|
|
117
|
-
report_id: reportId(record.settlement_key, addressOf(owner), digest), relays_accepted: accepted })}\n`);
|
|
118
|
-
return accepted.length ? 0 : 2;
|
|
133
|
+
const owner = await ownerKey(values["owner-key-file"], dir);
|
|
134
|
+
const result = await dispute(network, owner, loadRequest(dir, rest[0] || "last"),
|
|
135
|
+
{ reasonCode: values.reason, statement: values.statement || rest.slice(1).join(" ") });
|
|
136
|
+
stdout.write(`${JSON.stringify(result)}\n`);
|
|
137
|
+
return result.relays_accepted.length ? 0 : 2;
|
|
119
138
|
}
|
|
120
139
|
const keyPrivate = paymentKey(dir);
|
|
121
140
|
const consumer = new Consumer({ network, keyPrivate, maxFee: Number(values["max-fee"]), journalDir: dir });
|
|
122
141
|
if (command === "request") {
|
|
123
142
|
const { response, receipt } = await consumer.request({
|
|
124
143
|
endpoint: "responses", model: values.model, content: rest.join(" "), maxOutputTokens: Number(values["max-output-tokens"]),
|
|
125
|
-
provider: values.provider,
|
|
144
|
+
provider: values.provider, onDelta: values.stream ? (text) => process.stderr.write(text) : undefined,
|
|
126
145
|
});
|
|
146
|
+
if (values.stream) process.stderr.write("\n");
|
|
127
147
|
const output = response.output;
|
|
128
148
|
stdout.write(`${JSON.stringify({ output_text: output.output_text ?? output, fee: receipt.receipt.actual_fee,
|
|
129
149
|
provider: receipt.authorization.provider_signer, request_id: receipt.authorization.request_id,
|
|
@@ -131,9 +151,11 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
|
|
|
131
151
|
return 0;
|
|
132
152
|
}
|
|
133
153
|
if (command === "serve") {
|
|
134
|
-
const server = await serveConsumer(consumer, { host: values.host, port: Number(values.port),
|
|
154
|
+
const server = await serveConsumer(consumer, { host: values.host, port: Number(values.port),
|
|
155
|
+
apiKey: process.env.MYCOMESH_CONSUMER_API_KEY, dataDir: dir });
|
|
135
156
|
const { port } = server.address();
|
|
136
157
|
stdout.write(`MycoMesh V11 Consumer on http://${values.host}:${port}/v1 (payment key ${consumer.key})\n`);
|
|
158
|
+
stdout.write(`Web console: http://127.0.0.1:${port}/\n`);
|
|
137
159
|
return new Promise(() => {});
|
|
138
160
|
}
|
|
139
161
|
throw new Error(`unknown command ${command}\n${USAGE}`);
|
package/src/console.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The local web console: the Bitcoin-node model, a UI served by this machine to this machine.
|
|
2
|
+
// The browser only ever talks to 127.0.0.1; this process reaches the network and verifies everything.
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { addressOf } from "./eip712.mjs";
|
|
5
|
+
import { completeWithdrawal, dispute, faucet, requestWithdrawal, settlementStatus, setup, walletStatus } from "./account.mjs";
|
|
6
|
+
import { listRequests, loadRequest } from "./disputes.mjs";
|
|
7
|
+
import { outputText } from "./protocol.mjs";
|
|
8
|
+
import { createWallet, ownerAddress, unlockWallet, walletAddress } from "./wallet.mjs";
|
|
9
|
+
|
|
10
|
+
const PAGE = new URL("./web/console.html", import.meta.url);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Only this machine's own pages may call the local node: reject other Host
|
|
14
|
+
* names (DNS rebinding) and cross-site Origins (a web page spending the deposit).
|
|
15
|
+
*/
|
|
16
|
+
export function localRequestAllowed(req, port) {
|
|
17
|
+
const allowed = new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`]);
|
|
18
|
+
if (!allowed.has(String(req.headers.host || ""))) return false;
|
|
19
|
+
const origin = req.headers.origin;
|
|
20
|
+
return !origin || allowed.has(origin.replace(/^http:\/\//, ""));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function consoleRoutes({ consumer, dataDir }) {
|
|
24
|
+
const network = consumer.network;
|
|
25
|
+
const owner = () => ownerAddress(undefined, dataDir);
|
|
26
|
+
const withWallet = (body) => unlockWallet(dataDir, body.password);
|
|
27
|
+
const units = (value, name) => {
|
|
28
|
+
if (!/^[0-9]+$/.test(String(value ?? ""))) throw Object.assign(new Error(`${name} must be a whole number of base units`), { status: 400 });
|
|
29
|
+
return BigInt(value);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
"GET /": () => ({ type: "text/html; charset=utf-8", body: readFileSync(PAGE, "utf8") }),
|
|
33
|
+
"GET /api/status": async () => {
|
|
34
|
+
const address = owner();
|
|
35
|
+
return {
|
|
36
|
+
network: { id: network.network_id, chain_id: network.chain_id, settlement: network.settlement, faucet: Boolean(network.faucet_url) },
|
|
37
|
+
payment_key: consumer.key, max_fee: String(consumer.maxFee),
|
|
38
|
+
wallet: address ? await walletStatus(network, address, consumer.key) : null,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
"GET /api/network": async () => {
|
|
42
|
+
const relays = [];
|
|
43
|
+
for (const relay of await consumer.relays()) {
|
|
44
|
+
let health = null; let providers = [];
|
|
45
|
+
try { health = (await consumer.fetchJson(`${relay.url}/health`, { ca: network.tls_ca, timeoutMs: 5_000 })).body; } catch {}
|
|
46
|
+
try { providers = await consumer.providers(relay); } catch {}
|
|
47
|
+
relays.push({ url: relay.url, signer: relay.signer, healthy: Boolean(health?.ok), providers: providers.map((p) => ({
|
|
48
|
+
signer: p.provider_signer, models: p.models, prices: p.prices, capacity: p.capacity })) });
|
|
49
|
+
}
|
|
50
|
+
return { relays };
|
|
51
|
+
},
|
|
52
|
+
"GET /api/history": async () => {
|
|
53
|
+
const entries = listRequests(dataDir, outputText, 50);
|
|
54
|
+
await Promise.all(entries.map(async (entry) => {
|
|
55
|
+
try { Object.assign(entry, await settlementStatus(network, entry.settlement_key)); } catch { entry.status = "unknown"; }
|
|
56
|
+
}));
|
|
57
|
+
return { entries };
|
|
58
|
+
},
|
|
59
|
+
"POST /api/wallet": async (body) => ({ owner: walletAddress(await createWallet(dataDir, body.password)) }),
|
|
60
|
+
"POST /api/faucet": async () => {
|
|
61
|
+
const address = owner();
|
|
62
|
+
if (!address) throw Object.assign(new Error("create the wallet first"), { status: 400 });
|
|
63
|
+
return faucet(network, address);
|
|
64
|
+
},
|
|
65
|
+
"POST /api/setup": async (body) => setup(network, {
|
|
66
|
+
ownerPrivate: withWallet(body), key: consumer.key, deposit: units(body.deposit, "deposit"),
|
|
67
|
+
limit: units(body.max_per_request ?? consumer.maxFee, "max_per_request"),
|
|
68
|
+
}),
|
|
69
|
+
"POST /api/withdraw": async (body) => {
|
|
70
|
+
const ownerPrivate = withWallet(body);
|
|
71
|
+
const status = await walletStatus(network, addressOf(ownerPrivate), null);
|
|
72
|
+
if (BigInt(status.withdrawal.amount) > 0n) {
|
|
73
|
+
if (status.withdrawal.available_at > Math.floor(Date.now() / 1000)) {
|
|
74
|
+
throw Object.assign(new Error("the pending withdrawal is still locked"), { status: 409 });
|
|
75
|
+
}
|
|
76
|
+
await completeWithdrawal(network, ownerPrivate);
|
|
77
|
+
return { withdrawn: status.withdrawal.amount };
|
|
78
|
+
}
|
|
79
|
+
const amount = body.amount ? units(body.amount, "amount") : BigInt(status.deposit);
|
|
80
|
+
await requestWithdrawal(network, ownerPrivate, amount);
|
|
81
|
+
return { requested: amount.toString() };
|
|
82
|
+
},
|
|
83
|
+
"POST /api/dispute": async (body) => dispute(network, withWallet(body), loadRequest(dataDir, String(body.settlement_key)),
|
|
84
|
+
{ reasonCode: String(body.reason_code || "unrelated_response"), statement: String(body.statement || "") }),
|
|
85
|
+
};
|
|
86
|
+
}
|