mtok-relay 0.1.2 → 0.1.4
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 +582 -275
- package/package.json +3 -3
package/dist/mtok-relay.mjs
CHANGED
|
@@ -1,32 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// GENERATED from mtok-relay.mjs — do not edit by hand.
|
|
3
3
|
|
|
4
|
-
// mtok-relay.mjs
|
|
5
|
-
import http from "node:http";
|
|
6
|
-
|
|
7
|
-
// lib.mjs
|
|
8
|
-
function enforceModelEcho(upstreamModel, offerModel) {
|
|
9
|
-
if (String(upstreamModel) !== String(offerModel))
|
|
10
|
-
throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
|
|
11
|
-
}
|
|
12
|
-
var buildFundReport = ({ offerId: offerId2, buyerId, bookingId, n, priceUsd, sellerTxHash, feeTxHash }) => ({
|
|
13
|
-
offerId: offerId2,
|
|
14
|
-
buyerId,
|
|
15
|
-
...bookingId ? { bookingId } : {},
|
|
16
|
-
n,
|
|
17
|
-
priceUsd,
|
|
18
|
-
sellerTxHash,
|
|
19
|
-
feeTxHash
|
|
20
|
-
});
|
|
21
|
-
var buildDrawReport = ({ offerId: offerId2, buyerId, bookingId, n, usage }) => ({
|
|
22
|
-
offerId: offerId2,
|
|
23
|
-
buyerId,
|
|
24
|
-
bookingId,
|
|
25
|
-
n,
|
|
26
|
-
inputTokens: usage?.prompt_tokens ?? 0,
|
|
27
|
-
outputTokens: usage?.completion_tokens ?? 0
|
|
28
|
-
});
|
|
29
|
-
|
|
30
4
|
// node_modules/@noble/hashes/esm/cryptoNode.js
|
|
31
5
|
import * as nc from "node:crypto";
|
|
32
6
|
var crypto = nc && typeof nc === "object" && "webcrypto" in nc ? nc.webcrypto : nc && typeof nc === "object" && "randomBytes" in nc ? nc : void 0;
|
|
@@ -346,11 +320,11 @@ function validateObject(object, validators, optValidators = {}) {
|
|
|
346
320
|
}
|
|
347
321
|
function memoized(fn) {
|
|
348
322
|
const map = /* @__PURE__ */ new WeakMap();
|
|
349
|
-
return (arg, ...
|
|
323
|
+
return (arg, ...args) => {
|
|
350
324
|
const val = map.get(arg);
|
|
351
325
|
if (val !== void 0)
|
|
352
326
|
return val;
|
|
353
|
-
const computed = fn(arg, ...
|
|
327
|
+
const computed = fn(arg, ...args);
|
|
354
328
|
map.set(arg, computed);
|
|
355
329
|
return computed;
|
|
356
330
|
};
|
|
@@ -2183,7 +2157,7 @@ var secp256k1 = createCurve({
|
|
|
2183
2157
|
}, sha256);
|
|
2184
2158
|
|
|
2185
2159
|
// node_modules/viem/_esm/errors/version.js
|
|
2186
|
-
var version = "2.
|
|
2160
|
+
var version = "2.54.4";
|
|
2187
2161
|
|
|
2188
2162
|
// node_modules/viem/_esm/errors/base.js
|
|
2189
2163
|
var errorConfig = {
|
|
@@ -2191,29 +2165,29 @@ var errorConfig = {
|
|
|
2191
2165
|
version: `viem@${version}`
|
|
2192
2166
|
};
|
|
2193
2167
|
var BaseError = class _BaseError extends Error {
|
|
2194
|
-
constructor(shortMessage,
|
|
2168
|
+
constructor(shortMessage, args = {}) {
|
|
2195
2169
|
const details = (() => {
|
|
2196
|
-
if (
|
|
2197
|
-
return
|
|
2198
|
-
if (
|
|
2199
|
-
return
|
|
2200
|
-
return
|
|
2170
|
+
if (args.cause instanceof _BaseError)
|
|
2171
|
+
return args.cause.details;
|
|
2172
|
+
if (args.cause?.message)
|
|
2173
|
+
return args.cause.message;
|
|
2174
|
+
return args.details;
|
|
2201
2175
|
})();
|
|
2202
2176
|
const docsPath = (() => {
|
|
2203
|
-
if (
|
|
2204
|
-
return
|
|
2205
|
-
return
|
|
2177
|
+
if (args.cause instanceof _BaseError)
|
|
2178
|
+
return args.cause.docsPath || args.docsPath;
|
|
2179
|
+
return args.docsPath;
|
|
2206
2180
|
})();
|
|
2207
|
-
const docsUrl = errorConfig.getDocsUrl?.({ ...
|
|
2181
|
+
const docsUrl = errorConfig.getDocsUrl?.({ ...args, docsPath });
|
|
2208
2182
|
const message = [
|
|
2209
2183
|
shortMessage || "An error occurred.",
|
|
2210
2184
|
"",
|
|
2211
|
-
...
|
|
2185
|
+
...args.metaMessages ? [...args.metaMessages, ""] : [],
|
|
2212
2186
|
...docsUrl ? [`Docs: ${docsUrl}`] : [],
|
|
2213
2187
|
...details ? [`Details: ${details}`] : [],
|
|
2214
2188
|
...errorConfig.version ? [`Version: ${errorConfig.version}`] : []
|
|
2215
2189
|
].join("\n");
|
|
2216
|
-
super(message,
|
|
2190
|
+
super(message, args.cause ? { cause: args.cause } : void 0);
|
|
2217
2191
|
Object.defineProperty(this, "details", {
|
|
2218
2192
|
enumerable: true,
|
|
2219
2193
|
configurable: true,
|
|
@@ -2252,8 +2226,8 @@ var BaseError = class _BaseError extends Error {
|
|
|
2252
2226
|
});
|
|
2253
2227
|
this.details = details;
|
|
2254
2228
|
this.docsPath = docsPath;
|
|
2255
|
-
this.metaMessages =
|
|
2256
|
-
this.name =
|
|
2229
|
+
this.metaMessages = args.metaMessages;
|
|
2230
|
+
this.name = args.name ?? this.name;
|
|
2257
2231
|
this.shortMessage = shortMessage;
|
|
2258
2232
|
this.version = version;
|
|
2259
2233
|
}
|
|
@@ -3268,8 +3242,8 @@ function formatGwei(wei, unit = "wei") {
|
|
|
3268
3242
|
}
|
|
3269
3243
|
|
|
3270
3244
|
// node_modules/viem/_esm/errors/transaction.js
|
|
3271
|
-
function prettyPrint(
|
|
3272
|
-
const entries = Object.entries(
|
|
3245
|
+
function prettyPrint(args) {
|
|
3246
|
+
const entries = Object.entries(args).map(([key, value]) => {
|
|
3273
3247
|
if (value === void 0 || value === false)
|
|
3274
3248
|
return null;
|
|
3275
3249
|
return [key, value];
|
|
@@ -4193,10 +4167,7 @@ function encodeAbiParameters(params, values) {
|
|
|
4193
4167
|
params,
|
|
4194
4168
|
values
|
|
4195
4169
|
});
|
|
4196
|
-
|
|
4197
|
-
if (data.length === 0)
|
|
4198
|
-
return "0x";
|
|
4199
|
-
return data;
|
|
4170
|
+
return encodeParams(preparedParams);
|
|
4200
4171
|
}
|
|
4201
4172
|
function prepareParams({ params, values }) {
|
|
4202
4173
|
const preparedParams = [];
|
|
@@ -4262,7 +4233,7 @@ function encodeParams(preparedParams) {
|
|
|
4262
4233
|
staticParams.push(encoded);
|
|
4263
4234
|
}
|
|
4264
4235
|
}
|
|
4265
|
-
return
|
|
4236
|
+
return concatHex([...staticParams, ...dynamicParams]);
|
|
4266
4237
|
}
|
|
4267
4238
|
function encodeAddress(value) {
|
|
4268
4239
|
if (!isAddress(value))
|
|
@@ -4279,7 +4250,7 @@ function encodeArray(value, { length, param }) {
|
|
|
4279
4250
|
givenLength: value.length,
|
|
4280
4251
|
type: `${param.type}[${length}]`
|
|
4281
4252
|
});
|
|
4282
|
-
let dynamicChild =
|
|
4253
|
+
let dynamicChild = value.length === 0 && isDynamicType(param);
|
|
4283
4254
|
const preparedParams = [];
|
|
4284
4255
|
for (let i = 0; i < value.length; i++) {
|
|
4285
4256
|
const preparedParam = prepareParam({ param, value: value[i] });
|
|
@@ -4293,7 +4264,7 @@ function encodeArray(value, { length, param }) {
|
|
|
4293
4264
|
const length2 = numberToHex(preparedParams.length, { size: 32 });
|
|
4294
4265
|
return {
|
|
4295
4266
|
dynamic: true,
|
|
4296
|
-
encoded:
|
|
4267
|
+
encoded: concatHex([length2, data])
|
|
4297
4268
|
};
|
|
4298
4269
|
}
|
|
4299
4270
|
if (dynamicChild)
|
|
@@ -4301,7 +4272,7 @@ function encodeArray(value, { length, param }) {
|
|
|
4301
4272
|
}
|
|
4302
4273
|
return {
|
|
4303
4274
|
dynamic: false,
|
|
4304
|
-
encoded:
|
|
4275
|
+
encoded: concatHex(preparedParams.map(({ encoded }) => encoded))
|
|
4305
4276
|
};
|
|
4306
4277
|
}
|
|
4307
4278
|
function encodeBytes(value, { param }) {
|
|
@@ -4316,7 +4287,10 @@ function encodeBytes(value, { param }) {
|
|
|
4316
4287
|
});
|
|
4317
4288
|
return {
|
|
4318
4289
|
dynamic: true,
|
|
4319
|
-
encoded:
|
|
4290
|
+
encoded: concatHex([
|
|
4291
|
+
padHex(numberToHex(bytesSize, { size: 32 })),
|
|
4292
|
+
value_
|
|
4293
|
+
])
|
|
4320
4294
|
};
|
|
4321
4295
|
}
|
|
4322
4296
|
if (bytesSize !== Number.parseInt(paramSize, 10))
|
|
@@ -4363,7 +4337,7 @@ function encodeString(value) {
|
|
|
4363
4337
|
}
|
|
4364
4338
|
return {
|
|
4365
4339
|
dynamic: true,
|
|
4366
|
-
encoded:
|
|
4340
|
+
encoded: concatHex([
|
|
4367
4341
|
padHex(numberToHex(size(hexValue), { size: 32 })),
|
|
4368
4342
|
...parts
|
|
4369
4343
|
])
|
|
@@ -4385,7 +4359,7 @@ function encodeTuple(value, { param }) {
|
|
|
4385
4359
|
}
|
|
4386
4360
|
return {
|
|
4387
4361
|
dynamic,
|
|
4388
|
-
encoded: dynamic ? encodeParams(preparedParams) :
|
|
4362
|
+
encoded: dynamic ? encodeParams(preparedParams) : concatHex(preparedParams.map(({ encoded }) => encoded))
|
|
4389
4363
|
};
|
|
4390
4364
|
}
|
|
4391
4365
|
function getArrayComponents(type) {
|
|
@@ -4395,6 +4369,21 @@ function getArrayComponents(type) {
|
|
|
4395
4369
|
[matches[2] ? Number(matches[2]) : null, matches[1]]
|
|
4396
4370
|
) : void 0;
|
|
4397
4371
|
}
|
|
4372
|
+
function isDynamicType(param) {
|
|
4373
|
+
const { type } = param;
|
|
4374
|
+
if (type === "string")
|
|
4375
|
+
return true;
|
|
4376
|
+
if (type === "bytes")
|
|
4377
|
+
return true;
|
|
4378
|
+
if (type.endsWith("[]"))
|
|
4379
|
+
return true;
|
|
4380
|
+
if (type === "tuple")
|
|
4381
|
+
return param.components.some(isDynamicType);
|
|
4382
|
+
const arrayComponents = getArrayComponents(type);
|
|
4383
|
+
if (arrayComponents)
|
|
4384
|
+
return isDynamicType({ ...param, type: arrayComponents[1] });
|
|
4385
|
+
return false;
|
|
4386
|
+
}
|
|
4398
4387
|
|
|
4399
4388
|
// node_modules/viem/_esm/utils/stringify.js
|
|
4400
4389
|
var stringify = (value, replacer, space) => JSON.stringify(value, (key, value_) => {
|
|
@@ -4643,6 +4632,225 @@ function privateKeyToAccount(privateKey, options = {}) {
|
|
|
4643
4632
|
};
|
|
4644
4633
|
}
|
|
4645
4634
|
|
|
4635
|
+
// src/config.mjs
|
|
4636
|
+
var flag = (args, name) => {
|
|
4637
|
+
const i = args.indexOf(name);
|
|
4638
|
+
return i !== -1 ? args[i + 1] : null;
|
|
4639
|
+
};
|
|
4640
|
+
function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {}) {
|
|
4641
|
+
const offerId = flag(argv, "--offer");
|
|
4642
|
+
const model = flag(argv, "--model");
|
|
4643
|
+
const upstream = (flag(argv, "--upstream") ?? "").replace(/\/$/, "");
|
|
4644
|
+
const apiBase = (flag(argv, "--api") ?? "https://mtok.market").replace(/\/$/, "");
|
|
4645
|
+
const port = Number(flag(argv, "--port") ?? 8788);
|
|
4646
|
+
const rpcFlag = flag(argv, "--rpc");
|
|
4647
|
+
const settlementPubkeyFlag = flag(argv, "--settlement-pubkey");
|
|
4648
|
+
const sellerAgentId = flag(argv, "--seller-agent");
|
|
4649
|
+
const outPrice = Number(flag(argv, "--out-price") ?? 0);
|
|
4650
|
+
const inPrice = Number(flag(argv, "--in-price") ?? outPrice);
|
|
4651
|
+
const redemptionFile = flag(argv, "--redemption-file") ?? env.RELAY_REDEMPTION_FILE ?? null;
|
|
4652
|
+
const denylistRaw = flag(argv, "--payer-denylist") ?? env.RELAY_PAYER_DENYLIST ?? "";
|
|
4653
|
+
const payerDenylist = String(denylistRaw).split(",").map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
4654
|
+
if (!offerId) throw new Error("--offer <id> is required");
|
|
4655
|
+
if (!model) throw new Error("--model <id> is required (the offer model you serve)");
|
|
4656
|
+
if (!upstream) throw new Error("--upstream <url> is required");
|
|
4657
|
+
const mtokApiKey = env.MTOK_API_KEY;
|
|
4658
|
+
const upstreamKey = env.UPSTREAM_KEY;
|
|
4659
|
+
const relayWalletKey = env.RELAY_WALLET_KEY;
|
|
4660
|
+
if (!upstreamKey) throw new Error("UPSTREAM_KEY env var is required");
|
|
4661
|
+
if (!relayWalletKey && !settlementPubkeyFlag) {
|
|
4662
|
+
throw new Error("RELAY_WALLET_KEY env var (or --settlement-pubkey) is required");
|
|
4663
|
+
}
|
|
4664
|
+
let settlementAddr = settlementPubkeyFlag;
|
|
4665
|
+
if (!settlementAddr) {
|
|
4666
|
+
try {
|
|
4667
|
+
settlementAddr = privateKeyToAccount(relayWalletKey.startsWith("0x") ? relayWalletKey : "0x" + relayWalletKey).address;
|
|
4668
|
+
} catch (e) {
|
|
4669
|
+
throw new Error(`invalid RELAY_WALLET_KEY: ${e.message}`);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
return {
|
|
4673
|
+
offerId,
|
|
4674
|
+
model,
|
|
4675
|
+
sellerAgentId,
|
|
4676
|
+
upstream,
|
|
4677
|
+
apiBase,
|
|
4678
|
+
port,
|
|
4679
|
+
rpcFlag,
|
|
4680
|
+
outPrice,
|
|
4681
|
+
inPrice,
|
|
4682
|
+
redemptionFile,
|
|
4683
|
+
mtokApiKey,
|
|
4684
|
+
upstreamKey,
|
|
4685
|
+
settlementAddr,
|
|
4686
|
+
payerDenylist
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
// src/http.mjs
|
|
4691
|
+
import http from "node:http";
|
|
4692
|
+
var MAX_BODY_BYTES = 256e3;
|
|
4693
|
+
function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
|
|
4694
|
+
return new Promise((resolve, reject) => {
|
|
4695
|
+
const chunks = [];
|
|
4696
|
+
let bytes = 0;
|
|
4697
|
+
let settled = false;
|
|
4698
|
+
req.on("data", (d) => {
|
|
4699
|
+
bytes += d.length;
|
|
4700
|
+
if (bytes > maxBytes && !settled) {
|
|
4701
|
+
settled = true;
|
|
4702
|
+
reject(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
|
|
4703
|
+
req.resume();
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
if (settled) return;
|
|
4707
|
+
chunks.push(d);
|
|
4708
|
+
});
|
|
4709
|
+
req.on("end", () => {
|
|
4710
|
+
if (settled) return;
|
|
4711
|
+
settled = true;
|
|
4712
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
4713
|
+
try {
|
|
4714
|
+
resolve(JSON.parse(raw || "{}"));
|
|
4715
|
+
} catch {
|
|
4716
|
+
resolve({});
|
|
4717
|
+
}
|
|
4718
|
+
});
|
|
4719
|
+
req.on("error", (e) => {
|
|
4720
|
+
if (settled) return;
|
|
4721
|
+
settled = true;
|
|
4722
|
+
reject(e);
|
|
4723
|
+
});
|
|
4724
|
+
});
|
|
4725
|
+
}
|
|
4726
|
+
function send(res, status, body) {
|
|
4727
|
+
const payload = JSON.stringify(body);
|
|
4728
|
+
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
|
|
4729
|
+
res.end(payload);
|
|
4730
|
+
}
|
|
4731
|
+
function startRelayServer({ config, handleDraw }) {
|
|
4732
|
+
const windows = /* @__PURE__ */ new Map();
|
|
4733
|
+
const windowMs = 6e4;
|
|
4734
|
+
const maxPerMinute = Number(config.maxRequestsPerMinute ?? 120);
|
|
4735
|
+
const checkRate = (req) => {
|
|
4736
|
+
if (!Number.isFinite(maxPerMinute)) return true;
|
|
4737
|
+
const key = req.socket.remoteAddress || "unknown";
|
|
4738
|
+
const now = Date.now();
|
|
4739
|
+
let w = windows.get(key);
|
|
4740
|
+
if (!w || now - w.start >= windowMs) {
|
|
4741
|
+
w = { start: now, count: 0 };
|
|
4742
|
+
windows.set(key, w);
|
|
4743
|
+
if (windows.size > 1e4) {
|
|
4744
|
+
for (const [k, v] of windows) if (now - v.start >= windowMs) windows.delete(k);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
w.count += 1;
|
|
4748
|
+
return w.count <= maxPerMinute;
|
|
4749
|
+
};
|
|
4750
|
+
const server = http.createServer(async (req, res) => {
|
|
4751
|
+
if (req.method !== "POST" || req.url !== "/chunk") {
|
|
4752
|
+
return send(res, 404, { error: "not found" });
|
|
4753
|
+
}
|
|
4754
|
+
if (!checkRate(req)) return send(res, 429, { error: "rate_limited" });
|
|
4755
|
+
let body;
|
|
4756
|
+
try {
|
|
4757
|
+
body = await readBody(req);
|
|
4758
|
+
} catch (e) {
|
|
4759
|
+
if (e?.code === "body_too_large") return send(res, 413, { error: "body_too_large" });
|
|
4760
|
+
return send(res, 400, { error: "bad body" });
|
|
4761
|
+
}
|
|
4762
|
+
if (body.request == null) {
|
|
4763
|
+
return send(res, 400, { error: "bad_request", detail: "need a DRAW (request); the legacy FUND lane is retired, pay per draw on-chain" });
|
|
4764
|
+
}
|
|
4765
|
+
return handleDraw(body, res);
|
|
4766
|
+
});
|
|
4767
|
+
server.listen(config.port, () => {
|
|
4768
|
+
console.log(`mtok-relay: listening on port ${config.port} offer=${config.offerId} model=${config.model} upstream=${config.upstream} api=${config.apiBase} settlement=${config.settlementAddr}`);
|
|
4769
|
+
});
|
|
4770
|
+
return server;
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4773
|
+
// lib.mjs
|
|
4774
|
+
function enforceModelEcho(upstreamModel, offerModel) {
|
|
4775
|
+
if (String(upstreamModel) !== String(offerModel))
|
|
4776
|
+
throw new Error(`model mismatch: upstream ${upstreamModel} != offer ${offerModel}`);
|
|
4777
|
+
}
|
|
4778
|
+
function configuredFeeAtomic({ sellerUsdAtomic, feeAddress, feeBps }) {
|
|
4779
|
+
const bps = BigInt(Math.trunc(Math.max(0, Number(feeBps) || 0)));
|
|
4780
|
+
if (!feeAddress || bps === 0n) return 0n;
|
|
4781
|
+
return (BigInt(sellerUsdAtomic || 0) * bps + 5000n) / 10000n;
|
|
4782
|
+
}
|
|
4783
|
+
var CHARS_PER_TOKEN_EST = 3.2;
|
|
4784
|
+
function estimateInputTokens(messages) {
|
|
4785
|
+
let chars = 0;
|
|
4786
|
+
for (const m of messages ?? []) {
|
|
4787
|
+
const c = m?.content;
|
|
4788
|
+
if (typeof c === "string") chars += c.length;
|
|
4789
|
+
else if (Array.isArray(c)) for (const part of c) chars += String(part?.text ?? "").length;
|
|
4790
|
+
}
|
|
4791
|
+
return Math.ceil(chars / CHARS_PER_TOKEN_EST);
|
|
4792
|
+
}
|
|
4793
|
+
function boundServe({ messages, budgetUsd, inPrice, outPrice, reqMax, contextCeil = 4096 }) {
|
|
4794
|
+
const estIn = estimateInputTokens(messages);
|
|
4795
|
+
const estInCostUsd = estIn * (Number(inPrice) > 0 ? Number(inPrice) : 0) / 1e6;
|
|
4796
|
+
if (estInCostUsd >= budgetUsd) return { refuse: true, estIn, estInCostUsd };
|
|
4797
|
+
const outBudgetUsd = budgetUsd - estInCostUsd;
|
|
4798
|
+
let maxTok = contextCeil;
|
|
4799
|
+
if (Number(reqMax) > 0) maxTok = Math.min(maxTok, Math.floor(Number(reqMax)));
|
|
4800
|
+
if (Number(outPrice) > 0) maxTok = Math.min(maxTok, Math.floor(outBudgetUsd / Number(outPrice) * 1e6));
|
|
4801
|
+
return { refuse: false, maxTok: Math.max(1, maxTok), estIn, estInCostUsd };
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
// src/redemption.mjs
|
|
4805
|
+
import fs from "node:fs";
|
|
4806
|
+
var DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
4807
|
+
function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS, now = () => Date.now(), log = console } = {}) {
|
|
4808
|
+
const map = /* @__PURE__ */ new Map();
|
|
4809
|
+
let durable = false;
|
|
4810
|
+
if (file) {
|
|
4811
|
+
try {
|
|
4812
|
+
if (fs.existsSync(file)) {
|
|
4813
|
+
const cutoff = now() - retentionMs;
|
|
4814
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
4815
|
+
if (!line.trim()) continue;
|
|
4816
|
+
try {
|
|
4817
|
+
const { k, at, payload } = JSON.parse(line);
|
|
4818
|
+
if (k && Number(at) >= cutoff) map.set(k, { payload, at: Number(at) });
|
|
4819
|
+
} catch {
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
}
|
|
4823
|
+
fs.writeFileSync(file, [...map].map(([k, e]) => JSON.stringify({ k, at: e.at, payload: e.payload })).join("\n") + (map.size ? "\n" : ""));
|
|
4824
|
+
durable = true;
|
|
4825
|
+
log.log?.(`mtok-relay: durable redemption at ${file} (${map.size} entries loaded)`);
|
|
4826
|
+
} catch (e) {
|
|
4827
|
+
log.warn?.(`mtok-relay: redemption file ${file} not writable (${e.message}); redemption is IN-MEMORY ONLY, a restart can re-serve a paid draw (#495). Point --redemption-file at a durable path.`);
|
|
4828
|
+
durable = false;
|
|
4829
|
+
}
|
|
4830
|
+
} else {
|
|
4831
|
+
log.warn?.("mtok-relay: no --redemption-file, redemption is IN-MEMORY ONLY, a restart or long idle can re-serve one payment for a fresh inference (#495). Set --redemption-file to a durable path.");
|
|
4832
|
+
}
|
|
4833
|
+
return {
|
|
4834
|
+
durable,
|
|
4835
|
+
has(key) {
|
|
4836
|
+
return map.has(key);
|
|
4837
|
+
},
|
|
4838
|
+
get(key) {
|
|
4839
|
+
return map.get(key)?.payload;
|
|
4840
|
+
},
|
|
4841
|
+
set(key, payload) {
|
|
4842
|
+
const at = now();
|
|
4843
|
+
map.set(key, { payload, at });
|
|
4844
|
+
if (durable) {
|
|
4845
|
+
try {
|
|
4846
|
+
fs.appendFileSync(file, JSON.stringify({ k: key, at, payload }) + "\n");
|
|
4847
|
+
} catch {
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
};
|
|
4852
|
+
}
|
|
4853
|
+
|
|
4646
4854
|
// core/errors.js
|
|
4647
4855
|
function apiError(status, code, message, details) {
|
|
4648
4856
|
const err = new Error(message);
|
|
@@ -4654,12 +4862,66 @@ function apiError(status, code, message, details) {
|
|
|
4654
4862
|
|
|
4655
4863
|
// core/onchain.js
|
|
4656
4864
|
var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
4865
|
+
var DRAW_PAID_TOPIC = "0xb0243f80521d0dccd159389597aba96047e60ba5d7a9df12b67e5cb75230ac41";
|
|
4866
|
+
var DRAW_PAID_TOPIC_V2 = "0x94f9e7578a5c019f78ad332dcb2dc5563bcf257d777009ff5cef4118f084a70b";
|
|
4867
|
+
var DRAW_PAID_TOPICS = [DRAW_PAID_TOPIC, DRAW_PAID_TOPIC_V2];
|
|
4868
|
+
var isDrawPaidTopic = (topic) => DRAW_PAID_TOPICS.includes(String(topic || "").toLowerCase());
|
|
4657
4869
|
var topicToAddress = (topic) => "0x" + String(topic).slice(-40).toLowerCase();
|
|
4658
4870
|
var lc = (a) => String(a || "").toLowerCase();
|
|
4871
|
+
var strip0x = (v) => String(v || "").replace(/^0x/i, "");
|
|
4872
|
+
var wordAt = (hex, i) => strip0x(hex).slice(i * 64, i * 64 + 64);
|
|
4873
|
+
var uintWord = (hex, i) => BigInt("0x" + (wordAt(hex, i) || "0"));
|
|
4874
|
+
var asciiFromHex = (hex) => {
|
|
4875
|
+
const bytes = strip0x(hex);
|
|
4876
|
+
const out = [];
|
|
4877
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
|
4878
|
+
const b = parseInt(bytes.slice(i, i + 2), 16);
|
|
4879
|
+
if (!Number.isFinite(b)) break;
|
|
4880
|
+
out.push(b);
|
|
4881
|
+
}
|
|
4882
|
+
return new TextDecoder().decode(new Uint8Array(out));
|
|
4883
|
+
};
|
|
4884
|
+
function decodeAbiString(dataHex, offsetBytes) {
|
|
4885
|
+
const body = strip0x(dataHex);
|
|
4886
|
+
const start = Number(offsetBytes) * 2;
|
|
4887
|
+
if (!Number.isFinite(start) || start < 0 || start + 64 > body.length) throw new Error("bad_string_offset");
|
|
4888
|
+
const len = Number(BigInt("0x" + body.slice(start, start + 64)));
|
|
4889
|
+
const textStart = start + 64;
|
|
4890
|
+
const textEnd = textStart + len * 2;
|
|
4891
|
+
if (!Number.isFinite(len) || len < 0 || textEnd > body.length) throw new Error("bad_string_length");
|
|
4892
|
+
return asciiFromHex(body.slice(textStart, textEnd));
|
|
4893
|
+
}
|
|
4894
|
+
function decodeDrawPaidLog(log) {
|
|
4895
|
+
const data = log?.data || "0x";
|
|
4896
|
+
const isV2 = String(log?.topics?.[0] || "").toLowerCase() === DRAW_PAID_TOPIC_V2;
|
|
4897
|
+
if (strip0x(data).length < (isV2 ? 13 : 11) * 64) throw new Error("short_draw_paid_data");
|
|
4898
|
+
const out = {
|
|
4899
|
+
drawId: log?.topics?.[1],
|
|
4900
|
+
sellerAgentKey: log?.topics?.[2],
|
|
4901
|
+
buyerAgentKey: log?.topics?.[3],
|
|
4902
|
+
sellerAgentId: decodeAbiString(data, uintWord(data, 0)),
|
|
4903
|
+
buyerAgentId: decodeAbiString(data, uintWord(data, 1)),
|
|
4904
|
+
bookingId: decodeAbiString(data, uintWord(data, 2)),
|
|
4905
|
+
offerId: decodeAbiString(data, uintWord(data, 3)),
|
|
4906
|
+
model: decodeAbiString(data, uintWord(data, 4)),
|
|
4907
|
+
n: Number(uintWord(data, 5)),
|
|
4908
|
+
sellerUsdAtomic: uintWord(data, 6).toString(),
|
|
4909
|
+
feeUsdAtomic: uintWord(data, 7).toString(),
|
|
4910
|
+
inputPricePerMTokAtomic: uintWord(data, 8).toString(),
|
|
4911
|
+
outputPricePerMTokAtomic: uintWord(data, 9).toString(),
|
|
4912
|
+
requestHash: "0x" + wordAt(data, 10)
|
|
4913
|
+
};
|
|
4914
|
+
if (isV2) {
|
|
4915
|
+
out.seller = topicToAddress(wordAt(data, 11));
|
|
4916
|
+
out.buyer = topicToAddress(wordAt(data, 12));
|
|
4917
|
+
}
|
|
4918
|
+
return out;
|
|
4919
|
+
}
|
|
4659
4920
|
function createOnchainVerifier({
|
|
4660
4921
|
rpcUrl,
|
|
4661
4922
|
rpcUrls,
|
|
4662
4923
|
usdcAddress,
|
|
4924
|
+
expectedChainId,
|
|
4663
4925
|
fetchImpl = globalThis.fetch,
|
|
4664
4926
|
// A just-submitted payment can be mined on the payer's RPC yet not YET indexed by
|
|
4665
4927
|
// ours (cross-RPC propagation lag), so a single receipt lookup returns null and the
|
|
@@ -4672,251 +4934,296 @@ function createOnchainVerifier({
|
|
|
4672
4934
|
const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
|
|
4673
4935
|
const configured = Boolean(urls.length && usdcAddress);
|
|
4674
4936
|
const usdc = lc(usdcAddress);
|
|
4937
|
+
async function rpcOn(url, method, params) {
|
|
4938
|
+
const res = await fetchImpl(url, {
|
|
4939
|
+
method: "POST",
|
|
4940
|
+
headers: { "content-type": "application/json" },
|
|
4941
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
4942
|
+
});
|
|
4943
|
+
if (!res.ok) throw apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
|
|
4944
|
+
const body = await res.json();
|
|
4945
|
+
if (body.error) throw apiError(502, "rpc_error", `chain RPC: ${body.error.message ?? "unknown"}`);
|
|
4946
|
+
return body.result;
|
|
4947
|
+
}
|
|
4675
4948
|
async function rpc(method, params) {
|
|
4676
4949
|
let lastErr;
|
|
4677
|
-
|
|
4950
|
+
const pool = chainPinned && chainOkUrls.size ? urls.filter((u) => chainOkUrls.has(u)) : urls;
|
|
4951
|
+
for (const url of pool) {
|
|
4678
4952
|
try {
|
|
4679
|
-
|
|
4680
|
-
method: "POST",
|
|
4681
|
-
headers: { "content-type": "application/json" },
|
|
4682
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
4683
|
-
});
|
|
4684
|
-
if (!res.ok) {
|
|
4685
|
-
lastErr = apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
|
|
4686
|
-
continue;
|
|
4687
|
-
}
|
|
4688
|
-
const body = await res.json();
|
|
4689
|
-
if (body.error) {
|
|
4690
|
-
lastErr = apiError(502, "rpc_error", `chain RPC: ${body.error.message ?? "unknown"}`);
|
|
4691
|
-
continue;
|
|
4692
|
-
}
|
|
4693
|
-
return body.result;
|
|
4953
|
+
return await rpcOn(url, method, params);
|
|
4694
4954
|
} catch (e) {
|
|
4695
4955
|
lastErr = e;
|
|
4696
4956
|
}
|
|
4697
4957
|
}
|
|
4698
4958
|
throw lastErr ?? apiError(502, "rpc_error", "no chain RPC configured");
|
|
4699
4959
|
}
|
|
4960
|
+
const chainOkUrls = /* @__PURE__ */ new Set();
|
|
4961
|
+
const expChain = Number(expectedChainId);
|
|
4962
|
+
const chainPinned = Number.isFinite(expChain) && expChain > 0;
|
|
4963
|
+
async function assertChain() {
|
|
4964
|
+
if (!chainPinned) return true;
|
|
4965
|
+
if (chainOkUrls.size) return true;
|
|
4966
|
+
for (const url of urls) {
|
|
4967
|
+
try {
|
|
4968
|
+
const hex = await rpcOn(url, "eth_chainId", []);
|
|
4969
|
+
if (typeof hex === "string" && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
|
|
4970
|
+
} catch {
|
|
4971
|
+
}
|
|
4972
|
+
}
|
|
4973
|
+
return chainOkUrls.size > 0;
|
|
4974
|
+
}
|
|
4975
|
+
async function fetchReceipt(txHash) {
|
|
4976
|
+
let receipt = await rpc("eth_getTransactionReceipt", [txHash]);
|
|
4977
|
+
for (let i = 0; !receipt && i < receiptRetries; i++) {
|
|
4978
|
+
await sleepImpl(receiptRetryMs);
|
|
4979
|
+
receipt = await rpc("eth_getTransactionReceipt", [txHash]);
|
|
4980
|
+
}
|
|
4981
|
+
if (!receipt) return { error: "tx_not_found_or_pending" };
|
|
4982
|
+
if (lc(receipt.transactionHash) !== lc(txHash)) return { error: "receipt_tx_mismatch" };
|
|
4983
|
+
if (receipt.status !== "0x1") return { error: "tx_failed" };
|
|
4984
|
+
return { receipt };
|
|
4985
|
+
}
|
|
4986
|
+
function findUsdcTransfer(receipt, { to, minAtomic, from, consumed } = {}) {
|
|
4987
|
+
const want = lc(to);
|
|
4988
|
+
const wantFrom = from ? lc(from) : null;
|
|
4989
|
+
const logs = receipt.logs || [];
|
|
4990
|
+
let sawRecipient = false;
|
|
4991
|
+
for (let i = 0; i < logs.length; i++) {
|
|
4992
|
+
if (consumed && consumed.has(i)) continue;
|
|
4993
|
+
const l = logs[i];
|
|
4994
|
+
if (lc(l.address) !== usdc || l.topics?.[0] !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
|
|
4995
|
+
if (topicToAddress(l.topics[2]) !== want) continue;
|
|
4996
|
+
if (wantFrom && topicToAddress(l.topics[1]) !== wantFrom) continue;
|
|
4997
|
+
sawRecipient = true;
|
|
4998
|
+
let amount;
|
|
4999
|
+
try {
|
|
5000
|
+
amount = BigInt(l.data);
|
|
5001
|
+
} catch {
|
|
5002
|
+
continue;
|
|
5003
|
+
}
|
|
5004
|
+
if (amount < BigInt(minAtomic)) continue;
|
|
5005
|
+
return { ok: true, from: topicToAddress(l.topics[1]), amount: amount.toString(), index: i };
|
|
5006
|
+
}
|
|
5007
|
+
return { ok: false, reason: sawRecipient ? "amount_too_low" : "no_matching_usdc_transfer" };
|
|
5008
|
+
}
|
|
4700
5009
|
return {
|
|
4701
5010
|
configured,
|
|
4702
5011
|
// Confirm txHash is a successful USDC transfer of >= minAtomic to `to`.
|
|
4703
5012
|
// Returns { ok, reason?, from?, amount? }; never throws on a bad payment
|
|
4704
5013
|
// (only on an RPC transport failure).
|
|
4705
5014
|
async verifyTransfer(txHash, { to, minAtomic }) {
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
5015
|
+
if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
|
|
5016
|
+
const got = await fetchReceipt(txHash);
|
|
5017
|
+
if (got.error) return { ok: false, reason: got.error };
|
|
5018
|
+
return findUsdcTransfer(got.receipt, { to, minAtomic });
|
|
5019
|
+
},
|
|
5020
|
+
async verifyDrawPaid(txHash, {
|
|
5021
|
+
contractAddress,
|
|
5022
|
+
buyerAgentId,
|
|
5023
|
+
sellerAgentId,
|
|
5024
|
+
bookingId,
|
|
5025
|
+
offerId,
|
|
5026
|
+
model,
|
|
5027
|
+
n,
|
|
5028
|
+
requestHash,
|
|
5029
|
+
sellerWallet,
|
|
5030
|
+
feeRecipient,
|
|
5031
|
+
minSellerAtomic = 0n
|
|
5032
|
+
} = {}) {
|
|
5033
|
+
if (!await assertChain()) return { ok: false, reason: "wrong_chain" };
|
|
5034
|
+
const contract = lc(contractAddress);
|
|
5035
|
+
if (!contract) return { ok: false, reason: "contract_not_configured" };
|
|
5036
|
+
const got = await fetchReceipt(txHash);
|
|
5037
|
+
if (got.error) return { ok: false, reason: got.error };
|
|
5038
|
+
const log = (got.receipt.logs || []).find(
|
|
5039
|
+
(l) => lc(l.address) === contract && isDrawPaidTopic(l.topics?.[0])
|
|
4716
5040
|
);
|
|
4717
|
-
if (!log) return { ok: false, reason: "
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
5041
|
+
if (!log) return { ok: false, reason: "no_draw_paid_event" };
|
|
5042
|
+
let event;
|
|
5043
|
+
try {
|
|
5044
|
+
event = decodeDrawPaidLog(log);
|
|
5045
|
+
} catch {
|
|
5046
|
+
return { ok: false, reason: "malformed_draw_paid_event" };
|
|
5047
|
+
}
|
|
5048
|
+
if (buyerAgentId != null && event.buyerAgentId !== String(buyerAgentId)) return { ok: false, reason: "buyer_agent_mismatch" };
|
|
5049
|
+
if (sellerAgentId != null && event.sellerAgentId !== String(sellerAgentId)) return { ok: false, reason: "seller_agent_mismatch" };
|
|
5050
|
+
if (bookingId != null && event.bookingId !== String(bookingId)) return { ok: false, reason: "booking_mismatch" };
|
|
5051
|
+
if (offerId != null && event.offerId !== String(offerId)) return { ok: false, reason: "offer_mismatch" };
|
|
5052
|
+
if (model != null && event.model !== String(model)) return { ok: false, reason: "model_mismatch" };
|
|
5053
|
+
if (n != null && event.n !== Number(n)) return { ok: false, reason: "draw_n_mismatch" };
|
|
5054
|
+
if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
|
|
5055
|
+
if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
|
|
5056
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
5057
|
+
let sellerTransfer = null;
|
|
5058
|
+
if (sellerWallet) {
|
|
5059
|
+
sellerTransfer = findUsdcTransfer(got.receipt, { to: sellerWallet, minAtomic: BigInt(event.sellerUsdAtomic), from: event.buyer });
|
|
5060
|
+
if (!sellerTransfer.ok) return { ok: false, reason: "seller_transfer_" + sellerTransfer.reason };
|
|
5061
|
+
if (sellerTransfer.index != null) consumed.add(sellerTransfer.index);
|
|
5062
|
+
}
|
|
5063
|
+
let feeTransfer = null;
|
|
5064
|
+
if (feeRecipient && BigInt(event.feeUsdAtomic) > 0n) {
|
|
5065
|
+
feeTransfer = findUsdcTransfer(got.receipt, { to: feeRecipient, minAtomic: BigInt(event.feeUsdAtomic), from: event.buyer, consumed });
|
|
5066
|
+
if (!feeTransfer.ok) return { ok: false, reason: "fee_transfer_" + feeTransfer.reason };
|
|
5067
|
+
}
|
|
5068
|
+
return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
|
|
4721
5069
|
}
|
|
4722
5070
|
};
|
|
4723
5071
|
}
|
|
4724
|
-
function usdToAtomic(usd) {
|
|
4725
|
-
return BigInt(Math.round(Number(usd) * 1e6));
|
|
4726
|
-
}
|
|
4727
5072
|
|
|
4728
|
-
//
|
|
4729
|
-
var
|
|
5073
|
+
// src/rpc.mjs
|
|
5074
|
+
var BASE_MAINNET_RPCS = [
|
|
5075
|
+
"https://mainnet.base.org",
|
|
5076
|
+
"https://base.llamarpc.com",
|
|
5077
|
+
"https://base-rpc.publicnode.com",
|
|
5078
|
+
"https://base.drpc.org"
|
|
5079
|
+
];
|
|
5080
|
+
var BASE_SEPOLIA_RPCS = [
|
|
5081
|
+
"https://sepolia.base.org",
|
|
5082
|
+
"https://base-sepolia-rpc.publicnode.com"
|
|
5083
|
+
];
|
|
5084
|
+
var rpcUrlsFor = (chainId, override) => override ? [override] : Number(chainId) === 8453 ? BASE_MAINNET_RPCS : BASE_SEPOLIA_RPCS;
|
|
5085
|
+
|
|
5086
|
+
// src/runtime.mjs
|
|
5087
|
+
import crypto2 from "node:crypto";
|
|
4730
5088
|
var BALANCE_EPSILON = 1e-6;
|
|
4731
|
-
var
|
|
4732
|
-
|
|
4733
|
-
const
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
}
|
|
4752
|
-
if (!upstream) {
|
|
4753
|
-
console.error("mtok-relay: --upstream <url> is required");
|
|
4754
|
-
process.exit(1);
|
|
4755
|
-
}
|
|
4756
|
-
var MTOK_API_KEY = process.env.MTOK_API_KEY;
|
|
4757
|
-
var UPSTREAM_KEY = process.env.UPSTREAM_KEY;
|
|
4758
|
-
var RELAY_WALLET_KEY = process.env.RELAY_WALLET_KEY;
|
|
4759
|
-
if (!MTOK_API_KEY) {
|
|
4760
|
-
console.error("mtok-relay: MTOK_API_KEY env var is required");
|
|
4761
|
-
process.exit(1);
|
|
4762
|
-
}
|
|
4763
|
-
if (!UPSTREAM_KEY) {
|
|
4764
|
-
console.error("mtok-relay: UPSTREAM_KEY env var is required");
|
|
4765
|
-
process.exit(1);
|
|
4766
|
-
}
|
|
4767
|
-
if (!RELAY_WALLET_KEY && !settlementPubkeyFlag) {
|
|
4768
|
-
console.error("mtok-relay: RELAY_WALLET_KEY env var (or --settlement-pubkey) is required");
|
|
4769
|
-
process.exit(1);
|
|
4770
|
-
}
|
|
4771
|
-
var SETTLEMENT_ADDR;
|
|
4772
|
-
if (settlementPubkeyFlag) {
|
|
4773
|
-
SETTLEMENT_ADDR = settlementPubkeyFlag;
|
|
4774
|
-
} else {
|
|
4775
|
-
try {
|
|
4776
|
-
SETTLEMENT_ADDR = privateKeyToAccount(RELAY_WALLET_KEY.startsWith("0x") ? RELAY_WALLET_KEY : "0x" + RELAY_WALLET_KEY).address;
|
|
4777
|
-
} catch (e) {
|
|
4778
|
-
console.error("mtok-relay: invalid RELAY_WALLET_KEY \u2014", e.message);
|
|
4779
|
-
process.exit(1);
|
|
4780
|
-
}
|
|
4781
|
-
}
|
|
4782
|
-
function readBody(req) {
|
|
4783
|
-
return new Promise((resolve, reject) => {
|
|
4784
|
-
let raw = "";
|
|
4785
|
-
req.on("data", (d) => {
|
|
4786
|
-
raw += d;
|
|
5089
|
+
var hash32 = (v) => "0x" + crypto2.createHash("sha256").update(typeof v === "string" ? v : JSON.stringify(v ?? null)).digest("hex");
|
|
5090
|
+
async function createRelayRuntime(config) {
|
|
5091
|
+
const served = createRedemptionStore({ file: config.redemptionFile });
|
|
5092
|
+
const drawLocks = /* @__PURE__ */ new Map();
|
|
5093
|
+
const platform = await fetchPlatformConfig(config);
|
|
5094
|
+
const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress });
|
|
5095
|
+
if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
|
|
5096
|
+
const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
|
|
5097
|
+
const screenPayer = typeof config.screenPayer === "function" ? config.screenPayer : null;
|
|
5098
|
+
const payerDenied = async (payer) => {
|
|
5099
|
+
if (!payer) return false;
|
|
5100
|
+
if (payerDenylist.has(payer)) return true;
|
|
5101
|
+
if (screenPayer && await screenPayer(payer)) return true;
|
|
5102
|
+
return false;
|
|
5103
|
+
};
|
|
5104
|
+
const withBookingLock = async (bookingId, fn) => {
|
|
5105
|
+
const previous = drawLocks.get(bookingId) || Promise.resolve();
|
|
5106
|
+
let release;
|
|
5107
|
+
const gate = new Promise((resolve) => {
|
|
5108
|
+
release = resolve;
|
|
4787
5109
|
});
|
|
4788
|
-
|
|
5110
|
+
const tail = previous.catch(() => {
|
|
5111
|
+
}).then(() => gate);
|
|
5112
|
+
drawLocks.set(bookingId, tail);
|
|
5113
|
+
await previous.catch(() => {
|
|
5114
|
+
});
|
|
5115
|
+
try {
|
|
5116
|
+
return await fn();
|
|
5117
|
+
} finally {
|
|
5118
|
+
release();
|
|
5119
|
+
if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
|
|
5120
|
+
}
|
|
5121
|
+
};
|
|
5122
|
+
const handleDraw = async (body, res) => {
|
|
5123
|
+
const { bookingId, n, buyerId, request, drawPaidTxHash } = body;
|
|
5124
|
+
if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
|
|
5125
|
+
if (n == null) return send(res, 400, { error: "bad_request", detail: "DRAW needs a delivery index n (per-booking idempotency key)" });
|
|
5126
|
+
return withBookingLock(bookingId, async () => {
|
|
5127
|
+
const requestHash = hash32(request);
|
|
5128
|
+
const cacheKey = `${bookingId}:${n}:${requestHash}`;
|
|
5129
|
+
if (served.has(cacheKey)) return send(res, 200, served.get(cacheKey));
|
|
5130
|
+
if (!platform.dripContractAddress) {
|
|
5131
|
+
return send(res, 402, { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" });
|
|
5132
|
+
}
|
|
5133
|
+
if (!drawPaidTxHash) return send(res, 402, { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" });
|
|
5134
|
+
let paid;
|
|
4789
5135
|
try {
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
5136
|
+
paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
|
|
5137
|
+
contractAddress: platform.dripContractAddress,
|
|
5138
|
+
buyerAgentId: buyerId,
|
|
5139
|
+
sellerAgentId: config.sellerAgentId,
|
|
5140
|
+
// when set, enforces the offer-owner match (#codex review)
|
|
5141
|
+
bookingId,
|
|
5142
|
+
offerId: config.offerId,
|
|
5143
|
+
model: config.model,
|
|
5144
|
+
n,
|
|
5145
|
+
requestHash,
|
|
5146
|
+
sellerWallet: config.settlementAddr,
|
|
5147
|
+
feeRecipient: platform.feeAddress
|
|
5148
|
+
});
|
|
5149
|
+
} catch (e) {
|
|
5150
|
+
return send(res, 402, { error: "payment_unverified", detail: e.message });
|
|
5151
|
+
}
|
|
5152
|
+
if (!paid?.ok) return send(res, 402, { error: "payment_unverified", detail: paid?.reason || "unknown" });
|
|
5153
|
+
const expectedFee = configuredFeeAtomic({
|
|
5154
|
+
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
5155
|
+
feeAddress: platform.feeAddress,
|
|
5156
|
+
feeBps: platform.feeBps
|
|
5157
|
+
});
|
|
5158
|
+
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5159
|
+
return send(res, 402, { error: "payment_unverified", detail: "fee_amount_too_low" });
|
|
5160
|
+
}
|
|
5161
|
+
try {
|
|
5162
|
+
if (await payerDenied(String(paid.from || "").toLowerCase())) {
|
|
5163
|
+
return send(res, 403, { error: "payer_denied", detail: "the verified payer wallet is denylisted by this relay" });
|
|
5164
|
+
}
|
|
5165
|
+
} catch (e) {
|
|
5166
|
+
return send(res, 403, { error: "payer_denied", detail: "payer screening failed: " + e.message });
|
|
5167
|
+
}
|
|
5168
|
+
const paidEvent = paid.event;
|
|
5169
|
+
const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
|
|
5170
|
+
if (remainingUsd <= BALANCE_EPSILON) {
|
|
5171
|
+
return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
|
|
5172
|
+
}
|
|
5173
|
+
const eventInPriceUsd = Number(paidEvent.inputPricePerMTokAtomic || 0) / 1e6;
|
|
5174
|
+
const inPrice = Math.max(Number(config.inPrice) || 0, eventInPriceUsd);
|
|
5175
|
+
const bound = boundServe({ messages: request?.messages, budgetUsd: remainingUsd, inPrice, outPrice: config.outPrice, reqMax: Number(request?.max_tokens) });
|
|
5176
|
+
if (bound.refuse) {
|
|
5177
|
+
return send(res, 402, { error: "input_too_large", detail: `estimated input (~${bound.estIn} tokens, $${bound.estInCostUsd.toFixed(6)}) meets or exceeds the paid amount ($${remainingUsd}); send a shorter prompt or pay for more`, _bookingId: bookingId, remainingUsd });
|
|
5178
|
+
}
|
|
5179
|
+
const safeRequest = { ...request, max_tokens: bound.maxTok };
|
|
5180
|
+
let completion;
|
|
5181
|
+
try {
|
|
5182
|
+
const upstreamRes = await fetch(config.upstream + "/v1/chat/completions", {
|
|
5183
|
+
method: "POST",
|
|
5184
|
+
headers: { "content-type": "application/json", authorization: "Bearer " + config.upstreamKey },
|
|
5185
|
+
body: JSON.stringify(safeRequest)
|
|
5186
|
+
});
|
|
5187
|
+
completion = await upstreamRes.json();
|
|
5188
|
+
} catch (e) {
|
|
5189
|
+
return send(res, 502, { error: "upstream_error", detail: e.message });
|
|
5190
|
+
}
|
|
5191
|
+
try {
|
|
5192
|
+
enforceModelEcho(completion.model, config.model);
|
|
5193
|
+
} catch (e) {
|
|
5194
|
+
return send(res, 502, { error: "model_mismatch", detail: e.message });
|
|
4793
5195
|
}
|
|
5196
|
+
const usage = completion.usage ?? {};
|
|
5197
|
+
const inTok = Number(usage.prompt_tokens ?? usage.input_tokens ?? 0) || 0;
|
|
5198
|
+
const outTok = Number(usage.completion_tokens ?? usage.output_tokens ?? 0) || 0;
|
|
5199
|
+
const usedUsd = (inTok * Number(paidEvent.inputPricePerMTokAtomic || 0) + outTok * Number(paidEvent.outputPricePerMTokAtomic || 0)) / 1e12;
|
|
5200
|
+
const payload = { ...completion, _bookingId: bookingId, remainingUsd: Math.max(0, Math.round((remainingUsd - usedUsd) * 1e6) / 1e6) };
|
|
5201
|
+
served.set(cacheKey, payload);
|
|
5202
|
+
return send(res, 200, payload);
|
|
4794
5203
|
});
|
|
4795
|
-
|
|
4796
|
-
}
|
|
4797
|
-
}
|
|
4798
|
-
function send(res, status, body) {
|
|
4799
|
-
const payload = JSON.stringify(body);
|
|
4800
|
-
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
|
|
4801
|
-
res.end(payload);
|
|
5204
|
+
};
|
|
5205
|
+
return { handleDraw };
|
|
4802
5206
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
var verifier;
|
|
4806
|
-
async function boot() {
|
|
4807
|
-
const r = await fetch(apiBase + "/api/config");
|
|
5207
|
+
async function fetchPlatformConfig(config) {
|
|
5208
|
+
const r = await fetch(config.apiBase + "/api/config");
|
|
4808
5209
|
if (!r.ok) throw new Error("config fetch failed: " + r.status);
|
|
4809
|
-
const
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5210
|
+
const body = await r.json();
|
|
5211
|
+
return {
|
|
5212
|
+
feeAddress: body.feeAddress,
|
|
5213
|
+
feeBps: body.feeBps,
|
|
5214
|
+
dustThresholdUsd: Number(body.dustThresholdUsd) || 1e-3,
|
|
5215
|
+
chainId: Number(body.chainId ?? 8453),
|
|
5216
|
+
usdcAddress: body.usdcAddress,
|
|
5217
|
+
dripContractAddress: body.dripContractAddress
|
|
5218
|
+
};
|
|
4816
5219
|
}
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
});
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
}
|
|
4826
|
-
async function handleFund(body, res) {
|
|
4827
|
-
const { bookingId, n, sellerTxHash, feeTxHash, priceUsd, buyerId } = body;
|
|
4828
|
-
try {
|
|
4829
|
-
const sellerOk = (await verifier.verifyTransfer(sellerTxHash, { to: SETTLEMENT_ADDR, minAtomic: usdToAtomic(priceUsd) })).ok;
|
|
4830
|
-
const feeAtomic = usdToAtomic((Number(priceUsd) || 0) * (Number(feeBps) || 0) / 1e4);
|
|
4831
|
-
const feeOk = (await verifier.verifyTransfer(feeTxHash, { to: feeAddress, minAtomic: feeAtomic })).ok;
|
|
4832
|
-
if (!sellerOk || !feeOk) return send(res, 402, { error: "payment_unverified", detail: `seller=${sellerOk} fee=${feeOk}` });
|
|
4833
|
-
} catch (e) {
|
|
4834
|
-
return send(res, 402, { error: "payment_unverified", detail: e.message });
|
|
4835
|
-
}
|
|
4836
|
-
let rep;
|
|
4837
|
-
try {
|
|
4838
|
-
rep = await reportToPlatform(buildFundReport({ offerId, buyerId, bookingId, n, priceUsd, sellerTxHash, feeTxHash }));
|
|
4839
|
-
} catch (e) {
|
|
4840
|
-
return send(res, 502, { error: "report_failed", detail: e.message });
|
|
4841
|
-
}
|
|
4842
|
-
if (!rep.ok || !rep.booking) {
|
|
4843
|
-
console.error("mtok-relay: FUND report rejected (%d) for n=%d offerId=%s: %s", rep.status, n, offerId, JSON.stringify(rep.body));
|
|
4844
|
-
return send(res, 502, { error: "report_failed", detail: rep.body?.error || rep.status });
|
|
4845
|
-
}
|
|
4846
|
-
return send(res, 200, { ...rep.booking, _bookingId: rep.booking.id, remainingUsd: rep.booking.remainingUsd });
|
|
4847
|
-
}
|
|
4848
|
-
async function handleDraw(body, res) {
|
|
4849
|
-
const { bookingId, n, buyerId, request } = body;
|
|
4850
|
-
if (!bookingId) return send(res, 400, { error: "bad_request", detail: "DRAW needs bookingId" });
|
|
4851
|
-
let booking;
|
|
4852
|
-
try {
|
|
4853
|
-
const r = await fetch(apiBase + `/api/bookings/${encodeURIComponent(bookingId)}`, { headers: { "x-api-key": MTOK_API_KEY } });
|
|
4854
|
-
const rb = await r.json().catch(() => ({}));
|
|
4855
|
-
if (r.status !== 200 || !rb?.booking) return send(res, 502, { error: "booking_read_failed", detail: rb?.error || r.status });
|
|
4856
|
-
booking = rb.booking;
|
|
4857
|
-
} catch (e) {
|
|
4858
|
-
return send(res, 502, { error: "booking_read_failed", detail: e.message });
|
|
4859
|
-
}
|
|
4860
|
-
const remainingUsd = Number(booking.remainingUsd) || 0;
|
|
4861
|
-
if (remainingUsd <= BALANCE_EPSILON) {
|
|
4862
|
-
return send(res, 402, { error: "balance_exhausted", detail: `remainingUsd=${remainingUsd}`, _bookingId: bookingId, remainingUsd });
|
|
4863
|
-
}
|
|
4864
|
-
let maxTok = CONTEXT_CEIL;
|
|
4865
|
-
const reqMax = Number(request?.max_tokens);
|
|
4866
|
-
if (reqMax > 0) maxTok = Math.min(maxTok, Math.floor(reqMax));
|
|
4867
|
-
if (outPrice > 0) maxTok = Math.min(maxTok, Math.floor(remainingUsd / outPrice * 1e6));
|
|
4868
|
-
const safeRequest = { ...request, max_tokens: Math.max(1, maxTok) };
|
|
4869
|
-
let completion;
|
|
4870
|
-
try {
|
|
4871
|
-
const upstreamRes = await fetch(upstream + "/v1/chat/completions", {
|
|
4872
|
-
method: "POST",
|
|
4873
|
-
headers: { "content-type": "application/json", authorization: "Bearer " + UPSTREAM_KEY },
|
|
4874
|
-
body: JSON.stringify(safeRequest)
|
|
4875
|
-
});
|
|
4876
|
-
completion = await upstreamRes.json();
|
|
4877
|
-
} catch (e) {
|
|
4878
|
-
return send(res, 502, { error: "upstream_error", detail: e.message });
|
|
4879
|
-
}
|
|
4880
|
-
try {
|
|
4881
|
-
enforceModelEcho(completion.model, MODEL);
|
|
4882
|
-
} catch (e) {
|
|
4883
|
-
return send(res, 502, { error: "model_mismatch", detail: e.message });
|
|
4884
|
-
}
|
|
4885
|
-
let rep;
|
|
4886
|
-
try {
|
|
4887
|
-
rep = await reportToPlatform(buildDrawReport({ offerId, buyerId, bookingId, n, usage: completion.usage }));
|
|
4888
|
-
} catch (e) {
|
|
4889
|
-
console.error("mtok-relay: DRAW report failed (network) for n=%d offerId=%s: %s", n, offerId, e.message);
|
|
4890
|
-
return send(res, 502, { error: "report_failed", detail: e.message, completion });
|
|
4891
|
-
}
|
|
4892
|
-
if (!rep.ok || !rep.booking) {
|
|
4893
|
-
console.error("mtok-relay: DRAW report rejected (%d) for n=%d offerId=%s: %s", rep.status, n, offerId, JSON.stringify(rep.body));
|
|
4894
|
-
return send(res, rep.status === 402 ? 402 : 502, { error: rep.body?.error?.code || rep.body?.error || "report_failed", detail: rep.body?.error?.message || rep.status, completion, _bookingId: bookingId });
|
|
4895
|
-
}
|
|
4896
|
-
return send(res, 200, { ...completion, _bookingId: rep.booking.id, remainingUsd: rep.booking.remainingUsd });
|
|
4897
|
-
}
|
|
4898
|
-
var server = http.createServer(async (req, res) => {
|
|
4899
|
-
if (req.method !== "POST" || req.url !== "/chunk") {
|
|
4900
|
-
return send(res, 404, { error: "not found" });
|
|
4901
|
-
}
|
|
4902
|
-
let body;
|
|
4903
|
-
try {
|
|
4904
|
-
body = await readBody(req);
|
|
4905
|
-
} catch {
|
|
4906
|
-
return send(res, 400, { error: "bad body" });
|
|
4907
|
-
}
|
|
4908
|
-
const hasFund = body.sellerTxHash != null && body.sellerTxHash !== "";
|
|
4909
|
-
const hasDraw = body.request != null;
|
|
4910
|
-
if (hasFund && hasDraw) return send(res, 400, { error: "bad_request", detail: "send a FUND (sellerTxHash) or a DRAW (request), not both" });
|
|
4911
|
-
if (hasFund) return handleFund(body, res);
|
|
4912
|
-
if (hasDraw) return handleDraw(body, res);
|
|
4913
|
-
return send(res, 400, { error: "bad_request", detail: "need a FUND (sellerTxHash) or a DRAW (request)" });
|
|
4914
|
-
});
|
|
4915
|
-
boot().then(() => {
|
|
4916
|
-
server.listen(port, () => {
|
|
4917
|
-
console.log(`mtok-relay: listening on port ${port} offer=${offerId} model=${MODEL} upstream=${upstream} api=${apiBase} settlement=${SETTLEMENT_ADDR}`);
|
|
4918
|
-
});
|
|
4919
|
-
}).catch((e) => {
|
|
4920
|
-
console.error("mtok-relay: boot failed \u2014", e.message);
|
|
5220
|
+
|
|
5221
|
+
// mtok-relay.mjs
|
|
5222
|
+
try {
|
|
5223
|
+
const config = readRelayConfig();
|
|
5224
|
+
const runtime = await createRelayRuntime(config);
|
|
5225
|
+
startRelayServer({ config, ...runtime });
|
|
5226
|
+
} catch (e) {
|
|
5227
|
+
console.error("mtok-relay: boot failed -", e.message);
|
|
4921
5228
|
process.exit(1);
|
|
4922
|
-
}
|
|
5229
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mtok-relay",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"prepublishOnly": "node build-relay-bundle.mjs"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"esbuild": "^0.28.1",
|
|
37
|
+
"viem": "^2.0.0"
|
|
38
38
|
},
|
|
39
39
|
"overrides": {
|
|
40
40
|
"ws": "^8.21.0"
|