mycomesh-consumer 0.6.0 → 0.7.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 +3 -0
- package/package.json +1 -1
- package/src/cli.mjs +15 -0
- package/src/console.mjs +9 -0
- package/src/rewards.mjs +84 -0
- package/src/web/console.html +26 -0
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
"registry": "0x0ff3210e8e8abf5f5bfa73a39e60db7416b6c8c7",
|
|
8
8
|
"relay_directory": "0xa0aaad9393f51cb5786c69f152133a174588bdab",
|
|
9
9
|
"probe_ledger": "0x30055eae6645cce8754f834a17689d20a3795ca3",
|
|
10
|
+
"emission": "0x85ee0e8f63400041f0c76010c590c330bc135a8d",
|
|
11
|
+
"token": "0x620d026ad854fb17c7b16534a0f28ef5499e2d4a",
|
|
12
|
+
"emission_block": 11815544,
|
|
10
13
|
"tiers": {
|
|
11
14
|
"1": {
|
|
12
15
|
"name": "OpenAI frontier",
|
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/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 () => {
|