mtok-verify 0.1.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/LICENSE +21 -0
- package/README.md +33 -0
- package/errors.js +9 -0
- package/onchain.js +264 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roy Ashbrook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# mtok-verify
|
|
2
|
+
|
|
3
|
+
The on-chain verify core a [mtok.market](https://mtok.market) seller uses before it delivers:
|
|
4
|
+
decode a `MtokDripLedger` **DrawPaid** receipt, check its USDC transfer legs (seller payout + the
|
|
5
|
+
platform fee), and confirm the event actually pays for the draw you are about to serve, all against
|
|
6
|
+
the canonical DrawPaid topic set. Dependency-free, and it runs anywhere `fetch` runs (node, a
|
|
7
|
+
Cloudflare Worker, the browser).
|
|
8
|
+
|
|
9
|
+
This is the single source of truth the platform, the reference relay (`mtok-relay`), and the house
|
|
10
|
+
seller all share, so nobody hand-rolls a verifier that can silently drift from the contract.
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
import { createOnchainVerifier, isDrawPaidTopic, DRAW_PAID_TOPICS } from 'mtok-verify';
|
|
14
|
+
|
|
15
|
+
const verifier = createOnchainVerifier({
|
|
16
|
+
rpcUrls: ['https://mainnet.base.org'],
|
|
17
|
+
usdcAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
18
|
+
expectedChainId: 8453, // pin Base; fail closed on a wrong/spoofed RPC
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
|
|
22
|
+
contractAddress, buyerAgentId, sellerAgentId, bookingId, offerId, model, n,
|
|
23
|
+
requestHash, sellerWallet, feeRecipient,
|
|
24
|
+
});
|
|
25
|
+
if (!paid.ok) refuse(paid.reason); // else serve, bounded by paid.event
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
Read-only public mirror. The source of truth is the private mtok.market
|
|
32
|
+
monorepo; this repo is synced automatically. Do not open pull requests here.
|
|
33
|
+
Home: https://mtok.market
|
package/errors.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function apiError(status, code, message, details) {
|
|
2
|
+
const err = new Error(message);
|
|
3
|
+
err.status = status;
|
|
4
|
+
err.code = code;
|
|
5
|
+
// Optional structured payload surfaced in the HTTP error body (e.g. the
|
|
6
|
+
// per-leg EIP-3009 nonces a buyer must sign each leg with).
|
|
7
|
+
if (details !== undefined) err.details = details;
|
|
8
|
+
return err;
|
|
9
|
+
}
|
package/onchain.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// On-chain payment verifier for non-custodial settlement. Given a buyer-supplied
|
|
2
|
+
// transaction hash, confirm — by reading the chain over plain JSON-RPC (no SDK,
|
|
3
|
+
// so core stays dependency-free + node --test testable with a mocked fetch) —
|
|
4
|
+
// that a USDC transfer of at least the owed amount landed in the right wallet.
|
|
5
|
+
// The platform never touches the money; it only OBSERVES that it moved, then
|
|
6
|
+
// records the chunk. Used by the seller-hosted chunk path (chunks.js) behind
|
|
7
|
+
// live config; in the sandbox the provider stub-accepts instead.
|
|
8
|
+
//
|
|
9
|
+
// The buyer submits the payment (a plain USDC transfer, or an EIP-3009
|
|
10
|
+
// transferWithAuthorization they sign + submit) — either way it emits a USDC
|
|
11
|
+
// Transfer event, which verifyTransfer confirms. The platform observes the
|
|
12
|
+
// result; it never submits the payment, so there is no platform-side
|
|
13
|
+
// authorization/escrow verification here.
|
|
14
|
+
import { apiError } from './errors.js';
|
|
15
|
+
|
|
16
|
+
// keccak256("Transfer(address,address,uint256)")
|
|
17
|
+
export const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
|
|
18
|
+
// DrawPaid has two on-chain shapes: the original (v1), and v2 (#494) which appends
|
|
19
|
+
// `address seller, address buyer` (the pay-time wallets). We match BOTH topics so
|
|
20
|
+
// the fold reads across a contract redeploy: v1 events (old contract, floored out
|
|
21
|
+
// by dripDeployBlock post-cutover) decode without wallets; v2 events carry them.
|
|
22
|
+
export const DRAW_PAID_TOPIC = '0xb0243f80521d0dccd159389597aba96047e60ba5d7a9df12b67e5cb75230ac41';
|
|
23
|
+
export const DRAW_PAID_TOPIC_V2 = '0x94f9e7578a5c019f78ad332dcb2dc5563bcf257d777009ff5cef4118f084a70b';
|
|
24
|
+
export const DRAW_PAID_TOPICS = [DRAW_PAID_TOPIC, DRAW_PAID_TOPIC_V2];
|
|
25
|
+
// The ONE way to test whether a log's topic0 is a DrawPaid (either version),
|
|
26
|
+
// normalized. Every FILTER/DISPATCH site must go through this, so a future
|
|
27
|
+
// event-version bump can never miss a hand-rolled comparison (the v2 redeploy
|
|
28
|
+
// missed exactly one such site, verifyDrawPaid -- an SSOT gap). The singular
|
|
29
|
+
// DRAW_PAID_TOPIC stays only for v1-vs-v2 discrimination in the decoder.
|
|
30
|
+
export const isDrawPaidTopic = (topic) => DRAW_PAID_TOPICS.includes(String(topic || '').toLowerCase());
|
|
31
|
+
|
|
32
|
+
const topicToAddress = (topic) => '0x' + String(topic).slice(-40).toLowerCase();
|
|
33
|
+
const lc = (a) => String(a || '').toLowerCase();
|
|
34
|
+
const strip0x = (v) => String(v || '').replace(/^0x/i, '');
|
|
35
|
+
const wordAt = (hex, i) => strip0x(hex).slice(i * 64, i * 64 + 64);
|
|
36
|
+
const uintWord = (hex, i) => BigInt('0x' + (wordAt(hex, i) || '0'));
|
|
37
|
+
const asciiFromHex = (hex) => {
|
|
38
|
+
const bytes = strip0x(hex);
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = 0; i < bytes.length; i += 2) {
|
|
41
|
+
const b = parseInt(bytes.slice(i, i + 2), 16);
|
|
42
|
+
if (!Number.isFinite(b)) break;
|
|
43
|
+
out.push(b);
|
|
44
|
+
}
|
|
45
|
+
return new TextDecoder().decode(new Uint8Array(out));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function decodeAbiString(dataHex, offsetBytes) {
|
|
49
|
+
const body = strip0x(dataHex);
|
|
50
|
+
const start = Number(offsetBytes) * 2;
|
|
51
|
+
if (!Number.isFinite(start) || start < 0 || start + 64 > body.length) throw new Error('bad_string_offset');
|
|
52
|
+
const len = Number(BigInt('0x' + body.slice(start, start + 64)));
|
|
53
|
+
const textStart = start + 64;
|
|
54
|
+
const textEnd = textStart + len * 2;
|
|
55
|
+
if (!Number.isFinite(len) || len < 0 || textEnd > body.length) throw new Error('bad_string_length');
|
|
56
|
+
return asciiFromHex(body.slice(textStart, textEnd));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function decodeDrawPaidLog(log) {
|
|
60
|
+
const data = log?.data || '0x';
|
|
61
|
+
// v2 (#494) appends two fixed head words after requestHash: seller, buyer wallets.
|
|
62
|
+
// The string offsets (words 0-4) are self-describing, so string decoding is
|
|
63
|
+
// unaffected by the larger head; only the two extra words are shape-dependent.
|
|
64
|
+
const isV2 = String(log?.topics?.[0] || '').toLowerCase() === DRAW_PAID_TOPIC_V2;
|
|
65
|
+
// #(fable review): the FIXED words (n, amounts, requestHash, and the v2 wallets) are
|
|
66
|
+
// read by index with no bounds check, so a truncated `data` would decode to silent
|
|
67
|
+
// ZEROS (n=0, amounts="0") instead of throwing -- a mal-shaped-but-valid-topic log
|
|
68
|
+
// could then slip past a caller that omits a cross-check. Require the full fixed head
|
|
69
|
+
// (11 words v1, 13 words v2) to be present; the strings are already bounds-checked.
|
|
70
|
+
if (strip0x(data).length < (isV2 ? 13 : 11) * 64) throw new Error('short_draw_paid_data');
|
|
71
|
+
const out = {
|
|
72
|
+
drawId: log?.topics?.[1],
|
|
73
|
+
sellerAgentKey: log?.topics?.[2],
|
|
74
|
+
buyerAgentKey: log?.topics?.[3],
|
|
75
|
+
sellerAgentId: decodeAbiString(data, uintWord(data, 0)),
|
|
76
|
+
buyerAgentId: decodeAbiString(data, uintWord(data, 1)),
|
|
77
|
+
bookingId: decodeAbiString(data, uintWord(data, 2)),
|
|
78
|
+
offerId: decodeAbiString(data, uintWord(data, 3)),
|
|
79
|
+
model: decodeAbiString(data, uintWord(data, 4)),
|
|
80
|
+
n: Number(uintWord(data, 5)),
|
|
81
|
+
sellerUsdAtomic: uintWord(data, 6).toString(),
|
|
82
|
+
feeUsdAtomic: uintWord(data, 7).toString(),
|
|
83
|
+
inputPricePerMTokAtomic: uintWord(data, 8).toString(),
|
|
84
|
+
outputPricePerMTokAtomic: uintWord(data, 9).toString(),
|
|
85
|
+
requestHash: '0x' + wordAt(data, 10),
|
|
86
|
+
};
|
|
87
|
+
if (isV2) {
|
|
88
|
+
out.seller = topicToAddress(wordAt(data, 11));
|
|
89
|
+
out.buyer = topicToAddress(wordAt(data, 12));
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createOnchainVerifier({ rpcUrl, rpcUrls, usdcAddress, expectedChainId, fetchImpl = globalThis.fetch,
|
|
95
|
+
// A just-submitted payment can be mined on the payer's RPC yet not YET indexed by
|
|
96
|
+
// ours (cross-RPC propagation lag), so a single receipt lookup returns null and the
|
|
97
|
+
// chunk/settlement is wrongly rejected as tx_not_found_or_pending. Retry the receipt
|
|
98
|
+
// a few times before giving up. Bounded so a genuinely-missing tx still fails fast.
|
|
99
|
+
receiptRetries = 3, receiptRetryMs = 700, sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
100
|
+
} = {}) {
|
|
101
|
+
// Accept one URL, a comma-separated list, or an array — rpc() rotates across them
|
|
102
|
+
// so a single rate-limited or down RPC can't strand a settlement verification (#108).
|
|
103
|
+
const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || '').split(',')).map((s) => String(s).trim()).filter(Boolean);
|
|
104
|
+
const configured = Boolean(urls.length && usdcAddress);
|
|
105
|
+
const usdc = lc(usdcAddress);
|
|
106
|
+
|
|
107
|
+
async function rpcOn(url, method, params) {
|
|
108
|
+
const res = await fetchImpl(url, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'content-type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) throw apiError(502, 'rpc_error', `chain RPC returned ${res.status}`);
|
|
114
|
+
const body = await res.json();
|
|
115
|
+
if (body.error) throw apiError(502, 'rpc_error', `chain RPC: ${body.error.message ?? 'unknown'}`);
|
|
116
|
+
return body.result;
|
|
117
|
+
}
|
|
118
|
+
async function rpc(method, params) {
|
|
119
|
+
let lastErr;
|
|
120
|
+
// #(fable review): once the chain is pinned + verified, only rotate over urls
|
|
121
|
+
// CONFIRMED on the expected chain, so a wrong-chain fallback url can never serve a
|
|
122
|
+
// receipt that gets trusted (the pool-wide cache + rotation let one leak through).
|
|
123
|
+
const pool = (chainPinned && chainOkUrls.size) ? urls.filter((u) => chainOkUrls.has(u)) : urls;
|
|
124
|
+
for (const url of pool) {
|
|
125
|
+
try { return await rpcOn(url, method, params); } catch (e) { lastErr = e; }
|
|
126
|
+
}
|
|
127
|
+
throw lastErr ?? apiError(502, 'rpc_error', 'no chain RPC configured');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// #287: confirm the RPC pool is on the EXPECTED chain before trusting any receipt.
|
|
131
|
+
// Without this, a misconfigured / wrong-chain RPC (or a fallback answering for another
|
|
132
|
+
// chain) could validate a worthless transfer at the same USDC address on a cheap chain
|
|
133
|
+
// as if it were real Base-mainnet USDC. Cache only a SUCCESS, so a transient first-call
|
|
134
|
+
// failure doesn't permanently wedge verification.
|
|
135
|
+
// #(fable review): confirm the chain PER URL, not once pool-wide. urls that report the
|
|
136
|
+
// expected chain go in chainOkUrls; rpc() then only rotates over them, so a
|
|
137
|
+
// wrong-chain fallback in the pool is excluded from every receipt read.
|
|
138
|
+
const chainOkUrls = new Set();
|
|
139
|
+
// #289: coerce up front so an empty/garbage chain id means "no pin" (skip the check),
|
|
140
|
+
// NOT "expected chain 0" -- the latter would brick EVERY verification (no chain reports
|
|
141
|
+
// id 0). Only a positive finite chain id activates the pin.
|
|
142
|
+
const expChain = Number(expectedChainId);
|
|
143
|
+
const chainPinned = Number.isFinite(expChain) && expChain > 0;
|
|
144
|
+
async function assertChain() {
|
|
145
|
+
if (!chainPinned) return true; // no (valid) expected chain configured -> today's behavior
|
|
146
|
+
if (chainOkUrls.size) return true; // already found at least one on-chain url (cached)
|
|
147
|
+
for (const url of urls) {
|
|
148
|
+
try {
|
|
149
|
+
const hex = await rpcOn(url, 'eth_chainId', []);
|
|
150
|
+
if (typeof hex === 'string' && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
|
|
151
|
+
} catch { /* transient / wrong-chain: leave it out, don't cache a failure */ }
|
|
152
|
+
}
|
|
153
|
+
return chainOkUrls.size > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function fetchReceipt(txHash) {
|
|
157
|
+
let receipt = await rpc('eth_getTransactionReceipt', [txHash]);
|
|
158
|
+
for (let i = 0; !receipt && i < receiptRetries; i++) {
|
|
159
|
+
await sleepImpl(receiptRetryMs);
|
|
160
|
+
receipt = await rpc('eth_getTransactionReceipt', [txHash]);
|
|
161
|
+
}
|
|
162
|
+
if (!receipt) return { error: 'tx_not_found_or_pending' };
|
|
163
|
+
if (lc(receipt.transactionHash) !== lc(txHash)) return { error: 'receipt_tx_mismatch' };
|
|
164
|
+
if (receipt.status !== '0x1') return { error: 'tx_failed' };
|
|
165
|
+
return { receipt };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// #(fable review): match a Transfer(from -> to, amount) that (a) has the full 3
|
|
169
|
+
// topics, (b) optionally binds `from` to the expected payer, and (c) is not already
|
|
170
|
+
// consumed by another leg (so one log can't satisfy BOTH the seller and fee legs when
|
|
171
|
+
// sellerWallet == feeRecipient). Returns the matched log `index` so the caller can
|
|
172
|
+
// mark it consumed.
|
|
173
|
+
function findUsdcTransfer(receipt, { to, minAtomic, from, consumed } = {}) {
|
|
174
|
+
const want = lc(to);
|
|
175
|
+
const wantFrom = from ? lc(from) : null;
|
|
176
|
+
const logs = receipt.logs || [];
|
|
177
|
+
let sawRecipient = false;
|
|
178
|
+
for (let i = 0; i < logs.length; i++) {
|
|
179
|
+
if (consumed && consumed.has(i)) continue;
|
|
180
|
+
const l = logs[i];
|
|
181
|
+
if (lc(l.address) !== usdc || l.topics?.[0] !== TRANSFER_TOPIC || l.topics.length !== 3) continue;
|
|
182
|
+
if (topicToAddress(l.topics[2]) !== want) continue;
|
|
183
|
+
if (wantFrom && topicToAddress(l.topics[1]) !== wantFrom) continue;
|
|
184
|
+
sawRecipient = true;
|
|
185
|
+
let amount;
|
|
186
|
+
try { amount = BigInt(l.data); } catch { continue; }
|
|
187
|
+
if (amount < BigInt(minAtomic)) continue;
|
|
188
|
+
return { ok: true, from: topicToAddress(l.topics[1]), amount: amount.toString(), index: i };
|
|
189
|
+
}
|
|
190
|
+
return { ok: false, reason: sawRecipient ? 'amount_too_low' : 'no_matching_usdc_transfer' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
configured,
|
|
195
|
+
|
|
196
|
+
// Confirm txHash is a successful USDC transfer of >= minAtomic to `to`.
|
|
197
|
+
// Returns { ok, reason?, from?, amount? }; never throws on a bad payment
|
|
198
|
+
// (only on an RPC transport failure).
|
|
199
|
+
async verifyTransfer(txHash, { to, minAtomic }) {
|
|
200
|
+
// #287: never trust a receipt from a wrong-chain RPC (a misconfigured / fallback
|
|
201
|
+
// RPC answering for another chain could otherwise validate a worthless transfer).
|
|
202
|
+
if (!(await assertChain())) return { ok: false, reason: 'wrong_chain' };
|
|
203
|
+
const got = await fetchReceipt(txHash);
|
|
204
|
+
if (got.error) return { ok: false, reason: got.error };
|
|
205
|
+
return findUsdcTransfer(got.receipt, { to, minAtomic });
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
async verifyDrawPaid(txHash, {
|
|
209
|
+
contractAddress,
|
|
210
|
+
buyerAgentId,
|
|
211
|
+
sellerAgentId,
|
|
212
|
+
bookingId,
|
|
213
|
+
offerId,
|
|
214
|
+
model,
|
|
215
|
+
n,
|
|
216
|
+
requestHash,
|
|
217
|
+
sellerWallet,
|
|
218
|
+
feeRecipient,
|
|
219
|
+
minSellerAtomic = 0n,
|
|
220
|
+
} = {}) {
|
|
221
|
+
if (!(await assertChain())) return { ok: false, reason: 'wrong_chain' };
|
|
222
|
+
const contract = lc(contractAddress);
|
|
223
|
+
if (!contract) return { ok: false, reason: 'contract_not_configured' };
|
|
224
|
+
const got = await fetchReceipt(txHash);
|
|
225
|
+
if (got.error) return { ok: false, reason: got.error };
|
|
226
|
+
|
|
227
|
+
const log = (got.receipt.logs || []).find((l) =>
|
|
228
|
+
lc(l.address) === contract
|
|
229
|
+
&& isDrawPaidTopic(l.topics?.[0])
|
|
230
|
+
);
|
|
231
|
+
if (!log) return { ok: false, reason: 'no_draw_paid_event' };
|
|
232
|
+
|
|
233
|
+
let event;
|
|
234
|
+
try { event = decodeDrawPaidLog(log); } catch { return { ok: false, reason: 'malformed_draw_paid_event' }; }
|
|
235
|
+
if (buyerAgentId != null && event.buyerAgentId !== String(buyerAgentId)) return { ok: false, reason: 'buyer_agent_mismatch' };
|
|
236
|
+
if (sellerAgentId != null && event.sellerAgentId !== String(sellerAgentId)) return { ok: false, reason: 'seller_agent_mismatch' };
|
|
237
|
+
if (bookingId != null && event.bookingId !== String(bookingId)) return { ok: false, reason: 'booking_mismatch' };
|
|
238
|
+
if (offerId != null && event.offerId !== String(offerId)) return { ok: false, reason: 'offer_mismatch' };
|
|
239
|
+
if (model != null && event.model !== String(model)) return { ok: false, reason: 'model_mismatch' };
|
|
240
|
+
if (n != null && event.n !== Number(n)) return { ok: false, reason: 'draw_n_mismatch' };
|
|
241
|
+
if (requestHash != null && lc(event.requestHash) !== lc(requestHash)) return { ok: false, reason: 'request_hash_mismatch' };
|
|
242
|
+
if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: 'amount_too_low' };
|
|
243
|
+
|
|
244
|
+
// #(fable review): bind each leg's `from` to the pay-time buyer (event.buyer,
|
|
245
|
+
// emitted on v2; undefined on v1 -> no from-check, backward-compatible), and
|
|
246
|
+
// CONSUME the seller-leg log so the fee leg can't be satisfied by the SAME transfer
|
|
247
|
+
// when sellerWallet == feeRecipient.
|
|
248
|
+
const consumed = new Set();
|
|
249
|
+
let sellerTransfer = null;
|
|
250
|
+
if (sellerWallet) {
|
|
251
|
+
sellerTransfer = findUsdcTransfer(got.receipt, { to: sellerWallet, minAtomic: BigInt(event.sellerUsdAtomic), from: event.buyer });
|
|
252
|
+
if (!sellerTransfer.ok) return { ok: false, reason: 'seller_transfer_' + sellerTransfer.reason };
|
|
253
|
+
if (sellerTransfer.index != null) consumed.add(sellerTransfer.index);
|
|
254
|
+
}
|
|
255
|
+
let feeTransfer = null;
|
|
256
|
+
if (feeRecipient && BigInt(event.feeUsdAtomic) > 0n) {
|
|
257
|
+
feeTransfer = findUsdcTransfer(got.receipt, { to: feeRecipient, minAtomic: BigInt(event.feeUsdAtomic), from: event.buyer, consumed });
|
|
258
|
+
if (!feeTransfer.ok) return { ok: false, reason: 'fee_transfer_' + feeTransfer.reason };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { ok: true, event, from: sellerTransfer?.from ?? feeTransfer?.from ?? null };
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mtok-verify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The on-chain verify core for mtok.market sellers: decode + verify a MtokDripLedger DrawPaid receipt (and its USDC transfer legs) against a paid draw, with the canonical DrawPaid topic set. Dependency-free, runs anywhere fetch runs (node, a CF Worker). The SSOT the platform + reference relay + house seller all share.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./onchain.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"onchain.js",
|
|
11
|
+
"errors.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mtok",
|
|
19
|
+
"base",
|
|
20
|
+
"usdc",
|
|
21
|
+
"on-chain",
|
|
22
|
+
"verify",
|
|
23
|
+
"drawpaid",
|
|
24
|
+
"settlement"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://mtok.market",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/mtok-market/verify.git"
|
|
34
|
+
}
|
|
35
|
+
}
|