mtok-relay 0.1.8 → 0.1.10
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/dist/mtok-relay.mjs +69 -11
- package/package.json +1 -1
package/dist/mtok-relay.mjs
CHANGED
|
@@ -4833,6 +4833,8 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
|
|
|
4833
4833
|
}
|
|
4834
4834
|
return {
|
|
4835
4835
|
durable,
|
|
4836
|
+
retentionMs,
|
|
4837
|
+
// the window this store is authoritative for; the runtime also uses it as the on-chain draw-age bound (#580).
|
|
4836
4838
|
has(key) {
|
|
4837
4839
|
return map.has(key);
|
|
4838
4840
|
},
|
|
@@ -4930,7 +4932,9 @@ function createOnchainVerifier({
|
|
|
4930
4932
|
// a few times before giving up. Bounded so a genuinely-missing tx still fails fast.
|
|
4931
4933
|
receiptRetries = 3,
|
|
4932
4934
|
receiptRetryMs = 700,
|
|
4933
|
-
sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
4935
|
+
sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
4936
|
+
nowMs = () => Date.now()
|
|
4937
|
+
// injectable clock for the optional draw-age guard (verifyDrawPaid maxPaidAgeMs).
|
|
4934
4938
|
} = {}) {
|
|
4935
4939
|
const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
|
|
4936
4940
|
const configured = Boolean(urls.length && usdcAddress);
|
|
@@ -4992,7 +4996,7 @@ function createOnchainVerifier({
|
|
|
4992
4996
|
for (let i = 0; i < logs.length; i++) {
|
|
4993
4997
|
if (consumed && consumed.has(i)) continue;
|
|
4994
4998
|
const l = logs[i];
|
|
4995
|
-
if (lc(l.address) !== usdc || l.topics?.[0] !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
|
|
4999
|
+
if (lc(l.address) !== usdc || lc(l.topics?.[0]) !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
|
|
4996
5000
|
if (topicToAddress(l.topics[2]) !== want) continue;
|
|
4997
5001
|
if (wantFrom && topicToAddress(l.topics[1]) !== wantFrom) continue;
|
|
4998
5002
|
sawRecipient = true;
|
|
@@ -5029,22 +5033,51 @@ function createOnchainVerifier({
|
|
|
5029
5033
|
requestHash,
|
|
5030
5034
|
sellerWallet,
|
|
5031
5035
|
feeRecipient,
|
|
5032
|
-
minSellerAtomic = 0n
|
|
5036
|
+
minSellerAtomic = 0n,
|
|
5037
|
+
// Optional draw-age guard (#580). A DrawPaid receipt is a PERMANENT chain fact, so a
|
|
5038
|
+
// buyer who holds the request preimage can replay the same /chunk body long after
|
|
5039
|
+
// paying; if the relay's local redemption record was pruned (retention lapse) or lost
|
|
5040
|
+
// (in-memory restart), served.has() misses and a stale payment buys a fresh inference.
|
|
5041
|
+
// When set, refuse a payment whose block is older than maxPaidAgeMs: a legitimate
|
|
5042
|
+
// first-serve or honest retry happens seconds-to-minutes after payDraw, never days.
|
|
5043
|
+
maxPaidAgeMs
|
|
5033
5044
|
} = {}) {
|
|
5034
5045
|
if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
|
|
5035
5046
|
const contract = lc(contractAddress);
|
|
5036
5047
|
if (!contract) return { ok: false, reason: "contract_not_configured" };
|
|
5037
5048
|
const got = await fetchReceipt(txHash);
|
|
5038
5049
|
if (got.error) return { ok: false, reason: got.error };
|
|
5039
|
-
const
|
|
5050
|
+
const drawLogs = (got.receipt.logs || []).filter(
|
|
5040
5051
|
(l) => lc(l.address) === contract && isDrawPaidTopic(l.topics?.[0])
|
|
5041
5052
|
);
|
|
5042
|
-
if (!
|
|
5043
|
-
let event;
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5053
|
+
if (!drawLogs.length) return { ok: false, reason: "no_draw_paid_event" };
|
|
5054
|
+
let event = null;
|
|
5055
|
+
let matchedLog = null;
|
|
5056
|
+
let sawDecodable = false;
|
|
5057
|
+
for (const l of drawLogs) {
|
|
5058
|
+
let e;
|
|
5059
|
+
try {
|
|
5060
|
+
e = decodeDrawPaidLog(l);
|
|
5061
|
+
} catch {
|
|
5062
|
+
continue;
|
|
5063
|
+
}
|
|
5064
|
+
sawDecodable = true;
|
|
5065
|
+
if (bookingId != null && e.bookingId !== String(bookingId)) continue;
|
|
5066
|
+
if (n != null && e.n !== Number(n)) continue;
|
|
5067
|
+
event = e;
|
|
5068
|
+
matchedLog = l;
|
|
5069
|
+
break;
|
|
5070
|
+
}
|
|
5071
|
+
if (!event) {
|
|
5072
|
+
if (!sawDecodable) return { ok: false, reason: "malformed_draw_paid_event" };
|
|
5073
|
+
for (const l of drawLogs) {
|
|
5074
|
+
try {
|
|
5075
|
+
event = decodeDrawPaidLog(l);
|
|
5076
|
+
matchedLog = l;
|
|
5077
|
+
break;
|
|
5078
|
+
} catch {
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5048
5081
|
}
|
|
5049
5082
|
if (buyerAgentId != null && event.buyerAgentId !== String(buyerAgentId)) return { ok: false, reason: "buyer_agent_mismatch" };
|
|
5050
5083
|
if (sellerAgentId != null && event.sellerAgentId !== String(sellerAgentId)) return { ok: false, reason: "seller_agent_mismatch" };
|
|
@@ -5054,6 +5087,26 @@ function createOnchainVerifier({
|
|
|
5054
5087
|
if (n != null && event.n !== Number(n)) return { ok: false, reason: "draw_n_mismatch" };
|
|
5055
5088
|
if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
|
|
5056
5089
|
if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
|
|
5090
|
+
const maxAge = Number(maxPaidAgeMs);
|
|
5091
|
+
if (Number.isFinite(maxAge) && maxAge > 0) {
|
|
5092
|
+
const bn = matchedLog?.blockNumber;
|
|
5093
|
+
let paidAtMs = null;
|
|
5094
|
+
if (bn != null) {
|
|
5095
|
+
const blockTag = typeof bn === "string" && bn.startsWith("0x") ? bn : "0x" + BigInt(bn).toString(16);
|
|
5096
|
+
for (let i = 0; i <= receiptRetries; i++) {
|
|
5097
|
+
try {
|
|
5098
|
+
const block = await rpc("eth_getBlockByNumber", [blockTag, false]);
|
|
5099
|
+
if (block?.timestamp != null) {
|
|
5100
|
+
paidAtMs = Number(BigInt(block.timestamp)) * 1e3;
|
|
5101
|
+
break;
|
|
5102
|
+
}
|
|
5103
|
+
} catch {
|
|
5104
|
+
}
|
|
5105
|
+
if (i < receiptRetries) await sleepImpl(receiptRetryMs);
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
if (paidAtMs != null && nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
|
|
5109
|
+
}
|
|
5057
5110
|
const consumed = /* @__PURE__ */ new Set();
|
|
5058
5111
|
let sellerTransfer = null;
|
|
5059
5112
|
if (sellerWallet) {
|
|
@@ -5169,7 +5222,12 @@ async function createRelayRuntime(config) {
|
|
|
5169
5222
|
n,
|
|
5170
5223
|
requestHash,
|
|
5171
5224
|
sellerWallet: config.settlementAddr,
|
|
5172
|
-
feeRecipient: platform.feeAddress
|
|
5225
|
+
feeRecipient: platform.feeAddress,
|
|
5226
|
+
// #580: refuse a payment older than the redemption window. served.has() is the
|
|
5227
|
+
// primary one-serve guard, but it's pruned by age and lost on an in-memory restart;
|
|
5228
|
+
// this on-chain age bound closes the re-serve hole those cases open (a stale replay
|
|
5229
|
+
// buying a fresh inference). An honest retry is seconds-to-minutes old, never days.
|
|
5230
|
+
maxPaidAgeMs: served.retentionMs
|
|
5173
5231
|
});
|
|
5174
5232
|
} catch (e) {
|
|
5175
5233
|
return send(res, 402, { error: "payment_unverified", detail: e.message });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtok-relay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control. Run with: npx mtok-relay --offer <id> --model <id> --upstream <url>.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|