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