@elisym/commerce 0.2.0 → 0.4.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/dist/buyer.js ADDED
@@ -0,0 +1,2267 @@
1
+ import { MAX_FUTURE_SKEW_SECS, isPublicHostname, KIND_INBOX_RELAYS, decodeProductNaddr, verifyOffer, priceInSubunits, productAddress, deriveOrderPaymentReference, buildOrderMessage, wrapOrderMessage, unwrapOrderMessage, KIND_GIFT_WRAP, isOfferPayout, parseCaip19 } from './chunk-DYXVY6VD.js';
2
+ import { verifyEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
3
+ import { getTransactionDecoder, partiallySignTransaction, getTransactionEncoder, isAddress, address, getCompiledTransactionMessageDecoder, verifySignature, getPublicKeyFromAddress, getBase64Decoder, getBase58Decoder, createNoopSigner, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, compileTransaction } from '@solana/kit';
4
+ import { bytesToHex, hexToBytes, normalizeURL } from 'nostr-tools/utils';
5
+ import { parsePaymentRequest, composeSolanaPaymentRequest, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, directInstructionsFromCompiledMessage, boundTransferAmount, PaymentRequestV2Schema, buildPaymentInstructions, getProtocolProgramId, verifyDirectSolanaPayment } from '@elisym/pay-core';
6
+ import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda } from '@solana-program/token';
7
+ import { readFinalizedBlock, MIN_PAY_WINDOW_SECS, buildTempoPaymentCalls, readTxHash, verifyTempoPayment, EVM_LATE_PAYMENT_GRACE_SECS, checkEvmChain, checkTempoReceivePolicies, composeTempoPaymentRequest } from '@elisym/pay-core/evm';
8
+ import { SimplePool } from 'nostr-tools/pool';
9
+
10
+ // src/buyer/constants.ts
11
+ var DEFAULT_RELAYS = [
12
+ "wss://relay.elisym.network",
13
+ "wss://relay.damus.io",
14
+ "wss://nos.lol",
15
+ "wss://relay.nostr.band"
16
+ ];
17
+ var STORE_RELAY_CAP = 8;
18
+ var MAX_RELAY_URL_LENGTH = 256;
19
+ var OFFER_SNAPSHOT_MAX_AGE_SECS = 120;
20
+ var ORDER_ACK_TARGET = 2;
21
+ var RELAY_QUERY_MAX_WAIT_MS = 4e3;
22
+ var RELAY_QUERY_DEADLINE_MS = 15e3;
23
+ var RELAY_CONNECT_MAX_WAIT_MS = 6e3;
24
+ var RELAY_PUBLISH_DEADLINE_MS = 2e4;
25
+ var SUBSCRIBE_RETRY_MAX_MS = 6e4;
26
+ var SUBSCRIBE_RETRY_MS = [1e3, 5e3, 15e3, SUBSCRIBE_RETRY_MAX_MS];
27
+ var SUBSCRIBE_STABLE_MS = 3e4;
28
+ var STORE_WRITE_ATTEMPTS = 5;
29
+ var MAX_CLOCK_SKEW_SECS = 5 * 60;
30
+ var WRAP_BACKDATE_SECS = 2 * 24 * 60 * 60;
31
+ var MERCHANT_CATCH_UP_SECS = 3 * 24 * 60 * 60;
32
+ var PAY_CUTOFF_SECS = 60 * 60;
33
+ var PAYMENT_SCAN_MARGIN_SECS = 30 * 60;
34
+ var SOLANA_COMPUTE_UNIT_LIMIT = 2e5;
35
+ function isEventShaped(event) {
36
+ if (typeof event !== "object" || event === null) {
37
+ return false;
38
+ }
39
+ const record = { ...event };
40
+ const { id, pubkey, created_at: createdAt, kind, tags, content, sig } = record;
41
+ return typeof id === "string" && typeof pubkey === "string" && typeof createdAt === "number" && Number.isInteger(createdAt) && typeof kind === "number" && Number.isInteger(kind) && typeof content === "string" && typeof sig === "string" && Array.isArray(tags) && tags.every((tag) => Array.isArray(tag) && tag.every((item) => typeof item === "string"));
42
+ }
43
+ function isGenuineEvent(event) {
44
+ if (!isEventShaped(event)) {
45
+ return false;
46
+ }
47
+ return verifyEvent({
48
+ id: event.id,
49
+ pubkey: event.pubkey,
50
+ created_at: event.created_at,
51
+ kind: event.kind,
52
+ tags: event.tags,
53
+ content: event.content,
54
+ sig: event.sig
55
+ });
56
+ }
57
+ function newestGenuine(events, kind, author, now) {
58
+ let newest;
59
+ for (const event of events) {
60
+ if (!isEventShaped(event) || event.kind !== kind || event.pubkey !== author || event.created_at > now + MAX_FUTURE_SKEW_SECS || !isGenuineEvent(event)) {
61
+ continue;
62
+ }
63
+ if (newest === void 0 || event.created_at > newest.created_at || event.created_at === newest.created_at && event.id < newest.id) {
64
+ newest = event;
65
+ }
66
+ }
67
+ return newest;
68
+ }
69
+ function nowSecs() {
70
+ return Math.floor(Date.now() / 1e3);
71
+ }
72
+
73
+ // src/buyer/relays.ts
74
+ function normalizeRelayUrl(value) {
75
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_RELAY_URL_LENGTH) {
76
+ return void 0;
77
+ }
78
+ let url;
79
+ try {
80
+ url = new URL(value);
81
+ } catch {
82
+ return void 0;
83
+ }
84
+ if (url.protocol !== "wss:" || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !isPublicHostname(url.hostname)) {
85
+ return void 0;
86
+ }
87
+ const path = url.pathname.replace(/\/+/g, "/").replace(/\/$/, "");
88
+ return `wss://${url.host}${path}`;
89
+ }
90
+ function relayHost(url) {
91
+ try {
92
+ return new URL(url).host;
93
+ } catch {
94
+ return url;
95
+ }
96
+ }
97
+ function uniqueRelays(values) {
98
+ const relays = [];
99
+ for (const value of values) {
100
+ const relay = normalizeRelayUrl(value);
101
+ if (relay !== void 0 && !relays.includes(relay)) {
102
+ relays.push(relay);
103
+ }
104
+ }
105
+ return relays;
106
+ }
107
+ function storeRelays(sources) {
108
+ return uniqueRelays([
109
+ ...sources.inbox ?? [],
110
+ ...sources.acknowledged ?? [],
111
+ ...sources.hints ?? []
112
+ ]).slice(0, STORE_RELAY_CAP);
113
+ }
114
+ function readRelays(sources) {
115
+ const named = storeRelays(sources);
116
+ return [...DEFAULT_RELAYS, ...named.filter((relay) => !DEFAULT_RELAYS.includes(relay))];
117
+ }
118
+
119
+ // src/buyer/inbox.ts
120
+ function newestStoreInbox(events, storePubkey, now = nowSecs()) {
121
+ const event = newestGenuine(events, KIND_INBOX_RELAYS, storePubkey, now);
122
+ if (event === void 0) {
123
+ return void 0;
124
+ }
125
+ const relays = uniqueRelays(
126
+ event.tags.filter((tag) => tag[0] === "relay").map((tag) => tag[1])
127
+ ).slice(0, STORE_RELAY_CAP);
128
+ return relays.length === 0 ? void 0 : { relays, event };
129
+ }
130
+ async function readStoreInbox(client, sources, storePubkey, now) {
131
+ const events = await client.query(readRelays(sources), [
132
+ { kinds: [KIND_INBOX_RELAYS], authors: [storePubkey] }
133
+ ]);
134
+ return newestStoreInbox(events, storePubkey, now);
135
+ }
136
+ function localSolanaWallet(signer) {
137
+ return {
138
+ address: signer.address,
139
+ async signTransaction(bytes) {
140
+ const decoded = getTransactionDecoder().decode(bytes);
141
+ const signed = await partiallySignTransaction([signer.keyPair], decoded);
142
+ return new Uint8Array(getTransactionEncoder().encode(signed));
143
+ }
144
+ };
145
+ }
146
+ async function readChainTime(rpc) {
147
+ const slot = await rpc.getSlot({ commitment: "finalized" }).send();
148
+ for (let back = 0n; back < 5n; back += 1n) {
149
+ const time = await rpc.getBlockTime(slot - back).send();
150
+ if (time !== null) {
151
+ return Number(time);
152
+ }
153
+ }
154
+ throw new Error("no block time");
155
+ }
156
+
157
+ // src/buyer/offer.ts
158
+ var CONFIRM_WARNINGS = [
159
+ "payout_recently_changed",
160
+ "payout_changed"
161
+ ];
162
+ function isPageOrigin(value) {
163
+ if (typeof value !== "string" || value === "null") {
164
+ return false;
165
+ }
166
+ try {
167
+ const url = new URL(value);
168
+ return (url.protocol === "https:" || url.protocol === "http:") && url.origin === value;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ function isSnapshotStale(snapshotAt, now = nowSecs()) {
174
+ return now - snapshotAt > OFFER_SNAPSHOT_MAX_AGE_SECS;
175
+ }
176
+ function refuse(refusal2, message) {
177
+ return { ok: false, refusal: refusal2, message };
178
+ }
179
+ async function loadOffer(naddr, options) {
180
+ if (!isPageOrigin(options.pageOrigin)) {
181
+ return refuse("bad_page_origin", "The page embedding the checkout has no usable origin");
182
+ }
183
+ return loadVerified(naddr, options, options.pageOrigin);
184
+ }
185
+ function loadOfferForAgent(naddr, options) {
186
+ return loadVerified(naddr, options, void 0);
187
+ }
188
+ async function loadVerified(naddr, options, pageOrigin) {
189
+ const pointer = decodeProductNaddr(naddr);
190
+ if (pointer === void 0) {
191
+ return refuse("bad_pointer", "Not a product naddr");
192
+ }
193
+ const now = options.now ?? nowSecs();
194
+ const hints = uniqueRelays(pointer.relays);
195
+ const relays = readRelays({ hints });
196
+ const verification = await verifyOffer(
197
+ naddr,
198
+ {
199
+ fetchEvents: (filters) => options.client.query(relays, filters),
200
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch },
201
+ ...options.resolveDomain === void 0 ? {} : { resolveDomain: options.resolveDomain }
202
+ },
203
+ {
204
+ now,
205
+ ...pageOrigin === void 0 ? {} : { pageOrigin },
206
+ ...options.strictOrigin === true ? { strictOrigin: true } : {},
207
+ ...options.pins?.pinnedOwnerPubkey === void 0 ? {} : { pinnedOwnerPubkey: options.pins.pinnedOwnerPubkey },
208
+ ...options.pins?.knownPayouts === void 0 ? {} : { knownPayouts: options.pins.knownPayouts }
209
+ }
210
+ );
211
+ if (!verification.ok) {
212
+ return refuse(verification.refusal, verification.message);
213
+ }
214
+ const { offer } = verification;
215
+ if (offer.warnings.includes("origin_mismatch")) {
216
+ return refuse("origin_mismatch", "This store does not sell on this site");
217
+ }
218
+ const payouts = [];
219
+ for (const target of offer.payouts) {
220
+ const { chain, asset } = target.caip19;
221
+ if (!options.families.includes(chain.family)) {
222
+ continue;
223
+ }
224
+ if (options.network !== void 0 && chain.network !== options.network) {
225
+ continue;
226
+ }
227
+ let amount;
228
+ try {
229
+ amount = priceInSubunits(offer.product.price, asset);
230
+ } catch {
231
+ continue;
232
+ }
233
+ if (amount > 0n) {
234
+ payouts.push({ target, amount });
235
+ }
236
+ }
237
+ if (payouts.length === 0) {
238
+ return refuse("no_payable_payout", "This product cannot be paid here");
239
+ }
240
+ return {
241
+ ok: true,
242
+ offer,
243
+ productAddress: productAddress(offer.product),
244
+ payouts,
245
+ confirm: offer.warnings.filter((warning) => CONFIRM_WARNINGS.includes(warning)),
246
+ notices: offer.warnings.filter((warning) => !CONFIRM_WARNINGS.includes(warning)),
247
+ hints,
248
+ relays,
249
+ snapshotAt: now
250
+ };
251
+ }
252
+
253
+ // src/buyer/order-record.ts
254
+ var TERMINAL_STATES = ["completed", "refunded"];
255
+ function isTerminal(record) {
256
+ return TERMINAL_STATES.includes(record.state);
257
+ }
258
+ function holdsPayExclusion(record) {
259
+ if (isTerminal(record)) {
260
+ return false;
261
+ }
262
+ if (record.state === "paid" || record.paidTx !== void 0) {
263
+ return true;
264
+ }
265
+ if (record.state === "ended-unpaid" || record.state === "blocked") {
266
+ return false;
267
+ }
268
+ return record.marker !== void 0;
269
+ }
270
+ function showRank(record) {
271
+ if (isTerminal(record)) {
272
+ return 2;
273
+ }
274
+ if (record.state === "ended-unpaid") {
275
+ return 3;
276
+ }
277
+ if (record.state === "paid" || record.state === "blocked" || record.paidTx !== void 0) {
278
+ return 0;
279
+ }
280
+ const cancelled = record.status?.status === "cancelled";
281
+ return record.status !== void 0 && (!cancelled || holdsPayExclusion(record)) ? 0 : 1;
282
+ }
283
+ function newer(left, right) {
284
+ if (left.createdAt !== right.createdAt) {
285
+ return left.createdAt > right.createdAt ? left : right;
286
+ }
287
+ return left.orderId < right.orderId ? left : right;
288
+ }
289
+ function recordToShow(records) {
290
+ let shown;
291
+ for (const record of records) {
292
+ if (shown === void 0 || showRank(record) < showRank(shown)) {
293
+ shown = record;
294
+ } else if (showRank(record) === showRank(shown)) {
295
+ shown = newer(shown, record);
296
+ }
297
+ }
298
+ return shown;
299
+ }
300
+
301
+ // src/buyer/order-flow.ts
302
+ function mediumOf(chain) {
303
+ if (chain.network === "mainnet") {
304
+ return chain.slug;
305
+ }
306
+ return chain.slug === "tempo" ? "tempo-moderato" : `${chain.slug}-${chain.network}`;
307
+ }
308
+ function clockAgrees(chainTime, deviceTime) {
309
+ return Math.abs(chainTime - deviceTime) <= MAX_CLOCK_SKEW_SECS;
310
+ }
311
+ function compareOffers(shown, confirmed, fresh) {
312
+ const match = fresh.payouts.find(
313
+ (payout) => payout.target.caip19.id === shown.target.caip19.id && payout.target.address === shown.target.address
314
+ );
315
+ if (match === void 0) {
316
+ return "gone";
317
+ }
318
+ if (match.amount !== shown.amount) {
319
+ return "changed";
320
+ }
321
+ return fresh.confirm.every((warning) => confirmed.includes(warning)) ? "same" : "changed";
322
+ }
323
+ async function placeOrder(input, deps) {
324
+ const { offer, payout } = input;
325
+ if (!clockAgrees(input.chainTime, input.deviceTime)) {
326
+ return { ok: false, reason: "clock_skew" };
327
+ }
328
+ const offered = offer.payouts.some(
329
+ (candidate) => candidate.target.caip19.id === payout.target.caip19.id && candidate.target.address === payout.target.address && candidate.amount === payout.amount
330
+ );
331
+ if (isSnapshotStale(offer.snapshotAt, input.deviceTime) || !offered) {
332
+ return { ok: false, reason: "stale_offer" };
333
+ }
334
+ const storePubkey = offer.offer.storePubkey;
335
+ const inbox = await readStoreInbox(
336
+ deps.readClient,
337
+ { hints: offer.hints },
338
+ storePubkey,
339
+ input.deviceTime
340
+ );
341
+ if (inbox === void 0) {
342
+ return { ok: false, reason: "no_store_inbox" };
343
+ }
344
+ const buyerKey = generateSecretKey();
345
+ const buyerPubkey = getPublicKey(buyerKey);
346
+ const orderId = crypto.randomUUID();
347
+ const reference = deriveOrderPaymentReference({ storePubkey, buyerPubkey, orderId });
348
+ const rumor = buildOrderMessage(
349
+ {
350
+ type: "order",
351
+ storePubkey,
352
+ orderId,
353
+ items: [{ product: offer.productAddress, quantity: 1 }],
354
+ total: {
355
+ amount: offer.offer.product.price.amount,
356
+ currency: offer.offer.product.price.currency
357
+ },
358
+ ...input.email === void 0 ? {} : { email: input.email }
359
+ },
360
+ input.chainTime
361
+ );
362
+ const wrapped = wrapOrderMessage(rumor, buyerKey, storePubkey);
363
+ const chain = payout.target.caip19.chain;
364
+ const record = {
365
+ orderId,
366
+ productAddress: offer.productAddress,
367
+ storePubkey,
368
+ buyerSecretKey: bytesToHex(buyerKey),
369
+ buyerPubkey,
370
+ createdAt: input.chainTime,
371
+ version: 1,
372
+ state: "created",
373
+ payout: { caip19: payout.target.caip19.id, address: payout.target.address },
374
+ amount: payout.amount.toString(),
375
+ medium: mediumOf(chain),
376
+ reference: chain.family === "solana" ? reference.solana : reference.tempo,
377
+ offer: offer.offer,
378
+ // Kept before it is sent: a crash in between is resumed by sending the same wrap.
379
+ orderWrap: wrapped.recipientWrap,
380
+ inboxRelays: inbox.relays,
381
+ acknowledgedRelays: []
382
+ };
383
+ await deps.store.add(record);
384
+ return acknowledge(record, inbox.relays, inbox.relays, deps);
385
+ }
386
+ async function acknowledge(record, sendTo, inbox, deps) {
387
+ if (record.orderWrap === void 0) {
388
+ return { ok: false, reason: "not_acknowledged", record };
389
+ }
390
+ const client = deps.clientFor(hexToBytes(record.buyerSecretKey));
391
+ try {
392
+ const sent = await client.publish(sendTo, record.orderWrap);
393
+ let current = record;
394
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS; attempt += 1) {
395
+ const acknowledged = [.../* @__PURE__ */ new Set([...current.acknowledgedRelays, ...sent.accepted])];
396
+ const written = await deps.store.update(current.orderId, current.version, {
397
+ state: "ordered",
398
+ inboxRelays: [...inbox],
399
+ acknowledgedRelays: acknowledged
400
+ });
401
+ if (written.ok) {
402
+ return { ok: true, record: await settleHeldStatus(deps.store, written.record) };
403
+ }
404
+ if (written.reason !== "conflict") {
405
+ const kept = await deps.store.update(current.orderId, current.version, {
406
+ inboxRelays: [...inbox],
407
+ acknowledgedRelays: acknowledged
408
+ });
409
+ if (kept.ok) {
410
+ return { ok: false, reason: "not_acknowledged", record: kept.record };
411
+ }
412
+ if (kept.reason !== "conflict") {
413
+ return { ok: false, reason: "not_acknowledged", record: current };
414
+ }
415
+ }
416
+ const stored = await deps.store.get(current.orderId);
417
+ if (stored === void 0) {
418
+ return { ok: false, reason: "not_acknowledged", record: current };
419
+ }
420
+ if (stored.state !== "created") {
421
+ return { ok: true, record: stored };
422
+ }
423
+ current = stored;
424
+ }
425
+ return { ok: false, reason: "not_acknowledged", record: current };
426
+ } finally {
427
+ client.close();
428
+ }
429
+ }
430
+ async function sendReceipt(record, tx, createdAt, deps) {
431
+ const buyerKey = hexToBytes(record.buyerSecretKey);
432
+ const rumor = buildOrderMessage(
433
+ {
434
+ type: "receipt",
435
+ storePubkey: record.storePubkey,
436
+ orderId: record.orderId,
437
+ payment: { medium: record.medium, reference: record.reference, tx }
438
+ },
439
+ createdAt
440
+ );
441
+ const wrapped = wrapOrderMessage(rumor, buyerKey, record.storePubkey);
442
+ const written = await deps.store.update(record.orderId, record.version, {
443
+ receiptWrap: wrapped.recipientWrap
444
+ });
445
+ if (written.ok) {
446
+ const client = deps.clientFor(buyerKey);
447
+ try {
448
+ await client.publish(record.inboxRelays, wrapped.recipientWrap);
449
+ } finally {
450
+ client.close();
451
+ }
452
+ }
453
+ return written;
454
+ }
455
+ async function resumeOrder(record, deps, now) {
456
+ const fresh = await readStoreInbox(
457
+ deps.readClient,
458
+ { acknowledged: record.acknowledgedRelays },
459
+ record.storePubkey,
460
+ now
461
+ );
462
+ const relays = storeRelays({
463
+ inbox: fresh?.relays ?? record.inboxRelays,
464
+ acknowledged: record.acknowledgedRelays
465
+ });
466
+ if (record.state === "created") {
467
+ if (fresh === void 0) {
468
+ return { record, relays };
469
+ }
470
+ const placed = await acknowledge(record, relays, fresh.relays, deps);
471
+ return { record: placed.record ?? record, relays };
472
+ }
473
+ const client = deps.clientFor(hexToBytes(record.buyerSecretKey));
474
+ try {
475
+ for (const wrap of [record.orderWrap, record.receiptWrap]) {
476
+ if (wrap !== void 0) {
477
+ await client.publish(relays, wrap);
478
+ }
479
+ }
480
+ } finally {
481
+ client.close();
482
+ }
483
+ const sameInbox = fresh !== void 0 && fresh.relays.length === record.inboxRelays.length && fresh.relays.every((relay, index) => record.inboxRelays[index] === relay);
484
+ if (fresh !== void 0 && !sameInbox && !isTerminal(record)) {
485
+ const moved = await deps.store.update(record.orderId, record.version, {
486
+ inboxRelays: fresh.relays
487
+ });
488
+ if (moved.ok) {
489
+ return { record: moved.record, relays };
490
+ }
491
+ }
492
+ return { record, relays };
493
+ }
494
+ function statusFor(record, wrap) {
495
+ const unwrapped = unwrapOrderMessage(wrap, hexToBytes(record.buyerSecretKey));
496
+ if (unwrapped === void 0 || unwrapped.senderPubkey !== record.storePubkey || unwrapped.message.type !== "status" || unwrapped.message.orderId !== record.orderId || unwrapped.message.buyerPubkey !== record.buyerPubkey) {
497
+ return void 0;
498
+ }
499
+ return unwrapped.message;
500
+ }
501
+ function deliveryLink(value) {
502
+ try {
503
+ const url = new URL(value);
504
+ return url.protocol === "https:" ? url.href : void 0;
505
+ } catch {
506
+ return void 0;
507
+ }
508
+ }
509
+ function settledState(message) {
510
+ if (message.status === "completed") {
511
+ return "completed";
512
+ }
513
+ if (message.status === "cancelled" && message.refund !== void 0) {
514
+ return "refunded";
515
+ }
516
+ return void 0;
517
+ }
518
+ async function rememberDelivery(store, record) {
519
+ await store.rememberDelivery(
520
+ record.storePubkey,
521
+ record.offer.ownerPubkey,
522
+ record.offer.payouts.map((target) => ({ caip19: target.caip19.id, address: target.address }))
523
+ );
524
+ }
525
+ async function settleHeldStatus(store, record) {
526
+ const held = record.status;
527
+ let settles;
528
+ if (held?.status === "completed") {
529
+ settles = "completed";
530
+ } else if (held?.status === "cancelled" && held.refunded === true) {
531
+ settles = "refunded";
532
+ }
533
+ if (settles === void 0) {
534
+ return record;
535
+ }
536
+ const settled = await store.update(record.orderId, record.version, { state: settles });
537
+ if (!settled.ok) {
538
+ return record;
539
+ }
540
+ if (settles === "completed") {
541
+ await rememberDelivery(store, settled.record);
542
+ }
543
+ return settled.record;
544
+ }
545
+ async function applyStatus(store, orderId, message, at) {
546
+ const status = {
547
+ status: message.status,
548
+ at,
549
+ ...message.delivery === void 0 ? {} : { delivery: message.delivery.value },
550
+ ...message.refund === void 0 ? {} : { refunded: true }
551
+ };
552
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS; attempt += 1) {
553
+ const current = await store.get(orderId);
554
+ if (current === void 0) {
555
+ return void 0;
556
+ }
557
+ const settles = settledState(message);
558
+ const heardBefore = current.status?.status === status.status && current.status.delivery === status.delivery && current.status.refunded === status.refunded;
559
+ if (heardBefore && (settles === void 0 || current.state === settles)) {
560
+ return current;
561
+ }
562
+ const withState = await store.update(orderId, current.version, {
563
+ status,
564
+ ...settles === void 0 ? {} : { state: settles }
565
+ });
566
+ if (withState.ok) {
567
+ if (settles === "completed") {
568
+ await rememberDelivery(store, withState.record);
569
+ }
570
+ return withState.record;
571
+ }
572
+ if (withState.reason === "conflict") {
573
+ continue;
574
+ }
575
+ if (heardBefore) {
576
+ return current;
577
+ }
578
+ const alone = await store.update(orderId, current.version, { status });
579
+ if (alone.ok) {
580
+ return alone.record;
581
+ }
582
+ if (alone.reason !== "conflict") {
583
+ return void 0;
584
+ }
585
+ }
586
+ return void 0;
587
+ }
588
+ function listenForStatus(record, relays, deps, onStatus) {
589
+ const client = deps.clientFor(hexToBytes(record.buyerSecretKey));
590
+ const subscription = client.subscribe(
591
+ relays,
592
+ {
593
+ kinds: [KIND_GIFT_WRAP],
594
+ "#p": [record.buyerPubkey],
595
+ since: record.createdAt - WRAP_BACKDATE_SECS - MAX_FUTURE_SKEW_SECS
596
+ },
597
+ (wrap) => {
598
+ const status = statusFor(record, wrap);
599
+ if (status !== void 0) {
600
+ onStatus(status);
601
+ }
602
+ }
603
+ );
604
+ return {
605
+ close() {
606
+ subscription.close();
607
+ client.close();
608
+ }
609
+ };
610
+ }
611
+
612
+ // src/buyer/order-store.ts
613
+ var PATCH_KEYS = [
614
+ "state",
615
+ "paymentRequest",
616
+ "orderWrap",
617
+ "receiptWrap",
618
+ "inboxRelays",
619
+ "acknowledgedRelays",
620
+ "paidTx",
621
+ "paidAt",
622
+ "status",
623
+ "endedBy",
624
+ "confirmedOverIds"
625
+ ];
626
+ var STATUS_SUCCESSORS = {
627
+ pending: ["pending", "confirmed", "completed", "cancelled"],
628
+ confirmed: ["confirmed", "completed", "cancelled"],
629
+ cancelled: ["cancelled", "completed"],
630
+ completed: ["completed"]
631
+ };
632
+ var PAID_STATES = ["paid", "completed", "refunded"];
633
+ var NEXT_STATES = {
634
+ created: ["ordered"],
635
+ ordered: ["paid", "completed", "refunded", "ended-unpaid"],
636
+ paying: ["paid", "completed", "refunded", "blocked"],
637
+ paid: ["completed", "refunded"],
638
+ "ended-unpaid": ["paid", "completed", "refunded", "blocked"],
639
+ blocked: ["paid", "completed", "refunded"],
640
+ completed: [],
641
+ refunded: []
642
+ };
643
+ function patchRefusal(current, patch) {
644
+ if (isTerminal(current)) {
645
+ return "not_ready";
646
+ }
647
+ if (patch.state !== void 0 && patch.state !== current.state && !NEXT_STATES[current.state].includes(patch.state)) {
648
+ return "not_ready";
649
+ }
650
+ const nextState = patch.state ?? current.state;
651
+ if (patch.paymentRequest !== void 0 && (current.paymentRequest !== void 0 || nextState !== "ordered")) {
652
+ return "not_ready";
653
+ }
654
+ if (patch.state === "blocked" && current.state !== "blocked" && !current.payout.caip19.startsWith("eip155:")) {
655
+ return "not_ready";
656
+ }
657
+ if (patch.paidTx !== void 0 && (current.paidTx !== void 0 || !PAID_STATES.includes(nextState))) {
658
+ return "not_ready";
659
+ }
660
+ if (patch.paidAt !== void 0 && patch.paidTx === void 0) {
661
+ return "not_ready";
662
+ }
663
+ const tempo = railOf(current) === "tempo";
664
+ const endsUnpaid = patch.state === "ended-unpaid" && current.state !== "ended-unpaid";
665
+ if (patch.endedBy !== void 0 && (!tempo || !endsUnpaid || patch.endedBy !== "nothing")) {
666
+ return "not_ready";
667
+ }
668
+ if (tempo && endsUnpaid && patch.endedBy === void 0) {
669
+ return "not_ready";
670
+ }
671
+ if (patch.confirmedOverIds !== void 0 && !(current.confirmedOverIds ?? []).every((id) => patch.confirmedOverIds?.includes(id))) {
672
+ return "not_ready";
673
+ }
674
+ if (patch.receiptWrap !== void 0 && current.receiptWrap !== void 0 && current.state !== "paying") {
675
+ return "not_ready";
676
+ }
677
+ if (patch.status !== void 0 && current.status !== void 0) {
678
+ const allowed = STATUS_SUCCESSORS[current.status.status];
679
+ if (!allowed.includes(patch.status.status)) {
680
+ return "not_ready";
681
+ }
682
+ if (current.status.refunded === true && patch.status.refunded !== true) {
683
+ return "not_ready";
684
+ }
685
+ }
686
+ if (patch.state === "ordered" && current.state === "created") {
687
+ const inbox = patch.inboxRelays ?? current.inboxRelays;
688
+ const inboxHosts = new Set(inbox.map(relayHost));
689
+ const acknowledgedHosts = new Set(
690
+ (patch.acknowledgedRelays ?? current.acknowledgedRelays).filter((relay) => inbox.includes(relay)).map(relayHost)
691
+ );
692
+ const wrap = patch.orderWrap ?? current.orderWrap;
693
+ if (wrap === void 0 || !isGenuineEvent(wrap) || inboxHosts.size === 0 || acknowledgedHosts.size < Math.min(ORDER_ACK_TARGET, inboxHosts.size)) {
694
+ return "not_ready";
695
+ }
696
+ }
697
+ if (patch.orderWrap !== void 0 && current.orderWrap !== void 0) {
698
+ return "not_ready";
699
+ }
700
+ return void 0;
701
+ }
702
+ function storeClosed(record) {
703
+ return record.status?.status === "cancelled" || record.status?.status === "completed";
704
+ }
705
+ function railOf(record) {
706
+ return record.payout.caip19.startsWith("eip155:") ? "tempo" : "solana";
707
+ }
708
+ function markerEvidence(marker) {
709
+ return marker.rail === "solana" ? [marker.signature ?? "", marker.signedTransaction ?? ""] : [marker.txHash ?? "", marker.bundleId ?? ""];
710
+ }
711
+ function holdsEvidence(marker) {
712
+ return markerEvidence(marker).some((value) => value !== "");
713
+ }
714
+ function keepsEvidence(current, next) {
715
+ const before = markerEvidence(current);
716
+ const after = markerEvidence(next);
717
+ return before.every((value, index) => value === "" || value === after[index]);
718
+ }
719
+ function refused(reason, holder) {
720
+ return { result: holder === void 0 ? { ok: false, reason } : { ok: false, reason, holder } };
721
+ }
722
+ function accepted(record) {
723
+ return { result: { ok: true, record }, write: [record] };
724
+ }
725
+ function exclusionHolder(records, current) {
726
+ return records.find(
727
+ (sibling) => sibling.orderId !== current.orderId && holdsPayExclusion(sibling)
728
+ );
729
+ }
730
+ function judgedRecord(records, orderId, expectedVersion) {
731
+ const current = records.find((record) => record.orderId === orderId);
732
+ if (current === void 0) {
733
+ return refused("missing");
734
+ }
735
+ if (current.version !== expectedVersion) {
736
+ return refused("conflict");
737
+ }
738
+ return current;
739
+ }
740
+ function isJudged(value) {
741
+ return "result" in value;
742
+ }
743
+ function judgeAdd(records, record) {
744
+ if (record.state !== "created" || record.version !== 1 || record.marker !== void 0) {
745
+ throw new Error("A new order record starts as created, at version 1, with no marker");
746
+ }
747
+ if (records.some((existing) => existing.orderId === record.orderId)) {
748
+ throw new Error("An order record with this id already exists");
749
+ }
750
+ return { result: { ok: true, record }, write: [record] };
751
+ }
752
+ function judgeUpdate(records, orderId, expectedVersion, patch) {
753
+ const current = judgedRecord(records, orderId, expectedVersion);
754
+ if (isJudged(current)) {
755
+ return current;
756
+ }
757
+ const defined = {};
758
+ for (const key of PATCH_KEYS) {
759
+ if (patch[key] !== void 0) {
760
+ Object.assign(defined, { [key]: patch[key] });
761
+ }
762
+ }
763
+ const refusal2 = patchRefusal(current, defined);
764
+ if (refusal2 !== void 0) {
765
+ return refused(refusal2);
766
+ }
767
+ return accepted({ ...current, ...defined, version: current.version + 1 });
768
+ }
769
+ function unconfirmedOver(records, current, now) {
770
+ return records.filter(
771
+ (sibling) => sibling.orderId !== current.orderId && railOf(sibling) === "tempo" && sibling.state === "ended-unpaid" && (sibling.endedBy === "over" || sibling.endedBy === void 0) && (now === void 0 || now - sibling.createdAt <= MERCHANT_CATCH_UP_SECS + MAX_CLOCK_SKEW_SECS) && !(current.confirmedOverIds ?? []).includes(sibling.orderId)
772
+ ).map((sibling) => sibling.orderId);
773
+ }
774
+ function needsConfirmation(unconfirmed) {
775
+ return { result: { ok: false, reason: "needs_confirmation", unconfirmed } };
776
+ }
777
+ function judgeSetMarker(records, orderId, expectedVersion, marker, now) {
778
+ const current = judgedRecord(records, orderId, expectedVersion);
779
+ if (isJudged(current)) {
780
+ return current;
781
+ }
782
+ if (current.state !== "ordered" || current.marker !== void 0 || current.paymentRequest === void 0 || storeClosed(current) || marker.rail !== railOf(current)) {
783
+ return refused("not_ready");
784
+ }
785
+ const holder = exclusionHolder(records, current);
786
+ if (holder !== void 0) {
787
+ return refused("exclusion", holder.orderId);
788
+ }
789
+ const unconfirmed = unconfirmedOver(records, current, now);
790
+ if (unconfirmed.length > 0) {
791
+ return needsConfirmation(unconfirmed);
792
+ }
793
+ return accepted({ ...current, marker, state: "paying", version: current.version + 1 });
794
+ }
795
+ function judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next, now) {
796
+ const current = records.find((record) => record.orderId === orderId);
797
+ if (current === void 0) {
798
+ return refused("missing");
799
+ }
800
+ if (current.version !== expectedVersion || current.marker?.attemptId !== attemptId || current.marker.rail !== next.rail) {
801
+ return refused("conflict");
802
+ }
803
+ if (current.state !== "paying") {
804
+ return refused("not_ready");
805
+ }
806
+ const retry = next.attemptId !== attemptId;
807
+ const windowMoved = current.marker.rail === "solana" && next.rail === "solana" ? next.blockhash !== current.marker.blockhash || next.lastValidBlockHeight !== current.marker.lastValidBlockHeight || next.slot !== current.marker.slot : current.marker.rail === "tempo" && next.rail === "tempo" && next.floorBlock !== current.marker.floorBlock;
808
+ if (retry && next.rail === "tempo" || !retry && !keepsEvidence(current.marker, next) || !retry && windowMoved) {
809
+ return refused("not_ready");
810
+ }
811
+ if (retry && storeClosed(current)) {
812
+ return refused("not_ready");
813
+ }
814
+ if (retry) {
815
+ const holder = exclusionHolder(records, current);
816
+ if (holder !== void 0) {
817
+ return refused("exclusion", holder.orderId);
818
+ }
819
+ const unconfirmed = unconfirmedOver(records, current, now);
820
+ if (unconfirmed.length > 0) {
821
+ return needsConfirmation(unconfirmed);
822
+ }
823
+ }
824
+ return accepted({ ...current, marker: next, version: current.version + 1 });
825
+ }
826
+ function judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState, endedBy) {
827
+ const current = records.find((record) => record.orderId === orderId);
828
+ if (current === void 0) {
829
+ return refused("missing");
830
+ }
831
+ if (current.version !== expectedVersion || current.marker?.attemptId !== attemptId) {
832
+ return refused("conflict");
833
+ }
834
+ if (current.state !== "paying") {
835
+ return refused("not_ready");
836
+ }
837
+ const tempo = current.marker?.rail === "tempo";
838
+ if (nextState === "ended-unpaid" && tempo) {
839
+ if (current.marker !== void 0 && holdsEvidence(current.marker)) {
840
+ return refused("not_ready");
841
+ }
842
+ if (endedBy === void 0) {
843
+ return refused("not_ready");
844
+ }
845
+ }
846
+ if (endedBy !== void 0 && (!tempo || nextState !== "ended-unpaid")) {
847
+ return refused("not_ready");
848
+ }
849
+ if (nextState === "ordered" && current.marker !== void 0 && holdsEvidence(current.marker)) {
850
+ return refused("not_ready");
851
+ }
852
+ if (nextState === "ordered") {
853
+ const { marker: _removed, ...rest } = current;
854
+ return accepted({ ...rest, state: nextState, version: current.version + 1 });
855
+ }
856
+ return accepted({
857
+ ...current,
858
+ state: nextState,
859
+ ...endedBy === void 0 ? {} : { endedBy },
860
+ version: current.version + 1
861
+ });
862
+ }
863
+ function mergePins(current, storePubkey, ownerPubkey, payouts) {
864
+ if (current !== void 0 && current.pinnedOwnerPubkey !== ownerPubkey) {
865
+ return { result: current };
866
+ }
867
+ const known = [...current?.knownPayouts ?? []];
868
+ for (const payout of payouts) {
869
+ if (!known.some((entry) => entry.caip19 === payout.caip19 && entry.address === payout.address)) {
870
+ known.push({ caip19: payout.caip19, address: payout.address });
871
+ }
872
+ }
873
+ const record = {
874
+ storePubkey,
875
+ pinnedOwnerPubkey: ownerPubkey,
876
+ knownPayouts: known
877
+ };
878
+ return { write: record, result: record };
879
+ }
880
+ var OrderStore = class {
881
+ constructor(backend) {
882
+ this.backend = backend;
883
+ }
884
+ /** Add a new order: `created`, version 1, no marker. Anything else is refused. */
885
+ async add(record) {
886
+ await this.backend.transactProduct(
887
+ record.productAddress,
888
+ (records) => judgeAdd(records, record)
889
+ );
890
+ }
891
+ get(orderId) {
892
+ return this.backend.read(orderId);
893
+ }
894
+ /** Every record for a product, oldest first. */
895
+ async forProduct(productAddress2) {
896
+ const records = await this.backend.forProduct(productAddress2);
897
+ return [...records].sort((left, right) => left.createdAt - right.createdAt);
898
+ }
899
+ /** Run a rule on the product of `orderId` (it never changes), in one transaction. */
900
+ async judge(orderId, rule) {
901
+ const known = await this.backend.read(orderId);
902
+ if (known === void 0) {
903
+ return { ok: false, reason: "missing" };
904
+ }
905
+ return this.backend.transactProduct(known.productAddress, rule);
906
+ }
907
+ /** Change plain fields of the record the caller judged at `expectedVersion`. */
908
+ update(orderId, expectedVersion, patch) {
909
+ return this.judge(orderId, (records) => judgeUpdate(records, orderId, expectedVersion, patch));
910
+ }
911
+ /** See `judgeSetMarker`. */
912
+ setMarker(orderId, expectedVersion, marker, now) {
913
+ return this.judge(
914
+ orderId,
915
+ (records) => judgeSetMarker(records, orderId, expectedVersion, marker, now)
916
+ );
917
+ }
918
+ /** See `judgeUpdateMarker`. */
919
+ updateMarker(orderId, expectedVersion, attemptId, next, now) {
920
+ return this.judge(
921
+ orderId,
922
+ (records) => judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next, now)
923
+ );
924
+ }
925
+ /** See `judgeClearMarker`. */
926
+ clearMarker(orderId, expectedVersion, attemptId, nextState, endedBy) {
927
+ return this.judge(
928
+ orderId,
929
+ (records) => judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState, endedBy)
930
+ );
931
+ }
932
+ pins(storePubkey) {
933
+ return this.backend.readPins(storePubkey);
934
+ }
935
+ /** See `mergePins`. */
936
+ rememberDelivery(storePubkey, ownerPubkey, payouts) {
937
+ return this.backend.transactPins(
938
+ storePubkey,
939
+ (current) => mergePins(current, storePubkey, ownerPubkey, payouts)
940
+ );
941
+ }
942
+ };
943
+ var MemoryOrderBackend = class {
944
+ orders = /* @__PURE__ */ new Map();
945
+ stores = /* @__PURE__ */ new Map();
946
+ async read(orderId) {
947
+ return structuredClone(this.orders.get(orderId));
948
+ }
949
+ snapshot(productAddress2) {
950
+ return [...this.orders.values()].filter((record) => record.productAddress === productAddress2).map((record) => structuredClone(record));
951
+ }
952
+ async forProduct(productAddress2) {
953
+ return this.snapshot(productAddress2);
954
+ }
955
+ // Read, rule and write run with no await between them: no other call can interleave.
956
+ async transactProduct(productAddress2, fn) {
957
+ const { write, result } = fn(this.snapshot(productAddress2));
958
+ for (const record of write ?? []) {
959
+ this.orders.set(record.orderId, structuredClone(record));
960
+ }
961
+ return result;
962
+ }
963
+ async readPins(storePubkey) {
964
+ return structuredClone(this.stores.get(storePubkey));
965
+ }
966
+ async transactPins(storePubkey, fn) {
967
+ const { write, result } = fn(structuredClone(this.stores.get(storePubkey)));
968
+ if (write !== void 0) {
969
+ this.stores.set(storePubkey, structuredClone(write));
970
+ }
971
+ return result;
972
+ }
973
+ };
974
+ var SYSTEM_PROGRAM = "11111111111111111111111111111111";
975
+ var COMPUTE_BUDGET_PROGRAM = "ComputeBudget111111111111111111111111111111";
976
+ var SET_COMPUTE_UNIT_LIMIT = 2;
977
+ var SET_COMPUTE_UNIT_PRICE = 3;
978
+ var BASE_FEE_LAMPORTS = 5000n;
979
+ var SIGNATURE_PAGE_LIMIT = 1e3;
980
+ var MAX_SIGNATURE_PAGES = 10;
981
+ var EXPIRY_SETTLE_BLOCKS = 32n;
982
+ var ADVANCE_NONCE_DISCRIMINATOR = 4;
983
+ var MAX_REFERENCE_CANDIDATES = 100;
984
+ var UNSURE_VERDICTS = ["rpc_error", "unreadable", "not_found", "bad_request"];
985
+ function storedSolanaRequest(record) {
986
+ if (record.paymentRequest === void 0) {
987
+ return void 0;
988
+ }
989
+ const parsed = parsePaymentRequest(record.paymentRequest);
990
+ return parsed.ok ? parsed.data : void 0;
991
+ }
992
+ function solanaAssetOf(record) {
993
+ const caip19 = parseCaip19(record.payout.caip19);
994
+ if (caip19 === void 0 || caip19.chain.family !== "solana") {
995
+ return void 0;
996
+ }
997
+ return { asset: caip19.asset, network: caip19.chain.network };
998
+ }
999
+ async function composeOrderPayment(record, store) {
1000
+ if (record.paymentRequest !== void 0) {
1001
+ return { ok: true, record };
1002
+ }
1003
+ const coin = solanaAssetOf(record);
1004
+ if (coin === void 0) {
1005
+ return { ok: false, reason: "not_ready" };
1006
+ }
1007
+ let request;
1008
+ try {
1009
+ request = composeSolanaPaymentRequest({
1010
+ recipient: record.payout.address,
1011
+ amount: BigInt(record.amount),
1012
+ asset: coin.asset,
1013
+ network: coin.network,
1014
+ reference: record.reference,
1015
+ createdAt: record.createdAt
1016
+ });
1017
+ } catch {
1018
+ return { ok: false, reason: "not_ready" };
1019
+ }
1020
+ const written = await store.update(record.orderId, record.version, {
1021
+ paymentRequest: JSON.stringify(request)
1022
+ });
1023
+ if (!written.ok && written.reason === "conflict") {
1024
+ const current = await store.get(record.orderId);
1025
+ if (current?.paymentRequest !== void 0) {
1026
+ return { ok: true, record: current };
1027
+ }
1028
+ }
1029
+ return written;
1030
+ }
1031
+ function requestMatches(request, record, coin) {
1032
+ return request.recipient === record.payout.address && request.reference === record.reference && BigInt(request.amount) === BigInt(record.amount) && (request.network ?? "devnet") === coin.network && request.asset?.mint === coin.asset.mint && (request.fee_amount ?? 0) === 0 && request.fee_address === void 0;
1033
+ }
1034
+ async function checkBeforePaying(record, payer, input, deps) {
1035
+ const request = storedSolanaRequest(record);
1036
+ const coin = solanaAssetOf(record);
1037
+ if (request === void 0 || coin === void 0 || !isAddress(payer) || !requestMatches(request, record, coin)) {
1038
+ return { ok: false, reason: "not_payable", record };
1039
+ }
1040
+ if (isSnapshotStale(input.fresh.snapshotAt, (deps.now ?? nowSecs)())) {
1041
+ return { ok: false, reason: "stale_offer", record };
1042
+ }
1043
+ const match = input.fresh.payouts.find(
1044
+ (payout) => payout.target.caip19.id === record.payout.caip19 && payout.target.address === record.payout.address
1045
+ );
1046
+ if (input.fresh.productAddress !== record.productAddress || !isOfferPayout(input.fresh.offer, record.payout.caip19, record.payout.address) || match === void 0 || match.amount !== BigInt(record.amount)) {
1047
+ return { ok: false, reason: "offer_changed", record };
1048
+ }
1049
+ if (input.chainTime > record.createdAt + MERCHANT_CATCH_UP_SECS - PAY_CUTOFF_SECS) {
1050
+ return { ok: false, reason: "too_late", record };
1051
+ }
1052
+ if (payer === record.payout.address) {
1053
+ return { ok: false, reason: "self_payment", record };
1054
+ }
1055
+ const funds = await checkFunds(deps.rpc, request, payer, coin);
1056
+ if (!funds.ok) {
1057
+ return { ...funds, record };
1058
+ }
1059
+ return { ok: true, request, asset: coin.asset, network: coin.network, funds: funds.funds };
1060
+ }
1061
+ async function assetAccounts(payer, recipient, asset) {
1062
+ if (asset.mint === void 0) {
1063
+ return { payer, payee: recipient };
1064
+ }
1065
+ const tokenProgram = address(asset.tokenProgram ?? TOKEN_PROGRAM_ADDRESS);
1066
+ const mint = address(asset.mint);
1067
+ const [payerAccount] = await findAssociatedTokenPda({
1068
+ owner: address(payer),
1069
+ mint,
1070
+ tokenProgram
1071
+ });
1072
+ const [payeeAccount] = await findAssociatedTokenPda({
1073
+ owner: address(recipient),
1074
+ mint,
1075
+ tokenProgram
1076
+ });
1077
+ return { payer: payerAccount, payee: payeeAccount };
1078
+ }
1079
+ async function tokenBalance(rpc, account) {
1080
+ const info = await rpc.getAccountInfo(address(account), { commitment: "confirmed", encoding: "base64" }).send();
1081
+ if (info.value === null) {
1082
+ return 0n;
1083
+ }
1084
+ const balance = await rpc.getTokenAccountBalance(address(account), { commitment: "confirmed" }).send();
1085
+ return BigInt(balance.value.amount);
1086
+ }
1087
+ async function checkFunds(rpc, request, payer, coin) {
1088
+ try {
1089
+ const amount = BigInt(request.amount);
1090
+ const accounts = await assetAccounts(payer, request.recipient, coin.asset);
1091
+ const priceMicroLamports = await estimatePriorityFeeMicroLamports(rpc, {
1092
+ network: coin.network,
1093
+ accounts: [address(accounts.payer), address(accounts.payee)]
1094
+ });
1095
+ const fees = await estimateSolFeeLamports(rpc, request, payer, coin.network, {
1096
+ computeUnitLimit: SOLANA_COMPUTE_UNIT_LIMIT,
1097
+ priorityFeeMicroLamports: priceMicroLamports
1098
+ });
1099
+ const rentFloor = await rpc.getMinimumBalanceForRentExemption(0n).send();
1100
+ const lamports = (await rpc.getBalance(address(payer), { commitment: "confirmed" }).send()).value;
1101
+ const otherLamports = fees.rentLamports + fees.assetStatsRentLamports + rentFloor + (coin.asset.mint === void 0 ? amount : 0n);
1102
+ const feeLamports = fees.baseFeeLamports + fees.priorityFeeLamports;
1103
+ if (coin.asset.mint !== void 0) {
1104
+ const tokens = await tokenBalance(rpc, accounts.payer);
1105
+ if (tokens < amount) {
1106
+ return { ok: false, reason: "insufficient_token", needed: amount, available: tokens };
1107
+ }
1108
+ }
1109
+ if (lamports < otherLamports + feeLamports) {
1110
+ return {
1111
+ ok: false,
1112
+ reason: "insufficient_sol",
1113
+ needed: otherLamports + feeLamports,
1114
+ available: lamports
1115
+ };
1116
+ }
1117
+ const costs = {
1118
+ asset: coin.asset,
1119
+ tokenAmount: coin.asset.mint === void 0 ? 0n : amount,
1120
+ lamports: fees.rentLamports + fees.assetStatsRentLamports + feeLamports + (coin.asset.mint === void 0 ? amount : 0n),
1121
+ feeLamports
1122
+ };
1123
+ return { ok: true, funds: { lamports, otherLamports, priceMicroLamports, costs } };
1124
+ } catch {
1125
+ return { ok: false, reason: "rpc_error" };
1126
+ }
1127
+ }
1128
+ async function unsignedPayment(rpc, checked, payer) {
1129
+ const payerAddress = address(payer);
1130
+ const instructions = await buildPaymentInstructions(
1131
+ checked.request,
1132
+ createNoopSigner(payerAddress),
1133
+ { programId: getProtocolProgramId(checked.network) }
1134
+ );
1135
+ const { context, value: latest } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();
1136
+ const message = pipe(
1137
+ createTransactionMessage({ version: 0 }),
1138
+ (draft) => setTransactionMessageFeePayer(payerAddress, draft),
1139
+ (draft) => setTransactionMessageLifetimeUsingBlockhash(latest, draft),
1140
+ (draft) => setTransactionMessageComputeUnitLimit(SOLANA_COMPUTE_UNIT_LIMIT, draft),
1141
+ (draft) => setTransactionMessageComputeUnitPrice(checked.funds.priceMicroLamports, draft),
1142
+ (draft) => appendTransactionMessageInstructions(
1143
+ // pay-core types its instructions loosely; the same cast its own builder uses.
1144
+ instructions,
1145
+ draft
1146
+ )
1147
+ );
1148
+ return {
1149
+ bytes: new Uint8Array(getTransactionEncoder().encode(compileTransaction(message))),
1150
+ blockhash: latest.blockhash,
1151
+ lastValidBlockHeight: latest.lastValidBlockHeight,
1152
+ slot: BigInt(context.slot)
1153
+ };
1154
+ }
1155
+ function transactionFee(instructions, signatures) {
1156
+ let limit;
1157
+ let price = 0n;
1158
+ let limits = 0;
1159
+ let prices = 0;
1160
+ let others = 0;
1161
+ for (const instruction of instructions) {
1162
+ if (instruction.program !== COMPUTE_BUDGET_PROGRAM) {
1163
+ others += 1;
1164
+ continue;
1165
+ }
1166
+ const view = new DataView(
1167
+ instruction.data.buffer,
1168
+ instruction.data.byteOffset,
1169
+ instruction.data.byteLength
1170
+ );
1171
+ if (instruction.data[0] === SET_COMPUTE_UNIT_LIMIT && instruction.data.length === 5) {
1172
+ limits += 1;
1173
+ limit = BigInt(view.getUint32(1, true));
1174
+ } else if (instruction.data[0] === SET_COMPUTE_UNIT_PRICE && instruction.data.length === 9) {
1175
+ prices += 1;
1176
+ price = view.getBigUint64(1, true);
1177
+ }
1178
+ }
1179
+ if (limits > 1 || prices > 1) {
1180
+ return void 0;
1181
+ }
1182
+ const units = limit ?? BigInt(Math.min(others * 2e5, 14e5));
1183
+ return BASE_FEE_LAMPORTS * BigInt(signatures) + (price * units + 999999n) / 1000000n;
1184
+ }
1185
+ async function checkSignedTransaction(bytes, expected) {
1186
+ let transaction;
1187
+ let message;
1188
+ let instructions;
1189
+ try {
1190
+ transaction = getTransactionDecoder().decode(bytes);
1191
+ message = getCompiledTransactionMessageDecoder().decode(transaction.messageBytes);
1192
+ instructions = directInstructionsFromCompiledMessage(message);
1193
+ } catch {
1194
+ return { ok: false, reason: "unreadable" };
1195
+ }
1196
+ const first = instructions[0];
1197
+ const durableNonce = first !== void 0 && first.program === SYSTEM_PROGRAM && first.data.length >= 4 && new DataView(first.data.buffer, first.data.byteOffset, 4).getUint32(0, true) === ADVANCE_NONCE_DISCRIMINATOR;
1198
+ if (message.lifetimeToken !== expected.blockhash || durableNonce) {
1199
+ return { ok: false, reason: "lifetime_changed" };
1200
+ }
1201
+ if (String(message.staticAccounts[0]) !== expected.payer) {
1202
+ return { ok: false, reason: "wrong_payer" };
1203
+ }
1204
+ const signatures = Object.entries(transaction.signatures);
1205
+ if (signatures.length === 0) {
1206
+ return { ok: false, reason: "unsigned" };
1207
+ }
1208
+ for (const [signer, signature] of signatures) {
1209
+ let valid = false;
1210
+ try {
1211
+ valid = signature !== null && await verifySignature(
1212
+ await getPublicKeyFromAddress(address(signer)),
1213
+ signature,
1214
+ transaction.messageBytes
1215
+ );
1216
+ } catch {
1217
+ valid = false;
1218
+ }
1219
+ if (!valid) {
1220
+ return { ok: false, reason: "unsigned" };
1221
+ }
1222
+ }
1223
+ const payerSignature = transaction.signatures[address(expected.payer)] ?? null;
1224
+ const feeLamports = transactionFee(instructions, signatures.length);
1225
+ if (feeLamports === void 0) {
1226
+ return { ok: false, reason: "duplicate_compute_budget" };
1227
+ }
1228
+ const bound = await boundTransferAmount(instructions, {
1229
+ reference: expected.request.reference,
1230
+ recipient: expected.request.recipient,
1231
+ asset: expected.asset
1232
+ });
1233
+ if (payerSignature === null || bound !== BigInt(expected.request.amount)) {
1234
+ return { ok: false, reason: payerSignature === null ? "unsigned" : "not_bound" };
1235
+ }
1236
+ return {
1237
+ ok: true,
1238
+ signature: getBase58Decoder().decode(payerSignature),
1239
+ wire: getBase64Decoder().decode(bytes),
1240
+ feeLamports
1241
+ };
1242
+ }
1243
+ async function broadcast(rpc, wire) {
1244
+ try {
1245
+ await rpc.sendTransaction(wire, {
1246
+ encoding: "base64",
1247
+ skipPreflight: true
1248
+ }).send();
1249
+ } catch {
1250
+ }
1251
+ }
1252
+ async function signAndSend(record, marker, unsigned, wallet, checked, deps) {
1253
+ let signedBytes;
1254
+ try {
1255
+ signedBytes = await wallet.signTransaction(unsigned.bytes);
1256
+ } catch {
1257
+ return { ok: false, reason: "wallet_failed", record };
1258
+ }
1259
+ const signed = await checkSignedTransaction(signedBytes, {
1260
+ payer: wallet.address,
1261
+ blockhash: unsigned.blockhash,
1262
+ request: checked.request,
1263
+ asset: checked.asset
1264
+ });
1265
+ if (!signed.ok) {
1266
+ return { ok: false, reason: "wallet_unsupported", record, detail: signed.reason };
1267
+ }
1268
+ const needed = checked.funds.otherLamports + signed.feeLamports;
1269
+ if (checked.funds.lamports < needed) {
1270
+ deps.release?.(marker.attemptId);
1271
+ return {
1272
+ ok: false,
1273
+ reason: "insufficient_sol",
1274
+ record,
1275
+ needed,
1276
+ available: checked.funds.lamports
1277
+ };
1278
+ }
1279
+ let current = record;
1280
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
1281
+ if (current.marker?.attemptId !== marker.attemptId) {
1282
+ deps.release?.(marker.attemptId);
1283
+ return { ok: false, reason: "conflict", record: current };
1284
+ }
1285
+ const written = await deps.store.updateMarker(
1286
+ current.orderId,
1287
+ current.version,
1288
+ marker.attemptId,
1289
+ { ...marker, signature: signed.signature, signedTransaction: signed.wire }
1290
+ );
1291
+ if (written.ok) {
1292
+ await broadcast(deps.rpc, signed.wire);
1293
+ const receipt = await sendReceipt(
1294
+ written.record,
1295
+ signed.signature,
1296
+ (deps.now ?? nowSecs)(),
1297
+ deps
1298
+ );
1299
+ return {
1300
+ ok: true,
1301
+ record: receipt.ok ? receipt.record : written.record,
1302
+ signature: signed.signature
1303
+ };
1304
+ }
1305
+ if (written.reason !== "conflict") {
1306
+ deps.release?.(marker.attemptId);
1307
+ return { ok: false, reason: "conflict", record: current };
1308
+ }
1309
+ current = await deps.store.get(record.orderId);
1310
+ }
1311
+ deps.release?.(marker.attemptId);
1312
+ return { ok: false, reason: "conflict", ...current === void 0 ? {} : { record: current } };
1313
+ }
1314
+ function storeRefusal(written, record) {
1315
+ if (written.reason === "exclusion") {
1316
+ return {
1317
+ ok: false,
1318
+ reason: "exclusion",
1319
+ record,
1320
+ ...written.holder === void 0 ? {} : { holder: written.holder }
1321
+ };
1322
+ }
1323
+ if (written.reason === "needs_confirmation") {
1324
+ return {
1325
+ ok: false,
1326
+ reason: "needs_confirmation",
1327
+ record,
1328
+ unconfirmed: written.unconfirmed ?? []
1329
+ };
1330
+ }
1331
+ return { ok: false, reason: written.reason === "conflict" ? "conflict" : "not_payable", record };
1332
+ }
1333
+ async function withReleaseOnThrow(attemptId, deps, write) {
1334
+ try {
1335
+ return await write();
1336
+ } catch (error) {
1337
+ deps.release?.(attemptId);
1338
+ throw error;
1339
+ }
1340
+ }
1341
+ function reserveCosts(costs, attemptId, deps) {
1342
+ if (deps.reserve === void 0) {
1343
+ return true;
1344
+ }
1345
+ try {
1346
+ deps.reserve(costs, attemptId);
1347
+ return true;
1348
+ } catch {
1349
+ return false;
1350
+ }
1351
+ }
1352
+ function newMarker(unsigned, deps) {
1353
+ return {
1354
+ rail: "solana",
1355
+ attemptId: (deps.newAttemptId ?? (() => crypto.randomUUID()))(),
1356
+ setAt: (deps.now ?? nowSecs)(),
1357
+ blockhash: unsigned.blockhash,
1358
+ lastValidBlockHeight: unsigned.lastValidBlockHeight.toString(),
1359
+ slot: unsigned.slot.toString()
1360
+ };
1361
+ }
1362
+ async function payWithSolana(record, wallet, input, deps) {
1363
+ if (record.state !== "ordered" || record.marker !== void 0) {
1364
+ return { ok: false, reason: "not_payable", record };
1365
+ }
1366
+ const checked = await checkBeforePaying(record, wallet.address, input, deps);
1367
+ if (!checked.ok) {
1368
+ return checked;
1369
+ }
1370
+ let unsigned;
1371
+ try {
1372
+ unsigned = await unsignedPayment(deps.rpc, checked, wallet.address);
1373
+ } catch {
1374
+ return { ok: false, reason: "rpc_error", record };
1375
+ }
1376
+ const marker = newMarker(unsigned, deps);
1377
+ if (!reserveCosts(checked.funds.costs, marker.attemptId, deps)) {
1378
+ return { ok: false, reason: "spend_limit", record };
1379
+ }
1380
+ const marked = await withReleaseOnThrow(
1381
+ marker.attemptId,
1382
+ deps,
1383
+ () => deps.store.setMarker(record.orderId, record.version, marker, (deps.now ?? nowSecs)())
1384
+ );
1385
+ if (!marked.ok) {
1386
+ deps.release?.(marker.attemptId);
1387
+ return storeRefusal(marked, record);
1388
+ }
1389
+ return signAndSend(marked.record, marker, unsigned, wallet, checked, deps);
1390
+ }
1391
+ async function listReference(rpc, reference, notBefore, minContextSlot) {
1392
+ const signatures = [];
1393
+ let before;
1394
+ for (let page = 0; page < MAX_SIGNATURE_PAGES; page += 1) {
1395
+ const rows = await rpc.getSignaturesForAddress(address(reference), {
1396
+ limit: SIGNATURE_PAGE_LIMIT,
1397
+ commitment: "confirmed",
1398
+ ...minContextSlot === void 0 ? {} : { minContextSlot },
1399
+ ...before === void 0 ? {} : { before }
1400
+ }).send();
1401
+ let reachedFloor = false;
1402
+ for (const row of rows) {
1403
+ signatures.push({ signature: row.signature, failed: row.err !== null });
1404
+ if (row.blockTime !== null && Number(row.blockTime) < notBefore) {
1405
+ reachedFloor = true;
1406
+ }
1407
+ }
1408
+ const last = rows[rows.length - 1];
1409
+ if (reachedFloor || rows.length < SIGNATURE_PAGE_LIMIT || last === void 0) {
1410
+ return { signatures, complete: true };
1411
+ }
1412
+ before = last.signature;
1413
+ }
1414
+ return { signatures, complete: false };
1415
+ }
1416
+ async function searchPayment(record, request, rpc, minContextSlot) {
1417
+ const own = record.marker?.rail === "solana" ? record.marker.signature : void 0;
1418
+ const candidates = own === void 0 ? [] : [own];
1419
+ const listed = /* @__PURE__ */ new Set();
1420
+ let unsure = false;
1421
+ try {
1422
+ const listing = await listReference(
1423
+ rpc,
1424
+ request.reference,
1425
+ record.createdAt - PAYMENT_SCAN_MARGIN_SECS,
1426
+ minContextSlot
1427
+ );
1428
+ unsure = !listing.complete;
1429
+ for (const entry of listing.signatures) {
1430
+ listed.add(entry.signature);
1431
+ if (!entry.failed && !candidates.includes(entry.signature)) {
1432
+ candidates.push(entry.signature);
1433
+ }
1434
+ }
1435
+ } catch {
1436
+ unsure = true;
1437
+ }
1438
+ if (candidates.length > MAX_REFERENCE_CANDIDATES) {
1439
+ unsure = true;
1440
+ }
1441
+ for (const signature of candidates.slice(0, MAX_REFERENCE_CANDIDATES)) {
1442
+ let verdict;
1443
+ try {
1444
+ verdict = await verifyDirectSolanaPayment(rpc, request, signature);
1445
+ } catch {
1446
+ unsure = true;
1447
+ continue;
1448
+ }
1449
+ if (verdict.verified) {
1450
+ return { found: signature };
1451
+ }
1452
+ if (!UNSURE_VERDICTS.includes(verdict.reason)) {
1453
+ continue;
1454
+ }
1455
+ if (verdict.reason === "not_found" && !listed.has(signature) && (signature !== own || minContextSlot === void 0 || !await statusKnown(rpc, signature, minContextSlot))) {
1456
+ continue;
1457
+ }
1458
+ unsure = true;
1459
+ }
1460
+ return unsure ? "unsure" : "none";
1461
+ }
1462
+ async function statusKnown(rpc, signature, minContextSlot) {
1463
+ try {
1464
+ const statuses = await rpc.getSignatureStatuses([signature], { searchTransactionHistory: true }).send();
1465
+ const status = statuses.value[0];
1466
+ return BigInt(statuses.context.slot) < minContextSlot || status !== null && status !== void 0;
1467
+ } catch {
1468
+ return true;
1469
+ }
1470
+ }
1471
+ async function recordPaid(record, signature, deps) {
1472
+ let current = record;
1473
+ const own = record.marker?.rail === "solana" ? record.marker.signature : void 0;
1474
+ if (current.receiptWrap === void 0 || current.state === "paying" && own !== signature) {
1475
+ const receipt = await sendReceipt(current, signature, (deps.now ?? nowSecs)(), deps);
1476
+ if (receipt.ok) {
1477
+ current = receipt.record;
1478
+ }
1479
+ }
1480
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
1481
+ if (current.paidTx !== void 0) {
1482
+ return current;
1483
+ }
1484
+ const written = await deps.store.update(current.orderId, current.version, {
1485
+ state: "paid",
1486
+ paidTx: signature,
1487
+ paidAt: (deps.now ?? nowSecs)()
1488
+ });
1489
+ if (written.ok) {
1490
+ return written.record;
1491
+ }
1492
+ if (written.reason !== "conflict") {
1493
+ return current;
1494
+ }
1495
+ current = await deps.store.get(record.orderId);
1496
+ }
1497
+ return current ?? record;
1498
+ }
1499
+ async function ledgerReaches(rpc, slot) {
1500
+ if (slot === void 0) {
1501
+ return false;
1502
+ }
1503
+ try {
1504
+ return BigInt(await rpc.getFirstAvailableBlock().send()) <= BigInt(slot);
1505
+ } catch {
1506
+ return false;
1507
+ }
1508
+ }
1509
+ async function watchSolanaPayment(record, deps) {
1510
+ if (isTerminal(record)) {
1511
+ return { state: "closed", record };
1512
+ }
1513
+ if (record.paidTx !== void 0) {
1514
+ if (record.receiptWrap === void 0) {
1515
+ const receipt = await sendReceipt(record, record.paidTx, (deps.now ?? nowSecs)(), deps);
1516
+ return { state: "paid", record: receipt.ok ? receipt.record : record };
1517
+ }
1518
+ return { state: "paid", record };
1519
+ }
1520
+ const marker = record.marker;
1521
+ const request = storedSolanaRequest(record);
1522
+ if (marker?.rail !== "solana" || request === void 0) {
1523
+ return { state: "waiting", record };
1524
+ }
1525
+ let expired = false;
1526
+ let settledAt;
1527
+ try {
1528
+ const epoch = await deps.rpc.getEpochInfo({ commitment: "finalized" }).send();
1529
+ const lastValid = BigInt(marker.lastValidBlockHeight);
1530
+ expired = BigInt(epoch.blockHeight) > lastValid;
1531
+ if (BigInt(epoch.blockHeight) > lastValid + EXPIRY_SETTLE_BLOCKS) {
1532
+ settledAt = BigInt(epoch.absoluteSlot);
1533
+ }
1534
+ } catch {
1535
+ expired = false;
1536
+ }
1537
+ if (settledAt !== void 0 && !await ledgerReaches(deps.rpc, marker.slot)) {
1538
+ settledAt = void 0;
1539
+ }
1540
+ const search = await searchPayment(record, request, deps.rpc, settledAt);
1541
+ if (search !== "none" && search !== "unsure") {
1542
+ return { state: "paid", record: await recordPaid(record, search.found, deps) };
1543
+ }
1544
+ if (!expired) {
1545
+ if (marker.signedTransaction !== void 0) {
1546
+ await broadcast(deps.rpc, marker.signedTransaction);
1547
+ }
1548
+ return { state: "waiting", record };
1549
+ }
1550
+ return search === "none" && settledAt !== void 0 && deps.canProveOver !== false ? { state: "over", record } : { state: "waiting", record };
1551
+ }
1552
+ async function retryWithSolana(record, wallet, input, deps) {
1553
+ if (record.state !== "paying" || record.marker?.rail !== "solana") {
1554
+ return { ok: false, reason: "not_payable", record };
1555
+ }
1556
+ const watch = await watchSolanaPayment(record, deps);
1557
+ if (watch.state !== "over") {
1558
+ const reasons = {
1559
+ paid: "already_paid",
1560
+ waiting: "still_waiting",
1561
+ closed: "not_payable"
1562
+ };
1563
+ return { ok: false, reason: reasons[watch.state], record: watch.record };
1564
+ }
1565
+ const judged = watch.record;
1566
+ const previous = judged.marker;
1567
+ if (previous === void 0) {
1568
+ return { ok: false, reason: "not_payable", record: judged };
1569
+ }
1570
+ const checked = await checkBeforePaying(judged, wallet.address, input, deps);
1571
+ if (!checked.ok) {
1572
+ return checked;
1573
+ }
1574
+ let unsigned;
1575
+ try {
1576
+ unsigned = await unsignedPayment(deps.rpc, checked, wallet.address);
1577
+ } catch {
1578
+ return { ok: false, reason: "rpc_error", record: judged };
1579
+ }
1580
+ const marker = newMarker(unsigned, deps);
1581
+ if (!reserveCosts(checked.funds.costs, marker.attemptId, deps)) {
1582
+ return { ok: false, reason: "spend_limit", record: judged };
1583
+ }
1584
+ const replaced = await withReleaseOnThrow(
1585
+ marker.attemptId,
1586
+ deps,
1587
+ () => deps.store.updateMarker(
1588
+ judged.orderId,
1589
+ judged.version,
1590
+ previous.attemptId,
1591
+ marker,
1592
+ (deps.now ?? nowSecs)()
1593
+ )
1594
+ );
1595
+ if (!replaced.ok) {
1596
+ deps.release?.(marker.attemptId);
1597
+ return storeRefusal(replaced, judged);
1598
+ }
1599
+ return signAndSend(replaced.record, marker, unsigned, wallet, checked, deps);
1600
+ }
1601
+ async function endSolanaOrder(record, deps) {
1602
+ if (record.state === "ordered" && record.marker === void 0) {
1603
+ const written2 = await deps.store.update(record.orderId, record.version, {
1604
+ state: "ended-unpaid"
1605
+ });
1606
+ return written2.ok ? { ended: true, record: written2.record } : { ended: false, record };
1607
+ }
1608
+ if (record.state !== "paying" || record.marker === void 0) {
1609
+ return { ended: false, record };
1610
+ }
1611
+ const watch = await watchSolanaPayment(record, deps);
1612
+ if (watch.state !== "over" || watch.record.marker === void 0) {
1613
+ return { ended: false, record: watch.record };
1614
+ }
1615
+ const written = await deps.store.clearMarker(
1616
+ watch.record.orderId,
1617
+ watch.record.version,
1618
+ watch.record.marker.attemptId,
1619
+ "ended-unpaid"
1620
+ );
1621
+ return written.ok ? { ended: true, record: written.record } : { ended: false, record: watch.record };
1622
+ }
1623
+ var BALANCE_OF_SELECTOR = "0x70a08231";
1624
+ var EVM_ADDRESS_RE = /^0x[0-9a-f]{40}$/;
1625
+ var UINT256_RE = /^0x[0-9a-fA-F]{64}$/;
1626
+ function tempoFeeMargin(decimals) {
1627
+ return decimals > 2 ? 10n ** BigInt(decimals - 2) : 1n;
1628
+ }
1629
+ function tempoTargetOf(record) {
1630
+ const caip19 = parseCaip19(record.payout.caip19);
1631
+ const mint = caip19?.asset.mint;
1632
+ if (caip19 === void 0 || caip19.chain.family !== "evm" || mint === void 0) {
1633
+ return void 0;
1634
+ }
1635
+ return { chain: caip19.chain, asset: caip19.asset, token: mint.toLowerCase() };
1636
+ }
1637
+ function storedTempoRequest(record) {
1638
+ if (record.paymentRequest === void 0) {
1639
+ return void 0;
1640
+ }
1641
+ try {
1642
+ const parsed = PaymentRequestV2Schema.safeParse(JSON.parse(record.paymentRequest));
1643
+ return parsed.success ? parsed.data : void 0;
1644
+ } catch {
1645
+ return void 0;
1646
+ }
1647
+ }
1648
+ function tempoRequestMatches(request, record, target) {
1649
+ return request.chain === target.chain.caip2 && request.asset.toLowerCase() === `${target.chain.caip2}/erc20:${target.token}` && request.recipient === record.payout.address.toLowerCase() && request.memo === record.reference && BigInt(request.amount) === BigInt(record.amount) && request.fee_address === void 0 && (request.fee_amount === void 0 || BigInt(request.fee_amount) === 0n);
1650
+ }
1651
+ async function readTempoChainTime(client) {
1652
+ const head = await readFinalizedBlock(client);
1653
+ if (head === null) {
1654
+ throw new Error("The Tempo chain could not be read.");
1655
+ }
1656
+ return head.timestamp;
1657
+ }
1658
+ async function tokenBalance2(client, token, owner) {
1659
+ try {
1660
+ const answer = await client.request({
1661
+ method: "eth_call",
1662
+ params: [
1663
+ { to: token, data: `${BALANCE_OF_SELECTOR}${owner.slice(2).padStart(64, "0")}` },
1664
+ "latest"
1665
+ ]
1666
+ });
1667
+ return typeof answer === "string" && UINT256_RE.test(answer) ? BigInt(answer) : null;
1668
+ } catch {
1669
+ return null;
1670
+ }
1671
+ }
1672
+ function refusal(reason, record) {
1673
+ return record === void 0 ? { ok: false, reason } : { ok: false, reason, record };
1674
+ }
1675
+ async function endNothingRequested(record, deps) {
1676
+ const written = await deps.store.update(record.orderId, record.version, {
1677
+ state: "ended-unpaid",
1678
+ endedBy: "nothing"
1679
+ });
1680
+ return written.ok ? written.record : record;
1681
+ }
1682
+ function storeRefusal2(written, record) {
1683
+ if (written.reason === "exclusion") {
1684
+ return {
1685
+ ...refusal("exclusion", record),
1686
+ ...written.holder === void 0 ? {} : { holder: written.holder }
1687
+ };
1688
+ }
1689
+ if (written.reason === "needs_confirmation") {
1690
+ return { ...refusal("needs_confirmation", record), unconfirmed: written.unconfirmed ?? [] };
1691
+ }
1692
+ return refusal(written.reason === "conflict" ? "conflict" : "not_payable", record);
1693
+ }
1694
+ async function checkBeforeTempoPaying(record, payer, fresh, target, head, deps) {
1695
+ if (record.state !== "ordered" || record.marker !== void 0) {
1696
+ return refusal("not_payable", record);
1697
+ }
1698
+ if (isSnapshotStale(fresh.snapshotAt, (deps.now ?? nowSecs)())) {
1699
+ return refusal("stale_offer", record);
1700
+ }
1701
+ const match = fresh.payouts.find(
1702
+ (payout) => payout.target.caip19.id === record.payout.caip19 && payout.target.address === record.payout.address
1703
+ );
1704
+ if (fresh.productAddress !== record.productAddress || !isOfferPayout(fresh.offer, record.payout.caip19, record.payout.address) || match === void 0 || match.amount !== BigInt(record.amount)) {
1705
+ return refusal("offer_changed", record);
1706
+ }
1707
+ if (head.timestamp > record.createdAt + MERCHANT_CATCH_UP_SECS - PAY_CUTOFF_SECS) {
1708
+ return refusal("too_late", record);
1709
+ }
1710
+ const recipient = record.payout.address.toLowerCase();
1711
+ if (payer === recipient) {
1712
+ return refusal("self_payment", record);
1713
+ }
1714
+ const named = await checkEvmChain(deps.client, target.chain).catch(() => "wrong");
1715
+ if (named === "wrong") {
1716
+ return refusal("wrong_chain", record);
1717
+ }
1718
+ if (named === null) {
1719
+ return refusal("rpc_error", record);
1720
+ }
1721
+ const policy = await checkTempoReceivePolicies(deps.client, {
1722
+ chain: target.chain,
1723
+ token: target.token,
1724
+ payer,
1725
+ recipient
1726
+ });
1727
+ if (!policy.ok) {
1728
+ return refusal(policy.reason === "blocked" ? "policy_blocked" : "rpc_error", record);
1729
+ }
1730
+ const balance = await tokenBalance2(deps.client, target.token, payer);
1731
+ if (balance === null) {
1732
+ return refusal("rpc_error", record);
1733
+ }
1734
+ const needed = BigInt(record.amount) + tempoFeeMargin(target.asset.decimals);
1735
+ if (balance < needed) {
1736
+ return { ...refusal("insufficient_token", record), needed, available: balance };
1737
+ }
1738
+ return void 0;
1739
+ }
1740
+ async function requestFor(record, target, head, deps) {
1741
+ if (record.paymentRequest !== void 0) {
1742
+ const stored = storedTempoRequest(record);
1743
+ return stored !== void 0 && tempoRequestMatches(stored, record, target) ? { request: stored, record } : refusal("not_payable", record);
1744
+ }
1745
+ let request;
1746
+ try {
1747
+ request = composeTempoPaymentRequest({
1748
+ chain: target.chain,
1749
+ asset: target.asset,
1750
+ recipient: record.payout.address.toLowerCase(),
1751
+ amount: BigInt(record.amount),
1752
+ feeBps: 0,
1753
+ treasury: record.payout.address.toLowerCase(),
1754
+ memo: record.reference,
1755
+ createdAt: head.timestamp
1756
+ });
1757
+ } catch {
1758
+ return refusal("unpayable", await endNothingRequested(record, deps));
1759
+ }
1760
+ const written = await deps.store.update(record.orderId, record.version, {
1761
+ paymentRequest: JSON.stringify(request)
1762
+ });
1763
+ if (!written.ok) {
1764
+ return storeRefusal2(written, record);
1765
+ }
1766
+ return { request, record: written.record };
1767
+ }
1768
+ function isRefusal(value) {
1769
+ return "ok" in value;
1770
+ }
1771
+ async function saveTempoHash(record, attemptId, hash, deps) {
1772
+ let current = record;
1773
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
1774
+ const marker = current.marker;
1775
+ if (marker?.rail !== "tempo" || marker.attemptId !== attemptId) {
1776
+ return { saved: false, record: current, stopped: true };
1777
+ }
1778
+ if (marker.txHash === hash) {
1779
+ return { saved: true, record: current };
1780
+ }
1781
+ const written = await deps.store.updateMarker(current.orderId, current.version, attemptId, {
1782
+ ...marker,
1783
+ txHash: hash
1784
+ });
1785
+ if (written.ok) {
1786
+ return { saved: true, record: written.record };
1787
+ }
1788
+ if (written.reason === "not_ready") {
1789
+ return { saved: false, record: current, stopped: true };
1790
+ }
1791
+ current = await deps.store.get(record.orderId);
1792
+ }
1793
+ return { saved: false, record: current ?? record };
1794
+ }
1795
+ async function endRejected(record, attemptId, deps) {
1796
+ let current = record;
1797
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
1798
+ if (current.marker?.attemptId !== attemptId || current.state !== "paying") {
1799
+ break;
1800
+ }
1801
+ const ended = await deps.store.clearMarker(
1802
+ current.orderId,
1803
+ current.version,
1804
+ attemptId,
1805
+ "ended-unpaid",
1806
+ "rejected"
1807
+ );
1808
+ if (ended.ok) {
1809
+ return refusal("rejected", ended.record);
1810
+ }
1811
+ if (ended.reason !== "conflict") {
1812
+ break;
1813
+ }
1814
+ current = await deps.store.get(record.orderId);
1815
+ }
1816
+ return refusal("conflict", current ?? record);
1817
+ }
1818
+ function isUserRejection(error) {
1819
+ return typeof error === "object" && error !== null && "code" in error && error.code === 4001;
1820
+ }
1821
+ async function payWithTempo(record, wallet, fresh, deps) {
1822
+ const target = tempoTargetOf(record);
1823
+ const payer = wallet.address.toLowerCase();
1824
+ if (target === void 0 || !EVM_ADDRESS_RE.test(payer)) {
1825
+ return refusal("not_payable", record);
1826
+ }
1827
+ const head = await readFinalizedBlock(deps.client);
1828
+ if (head === null) {
1829
+ return refusal("rpc_error", record);
1830
+ }
1831
+ const failed = await checkBeforeTempoPaying(record, payer, fresh, target, head, deps);
1832
+ if (failed !== void 0) {
1833
+ return failed;
1834
+ }
1835
+ const composed = await requestFor(record, target, head, deps);
1836
+ if (isRefusal(composed)) {
1837
+ return composed;
1838
+ }
1839
+ const { request } = composed;
1840
+ let current = composed.record;
1841
+ if (head.timestamp + MIN_PAY_WINDOW_SECS > request.created_at + request.expiry_secs) {
1842
+ return refusal("too_late", await endNothingRequested(current, deps));
1843
+ }
1844
+ let calls;
1845
+ try {
1846
+ calls = buildTempoPaymentCalls(request, payer);
1847
+ } catch {
1848
+ return refusal("unpayable", await endNothingRequested(current, deps));
1849
+ }
1850
+ const call = calls.calls[0];
1851
+ if (calls.calls.length !== 1 || call === void 0) {
1852
+ return refusal("not_payable", current);
1853
+ }
1854
+ const now = (deps.now ?? nowSecs)();
1855
+ const attemptId = (deps.newAttemptId ?? (() => crypto.randomUUID()))();
1856
+ const marked = await deps.store.setMarker(
1857
+ current.orderId,
1858
+ current.version,
1859
+ { rail: "tempo", attemptId, setAt: now, floorBlock: String(head.number) },
1860
+ now
1861
+ );
1862
+ if (!marked.ok) {
1863
+ return storeRefusal2(marked, current);
1864
+ }
1865
+ current = marked.record;
1866
+ const walletChain = await wallet.chainId().catch(() => void 0);
1867
+ if (walletChain !== target.chain.evmChainId) {
1868
+ const cleared = await deps.store.clearMarker(
1869
+ current.orderId,
1870
+ current.version,
1871
+ attemptId,
1872
+ "ordered"
1873
+ );
1874
+ return refusal("wrong_chain", cleared.ok ? cleared.record : current);
1875
+ }
1876
+ let returned;
1877
+ try {
1878
+ returned = await wallet.sendCall({
1879
+ from: payer,
1880
+ to: call.to,
1881
+ data: call.data,
1882
+ chainId: calls.chainId
1883
+ });
1884
+ } catch (error) {
1885
+ if (isUserRejection(error)) {
1886
+ return await endRejected(current, attemptId, deps);
1887
+ }
1888
+ return refusal("wallet_failed", current);
1889
+ }
1890
+ const hash = readTxHash(returned);
1891
+ if (hash === null) {
1892
+ return refusal("wallet_failed", current);
1893
+ }
1894
+ const saved = await saveTempoHash(current, attemptId, hash, deps);
1895
+ current = saved.record;
1896
+ if (!saved.saved) {
1897
+ return { ok: true, record: current, hash, hashUnsaved: true };
1898
+ }
1899
+ const receipt = await sendReceipt(current, hash, (deps.now ?? nowSecs)(), deps);
1900
+ return { ok: true, record: receipt.ok ? receipt.record : current, hash };
1901
+ }
1902
+ async function recordTempoPaid(record, hash, deps) {
1903
+ let current = record;
1904
+ if (current.receiptWrap === void 0) {
1905
+ const receipt = await sendReceipt(current, hash, (deps.now ?? nowSecs)(), deps);
1906
+ if (receipt.ok) {
1907
+ current = receipt.record;
1908
+ }
1909
+ }
1910
+ for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
1911
+ if (current.paidTx !== void 0 || isTerminal(current)) {
1912
+ return current;
1913
+ }
1914
+ const written = await deps.store.update(current.orderId, current.version, {
1915
+ state: "paid",
1916
+ paidTx: hash,
1917
+ paidAt: (deps.now ?? nowSecs)()
1918
+ });
1919
+ if (written.ok) {
1920
+ return written.record;
1921
+ }
1922
+ if (written.reason !== "conflict") {
1923
+ return current;
1924
+ }
1925
+ current = await deps.store.get(record.orderId);
1926
+ }
1927
+ return current ?? record;
1928
+ }
1929
+ async function recordBlocked(record, deps) {
1930
+ const written = await deps.store.update(record.orderId, record.version, { state: "blocked" });
1931
+ return written.ok ? written.record : record;
1932
+ }
1933
+ async function watchTempoPayment(record, deps, options = {}) {
1934
+ if (isTerminal(record)) {
1935
+ return { state: "closed", record };
1936
+ }
1937
+ if (record.paidTx !== void 0) {
1938
+ return { state: "paid", record: await recordTempoPaid(record, record.paidTx, deps) };
1939
+ }
1940
+ const request = storedTempoRequest(record);
1941
+ if (record.marker?.rail !== "tempo" || request === void 0) {
1942
+ return { state: "waiting", record };
1943
+ }
1944
+ let current = record;
1945
+ if (options.pendingHash !== void 0 && record.marker.txHash === void 0 && record.state === "paying") {
1946
+ current = (await saveTempoHash(record, record.marker.attemptId, options.pendingHash, deps)).record;
1947
+ }
1948
+ const marker = current.marker?.rail === "tempo" ? current.marker : record.marker;
1949
+ const hash = marker.txHash ?? options.pendingHash;
1950
+ const result = await verifyTempoPayment(deps.client, request, {
1951
+ ...hash === void 0 ? {} : { txSignature: hash },
1952
+ fromBlock: Number(marker.floorBlock),
1953
+ pollBudgetMs: 0
1954
+ });
1955
+ switch (result.outcome) {
1956
+ case "verified":
1957
+ return {
1958
+ state: "paid",
1959
+ record: await recordTempoPaid(current, result.providerLeg.transactionHash, deps)
1960
+ };
1961
+ case "none": {
1962
+ const held = hash !== void 0 || options.callPending === true || marker.bundleId !== void 0;
1963
+ if (!held && current.state === "paying") {
1964
+ return { state: "over", record: current };
1965
+ }
1966
+ return { state: "waiting", record: current, pastDeadline: hash !== void 0 };
1967
+ }
1968
+ case "refused":
1969
+ switch (result.code) {
1970
+ case "provider_leg_blocked":
1971
+ return { state: "blocked", record: await recordBlocked(current, deps) };
1972
+ case "wrong_chain":
1973
+ case "unknown_asset":
1974
+ case "unreadable_receipt":
1975
+ case "reverted":
1976
+ case "no_provider_leg":
1977
+ case "fee_leg_missing":
1978
+ case "fee_leg_blocked":
1979
+ return { state: "unsure", record: current };
1980
+ }
1981
+ return { state: "unsure", record: current };
1982
+ case "inconclusive":
1983
+ switch (result.reason) {
1984
+ case "not_yet_due":
1985
+ case "not_finalized":
1986
+ case "no_receipt":
1987
+ case "chain_unreadable":
1988
+ return { state: "waiting", record: current };
1989
+ case "incomplete_scan":
1990
+ case "control_failed":
1991
+ return { state: "unsure", record: current };
1992
+ }
1993
+ return { state: "unsure", record: current };
1994
+ }
1995
+ }
1996
+ function tempoLateDeadline(request) {
1997
+ return request.created_at + request.expiry_secs + EVM_LATE_PAYMENT_GRACE_SECS;
1998
+ }
1999
+ async function endTempoOrder(record, deps, options = {}) {
2000
+ if (record.state === "ordered" && record.marker === void 0) {
2001
+ const ended = await endNothingRequested(record, deps);
2002
+ return { ended: ended.state === "ended-unpaid", record: ended };
2003
+ }
2004
+ if (record.state !== "paying" || record.marker?.rail !== "tempo") {
2005
+ return { ended: false, record };
2006
+ }
2007
+ const watch = await watchTempoPayment(record, deps, options);
2008
+ if (watch.state !== "over" || watch.record.marker === void 0) {
2009
+ return { ended: false, record: watch.record };
2010
+ }
2011
+ const written = await deps.store.clearMarker(
2012
+ watch.record.orderId,
2013
+ watch.record.version,
2014
+ watch.record.marker.attemptId,
2015
+ "ended-unpaid",
2016
+ "over"
2017
+ );
2018
+ return written.ok ? { ended: true, record: written.record } : { ended: false, record: watch.record };
2019
+ }
2020
+ function mayStillBePaid(record, now) {
2021
+ return record.state === "ended-unpaid" && record.payout.caip19.startsWith("eip155:") && (record.endedBy === "over" || record.endedBy === void 0) && now - record.createdAt <= MERCHANT_CATCH_UP_SECS + MAX_CLOCK_SKEW_SECS;
2022
+ }
2023
+
2024
+ // src/buyer/purchase.ts
2025
+ function cancelledUnpaid(record) {
2026
+ return record.status?.status === "cancelled" && record.paidTx === void 0 && (record.state === "created" || record.state === "ordered");
2027
+ }
2028
+ function gone(record) {
2029
+ return record.state === "ended-unpaid" && record.paidTx === void 0;
2030
+ }
2031
+ function onOtherTerms(record, payout) {
2032
+ return cancelledUnpaid(record) || record.payout.caip19 !== payout.target.caip19.id || record.payout.address !== payout.target.address || record.amount !== payout.amount.toString();
2033
+ }
2034
+ async function endOrder(record, deps) {
2035
+ if (record.state === "created") {
2036
+ return { ended: true, record };
2037
+ }
2038
+ if (record.payout.caip19.startsWith("eip155:") && deps.tempo !== void 0) {
2039
+ return await endTempoOrder(record, { ...deps, client: deps.tempo });
2040
+ }
2041
+ if (deps.rpc !== void 0 && !record.payout.caip19.startsWith("eip155:")) {
2042
+ return await endSolanaOrder(record, { ...deps, rpc: deps.rpc });
2043
+ }
2044
+ if (record.state === "ordered" && record.marker === void 0) {
2045
+ const written = await deps.store.update(record.orderId, record.version, {
2046
+ state: "ended-unpaid",
2047
+ // Nothing was requested: no Tempo prompt can be open for it.
2048
+ ...record.payout.caip19.startsWith("eip155:") ? { endedBy: "nothing" } : {}
2049
+ });
2050
+ return written.ok ? { ended: true, record: written.record } : { ended: false, record };
2051
+ }
2052
+ return { ended: false, record };
2053
+ }
2054
+ function withDeadline(promise, ms) {
2055
+ let timer;
2056
+ const deadline = new Promise((_resolve, reject) => {
2057
+ timer = setTimeout(() => reject(new Error("timed out")), ms);
2058
+ });
2059
+ return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
2060
+ }
2061
+ function errorText(error) {
2062
+ return error instanceof Error ? error.message : String(error);
2063
+ }
2064
+ function createPool() {
2065
+ return new SimplePool();
2066
+ }
2067
+ function createRelayClient(options = {}) {
2068
+ const pool = options.pool ?? createPool();
2069
+ function queryRelay(relay, filter) {
2070
+ return new Promise((resolve) => {
2071
+ const events = [];
2072
+ let subscription;
2073
+ const deadline = setTimeout(() => {
2074
+ subscription?.close("deadline");
2075
+ resolve(events);
2076
+ }, RELAY_QUERY_DEADLINE_MS);
2077
+ try {
2078
+ subscription = pool.subscribeEose([relay], filter, {
2079
+ maxWait: RELAY_QUERY_MAX_WAIT_MS,
2080
+ ...options.auth === void 0 ? {} : { onauth: options.auth },
2081
+ onevent: (event) => events.push(event),
2082
+ onclose: () => {
2083
+ clearTimeout(deadline);
2084
+ resolve(events);
2085
+ }
2086
+ });
2087
+ } catch (error) {
2088
+ clearTimeout(deadline);
2089
+ throw error;
2090
+ }
2091
+ });
2092
+ }
2093
+ async function publishOne(url, event) {
2094
+ const relay = await pool.ensureRelay(url, { connectionTimeout: RELAY_CONNECT_MAX_WAIT_MS });
2095
+ try {
2096
+ return await relay.publish(event);
2097
+ } catch (error) {
2098
+ if (options.auth !== void 0 && errorText(error).startsWith("auth-required")) {
2099
+ await relay.auth(options.auth);
2100
+ return relay.publish(event);
2101
+ }
2102
+ throw error;
2103
+ }
2104
+ }
2105
+ function publishWithDeadline(url, event) {
2106
+ let timer;
2107
+ const deadline = new Promise((_resolve, reject) => {
2108
+ timer = setTimeout(() => reject(new Error("timed out")), RELAY_PUBLISH_DEADLINE_MS);
2109
+ });
2110
+ return Promise.race([publishOne(url, event), deadline]).finally(() => clearTimeout(timer));
2111
+ }
2112
+ const liveClosers = /* @__PURE__ */ new Set();
2113
+ function listenOn(url, filter, onEvent) {
2114
+ let closed = false;
2115
+ let attempt = 0;
2116
+ let authenticatedOn;
2117
+ let current;
2118
+ let timer;
2119
+ const again = () => {
2120
+ if (closed) {
2121
+ return;
2122
+ }
2123
+ const pause = SUBSCRIBE_RETRY_MS[Math.min(attempt, SUBSCRIBE_RETRY_MS.length - 1)] ?? SUBSCRIBE_RETRY_MAX_MS;
2124
+ attempt += 1;
2125
+ timer = setTimeout(() => void open(), pause);
2126
+ };
2127
+ const open = async () => {
2128
+ if (closed) {
2129
+ return;
2130
+ }
2131
+ let relay;
2132
+ try {
2133
+ relay = await pool.ensureRelay(url, { connectionTimeout: RELAY_CONNECT_MAX_WAIT_MS });
2134
+ } catch {
2135
+ again();
2136
+ return;
2137
+ }
2138
+ if (closed) {
2139
+ return;
2140
+ }
2141
+ let liveSince;
2142
+ const subscription = relay.subscribe([filter], {
2143
+ onevent: onEvent,
2144
+ // nostr-tools also fires this on its own EOSE timeout, even after a CLOSED:
2145
+ // only a subscription still open counts.
2146
+ oneose: () => {
2147
+ if (current === subscription) {
2148
+ liveSince = Date.now();
2149
+ }
2150
+ },
2151
+ onclose: (reason) => {
2152
+ if (current === subscription) {
2153
+ current = void 0;
2154
+ if (liveSince !== void 0 && Date.now() - liveSince >= SUBSCRIBE_STABLE_MS) {
2155
+ attempt = 0;
2156
+ }
2157
+ }
2158
+ if (closed) {
2159
+ return;
2160
+ }
2161
+ if (options.auth !== void 0 && authenticatedOn !== relay && reason.startsWith("auth-required")) {
2162
+ authenticatedOn = relay;
2163
+ withDeadline(relay.auth(options.auth), RELAY_PUBLISH_DEADLINE_MS).then(
2164
+ () => void open(),
2165
+ () => again()
2166
+ );
2167
+ return;
2168
+ }
2169
+ again();
2170
+ }
2171
+ });
2172
+ current = subscription;
2173
+ };
2174
+ void open();
2175
+ return () => {
2176
+ closed = true;
2177
+ clearTimeout(timer);
2178
+ current?.close("closed by caller");
2179
+ };
2180
+ }
2181
+ return {
2182
+ subscribe(relays, filter, onEvent) {
2183
+ const seen = /* @__PURE__ */ new Set();
2184
+ const handOn = (event) => {
2185
+ if (!seen.has(event.id) && isGenuineEvent(event)) {
2186
+ seen.add(event.id);
2187
+ onEvent(event);
2188
+ }
2189
+ };
2190
+ const closers = relays.map((relay) => listenOn(relay, filter, handOn));
2191
+ const closeAll = () => {
2192
+ for (const close of closers) {
2193
+ close();
2194
+ }
2195
+ liveClosers.delete(closeAll);
2196
+ };
2197
+ liveClosers.add(closeAll);
2198
+ return { close: closeAll };
2199
+ },
2200
+ async query(relays, filters) {
2201
+ if (relays.length === 0) {
2202
+ return [];
2203
+ }
2204
+ const seen = /* @__PURE__ */ new Map();
2205
+ const batches = await Promise.allSettled(
2206
+ filters.flatMap((filter) => relays.map((relay) => queryRelay(relay, filter)))
2207
+ );
2208
+ for (const batch of batches) {
2209
+ if (batch.status !== "fulfilled") {
2210
+ continue;
2211
+ }
2212
+ for (const event of batch.value) {
2213
+ if (!seen.has(event.id) && isGenuineEvent(event)) {
2214
+ seen.set(event.id, event);
2215
+ }
2216
+ }
2217
+ }
2218
+ return [...seen.values()];
2219
+ },
2220
+ async publish(relays, event) {
2221
+ const byConnection = /* @__PURE__ */ new Map();
2222
+ const unusable = [];
2223
+ for (const relay of relays) {
2224
+ let connection;
2225
+ try {
2226
+ connection = normalizeURL(relay);
2227
+ } catch {
2228
+ unusable.push(relay);
2229
+ continue;
2230
+ }
2231
+ if (!byConnection.has(connection)) {
2232
+ byConnection.set(connection, relay);
2233
+ }
2234
+ }
2235
+ const targets = [...byConnection.values()];
2236
+ const results = await Promise.allSettled(
2237
+ targets.map((relay) => publishWithDeadline(relay, event))
2238
+ );
2239
+ const outcome = {
2240
+ accepted: [],
2241
+ failed: unusable.map((relay) => ({ relay, reason: "not a relay URL" }))
2242
+ };
2243
+ results.forEach((result, index) => {
2244
+ const relay = targets[index];
2245
+ if (relay === void 0) {
2246
+ return;
2247
+ }
2248
+ if (result.status === "fulfilled") {
2249
+ outcome.accepted.push(relay);
2250
+ } else {
2251
+ outcome.failed.push({ relay, reason: errorText(result.reason) });
2252
+ }
2253
+ });
2254
+ return outcome;
2255
+ },
2256
+ close() {
2257
+ for (const close of [...liveClosers]) {
2258
+ close();
2259
+ }
2260
+ pool.destroy();
2261
+ }
2262
+ };
2263
+ }
2264
+
2265
+ export { CONFIRM_WARNINGS, DEFAULT_RELAYS, MAX_CLOCK_SKEW_SECS, MAX_RELAY_URL_LENGTH, MERCHANT_CATCH_UP_SECS, MemoryOrderBackend, OFFER_SNAPSHOT_MAX_AGE_SECS, ORDER_ACK_TARGET, OrderStore, PAYMENT_SCAN_MARGIN_SECS, PAY_CUTOFF_SECS, RELAY_CONNECT_MAX_WAIT_MS, RELAY_PUBLISH_DEADLINE_MS, RELAY_QUERY_DEADLINE_MS, RELAY_QUERY_MAX_WAIT_MS, SOLANA_COMPUTE_UNIT_LIMIT, STORE_RELAY_CAP, STORE_WRITE_ATTEMPTS, SUBSCRIBE_RETRY_MAX_MS, SUBSCRIBE_RETRY_MS, SUBSCRIBE_STABLE_MS, TERMINAL_STATES, WRAP_BACKDATE_SECS, applyStatus, cancelledUnpaid, checkBeforePaying, checkSignedTransaction, clockAgrees, compareOffers, composeOrderPayment, createPool, createRelayClient, deliveryLink, endOrder, endSolanaOrder, endTempoOrder, gone, holdsPayExclusion, isEventShaped, isGenuineEvent, isPageOrigin, isSnapshotStale, isTerminal, judgeAdd, judgeClearMarker, judgeSetMarker, judgeUpdate, judgeUpdateMarker, listenForStatus, loadOffer, loadOfferForAgent, localSolanaWallet, mayStillBePaid, mediumOf, mergePins, newestGenuine, newestStoreInbox, normalizeRelayUrl, nowSecs, onOtherTerms, payWithSolana, payWithTempo, placeOrder, readChainTime, readRelays, readStoreInbox, readTempoChainTime, recordToShow, relayHost, resumeOrder, retryWithSolana, saveTempoHash, sendReceipt, statusFor, storeRelays, storedSolanaRequest, storedTempoRequest, tempoFeeMargin, tempoLateDeadline, uniqueRelays, watchSolanaPayment, watchTempoPayment };
2266
+ //# sourceMappingURL=buyer.js.map
2267
+ //# sourceMappingURL=buyer.js.map