mycomesh-consumer 0.6.1 → 0.8.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/networks/mycomesh-v11-sepolia.json +6 -2
- package/package.json +1 -1
- package/src/cli.mjs +15 -0
- package/src/console.mjs +9 -0
- package/src/probes.mjs +102 -2
- package/src/reputation.mjs +29 -10
- package/src/rewards.mjs +84 -0
- package/src/web/console.html +37 -3
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
"stablecoin": "0xbce27efad4191277167fe76819581a97959048cf",
|
|
7
7
|
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
8
|
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
9
|
-
"probe_ledger": "
|
|
9
|
+
"probe_ledger": "0x3bbc6fc971a6c4e528af9228b95d4b83ef188795",
|
|
10
|
+
"emission": "0x85ee0e8f63400041f0c76010c590c330bc135a8d",
|
|
11
|
+
"token": "0x620d026ad854fb17c7b16534a0f28ef5499e2d4a",
|
|
12
|
+
"emission_block": 11815544,
|
|
10
13
|
"tiers": {
|
|
11
14
|
"1": {
|
|
12
15
|
"name": "OpenAI frontier",
|
|
13
16
|
"models": [
|
|
14
17
|
"gpt-5.5"
|
|
15
|
-
]
|
|
18
|
+
],
|
|
19
|
+
"capability_floor": 0.85
|
|
16
20
|
},
|
|
17
21
|
"2": {
|
|
18
22
|
"name": "Anthropic Claude",
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { completeWithdrawal, dispute, faucet, requestWithdrawal, setup, walletSt
|
|
|
11
11
|
import { Consumer, loadNetwork, serveConsumer } from "./consumer.mjs";
|
|
12
12
|
import { createWallet, ownerAddress, ownerKey, walletAddress } from "./wallet.mjs";
|
|
13
13
|
import { loadRequest } from "./disputes.mjs";
|
|
14
|
+
import { claimRewards, formatMyco, rewardsSummary } from "./rewards.mjs";
|
|
14
15
|
import { addTenant, loadTenants, revokeTenant, setBudget, tenantKey, tenantStatus } from "./tenants.mjs";
|
|
15
16
|
import { spawn } from "node:child_process";
|
|
16
17
|
import { statSync } from "node:fs";
|
|
@@ -27,6 +28,8 @@ const USAGE = `Usage: mycomesh-consumer <command> [options]
|
|
|
27
28
|
deposit into the settlement contract and authorize the payment key
|
|
28
29
|
(on testnets, tops the wallet up from the faucet first)
|
|
29
30
|
balance [--owner ADDR] show the custodied deposit
|
|
31
|
+
rewards [claim] MYCO earned by the owner wallet (80% of each hour's emission goes to the
|
|
32
|
+
Consumers who paid that hour's fees); \`claim\` mints it to the wallet
|
|
30
33
|
withdraw [UNITS] request a withdrawal of the deposit; run again after the delay to receive it
|
|
31
34
|
request "prompt" [--stream]
|
|
32
35
|
send one request and print the verified answer
|
|
@@ -136,6 +139,18 @@ export async function main(argv = process.argv.slice(2), { stdout = process.stdo
|
|
|
136
139
|
stdout.write(`${await availableBalance(network, owner)}\n`);
|
|
137
140
|
return 0;
|
|
138
141
|
}
|
|
142
|
+
if (command === "rewards") {
|
|
143
|
+
if (rest[0] === "claim") {
|
|
144
|
+
const owner = await ownerKey(values["owner-key-file"], dir);
|
|
145
|
+
stdout.write(`claimed ${JSON.stringify(await claimRewards(network, owner, addressOf(owner)))}\n`);
|
|
146
|
+
}
|
|
147
|
+
const account = values.owner || ownerAddress(values["owner-key-file"], dir);
|
|
148
|
+
if (!account) throw new Error("--owner, --owner-key-file or a local wallet (`init`) is required");
|
|
149
|
+
const summary = await rewardsSummary(network, account);
|
|
150
|
+
if (!summary) throw new Error("this network has no MYCO emission");
|
|
151
|
+
stdout.write(`${JSON.stringify(summary, null, 2)}\nMYCO balance ${formatMyco(summary.myco_balance_wei ?? 0)}\n`);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
139
154
|
if (command === "dispute") {
|
|
140
155
|
const owner = await ownerKey(values["owner-key-file"], dir);
|
|
141
156
|
const result = await dispute(network, owner, loadRequest(dir, rest[0] || "last"),
|
package/src/console.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { outputText } from "./protocol.mjs";
|
|
|
8
8
|
import { createWallet, ownerAddress, unlockWallet, walletAddress } from "./wallet.mjs";
|
|
9
9
|
import { addTenant, loadTenants, revokeTenant, setBudget, tenantStatus } from "./tenants.mjs";
|
|
10
10
|
import { networkPrices } from "./pricing.mjs";
|
|
11
|
+
import { claimRewards, rewardsSummary } from "./rewards.mjs";
|
|
11
12
|
|
|
12
13
|
const PAGE = new URL("./web/console.html", import.meta.url);
|
|
13
14
|
|
|
@@ -61,6 +62,14 @@ export function consoleRoutes({ consumer, dataDir }) {
|
|
|
61
62
|
}));
|
|
62
63
|
return { entries };
|
|
63
64
|
},
|
|
65
|
+
"GET /api/rewards": async () => {
|
|
66
|
+
const address = owner();
|
|
67
|
+
return { rewards: address ? await rewardsSummary(network, address) : null };
|
|
68
|
+
},
|
|
69
|
+
"POST /api/rewards/claim": async (body) => {
|
|
70
|
+
const ownerPrivate = withWallet(body);
|
|
71
|
+
return { claimed: await claimRewards(network, ownerPrivate, addressOf(ownerPrivate)) };
|
|
72
|
+
},
|
|
64
73
|
"POST /api/wallet": async (body) => ({ owner: walletAddress(await createWallet(dataDir, body.password)) }),
|
|
65
74
|
"POST /api/faucet": async () => {
|
|
66
75
|
const address = owner();
|
package/src/probes.mjs
CHANGED
|
@@ -4,9 +4,108 @@ import { verifySignedReceipt } from "./eip712.mjs";
|
|
|
4
4
|
|
|
5
5
|
export const PROBE_EVIDENCE_SCHEMA = "mycomesh.v11.probe-evidence.v1";
|
|
6
6
|
const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
7
|
-
const numbers = (text) => (text.match(
|
|
7
|
+
const numbers = (text) => (text.match(/[0-9]{1,3}(?:[,\s_][0-9]{3})+(?![0-9])|[0-9]+/g) || []).map((match) => match.replace(/[,\s_]/g, ""));
|
|
8
|
+
|
|
9
|
+
// ---------------- capability probes (mirrors mycomesh/capability.py) ----------------
|
|
10
|
+
|
|
11
|
+
export const CAPABILITY_KINDS = ["trace", "crt", "long_multiply", "path", "kth", "letters", "calendar"];
|
|
12
|
+
/** Default per-tier pass-rate floors; a manifest tier's capability_floor overrides them. */
|
|
13
|
+
export const CAPABILITY_FLOORS = { 1: 0.85 }; // tier 2 (Claude) has none until calibrated
|
|
14
|
+
|
|
15
|
+
export function ordinal(n) {
|
|
16
|
+
const suffix = n % 100 >= 10 && n % 100 <= 20 ? "th" : ({ 1: "st", 2: "nd", 3: "rd" })[n % 10] || "th";
|
|
17
|
+
return `${n}${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const LINE_BREAKS = /\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/;
|
|
21
|
+
const weekdays = (text) => WEEKDAYS.filter((day) => text.toLowerCase().includes(day.toLowerCase()));
|
|
22
|
+
|
|
23
|
+
function weekdayAfter(start, offset) {
|
|
24
|
+
const day = new Date(`${start}T00:00:00Z`);
|
|
25
|
+
day.setUTCDate(day.getUTCDate() + Number(offset));
|
|
26
|
+
return WEEKDAYS[(day.getUTCDay() + 6) % 7];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function capabilityTask(kind, p) {
|
|
30
|
+
if (kind === "trace") {
|
|
31
|
+
let [x, y] = [Number(p.x), Number(p.y)];
|
|
32
|
+
const [a, m, n] = [Number(p.a), Number(p.m), Number(p.n)];
|
|
33
|
+
const question = `Start with x = ${x} and y = ${y}. Repeat the following ${n} times: first set x to (x * ${a} + y) mod ${m}, `
|
|
34
|
+
+ `then set y to (y + 2 * x) mod ${m}. What is x + y at the end?`;
|
|
35
|
+
for (let i = 0; i < n; i += 1) { x = (x * a + y) % m; y = (y + 2 * x) % m; }
|
|
36
|
+
return { question, reference: String(x + y) };
|
|
37
|
+
}
|
|
38
|
+
if (kind === "crt") {
|
|
39
|
+
const moduli = p.moduli.map(Number); const residues = p.residues.map(Number);
|
|
40
|
+
const parts = moduli.map((m, i) => `remainder ${residues[i]} when divided by ${m}`);
|
|
41
|
+
const question = `What is the smallest positive integer that leaves ${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}?`;
|
|
42
|
+
// Constructive CRT (the moduli are distinct primes): the least positive solution.
|
|
43
|
+
const big = moduli.map(BigInt);
|
|
44
|
+
const product = big.reduce((a, b) => a * b, 1n);
|
|
45
|
+
const power = (base, exponent, m) => { let r = 1n; base %= m; for (; exponent > 0n; exponent >>= 1n, base = base * base % m) if (exponent & 1n) r = r * base % m; return r; };
|
|
46
|
+
const x = big.reduce((sum, m, i) => sum + BigInt(residues[i]) * (product / m) * power(product / m, m - 2n, m), 0n) % product;
|
|
47
|
+
return { question, reference: String(x || product) };
|
|
48
|
+
}
|
|
49
|
+
if (kind === "long_multiply") return { question: `What is ${p.a} times ${p.b}?`, reference: String(BigInt(p.a) * BigInt(p.b)) };
|
|
50
|
+
if (kind === "path") {
|
|
51
|
+
const edges = p.edges.map(([u, v, w]) => [String(u), String(v), Number(w)]);
|
|
52
|
+
const question = `In a road network the two-way roads and their lengths are: ${edges.map(([u, v, w]) => `${u}-${v} ${w}`).join(", ")}. `
|
|
53
|
+
+ `What is the length of the shortest route from ${p.start} to ${p.end}?`;
|
|
54
|
+
const distance = new Map([[String(p.start), 0]]); const done = new Set();
|
|
55
|
+
for (;;) {
|
|
56
|
+
const open = [...distance].filter(([node]) => !done.has(node)).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
|
|
57
|
+
if (!open.length) break;
|
|
58
|
+
const [node, d] = open[0];
|
|
59
|
+
done.add(node);
|
|
60
|
+
for (const [u, v, w] of edges) {
|
|
61
|
+
for (const [here, there] of [[u, v], [v, u]]) {
|
|
62
|
+
if (here === node && d + w < (distance.get(there) ?? Infinity)) distance.set(there, d + w);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { question, reference: String(distance.get(String(p.end))) };
|
|
67
|
+
}
|
|
68
|
+
if (kind === "kth") {
|
|
69
|
+
const values = p.values.map(Number); const k = Number(p.k);
|
|
70
|
+
return { question: `What is the ${ordinal(k)} smallest number in this list: ${values.join(", ")}?`,
|
|
71
|
+
reference: String([...values].sort((a, b) => a - b)[k - 1]) };
|
|
72
|
+
}
|
|
73
|
+
if (kind === "letters") {
|
|
74
|
+
return { question: `How many times does the letter "${p.letter}" appear in the following text: "${p.text}"?`,
|
|
75
|
+
reference: String(String(p.text).split(String(p.letter)).length - 1) };
|
|
76
|
+
}
|
|
77
|
+
if (kind === "calendar") {
|
|
78
|
+
return { question: `What day of the week will it be ${p.offset} days after ${p.start}?`, reference: weekdayAfter(p.start, p.offset) };
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`unknown capability kind ${kind}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function gradeCapability(task, answer) {
|
|
84
|
+
// The answer finally stated decides: the last \boxed{...}, else the last line that states a value.
|
|
85
|
+
// A handful of values at most: listing candidates is not answering.
|
|
86
|
+
const text = String(answer);
|
|
87
|
+
const pick = task.kind === "calendar" ? weekdays : numbers;
|
|
88
|
+
const boxed = [...text.matchAll(/\\boxed\{([^{}]*)\}/g)];
|
|
89
|
+
let stated = [];
|
|
90
|
+
if (boxed.length) stated = pick(boxed[boxed.length - 1][1]);
|
|
91
|
+
else for (const line of text.split(LINE_BREAKS).reverse()) { const found = pick(line); if (found.length) { stated = found; break; } }
|
|
92
|
+
return stated.length <= (task.kind === "calendar" ? 1 : 3) && stated.includes(task.reference) ? "pass" : "wrong";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Upper bound of a pass rate at 99% one-sided confidence (Wilson). */
|
|
96
|
+
export function wilsonUpper(passes, total, z = 2.326) {
|
|
97
|
+
if (!total) return 1;
|
|
98
|
+
const p = passes / total;
|
|
99
|
+
const centre = p + (z * z) / (2 * total);
|
|
100
|
+
const spread = z * Math.sqrt((p * (1 - p)) / total + (z * z) / (4 * total * total));
|
|
101
|
+
return Math.min(1, (centre + spread) / (1 + (z * z) / total));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 99% confident the pass rate is below the floor, after enough probes. */
|
|
105
|
+
export const capabilityFlagged = (passes, total, floor, minimum = 20) => total >= minimum && wilsonUpper(passes, total) < floor;
|
|
8
106
|
|
|
9
107
|
export function buildTask(kind, params) {
|
|
108
|
+
if (CAPABILITY_KINDS.includes(kind)) return { kind, capability: true, ...capabilityTask(kind, params) };
|
|
10
109
|
if (kind === "multiply") return { kind, question: `What is ${params.a} multiplied by ${params.b}?`, reference: String(BigInt(params.a) * BigInt(params.b)), numeric: true };
|
|
11
110
|
if (kind === "sum") {
|
|
12
111
|
return { kind, question: `What is the sum of ${params.values.join(", ")}?`, reference: String(params.values.reduce((a, b) => a + Number(b), 0)), numeric: true };
|
|
@@ -26,6 +125,7 @@ export function buildTask(kind, params) {
|
|
|
26
125
|
}
|
|
27
126
|
|
|
28
127
|
export function grade(task, answer) {
|
|
128
|
+
if (task.capability) return gradeCapability(task, answer);
|
|
29
129
|
const text = String(answer).trim();
|
|
30
130
|
if (!text) return "unrelated";
|
|
31
131
|
if (task.numeric) {
|
|
@@ -66,5 +166,5 @@ export function verifyProbeEvidence(evidence, deployment) {
|
|
|
66
166
|
}
|
|
67
167
|
const verdict = grade(task, outputText(JSON.parse(response.toString("utf8")).output));
|
|
68
168
|
if (verdict !== evidence.verdict || verdict === "unrelated") throw new Error(`re-grading gives ${verdict}`);
|
|
69
|
-
return { provider: signed.authorization.provider_signer, verdict };
|
|
169
|
+
return { provider: signed.authorization.provider_signer, verdict, capability: Boolean(task.capability) };
|
|
70
170
|
}
|
package/src/reputation.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { encodeCall, keccak, hex } from "./eip712.mjs";
|
|
3
3
|
import { rpcCall } from "./chain.mjs";
|
|
4
4
|
import { evidenceHash } from "./disputes.mjs";
|
|
5
|
-
import { verifyProbeEvidence } from "./probes.mjs";
|
|
5
|
+
import { CAPABILITY_FLOORS, capabilityFlagged, verifyProbeEvidence } from "./probes.mjs";
|
|
6
6
|
|
|
7
7
|
const PROBE_RECORDED = hex(keccak(Buffer.from("ProbeRecorded(address,address,bytes32,bytes32,uint8)")));
|
|
8
8
|
const LOOKBACK_BLOCKS = 50_400n; // about 7 days of 12-second blocks
|
|
@@ -10,6 +10,9 @@ const LOG_CHUNK = 10_000n;
|
|
|
10
10
|
const FRAUD_COOLDOWN = 30 * 86_400;
|
|
11
11
|
const MAX_VERIFIED = 20; // evidence downloads per refresh
|
|
12
12
|
|
|
13
|
+
// Ledger codes: basic probes 1/2, capability probes (is the advertised model really served?) 3/4.
|
|
14
|
+
const VERDICT_CODES = { 1: { verdict: "pass", capability: false }, 2: { verdict: "wrong", capability: false },
|
|
15
|
+
3: { verdict: "pass", capability: true }, 4: { verdict: "wrong", capability: true } };
|
|
13
16
|
const word = (raw, index) => BigInt(`0x${raw.slice(2 + index * 64, 2 + (index + 1) * 64)}`);
|
|
14
17
|
const address = (topic) => `0x${topic.slice(26)}`.toLowerCase();
|
|
15
18
|
|
|
@@ -31,7 +34,7 @@ async function ledgerEvents(network) {
|
|
|
31
34
|
for (const log of await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.probe_ledger, topics: [PROBE_RECORDED],
|
|
32
35
|
fromBlock: `0x${from.toString(16)}`, toBlock: `0x${to.toString(16)}` }])) {
|
|
33
36
|
events.push({ provider: address(log.topics[1]), relay: address(log.topics[2]), key: log.topics[3],
|
|
34
|
-
evidence: `0x${log.data.slice(2, 66)}`,
|
|
37
|
+
evidence: `0x${log.data.slice(2, 66)}`, ...VERDICT_CODES[Number(BigInt(`0x${log.data.slice(66, 130)}`))] });
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
40
|
return events;
|
|
@@ -50,34 +53,50 @@ export async function reputation(consumer, descriptors, relaysByOwner) {
|
|
|
50
53
|
let events = [];
|
|
51
54
|
try { events = await ledgerEvents(network); } catch {}
|
|
52
55
|
const byOwner = {};
|
|
53
|
-
for (const record of Object.values(records))
|
|
56
|
+
for (const record of Object.values(records)) {
|
|
57
|
+
byOwner[record.owner] = { ...record, probes_passed: 0, probes_failed_verified: 0, capability_passed: 0, capability_failed_verified: 0 };
|
|
58
|
+
}
|
|
54
59
|
let verified = 0;
|
|
55
60
|
for (const event of events) {
|
|
56
61
|
const entry = byOwner[event.provider];
|
|
57
|
-
if (!entry) continue;
|
|
58
|
-
if (event.verdict === "pass") { entry.probes_passed += 1; continue; }
|
|
62
|
+
if (!entry || !event.verdict) continue;
|
|
63
|
+
if (event.verdict === "pass") { entry[event.capability ? "capability_passed" : "probes_passed"] += 1; continue; }
|
|
59
64
|
const relay = relaysByOwner[event.relay];
|
|
60
65
|
if (!relay || verified >= MAX_VERIFIED) continue;
|
|
61
66
|
verified += 1;
|
|
62
67
|
try {
|
|
63
68
|
const { status, body } = await consumer.fetchJson(`${relay.url}/v11/evidence/${event.evidence}`,
|
|
64
69
|
{ ca: network.tls_ca, pin: relay.pin, timeoutMs: 8_000 });
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
const checked = status === 200 && evidenceHash(body) === event.evidence ? verifyProbeEvidence(body, consumer.deployment) : null;
|
|
71
|
+
if (checked?.verdict === "wrong" && checked.capability === event.capability) {
|
|
72
|
+
entry[event.capability ? "capability_failed_verified" : "probes_failed_verified"] += 1;
|
|
67
73
|
}
|
|
68
74
|
} catch {}
|
|
69
75
|
}
|
|
70
|
-
return Object.fromEntries(
|
|
76
|
+
return Object.fromEntries(descriptors.filter((d) => records[d.provider_signer]).map((descriptor) => {
|
|
77
|
+
const entry = { ...byOwner[records[descriptor.provider_signer].owner] };
|
|
78
|
+
const tier = Number(descriptor.tier || 0);
|
|
79
|
+
const floor = network.tiers?.[tier]?.capability_floor ?? CAPABILITY_FLOORS[tier];
|
|
80
|
+
const graded = entry.capability_passed + entry.capability_failed_verified;
|
|
81
|
+
// Passes are counted as reported; failures only once re-graded here, so no Relay can frame a Provider.
|
|
82
|
+
entry.downgrade_suspected = floor !== undefined && capabilityFlagged(entry.capability_passed, graded, floor);
|
|
83
|
+
entry.capability_floor = floor ?? null;
|
|
84
|
+
return [descriptor.provider_signer, entry];
|
|
85
|
+
}));
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Lower is better: recent confirmed fraud last, then a suspected model downgrade, then the verified probe
|
|
90
|
+
* failure rate, then reputation, then price.
|
|
91
|
+
*/
|
|
74
92
|
export function rankKey(descriptor, record, now = Math.floor(Date.now() / 1000)) {
|
|
75
93
|
const fraud = record && record.last_fraud_at && now < record.last_fraud_at + FRAUD_COOLDOWN ? 1 : 0;
|
|
94
|
+
const downgrade = record?.downgrade_suspected ? 1 : 0;
|
|
76
95
|
const probes = record ? record.probes_passed + record.probes_failed_verified : 0;
|
|
77
96
|
const failure = record && probes ? Math.round((10 * record.probes_failed_verified) / probes) : 0;
|
|
78
97
|
const volume = record ? -Number(BigInt(record.counted_volume) / 1000n) : 0;
|
|
79
98
|
const price = descriptor.prices.output_per_1k * 1e6 + descriptor.prices.input_per_1k;
|
|
80
|
-
return [fraud, failure, volume, price];
|
|
99
|
+
return [fraud, downgrade, failure, volume, price];
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
export function compareRank(a, b) {
|
package/src/rewards.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// MYCO rewards: every hour's fees mint MYCO for the Consumers who paid them (80%), their Providers (10%,
|
|
2
|
+
// 48 hours later, by success rate), Relays (7%) and keepers (3%). Find, show and claim an account's share.
|
|
3
|
+
import { encodeCall, encodeWords, hex, keccak } from "./eip712.mjs";
|
|
4
|
+
import { rpcCall, sendTransaction } from "./chain.mjs";
|
|
5
|
+
|
|
6
|
+
export const ROLES = ["consumer", "provider", "relay", "bridge"];
|
|
7
|
+
const POINTS = hex(keccak(new TextEncoder().encode("Points(uint64,uint8,address,uint256)")));
|
|
8
|
+
const LOG_CHUNK = 10_000;
|
|
9
|
+
const CLAIM_BATCH = 100;
|
|
10
|
+
|
|
11
|
+
async function word(network, to, signature, pairs = []) {
|
|
12
|
+
return BigInt(await rpcCall(network.rpc_urls, "eth_call", [{ to, data: encodeCall(signature, pairs) }, "latest"]));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** role index -> emission blocks in which the account earned points. */
|
|
16
|
+
export async function earnedBlocks(network, account) {
|
|
17
|
+
const head = Number(BigInt(await rpcCall(network.rpc_urls, "eth_blockNumber", [])));
|
|
18
|
+
const topic = `0x${account.toLowerCase().replace(/^0x/, "").padStart(64, "0")}`;
|
|
19
|
+
const found = new Map();
|
|
20
|
+
for (let start = Number(network.emission_block || 0); start <= head; start += LOG_CHUNK) {
|
|
21
|
+
const logs = await rpcCall(network.rpc_urls, "eth_getLogs", [{ address: network.emission, topics: [POINTS, null, null, topic],
|
|
22
|
+
fromBlock: `0x${start.toString(16)}`, toBlock: `0x${Math.min(head, start + LOG_CHUNK - 1).toString(16)}` }]);
|
|
23
|
+
for (const log of logs) {
|
|
24
|
+
const role = Number(BigInt(log.topics[2]));
|
|
25
|
+
if (!found.has(role)) found.set(role, new Set());
|
|
26
|
+
found.get(role).add(Number(BigInt(log.topics[1])));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return found;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const claimableAt = (network, block, role, account) => word(network, network.emission, "claimable(uint64,uint8,address)",
|
|
33
|
+
[["uint", block], ["uint", role], ["address", account]]);
|
|
34
|
+
|
|
35
|
+
/** MYCO claimable per role (wei strings), blocks still waiting (the current hour, or a Provider's 48 hours) and the balance. */
|
|
36
|
+
export async function rewardsSummary(network, account) {
|
|
37
|
+
if (!network.emission) return null;
|
|
38
|
+
const roles = {};
|
|
39
|
+
for (const [role, blocks] of await earnedBlocks(network, account)) {
|
|
40
|
+
let claimable = 0n; let waiting = 0;
|
|
41
|
+
for (const block of blocks) {
|
|
42
|
+
const amount = await claimableAt(network, block, role, account);
|
|
43
|
+
if (amount > 0n) claimable += amount;
|
|
44
|
+
else if (await word(network, network.emission, "points(uint64,uint8,address)",
|
|
45
|
+
[["uint", block], ["uint", role], ["address", account]]) > 0n) waiting += 1;
|
|
46
|
+
}
|
|
47
|
+
roles[ROLES[role]] = { claimable_wei: claimable.toString(), blocks_waiting: waiting, blocks: blocks.size };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
account, roles,
|
|
51
|
+
myco_balance_wei: network.token ? (await word(network, network.token, "balanceOf(address)", [["address", account]])).toString() : null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function encodeClaim(blocks, role) {
|
|
56
|
+
const selector = encodeCall("claim(uint64[],uint8)", []);
|
|
57
|
+
return selector + encodeWords([["uint", 64], ["uint", role], ["uint", blocks.length], ...blocks.map((b) => ["uint", b])]).toString("hex");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Close the finished hour if nobody has yet, then claim every claimable block in every role. */
|
|
61
|
+
export async function claimRewards(network, ownerPrivate, account) {
|
|
62
|
+
if (!network.emission) throw new Error("this network has no MYCO emission");
|
|
63
|
+
const [open, hasOpen, current] = await Promise.all(["openBlock()", "hasOpen()", "currentBlock()"]
|
|
64
|
+
.map((signature) => word(network, network.emission, signature)));
|
|
65
|
+
if (hasOpen && open < current) await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.emission, data: encodeCall("poke()", []) });
|
|
66
|
+
const claimed = {};
|
|
67
|
+
for (const [role, blocks] of await earnedBlocks(network, account)) {
|
|
68
|
+
const ready = [];
|
|
69
|
+
for (const block of [...blocks].sort((a, b) => a - b)) if (await claimableAt(network, block, role, account) > 0n) ready.push(block);
|
|
70
|
+
for (let i = 0; i < ready.length; i += CLAIM_BATCH) {
|
|
71
|
+
await sendTransaction(network.rpc_urls, ownerPrivate, { to: network.emission, data: encodeClaim(ready.slice(i, i + CLAIM_BATCH), role) });
|
|
72
|
+
}
|
|
73
|
+
if (ready.length) claimed[ROLES[role]] = ready.length;
|
|
74
|
+
}
|
|
75
|
+
return claimed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 18-decimal wei as a short MYCO figure. */
|
|
79
|
+
export function formatMyco(wei) {
|
|
80
|
+
const value = BigInt(wei);
|
|
81
|
+
const whole = value / 10n ** 18n;
|
|
82
|
+
const fraction = (value % 10n ** 18n).toString().padStart(18, "0").slice(0, 2);
|
|
83
|
+
return `${whole.toLocaleString("en-US")}.${fraction}`;
|
|
84
|
+
}
|
package/src/web/console.html
CHANGED
|
@@ -97,6 +97,14 @@
|
|
|
97
97
|
<input id="dep-limit" placeholder="单次上限(tUSDC)" value="2" size="12">
|
|
98
98
|
<button class="primary" id="deposit-btn">存入</button>
|
|
99
99
|
<button class="ghost" id="faucet-btn">领取测试币</button></div>
|
|
100
|
+
<h2>MYCO 奖励</h2>
|
|
101
|
+
<p class="muted">MYCO 总量 10 亿,无预挖,按小时出块、像比特币一样减半产出(首个周期 1 周,之后 2 周、4 周……最长 4 年)。每小时产出的 80% 按当小时实际支付的费用分给 Consumer,花得越多分得越多;这一小时结束后即可领取。</p>
|
|
102
|
+
<div class="cards">
|
|
103
|
+
<div class="card"><div class="label">可领取 MYCO</div><div class="value" id="myco-claimable">—</div></div>
|
|
104
|
+
<div class="card"><div class="label">钱包里的 MYCO</div><div class="value" id="myco-balance">—</div></div>
|
|
105
|
+
<div class="card"><div class="label">等待中的小时</div><div class="value" id="myco-waiting">—</div></div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="row"><button class="primary" id="myco-claim">领取 MYCO</button></div>
|
|
100
108
|
<h2>取回押金</h2>
|
|
101
109
|
<p class="muted" id="withdraw-hint">先申请,等待期结束后再次点击即可到账。</p>
|
|
102
110
|
<div class="row"><input id="wd-amount" placeholder="金额(留空为全部)" size="16"><button class="ghost" id="withdraw-btn">取回</button></div>
|
|
@@ -188,8 +196,18 @@ async function refreshWallet() {
|
|
|
188
196
|
$("withdraw-hint").textContent = pending > 0n
|
|
189
197
|
? (w.withdrawal.available_at * 1000 > Date.now() ? `已申请取回 ${fmt(w.withdrawal.amount)},${new Date(w.withdrawal.available_at * 1000).toLocaleString()} 之后可到账。`
|
|
190
198
|
: `已申请的 ${fmt(w.withdrawal.amount)} 可以到账了,点击“取回”。`) : "先申请,等待期结束后再次点击即可到账。";
|
|
199
|
+
refreshRewards().catch(() => {});
|
|
191
200
|
$("chat-hint").textContent = BigInt(w.deposit) === 0n || !w.grant?.active ? "还没有押金。" : `押金 ${fmt(w.deposit)} tUSDC`;
|
|
192
201
|
}
|
|
202
|
+
const myco = (wei) => (Number(BigInt(wei) / 10n ** 14n) / 1e4).toLocaleString(undefined, { maximumFractionDigits: 4 });
|
|
203
|
+
async function refreshRewards() {
|
|
204
|
+
const { rewards } = await api("/api/rewards");
|
|
205
|
+
if (!rewards) return;
|
|
206
|
+
const roles = Object.values(rewards.roles);
|
|
207
|
+
$("myco-claimable").textContent = myco(roles.reduce((sum, r) => sum + BigInt(r.claimable_wei), 0n));
|
|
208
|
+
$("myco-balance").textContent = myco(rewards.myco_balance_wei ?? 0);
|
|
209
|
+
$("myco-waiting").textContent = roles.reduce((sum, r) => sum + r.blocks_waiting, 0);
|
|
210
|
+
}
|
|
193
211
|
function renderWelcome() {
|
|
194
212
|
const ready = status?.wallet && BigInt(status.wallet.deposit) > 0n && status.wallet.grant?.active;
|
|
195
213
|
$("welcome").style.display = ready ? "none" : "block";
|
|
@@ -257,6 +275,14 @@ $("deposit-btn").onclick = async () => {
|
|
|
257
275
|
toast("押金已存入"); await refreshWallet();
|
|
258
276
|
});
|
|
259
277
|
};
|
|
278
|
+
$("myco-claim").onclick = async () => {
|
|
279
|
+
const answer = await ask("领取 MYCO"); if (!answer) return;
|
|
280
|
+
await busy($("myco-claim"), async () => {
|
|
281
|
+
const { claimed } = await api("/api/rewards/claim", { password: answer.password });
|
|
282
|
+
const hours = Object.values(claimed).reduce((a, b) => a + b, 0);
|
|
283
|
+
toast(hours ? `已领取 ${hours} 个小时的 MYCO` : "暂时没有可领取的 MYCO"); await refreshRewards();
|
|
284
|
+
});
|
|
285
|
+
};
|
|
260
286
|
$("withdraw-btn").onclick = async () => {
|
|
261
287
|
const answer = await ask("取回押金"); if (!answer) return;
|
|
262
288
|
await busy($("withdraw-btn"), async () => {
|
|
@@ -355,13 +381,21 @@ async function refreshNetwork() {
|
|
|
355
381
|
const card = document.createElement("div"); card.className = "card"; card.style.marginBottom = "12px";
|
|
356
382
|
const reputation = (r) => r ? `${fmt(r.counted_volume)} · ${r.counterparties} 个付款方` : "—";
|
|
357
383
|
const probes = (r) => r ? `通过 ${r.probes_passed} · 验证失败 ${r.probes_failed_verified}` : "—";
|
|
384
|
+
// Capability probes: hard tasks the advertised model passes and a cheaper substitute tends to miss.
|
|
385
|
+
const capability = (r) => {
|
|
386
|
+
if (!r) return "—";
|
|
387
|
+
const graded = r.capability_passed + r.capability_failed_verified;
|
|
388
|
+
if (!graded) return `<span class="muted">暂无</span>`;
|
|
389
|
+
const text = `${r.capability_passed}/${graded}`;
|
|
390
|
+
return r.downgrade_suspected ? `<span class="chip disputed" title="通过率在 99% 置信度下低于该档位下限 ${Math.round(r.capability_floor * 100)}%">疑似降级 ${text}</span>` : text;
|
|
391
|
+
};
|
|
358
392
|
const fraud = (r) => r && r.last_fraud_at && Date.now() / 1000 < r.last_fraud_at + 30 * 86400 ? `<span class="chip disputed">近期欺诈</span>` : "无";
|
|
359
393
|
card.innerHTML = `<div class="row" style="margin:0"><strong>${relay.url}</strong><span class="chip ${relay.healthy ? "released" : "disputed"}">${relay.healthy ? "在线" : "离线"}</span>
|
|
360
394
|
<span class="muted mono">签名 ${short(relay.signer)}</span>${relay.pinned ? `<span class="chip">证书指纹已上链</span>` : ""}</div>
|
|
361
|
-
<table style="margin-top:8px"><thead><tr><th>Provider</th><th>模型</th><th>输入</th><th>输出</th><th>最低</th><th>信誉(计入成交)</th><th>探针</th><th>欺诈</th></tr></thead><tbody>${relay.providers.map((p) =>
|
|
395
|
+
<table style="margin-top:8px"><thead><tr><th>Provider</th><th>模型</th><th>输入</th><th>输出</th><th>最低</th><th>信誉(计入成交)</th><th>探针</th><th>能力探针</th><th>欺诈</th></tr></thead><tbody>${relay.providers.map((p) =>
|
|
362
396
|
`<tr><td class="mono">${short(p.signer)}</td><td>${p.models.join(", ")}</td><td>${fmt(p.prices.input_per_1k)}</td><td>${fmt(p.prices.output_per_1k)}</td><td>${fmt(p.prices.minimum_fee)}</td>
|
|
363
|
-
<td>${reputation(p.reputation)}</td><td>${probes(p.reputation)}</td><td>${fraud(p.reputation)}</td></tr>`).join("")
|
|
364
|
-
|| `<tr><td colspan="
|
|
397
|
+
<td>${reputation(p.reputation)}</td><td>${probes(p.reputation)}</td><td>${capability(p.reputation)}</td><td>${fraud(p.reputation)}</td></tr>`).join("")
|
|
398
|
+
|| `<tr><td colspan="9" class="muted">没有已连接的 Provider</td></tr>`}</tbody></table>`;
|
|
365
399
|
$("network").append(card);
|
|
366
400
|
}
|
|
367
401
|
}
|