mtok-relay 0.2.5 → 0.2.7
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 +74 -41
- package/package.json +1 -1
package/dist/mtok-relay.mjs
CHANGED
|
@@ -5295,9 +5295,9 @@ function createOnchainVerifier({
|
|
|
5295
5295
|
if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: "request_hash_mismatch" };
|
|
5296
5296
|
if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
|
|
5297
5297
|
const maxAge = Number(maxPaidAgeMs);
|
|
5298
|
+
let paidAtMs = null;
|
|
5298
5299
|
if (Number.isFinite(maxAge) && maxAge > 0) {
|
|
5299
5300
|
const bn = matchedLog?.blockNumber ?? got.receipt.blockNumber;
|
|
5300
|
-
let paidAtMs = null;
|
|
5301
5301
|
if (bn != null) {
|
|
5302
5302
|
for (let i = 0; i <= receiptRetries; i++) {
|
|
5303
5303
|
try {
|
|
@@ -5330,11 +5330,37 @@ function createOnchainVerifier({
|
|
|
5330
5330
|
feeTransfer = findUsdcTransfer(got.receipt, { to: feeRecipient, minAtomic: BigInt(event.feeUsdAtomic), from: event.buyer, consumed });
|
|
5331
5331
|
if (!feeTransfer.ok) return { ok: false, reason: "fee_transfer_" + feeTransfer.reason };
|
|
5332
5332
|
}
|
|
5333
|
-
return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
|
|
5333
|
+
return { ok: true, event, paidAtMs, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
|
|
5334
5334
|
}
|
|
5335
5335
|
};
|
|
5336
5336
|
}
|
|
5337
5337
|
|
|
5338
|
+
// core/fee-policy.js
|
|
5339
|
+
function normalizeFeeSchedule(schedule) {
|
|
5340
|
+
if (!Array.isArray(schedule) || !schedule.length || schedule.length > 64) throw new TypeError("fee schedule must contain 1..64 activation records");
|
|
5341
|
+
let previous = -1;
|
|
5342
|
+
return schedule.map(({ effectiveAtMs, feeBps }, index) => {
|
|
5343
|
+
if (!Number.isSafeInteger(effectiveAtMs) || effectiveAtMs <= previous || index === 0 && effectiveAtMs !== 0) {
|
|
5344
|
+
throw new TypeError("fee activations must start at 0 and increase in integer milliseconds");
|
|
5345
|
+
}
|
|
5346
|
+
if (!Number.isSafeInteger(feeBps) || feeBps < 0 || feeBps > 1e4) throw new TypeError("feeBps must be an integer from 0 to 10000");
|
|
5347
|
+
previous = effectiveAtMs;
|
|
5348
|
+
return { effectiveAtMs, feeBps };
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
function feeBpsAt(schedule, timeMs) {
|
|
5352
|
+
if (!Number.isSafeInteger(timeMs) || timeMs < 0) throw new TypeError("verified payment time is required for fee policy");
|
|
5353
|
+
return schedule.findLast((entry) => entry.effectiveAtMs <= timeMs).feeBps;
|
|
5354
|
+
}
|
|
5355
|
+
function paymentFeeBpsAt(schedule, paidAtMs) {
|
|
5356
|
+
const start = Math.max(0, paidAtMs - 6e4);
|
|
5357
|
+
let minimum = Math.min(feeBpsAt(schedule, paidAtMs), feeBpsAt(schedule, start));
|
|
5358
|
+
for (const entry of schedule) {
|
|
5359
|
+
if (entry.effectiveAtMs > start && entry.effectiveAtMs <= paidAtMs) minimum = Math.min(minimum, entry.feeBps);
|
|
5360
|
+
}
|
|
5361
|
+
return minimum;
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5338
5364
|
// bridge/serve-core.mjs
|
|
5339
5365
|
var BALANCE_EPSILON = 1e-6;
|
|
5340
5366
|
var REQUEST_NONCE_RE = /^0x[0-9a-fA-F]{32}$/;
|
|
@@ -5489,7 +5515,6 @@ function createServeCore({
|
|
|
5489
5515
|
maxOutputTokens
|
|
5490
5516
|
}) {
|
|
5491
5517
|
const serve = async (body) => {
|
|
5492
|
-
const currentFeeBps = typeof feeBps === "function" ? feeBps() : feeBps;
|
|
5493
5518
|
const currentFeeRecipient = typeof feeRecipient === "function" ? feeRecipient() : feeRecipient;
|
|
5494
5519
|
const { bookingId, n, buyerId, request, requestNonce, drawPaidTxHash } = body;
|
|
5495
5520
|
const hasRequestNonce = Object.hasOwn(body, "requestNonce");
|
|
@@ -5507,19 +5532,20 @@ function createServeCore({
|
|
|
5507
5532
|
const requestHash = hasRequestNonce ? await hash32({ request, requestNonce }) : await hash32(request);
|
|
5508
5533
|
const cacheKey = `${requestHashScheme}:${bookingId}:${n}:${requestHash}`;
|
|
5509
5534
|
const oldLegacyKey = hasRequestNonce ? null : `${bookingId}:${n}:${requestHash}`;
|
|
5535
|
+
const redemptionContext = { claimKey: cacheKey };
|
|
5510
5536
|
if (!dripContractAddress) {
|
|
5511
5537
|
return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
|
|
5512
5538
|
}
|
|
5513
5539
|
if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
|
|
5514
5540
|
let storedKey = cacheKey;
|
|
5515
|
-
let redemptionState = await redemption.state(storedKey);
|
|
5541
|
+
let redemptionState = await redemption.state(storedKey, redemptionContext);
|
|
5516
5542
|
if (!redemptionState && oldLegacyKey) {
|
|
5517
|
-
const oldLegacyState = await redemption.state(oldLegacyKey);
|
|
5543
|
+
const oldLegacyState = await redemption.state(oldLegacyKey, redemptionContext);
|
|
5518
5544
|
if (oldLegacyState) {
|
|
5519
5545
|
storedKey = oldLegacyKey;
|
|
5520
5546
|
redemptionState = oldLegacyState;
|
|
5521
5547
|
} else {
|
|
5522
|
-
redemptionState = await redemption.state(cacheKey);
|
|
5548
|
+
redemptionState = await redemption.state(cacheKey, redemptionContext);
|
|
5523
5549
|
}
|
|
5524
5550
|
}
|
|
5525
5551
|
let paid;
|
|
@@ -5536,9 +5562,9 @@ function createServeCore({
|
|
|
5536
5562
|
requestHash,
|
|
5537
5563
|
sellerWallet,
|
|
5538
5564
|
feeRecipient: currentFeeRecipient,
|
|
5539
|
-
// A known
|
|
5565
|
+
// A known claim spends no new inference. New claims need a verified
|
|
5540
5566
|
// age because both payload and claim markers expire after retention.
|
|
5541
|
-
maxPaidAgeMs: redemptionState === "complete" ? void 0 : redemption.retentionMs
|
|
5567
|
+
maxPaidAgeMs: redemptionState === "complete" || redemptionState === "pending" ? void 0 : redemption.retentionMs
|
|
5542
5568
|
});
|
|
5543
5569
|
} catch (e) {
|
|
5544
5570
|
if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
|
|
@@ -5546,14 +5572,6 @@ function createServeCore({
|
|
|
5546
5572
|
}
|
|
5547
5573
|
if (paid?.reason === "payment_age_unavailable") return { status: 503, body: { error: "payment_age_unavailable", detail: "payment age could not be verified; retry this same paid draw", _bookingId: bookingId } };
|
|
5548
5574
|
if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
|
|
5549
|
-
const expectedFee = configuredFeeAtomic({
|
|
5550
|
-
sellerUsdAtomic: paid.event.sellerUsdAtomic,
|
|
5551
|
-
feeAddress: currentFeeRecipient,
|
|
5552
|
-
feeBps: currentFeeBps
|
|
5553
|
-
});
|
|
5554
|
-
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5555
|
-
return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
|
|
5556
|
-
}
|
|
5557
5575
|
if (screenPayer) {
|
|
5558
5576
|
try {
|
|
5559
5577
|
if (await screenPayer(String(paid.from || "").toLowerCase())) {
|
|
@@ -5563,10 +5581,25 @@ function createServeCore({
|
|
|
5563
5581
|
return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
|
|
5564
5582
|
}
|
|
5565
5583
|
}
|
|
5566
|
-
if (redemptionState === "complete")
|
|
5584
|
+
if (redemptionState === "complete") {
|
|
5585
|
+
const payload2 = await redemption.get(storedKey, redemptionContext);
|
|
5586
|
+
if (payload2 == null) return { status: 503, body: { error: "redemption_unavailable", detail: "saved completion is no longer readable; retry this same paid draw", _bookingId: bookingId } };
|
|
5587
|
+
return { status: 200, body: payload2 };
|
|
5588
|
+
}
|
|
5567
5589
|
if (redemptionState === "pending") {
|
|
5568
5590
|
return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
|
|
5569
5591
|
}
|
|
5592
|
+
let currentFeeBps;
|
|
5593
|
+
try {
|
|
5594
|
+
currentFeeBps = typeof feeBps === "function" ? await feeBps(paid) : feeBps ?? 0;
|
|
5595
|
+
if (!Number.isSafeInteger(currentFeeBps) || currentFeeBps < 0 || currentFeeBps > 1e4) throw new TypeError("invalid fee rate");
|
|
5596
|
+
} catch {
|
|
5597
|
+
return { status: 503, body: { error: "fee_policy_unavailable", detail: "fee policy could not be verified; retry this same paid draw", _bookingId: bookingId } };
|
|
5598
|
+
}
|
|
5599
|
+
const expectedFee = configuredFeeAtomic({ sellerUsdAtomic: paid.event.sellerUsdAtomic, feeAddress: currentFeeRecipient, feeBps: currentFeeBps });
|
|
5600
|
+
if (BigInt(paid.event.feeUsdAtomic || 0) < expectedFee) {
|
|
5601
|
+
return { status: 402, body: { error: "payment_unverified", detail: "fee_amount_too_low" } };
|
|
5602
|
+
}
|
|
5570
5603
|
const paidEvent = paid.event;
|
|
5571
5604
|
const remainingUsd = Number(paid.event.sellerUsdAtomic || 0) / 1e6;
|
|
5572
5605
|
if (remainingUsd < BALANCE_EPSILON) {
|
|
@@ -5583,7 +5616,7 @@ function createServeCore({
|
|
|
5583
5616
|
}
|
|
5584
5617
|
const safeRequest = { ...checked.safeRequest, model, max_tokens: bound.maxTok };
|
|
5585
5618
|
try {
|
|
5586
|
-
if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey)) {
|
|
5619
|
+
if (!await redemption.claim(cacheKey, oldLegacyKey ?? cacheKey, { paidAtMs: paid.paidAtMs })) {
|
|
5587
5620
|
return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
|
|
5588
5621
|
}
|
|
5589
5622
|
} catch (e) {
|
|
@@ -5660,23 +5693,24 @@ async function createRelayRuntime(config) {
|
|
|
5660
5693
|
const served = createRedemptionStore({ file: config.redemptionFile, log: config.log });
|
|
5661
5694
|
const drawLocks = /* @__PURE__ */ new Map();
|
|
5662
5695
|
const platform = await fetchPlatformConfig(config);
|
|
5663
|
-
const bootFeeBps = platform.feeBps;
|
|
5664
5696
|
const FEE_REFRESH_MS = 6e4;
|
|
5665
5697
|
let lastConfigFetch = Date.now();
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5698
|
+
let refreshing;
|
|
5699
|
+
const refreshPlatformFee = () => {
|
|
5700
|
+
if (!refreshing) refreshing = (async () => {
|
|
5701
|
+
try {
|
|
5702
|
+
const fresh = await fetchPlatformConfig(config);
|
|
5703
|
+
platform.feeSchedule = fresh.feeSchedule;
|
|
5704
|
+
lastConfigFetch = Date.now();
|
|
5705
|
+
return true;
|
|
5706
|
+
} catch (e) {
|
|
5707
|
+
(config.log ?? console).error?.(`mtok relay: platform config refresh failed (${e.message}); new claims require current policy`);
|
|
5708
|
+
return false;
|
|
5709
|
+
}
|
|
5710
|
+
})().finally(() => {
|
|
5711
|
+
refreshing = null;
|
|
5712
|
+
});
|
|
5713
|
+
return refreshing;
|
|
5680
5714
|
};
|
|
5681
5715
|
const verifier = createOnchainVerifier({ rpcUrls: rpcUrlsFor(platform.chainId, config.rpcFlag), usdcAddress: platform.usdcAddress, expectedChainId: platform.chainId });
|
|
5682
5716
|
if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
|
|
@@ -5701,11 +5735,11 @@ async function createRelayRuntime(config) {
|
|
|
5701
5735
|
sellerAgentId: config.sellerAgentId,
|
|
5702
5736
|
sellerWallet: config.settlementAddr,
|
|
5703
5737
|
dripContractAddress: platform.dripContractAddress,
|
|
5704
|
-
// #654: the fee floor is min(boot, current) so neither a fee increase nor a
|
|
5705
|
-
// decrease can over-demand and strand an honest already-paid draw; the recipient
|
|
5706
|
-
// stays pinned to the boot address (exact-match verify, see the note above).
|
|
5707
5738
|
feeRecipient: platform.feeAddress,
|
|
5708
|
-
feeBps: () =>
|
|
5739
|
+
feeBps: async ({ paidAtMs }) => {
|
|
5740
|
+
if (Date.now() - lastConfigFetch >= FEE_REFRESH_MS && !await refreshPlatformFee()) throw new Error("fee policy refresh failed");
|
|
5741
|
+
return paymentFeeBpsAt(platform.feeSchedule, paidAtMs);
|
|
5742
|
+
},
|
|
5709
5743
|
// #654: per-relay output sanity ceiling (unset => the shared generous default).
|
|
5710
5744
|
maxOutputTokens: config.maxOutputTokens,
|
|
5711
5745
|
screenPayer: payerDenied
|
|
@@ -5734,10 +5768,9 @@ async function createRelayRuntime(config) {
|
|
|
5734
5768
|
active++;
|
|
5735
5769
|
try {
|
|
5736
5770
|
return await withBookingLock(String(body?.bookingId ?? ""), async () => {
|
|
5737
|
-
await refreshPlatformFeeIfStale();
|
|
5738
5771
|
let out = await core.serve(body);
|
|
5739
|
-
if (out.status === 402 && out.body?.detail === "fee_amount_too_low"
|
|
5740
|
-
out = await core.serve(body);
|
|
5772
|
+
if (out.status === 402 && out.body?.detail === "fee_amount_too_low") {
|
|
5773
|
+
out = await refreshPlatformFee() ? await core.serve(body) : { status: 503, body: { error: "fee_policy_unavailable", _bookingId: body.bookingId } };
|
|
5741
5774
|
}
|
|
5742
5775
|
return send(res, out.status, out.body);
|
|
5743
5776
|
});
|
|
@@ -5753,7 +5786,7 @@ async function fetchPlatformConfig(config) {
|
|
|
5753
5786
|
const body = await r.json();
|
|
5754
5787
|
return {
|
|
5755
5788
|
feeAddress: body.feeAddress,
|
|
5756
|
-
feeBps: body.feeBps,
|
|
5789
|
+
feeSchedule: normalizeFeeSchedule(body.feeSchedule ?? [{ effectiveAtMs: 0, feeBps: body.feeBps ?? (body.feeAddress ? void 0 : 0) }]),
|
|
5757
5790
|
dustThresholdUsd: Number(body.dustThresholdUsd) || 1e-3,
|
|
5758
5791
|
chainId: Number(body.chainId ?? 8453),
|
|
5759
5792
|
usdcAddress: body.usdcAddress,
|
package/package.json
CHANGED