@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.d.ts
ADDED
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { NostrEvent, EventTemplate, VerifiedEvent, Filter } from 'nostr-tools';
|
|
2
|
+
import { SimplePool } from 'nostr-tools/pool';
|
|
3
|
+
import { Rpc, SolanaRpcApi, KeyPairSigner } from '@solana/kit';
|
|
4
|
+
import { ChainFamily, Network, ChainConfig, Asset, PaymentRequestData } from '@elisym/pay-core';
|
|
5
|
+
import { t as OfferWarning, F as FetchLike, b as DomainKeys, X as VerifiedOffer, H as PayoutTarget, r as OfferRefusal, x as OrderStatusMessage } from './verify-offer-BZ_a9x3L.js';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Relays the widget always queries for the offer and the store's inbox list,
|
|
10
|
+
* outside the cap on store-named relays: the store's own relays hold neither the
|
|
11
|
+
* owner's payout list nor its authorization, and the store key must not choose
|
|
12
|
+
* where those reads go.
|
|
13
|
+
*/
|
|
14
|
+
declare const DEFAULT_RELAYS: readonly string[];
|
|
15
|
+
/** At most this many relays named by the store (its inbox list, naddr hints). */
|
|
16
|
+
declare const STORE_RELAY_CAP = 8;
|
|
17
|
+
/** A relay URL longer than this is not one. */
|
|
18
|
+
declare const MAX_RELAY_URL_LENGTH = 256;
|
|
19
|
+
/** An offer snapshot older than this is verified again before ordering or paying. */
|
|
20
|
+
declare const OFFER_SNAPSHOT_MAX_AGE_SECS = 120;
|
|
21
|
+
/** Store inbox relays that must acknowledge the order before the wallet opens. */
|
|
22
|
+
declare const ORDER_ACK_TARGET = 2;
|
|
23
|
+
/** How long one relay query waits before it answers with what it has. */
|
|
24
|
+
declare const RELAY_QUERY_MAX_WAIT_MS = 4000;
|
|
25
|
+
/** The most one relay's query may take in all, AUTH and a re-subscription included. */
|
|
26
|
+
declare const RELAY_QUERY_DEADLINE_MS = 15000;
|
|
27
|
+
/** How long a publish waits to connect to one relay. */
|
|
28
|
+
declare const RELAY_CONNECT_MAX_WAIT_MS = 6000;
|
|
29
|
+
/**
|
|
30
|
+
* The most one relay's publish may take in all: connect, OK, AUTH and a retry.
|
|
31
|
+
* The OK itself is bounded by the relay library's own publish timeout.
|
|
32
|
+
*/
|
|
33
|
+
declare const RELAY_PUBLISH_DEADLINE_MS = 20000;
|
|
34
|
+
/** Pauses before a closed long-lived subscription is opened again, growing to the last. */
|
|
35
|
+
declare const SUBSCRIBE_RETRY_MAX_MS = 60000;
|
|
36
|
+
declare const SUBSCRIBE_RETRY_MS: readonly number[];
|
|
37
|
+
/**
|
|
38
|
+
* How long a subscription must stay open past its EOSE before a close starts the
|
|
39
|
+
* pauses over: a relay that drops right after EOSE is not retried every second.
|
|
40
|
+
*/
|
|
41
|
+
declare const SUBSCRIBE_STABLE_MS = 30000;
|
|
42
|
+
/** Tries of an order-record write that lost a race with another tab. */
|
|
43
|
+
declare const STORE_WRITE_ATTEMPTS = 5;
|
|
44
|
+
/** The device clock may differ from chain time by at most this much to order. */
|
|
45
|
+
declare const MAX_CLOCK_SKEW_SECS: number;
|
|
46
|
+
/** NIP-59 back-dates a gift wrap up to two days: a read for replies reaches back that far. */
|
|
47
|
+
declare const WRAP_BACKDATE_SECS: number;
|
|
48
|
+
/** The merchant keeps catching up on an order's payments this long after it (plan: 3 days). */
|
|
49
|
+
declare const MERCHANT_CATCH_UP_SECS: number;
|
|
50
|
+
/** No wallet request, first or retry, this close to the end of the merchant's catch-up. */
|
|
51
|
+
declare const PAY_CUTOFF_SECS: number;
|
|
52
|
+
/**
|
|
53
|
+
* A payment is looked for back to the order's `created_at` minus this margin
|
|
54
|
+
* (the merchant's own scan margin): the order is dated by the chain, a block a
|
|
55
|
+
* little behind it may hold the payment.
|
|
56
|
+
*/
|
|
57
|
+
declare const PAYMENT_SCAN_MARGIN_SECS: number;
|
|
58
|
+
/** Compute units the payment transaction asks for: what pay-core's own builder uses. */
|
|
59
|
+
declare const SOLANA_COMPUTE_UNIT_LIMIT = 200000;
|
|
60
|
+
|
|
61
|
+
/** Whether a value from a relay has the NIP-01 field types. */
|
|
62
|
+
declare function isEventShaped(event: unknown): event is NostrEvent;
|
|
63
|
+
/**
|
|
64
|
+
* Whether an event's id is its hash and its signature is genuine, checked every
|
|
65
|
+
* time on a fresh object: `verifyEvent` caches its verdict on the object it is
|
|
66
|
+
* given, and a spread copy of a genuine event would carry that cache.
|
|
67
|
+
*/
|
|
68
|
+
declare function isGenuineEvent(event: unknown): event is NostrEvent;
|
|
69
|
+
/**
|
|
70
|
+
* The newest genuine event of `kind` by `author` among everything the relays
|
|
71
|
+
* returned, or `undefined`. An event dated further ahead than the skew
|
|
72
|
+
* allowance is ignored, so a far-future copy cannot pin an old value; a tie on
|
|
73
|
+
* `created_at` goes to the lowest id (NIP-01).
|
|
74
|
+
*/
|
|
75
|
+
declare function newestGenuine(events: readonly unknown[], kind: number, author: string, now: number): NostrEvent | undefined;
|
|
76
|
+
declare function nowSecs(): number;
|
|
77
|
+
|
|
78
|
+
/** Signs a NIP-42 AUTH event with the buyer key when a relay asks for one. */
|
|
79
|
+
type AuthSigner = (template: EventTemplate) => Promise<VerifiedEvent>;
|
|
80
|
+
interface PublishResult {
|
|
81
|
+
/** Relays that answered OK. */
|
|
82
|
+
accepted: string[];
|
|
83
|
+
/** Relays that refused, failed or timed out, with their reason. */
|
|
84
|
+
failed: {
|
|
85
|
+
relay: string;
|
|
86
|
+
reason: string;
|
|
87
|
+
}[];
|
|
88
|
+
}
|
|
89
|
+
interface RelayClient {
|
|
90
|
+
/**
|
|
91
|
+
* Every event the relays hold for the filters, each once. Nothing is trusted:
|
|
92
|
+
* the caller checks signatures and authors itself. A relay that fails adds
|
|
93
|
+
* nothing; it does not fail the query.
|
|
94
|
+
*/
|
|
95
|
+
query(relays: readonly string[], filters: readonly Filter[]): Promise<NostrEvent[]>;
|
|
96
|
+
publish(relays: readonly string[], event: NostrEvent): Promise<PublishResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Keep listening on each relay for `filter`, each event handed on once. A
|
|
99
|
+
* relay that closes the subscription is opened again after a pause; one that
|
|
100
|
+
* asks for AUTH gets it once, signed by the client's key.
|
|
101
|
+
*/
|
|
102
|
+
subscribe(relays: readonly string[], filter: Filter, onEvent: (event: NostrEvent) => void): {
|
|
103
|
+
close(): void;
|
|
104
|
+
};
|
|
105
|
+
close(): void;
|
|
106
|
+
}
|
|
107
|
+
/** The part of a connected relay the client uses. */
|
|
108
|
+
interface RelayLike {
|
|
109
|
+
/** Resolves only on the relay's OK true; rejects on a refusal or a timeout. */
|
|
110
|
+
publish(event: NostrEvent): Promise<string>;
|
|
111
|
+
auth(signer: AuthSigner): Promise<string>;
|
|
112
|
+
/** A relay-level subscription: nostr-tools takes a LIST of filters here. */
|
|
113
|
+
subscribe(filters: Filter[], params: {
|
|
114
|
+
onevent?: (event: NostrEvent) => void;
|
|
115
|
+
oneose?: () => void;
|
|
116
|
+
onclose?: (reason: string) => void;
|
|
117
|
+
}): {
|
|
118
|
+
close(reason?: string): void;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** The part of `SimplePool` the client uses, so tests can stand in for relays. */
|
|
122
|
+
interface PoolLike {
|
|
123
|
+
subscribeEose(relays: string[], filter: Filter, params: {
|
|
124
|
+
maxWait?: number;
|
|
125
|
+
onauth?: AuthSigner;
|
|
126
|
+
onevent?: (event: NostrEvent) => void;
|
|
127
|
+
onclose?: (reasons: string[]) => void;
|
|
128
|
+
}): {
|
|
129
|
+
close(reason?: string): void;
|
|
130
|
+
};
|
|
131
|
+
ensureRelay(url: string, params?: {
|
|
132
|
+
connectionTimeout?: number;
|
|
133
|
+
}): Promise<RelayLike>;
|
|
134
|
+
destroy(): void;
|
|
135
|
+
}
|
|
136
|
+
interface RelayClientOptions {
|
|
137
|
+
/** Answers NIP-42 AUTH challenges (the buyer key). Without it a relay that asks is refused. */
|
|
138
|
+
auth?: AuthSigner;
|
|
139
|
+
pool?: PoolLike;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A pool that never answers an AUTH challenge on its own: the buyer key signs
|
|
143
|
+
* only for a relay that refuses a request with auth-required (the query's
|
|
144
|
+
* `onauth`, the publish retry), not for every relay that merely asks on connect.
|
|
145
|
+
* Exported for tests.
|
|
146
|
+
*/
|
|
147
|
+
declare function createPool(): SimplePool;
|
|
148
|
+
declare function createRelayClient(options?: RelayClientOptions): RelayClient;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* A store-named relay URL the widget may connect to, in one spelling, or
|
|
152
|
+
* `undefined`. Only `wss:` on a public DNS name: the URL comes from the store
|
|
153
|
+
* (its inbox list) or the page (naddr hints), so a loopback, private or IP
|
|
154
|
+
* literal host - or plain `ws:` - is never contacted.
|
|
155
|
+
*/
|
|
156
|
+
declare function normalizeRelayUrl(value: unknown): string | undefined;
|
|
157
|
+
/**
|
|
158
|
+
* The server a relay URL reaches (host and port): `wss://r.example.com` and
|
|
159
|
+
* `wss://r.example.com/inbox` are usually one relay, and count once.
|
|
160
|
+
*/
|
|
161
|
+
declare function relayHost(url: string): string;
|
|
162
|
+
/** Each usable URL once, in the order given. */
|
|
163
|
+
declare function uniqueRelays(values: readonly unknown[]): string[];
|
|
164
|
+
interface StoreRelaySources {
|
|
165
|
+
/** The relays of the store's current inbox list (kind 10050): where it reads and replies. */
|
|
166
|
+
inbox?: readonly unknown[];
|
|
167
|
+
/** Older inbox relays that acknowledged this order: kept for republishing only. */
|
|
168
|
+
acknowledged?: readonly unknown[];
|
|
169
|
+
/** Relay hints from the product naddr. */
|
|
170
|
+
hints?: readonly unknown[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The store-named relays, capped at `STORE_RELAY_CAP` in a fixed priority: the
|
|
174
|
+
* current inbox first (the widget listens there), then old relays that
|
|
175
|
+
* acknowledged the order, then naddr hints - so a page's hints can never crowd
|
|
176
|
+
* out the store's inbox.
|
|
177
|
+
*/
|
|
178
|
+
declare function storeRelays(sources: StoreRelaySources): string[];
|
|
179
|
+
/**
|
|
180
|
+
* Where the offer and the store's inbox list are read: the default relays,
|
|
181
|
+
* always and outside the cap, plus the capped store-named ones.
|
|
182
|
+
*/
|
|
183
|
+
declare function readRelays(sources: StoreRelaySources): string[];
|
|
184
|
+
|
|
185
|
+
interface StoreInbox {
|
|
186
|
+
/** The store's inbox relays the widget may use, capped, in the store's order. */
|
|
187
|
+
relays: string[];
|
|
188
|
+
/** The kind 10050 they came from. */
|
|
189
|
+
event: NostrEvent;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The store's inbox relays from the newest genuine kind 10050 signed by the store
|
|
193
|
+
* key among everything the relays returned - the same rule as for the payout list,
|
|
194
|
+
* since a stale copy from one relay would send the order where the merchant no
|
|
195
|
+
* longer reads. `undefined` when there is none, or it names no usable relay: the
|
|
196
|
+
* widget then refuses before ordering.
|
|
197
|
+
*/
|
|
198
|
+
declare function newestStoreInbox(events: readonly unknown[], storePubkey: string, now?: number): StoreInbox | undefined;
|
|
199
|
+
/**
|
|
200
|
+
* Read the store's inbox list from the default relays - always, outside the cap -
|
|
201
|
+
* plus the capped store-named relays, and pick it by `newestStoreInbox`.
|
|
202
|
+
*/
|
|
203
|
+
declare function readStoreInbox(client: RelayClient, sources: StoreRelaySources, storePubkey: string, now?: number): Promise<StoreInbox | undefined>;
|
|
204
|
+
|
|
205
|
+
/** Warnings the buyer must confirm, for the exact payout, before paying. */
|
|
206
|
+
declare const CONFIRM_WARNINGS: readonly OfferWarning[];
|
|
207
|
+
/** A payout the widget can pay: priced in that coin's subunits. */
|
|
208
|
+
interface PricedPayout {
|
|
209
|
+
target: PayoutTarget;
|
|
210
|
+
/** The product's price in the coin's subunits (quantity 1). */
|
|
211
|
+
amount: bigint;
|
|
212
|
+
}
|
|
213
|
+
type WidgetRefusal = OfferRefusal
|
|
214
|
+
/** The page's origin is opaque or not an http(s) origin. */
|
|
215
|
+
| 'bad_page_origin'
|
|
216
|
+
/** No payout the widget can price and pay on the allowed rails and networks. */
|
|
217
|
+
| 'no_payable_payout';
|
|
218
|
+
type LoadedOffer = {
|
|
219
|
+
ok: true;
|
|
220
|
+
offer: VerifiedOffer;
|
|
221
|
+
/** `30402:<store>:<d>`: the key the widget's records are indexed by. */
|
|
222
|
+
productAddress: string;
|
|
223
|
+
payouts: PricedPayout[];
|
|
224
|
+
/** Warnings that need an explicit confirmation before paying. */
|
|
225
|
+
confirm: OfferWarning[];
|
|
226
|
+
/** Warnings that are only shown. */
|
|
227
|
+
notices: OfferWarning[];
|
|
228
|
+
/** The naddr's usable relay hints (store-named, so capped with the inbox later). */
|
|
229
|
+
hints: string[];
|
|
230
|
+
/** Where the offer was read: the default relays plus the capped hints. */
|
|
231
|
+
relays: string[];
|
|
232
|
+
/** When this snapshot was taken (seconds). */
|
|
233
|
+
snapshotAt: number;
|
|
234
|
+
} | {
|
|
235
|
+
ok: false;
|
|
236
|
+
refusal: WidgetRefusal;
|
|
237
|
+
message: string;
|
|
238
|
+
};
|
|
239
|
+
interface StorePins {
|
|
240
|
+
/** The owner pinned at the first purchase from this store. */
|
|
241
|
+
pinnedOwnerPubkey?: string;
|
|
242
|
+
/** Every payout of every verified offer that delivered from this store. */
|
|
243
|
+
knownPayouts?: readonly {
|
|
244
|
+
caip19: string;
|
|
245
|
+
address: string;
|
|
246
|
+
}[];
|
|
247
|
+
}
|
|
248
|
+
interface LoadOfferOptions {
|
|
249
|
+
client: RelayClient;
|
|
250
|
+
/** The embedding page's origin, from the accepted `hello`. */
|
|
251
|
+
pageOrigin: string;
|
|
252
|
+
/** Set by the page's `strict-origin` attribute: also refuse at levels B and C. */
|
|
253
|
+
strictOrigin?: boolean;
|
|
254
|
+
pins?: StorePins;
|
|
255
|
+
/** Rails the widget can pay on now: required, so a new rail is offered only once it is built. */
|
|
256
|
+
families: readonly ChainFamily[];
|
|
257
|
+
/** The page's `network` attribute: only payouts on this network are offered. */
|
|
258
|
+
network?: Network;
|
|
259
|
+
now?: number;
|
|
260
|
+
/** Domain lookups (nostr.json, DoH); defaults to the global `fetch`. */
|
|
261
|
+
fetch?: FetchLike;
|
|
262
|
+
resolveDomain?: (nip05: string) => Promise<DomainKeys | undefined>;
|
|
263
|
+
}
|
|
264
|
+
/** Whether `value` is a real http(s) origin, in its canonical spelling. */
|
|
265
|
+
declare function isPageOrigin(value: unknown): value is string;
|
|
266
|
+
/** Whether a snapshot taken at `snapshotAt` must be verified again before use. */
|
|
267
|
+
declare function isSnapshotStale(snapshotAt: number, now?: number): boolean;
|
|
268
|
+
/**
|
|
269
|
+
* Verify the offer behind `naddr` for this page and apply the widget's own
|
|
270
|
+
* policy on top: a level A store the page is not on is refused (not only warned
|
|
271
|
+
* about), and only payouts the widget can price and pay are offered.
|
|
272
|
+
*/
|
|
273
|
+
declare function loadOffer(naddr: string, options: LoadOfferOptions): Promise<LoadedOffer>;
|
|
274
|
+
/**
|
|
275
|
+
* The offer for a buyer that is not a web page (an AI agent through the MCP):
|
|
276
|
+
* the same verification, with no page origin, so no origin rule applies. The
|
|
277
|
+
* caller shows the trust level and the verified domain to its user instead.
|
|
278
|
+
*/
|
|
279
|
+
declare function loadOfferForAgent(naddr: string, options: Omit<LoadOfferOptions, 'pageOrigin' | 'strictOrigin'>): Promise<LoadedOffer>;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Where one order stands. The order is sent and acknowledged BEFORE any wallet
|
|
283
|
+
* opens (`created` -> `ordered`); a payment attempt holds a marker (`paying`);
|
|
284
|
+
* a payment found on chain waits for the store (`paid`).
|
|
285
|
+
*/
|
|
286
|
+
type OrderState =
|
|
287
|
+
/** Written, the order not yet acknowledged by the store's inbox relays. */
|
|
288
|
+
'created'
|
|
289
|
+
/** Acknowledged; no payment attempt holds a marker. */
|
|
290
|
+
| 'ordered'
|
|
291
|
+
/** A payment attempt holds the marker. */
|
|
292
|
+
| 'paying'
|
|
293
|
+
/** The payment was found on chain; waiting for the store's status. */
|
|
294
|
+
| 'paid'
|
|
295
|
+
/** The store delivered. */
|
|
296
|
+
| 'completed'
|
|
297
|
+
/** The store cancelled with a refund. */
|
|
298
|
+
| 'refunded'
|
|
299
|
+
/** No payment by the deadline: a new order may start (it is still reconciled). */
|
|
300
|
+
| 'ended-unpaid'
|
|
301
|
+
/** Tempo: the money sits with the recipient's transfer policy guard. */
|
|
302
|
+
| 'blocked';
|
|
303
|
+
type PaymentMarker = {
|
|
304
|
+
rail: 'solana';
|
|
305
|
+
/** Random per attempt: every clear or replace is a compare-and-swap on it. */
|
|
306
|
+
attemptId: string;
|
|
307
|
+
setAt: number;
|
|
308
|
+
/** The blockhash the widget set; the widget broadcasts only a message with this lifetime. */
|
|
309
|
+
blockhash: string;
|
|
310
|
+
/** Decimal string: the attempt is over once `finalized` passes this height. */
|
|
311
|
+
lastValidBlockHeight: string;
|
|
312
|
+
/**
|
|
313
|
+
* Decimal string: the slot the blockhash was read at. Nothing of this attempt can
|
|
314
|
+
* sit below it, so an RPC whose ledger starts above it cannot prove "not paid".
|
|
315
|
+
*/
|
|
316
|
+
slot?: string;
|
|
317
|
+
/** The signature of the transaction the wallet signed, once known. */
|
|
318
|
+
signature?: string;
|
|
319
|
+
/**
|
|
320
|
+
* The signed wire transaction (base64), written with its signature BEFORE the
|
|
321
|
+
* first broadcast and rebroadcast until it lands or its blockhash expires.
|
|
322
|
+
*/
|
|
323
|
+
signedTransaction?: string;
|
|
324
|
+
} | {
|
|
325
|
+
rail: 'tempo';
|
|
326
|
+
attemptId: string;
|
|
327
|
+
setAt: number;
|
|
328
|
+
/** Decimal string: the finalized block read before the wallet call. */
|
|
329
|
+
floorBlock: string;
|
|
330
|
+
/** The hash the wallet returned. */
|
|
331
|
+
txHash?: string;
|
|
332
|
+
/** A bundle id (`wallet_sendCalls`): approved, the hash comes later. */
|
|
333
|
+
bundleId?: string;
|
|
334
|
+
};
|
|
335
|
+
interface OrderStatus {
|
|
336
|
+
status: 'pending' | 'confirmed' | 'completed' | 'cancelled';
|
|
337
|
+
/** When the widget accepted it (seconds). */
|
|
338
|
+
at: number;
|
|
339
|
+
delivery?: string;
|
|
340
|
+
refunded?: boolean;
|
|
341
|
+
}
|
|
342
|
+
interface OrderRecord {
|
|
343
|
+
orderId: string;
|
|
344
|
+
/** `30402:<store>:<d>`: records are indexed by it, never by the naddr string. */
|
|
345
|
+
productAddress: string;
|
|
346
|
+
storePubkey: string;
|
|
347
|
+
/** The one-time buyer key for this order (hex). */
|
|
348
|
+
buyerSecretKey: string;
|
|
349
|
+
buyerPubkey: string;
|
|
350
|
+
/** The order rumor's `created_at` (chain time, seconds). */
|
|
351
|
+
createdAt: number;
|
|
352
|
+
/** Bumped on every write: a writer states the version it judged. */
|
|
353
|
+
version: number;
|
|
354
|
+
state: OrderState;
|
|
355
|
+
payout: {
|
|
356
|
+
caip19: string;
|
|
357
|
+
address: string;
|
|
358
|
+
};
|
|
359
|
+
/** Decimal string of subunits. */
|
|
360
|
+
amount: string;
|
|
361
|
+
/** The receipt's `medium`: `solana`, `solana-devnet`, `tempo`, `tempo-moderato`. */
|
|
362
|
+
medium: string;
|
|
363
|
+
/** The derived payment reference (base58) or memo (0x hex). */
|
|
364
|
+
reference: string;
|
|
365
|
+
/** The verified offer the order was placed against. */
|
|
366
|
+
offer: VerifiedOffer;
|
|
367
|
+
/** The composed payment request, written once before the first marker. */
|
|
368
|
+
paymentRequest?: string;
|
|
369
|
+
/** Signed wraps, republished byte for byte on resume. */
|
|
370
|
+
orderWrap?: NostrEvent;
|
|
371
|
+
receiptWrap?: NostrEvent;
|
|
372
|
+
/** The store inbox relays the order went to. */
|
|
373
|
+
inboxRelays: string[];
|
|
374
|
+
/** Relays that answered OK to the order wrap. */
|
|
375
|
+
acknowledgedRelays: string[];
|
|
376
|
+
marker?: PaymentMarker;
|
|
377
|
+
/** The transaction that paid, once found. */
|
|
378
|
+
paidTx?: string;
|
|
379
|
+
status?: OrderStatus;
|
|
380
|
+
}
|
|
381
|
+
declare const TERMINAL_STATES: readonly OrderState[];
|
|
382
|
+
declare function isTerminal(record: Pick<OrderRecord, 'state'>): boolean;
|
|
383
|
+
/**
|
|
384
|
+
* Whether the record keeps any other order for the same product from paying: a
|
|
385
|
+
* set marker until the attempt provably ended (`ended-unpaid`, or `blocked` on
|
|
386
|
+
* Tempo), and a found payment until the store answers. "No stored signature" is
|
|
387
|
+
* never "never sent" on its own.
|
|
388
|
+
*/
|
|
389
|
+
declare function holdsPayExclusion(record: Pick<OrderRecord, 'state' | 'marker' | 'paidTx'>): boolean;
|
|
390
|
+
/**
|
|
391
|
+
* The record the widget shows for a product: the best rank (`showRank`), then
|
|
392
|
+
* the newest within it.
|
|
393
|
+
*/
|
|
394
|
+
declare function recordToShow(records: readonly OrderRecord[]): OrderRecord | undefined;
|
|
395
|
+
|
|
396
|
+
/** Per store, what earlier purchases pinned (TOFU). */
|
|
397
|
+
interface StorePinsRecord {
|
|
398
|
+
storePubkey: string;
|
|
399
|
+
pinnedOwnerPubkey: string;
|
|
400
|
+
/** The union of every payout of every verified offer that delivered from this store. */
|
|
401
|
+
knownPayouts: {
|
|
402
|
+
caip19: string;
|
|
403
|
+
address: string;
|
|
404
|
+
}[];
|
|
405
|
+
}
|
|
406
|
+
type StoreWrite = {
|
|
407
|
+
ok: true;
|
|
408
|
+
record: OrderRecord;
|
|
409
|
+
} | {
|
|
410
|
+
ok: false;
|
|
411
|
+
/**
|
|
412
|
+
* `missing`: no such record. `conflict`: the record (its version or its
|
|
413
|
+
* marker's attempt) is not the one the caller judged. `exclusion`: another
|
|
414
|
+
* record for this product holds a live payment. `not_ready`: the record's
|
|
415
|
+
* state does not allow this write.
|
|
416
|
+
*/
|
|
417
|
+
reason: 'missing' | 'conflict' | 'exclusion' | 'not_ready';
|
|
418
|
+
/** For `exclusion`: the order holding it. */
|
|
419
|
+
holder?: string;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Fields a plain update may change. What the order and its payment were made of
|
|
423
|
+
* (payout, amount, reference, offer, ...) is fixed when the record is added; the
|
|
424
|
+
* marker and the version have their own paths.
|
|
425
|
+
*/
|
|
426
|
+
type RecordPatch = Partial<Pick<OrderRecord, 'state' | 'paymentRequest' | 'orderWrap' | 'receiptWrap' | 'inboxRelays' | 'acknowledgedRelays' | 'paidTx' | 'status'>>;
|
|
427
|
+
/**
|
|
428
|
+
* Where order records live. Every money rule is judged by the core (below) on
|
|
429
|
+
* what `transactProduct` hands it; a backend only makes that read-judge-write
|
|
430
|
+
* atomic and durable: the checkout's IndexedDB (one readwrite transaction with
|
|
431
|
+
* strict durability), the MCP's locked, fsynced file, or memory in tests.
|
|
432
|
+
*
|
|
433
|
+
* `fn` is synchronous: the rule runs between the read and the write of ONE
|
|
434
|
+
* transaction (an `await` inside would let IndexedDB commit part-way). A
|
|
435
|
+
* backend resolves only once the writes are committed; a write that did not
|
|
436
|
+
* commit did not happen, and nothing is decided on it.
|
|
437
|
+
*/
|
|
438
|
+
interface OrderBackend {
|
|
439
|
+
read(orderId: string): Promise<OrderRecord | undefined>;
|
|
440
|
+
/** Every record for a product (any order; the store sorts). */
|
|
441
|
+
forProduct(productAddress: string): Promise<OrderRecord[]>;
|
|
442
|
+
/** Read the product's records, run `fn`, commit its writes (whole records, by `orderId`). */
|
|
443
|
+
transactProduct<T>(productAddress: string, fn: (records: readonly OrderRecord[]) => {
|
|
444
|
+
write?: readonly OrderRecord[];
|
|
445
|
+
result: T;
|
|
446
|
+
}): Promise<T>;
|
|
447
|
+
readPins(storePubkey: string): Promise<StorePinsRecord | undefined>;
|
|
448
|
+
/** Read one store's pins, run `fn`, commit its write. */
|
|
449
|
+
transactPins<T>(storePubkey: string, fn: (pins: StorePinsRecord | undefined) => {
|
|
450
|
+
write?: StorePinsRecord;
|
|
451
|
+
result: T;
|
|
452
|
+
}): Promise<T>;
|
|
453
|
+
}
|
|
454
|
+
/** A rule's verdict: what the caller is told, and the record to write when it is `ok`. */
|
|
455
|
+
interface Judged {
|
|
456
|
+
result: StoreWrite;
|
|
457
|
+
write?: readonly OrderRecord[];
|
|
458
|
+
}
|
|
459
|
+
/** A new order: `created`, version 1, no marker, an id not yet used. */
|
|
460
|
+
declare function judgeAdd(records: readonly OrderRecord[], record: OrderRecord): Judged;
|
|
461
|
+
/** Plain fields of the record the caller judged at `expectedVersion`. */
|
|
462
|
+
declare function judgeUpdate(records: readonly OrderRecord[], orderId: string, expectedVersion: number, patch: RecordPatch): Judged;
|
|
463
|
+
/**
|
|
464
|
+
* Test-and-set the payment marker immediately before the wallet call. Refused
|
|
465
|
+
* unless the record is still the one judged, acknowledged (`ordered`), holds
|
|
466
|
+
* its composed request and no marker - and no OTHER record for the product
|
|
467
|
+
* holds a live payment (two tabs cannot both pay).
|
|
468
|
+
*/
|
|
469
|
+
declare function judgeSetMarker(records: readonly OrderRecord[], orderId: string, expectedVersion: number, marker: PaymentMarker): Judged;
|
|
470
|
+
/**
|
|
471
|
+
* Change the marker of the attempt `attemptId` - record its signature or hash,
|
|
472
|
+
* or replace it with a retry's marker (same record, order and reference). Both
|
|
473
|
+
* the attempt and the record version must be the ones the caller judged: a
|
|
474
|
+
* signature recorded in between is never overrun.
|
|
475
|
+
*/
|
|
476
|
+
declare function judgeUpdateMarker(records: readonly OrderRecord[], orderId: string, expectedVersion: number, attemptId: string, next: PaymentMarker): Judged;
|
|
477
|
+
/**
|
|
478
|
+
* End the attempt `attemptId`, judged at `expectedVersion`: nothing was
|
|
479
|
+
* requested (`ordered`, the marker is removed), or the attempt provably ended
|
|
480
|
+
* (`ended-unpaid`, the marker is KEPT - it no longer excludes, but its floor,
|
|
481
|
+
* signature or hash is what later reconciliation starts from).
|
|
482
|
+
*/
|
|
483
|
+
declare function judgeClearMarker(records: readonly OrderRecord[], orderId: string, expectedVersion: number, attemptId: string, nextState: Extract<OrderState, 'ordered' | 'ended-unpaid'>): Judged;
|
|
484
|
+
/**
|
|
485
|
+
* After a delivery: pin the store's owner, and add every payout of the offer
|
|
486
|
+
* that delivered to the known ones (pinning only the paid one would flag every
|
|
487
|
+
* other rail the store lists as changed next time). A delivery under a
|
|
488
|
+
* different owner than the pinned one never replaces the pin.
|
|
489
|
+
*/
|
|
490
|
+
declare function mergePins(current: StorePinsRecord | undefined, storePubkey: string, ownerPubkey: string, payouts: readonly {
|
|
491
|
+
caip19: string;
|
|
492
|
+
address: string;
|
|
493
|
+
}[]): {
|
|
494
|
+
write?: StorePinsRecord;
|
|
495
|
+
result: StorePinsRecord;
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
* The order records, over any `OrderBackend`. Every write that decides
|
|
499
|
+
* something about money is judged inside the backend's transaction and
|
|
500
|
+
* re-checks what the caller judged (the record version, the marker's attempt
|
|
501
|
+
* id), so a stale tab, a second process or a late wallet answer can never
|
|
502
|
+
* overrun a newer write.
|
|
503
|
+
*/
|
|
504
|
+
declare class OrderStore {
|
|
505
|
+
private readonly backend;
|
|
506
|
+
constructor(backend: OrderBackend);
|
|
507
|
+
/** Add a new order: `created`, version 1, no marker. Anything else is refused. */
|
|
508
|
+
add(record: OrderRecord): Promise<void>;
|
|
509
|
+
get(orderId: string): Promise<OrderRecord | undefined>;
|
|
510
|
+
/** Every record for a product, oldest first. */
|
|
511
|
+
forProduct(productAddress: string): Promise<OrderRecord[]>;
|
|
512
|
+
/** Run a rule on the product of `orderId` (it never changes), in one transaction. */
|
|
513
|
+
private judge;
|
|
514
|
+
/** Change plain fields of the record the caller judged at `expectedVersion`. */
|
|
515
|
+
update(orderId: string, expectedVersion: number, patch: RecordPatch): Promise<StoreWrite>;
|
|
516
|
+
/** See `judgeSetMarker`. */
|
|
517
|
+
setMarker(orderId: string, expectedVersion: number, marker: PaymentMarker): Promise<StoreWrite>;
|
|
518
|
+
/** See `judgeUpdateMarker`. */
|
|
519
|
+
updateMarker(orderId: string, expectedVersion: number, attemptId: string, next: PaymentMarker): Promise<StoreWrite>;
|
|
520
|
+
/** See `judgeClearMarker`. */
|
|
521
|
+
clearMarker(orderId: string, expectedVersion: number, attemptId: string, nextState: Extract<OrderState, 'ordered' | 'ended-unpaid'>): Promise<StoreWrite>;
|
|
522
|
+
pins(storePubkey: string): Promise<StorePinsRecord | undefined>;
|
|
523
|
+
/** See `mergePins`. */
|
|
524
|
+
rememberDelivery(storePubkey: string, ownerPubkey: string, payouts: readonly {
|
|
525
|
+
caip19: string;
|
|
526
|
+
address: string;
|
|
527
|
+
}[]): Promise<StorePinsRecord>;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* An in-memory backend: for tests, and for a caller that keeps orders only for
|
|
531
|
+
* the life of the process. Transactions on it are trivially atomic (one thread).
|
|
532
|
+
*/
|
|
533
|
+
declare class MemoryOrderBackend implements OrderBackend {
|
|
534
|
+
private readonly orders;
|
|
535
|
+
private readonly stores;
|
|
536
|
+
read(orderId: string): Promise<OrderRecord | undefined>;
|
|
537
|
+
private snapshot;
|
|
538
|
+
forProduct(productAddress: string): Promise<OrderRecord[]>;
|
|
539
|
+
transactProduct<T>(productAddress: string, fn: (records: readonly OrderRecord[]) => {
|
|
540
|
+
write?: readonly OrderRecord[];
|
|
541
|
+
result: T;
|
|
542
|
+
}): Promise<T>;
|
|
543
|
+
readPins(storePubkey: string): Promise<StorePinsRecord | undefined>;
|
|
544
|
+
transactPins<T>(storePubkey: string, fn: (pins: StorePinsRecord | undefined) => {
|
|
545
|
+
write?: StorePinsRecord;
|
|
546
|
+
result: T;
|
|
547
|
+
}): Promise<T>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
type ReadyOffer$1 = Extract<LoadedOffer, {
|
|
551
|
+
ok: true;
|
|
552
|
+
}>;
|
|
553
|
+
/** The receipt `medium` of a chain: `solana`, `solana-devnet`, `tempo`, `tempo-moderato`. */
|
|
554
|
+
declare function mediumOf(chain: Pick<ChainConfig, 'slug' | 'network'>): string;
|
|
555
|
+
/**
|
|
556
|
+
* Whether the device clock is close enough to chain time to order: the rumor is
|
|
557
|
+
* dated by the chain, but the seal and the wrap by the device, and the widget
|
|
558
|
+
* drops a store status dated ahead of it - a slow clock would lose the delivery.
|
|
559
|
+
*/
|
|
560
|
+
declare function clockAgrees(chainTime: number, deviceTime: number): boolean;
|
|
561
|
+
/**
|
|
562
|
+
* How a fresh offer compares with the one the buyer confirmed: the same payout
|
|
563
|
+
* and price and no new warning to confirm (`same`); something the buyer must
|
|
564
|
+
* look at again (`changed`); or the chosen payout is gone (`gone`).
|
|
565
|
+
*/
|
|
566
|
+
declare function compareOffers(shown: PricedPayout, confirmed: readonly OfferWarning[], fresh: ReadyOffer$1): 'same' | 'changed' | 'gone';
|
|
567
|
+
interface PlaceOrderInput {
|
|
568
|
+
/** A FRESH verified offer (at most two minutes old). */
|
|
569
|
+
offer: ReadyOffer$1;
|
|
570
|
+
payout: PricedPayout;
|
|
571
|
+
email?: string;
|
|
572
|
+
/** Chain time read just before ordering (seconds). */
|
|
573
|
+
chainTime: number;
|
|
574
|
+
/** The device clock at the same moment (seconds). */
|
|
575
|
+
deviceTime: number;
|
|
576
|
+
}
|
|
577
|
+
interface OrderDeps {
|
|
578
|
+
store: OrderStore;
|
|
579
|
+
/** Reads (the offer, the inbox list): no key. */
|
|
580
|
+
readClient: RelayClient;
|
|
581
|
+
/**
|
|
582
|
+
* A NEW client for one order's writes, answering AUTH with its buyer key. The
|
|
583
|
+
* callee owns it and closes it when done, so it must never be one the caller
|
|
584
|
+
* also listens with.
|
|
585
|
+
*/
|
|
586
|
+
clientFor: (buyerSecretKey: Uint8Array) => RelayClient;
|
|
587
|
+
}
|
|
588
|
+
type PlaceOrderResult = {
|
|
589
|
+
ok: true;
|
|
590
|
+
record: OrderRecord;
|
|
591
|
+
} | {
|
|
592
|
+
ok: false;
|
|
593
|
+
reason:
|
|
594
|
+
/** The device clock is too far from chain time. */
|
|
595
|
+
'clock_skew'
|
|
596
|
+
/** The offer is older than two minutes, or the payout is not one of its own: verify again. */
|
|
597
|
+
| 'stale_offer'
|
|
598
|
+
/** The store names no usable inbox relay: nowhere to send the order. */
|
|
599
|
+
| 'no_store_inbox'
|
|
600
|
+
/** The order went out but too few inbox relays took it; `resume` may try again. */
|
|
601
|
+
| 'not_acknowledged';
|
|
602
|
+
record?: OrderRecord;
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Place a direct-mode order: a fresh one-time buyer key, a random order id, the
|
|
606
|
+
* reference derived from them, the order sealed and gift-wrapped to the store,
|
|
607
|
+
* recorded, then sent to the store's CURRENT inbox relays. It counts as placed
|
|
608
|
+
* only once enough of them said OK - the wallet opens only after that, since the
|
|
609
|
+
* merchant cannot attribute a payment without its order.
|
|
610
|
+
*/
|
|
611
|
+
declare function placeOrder(input: PlaceOrderInput, deps: OrderDeps): Promise<PlaceOrderResult>;
|
|
612
|
+
/**
|
|
613
|
+
* The buyer's receipt (kind 17) once a payment went out: the medium, the derived
|
|
614
|
+
* reference and the transaction, sealed by the buyer key, recorded (for byte-for-
|
|
615
|
+
* byte republishing) and sent to the order's inbox relays.
|
|
616
|
+
*/
|
|
617
|
+
declare function sendReceipt(record: OrderRecord, tx: string, createdAt: number, deps: OrderDeps): Promise<StoreWrite>;
|
|
618
|
+
/**
|
|
619
|
+
* On resume: read the store's inbox list again, and republish the SAME order and
|
|
620
|
+
* receipt wraps byte for byte to the union of its current inbox and the relays
|
|
621
|
+
* that took the order before (capped); a record not yet acknowledged may become
|
|
622
|
+
* so here. Returns the relays to listen on for the store's status.
|
|
623
|
+
*/
|
|
624
|
+
declare function resumeOrder(record: OrderRecord, deps: OrderDeps, now: number): Promise<{
|
|
625
|
+
record: OrderRecord;
|
|
626
|
+
relays: string[];
|
|
627
|
+
}>;
|
|
628
|
+
/**
|
|
629
|
+
* A store status for this record, or `undefined`: sealed by the store key, for
|
|
630
|
+
* this order id, naming this record's buyer key. Anything else - another
|
|
631
|
+
* sender, another order, another buyer - is not this order's answer.
|
|
632
|
+
*/
|
|
633
|
+
declare function statusFor(record: OrderRecord, wrap: NostrEvent): OrderStatusMessage | undefined;
|
|
634
|
+
/** The delivery as a link, only when it is `https:`; anything else is shown as text. */
|
|
635
|
+
declare function deliveryLink(value: string): string | undefined;
|
|
636
|
+
/**
|
|
637
|
+
* Record a store status: the status itself (it only moves forward), and the
|
|
638
|
+
* state it settles - `completed` on delivery, `refunded` on a cancellation with
|
|
639
|
+
* a refund. The write retries on a concurrent change; a status the record can no
|
|
640
|
+
* longer take is left out.
|
|
641
|
+
*/
|
|
642
|
+
declare function applyStatus(store: OrderStore, orderId: string, message: OrderStatusMessage, at: number): Promise<OrderRecord | undefined>;
|
|
643
|
+
/**
|
|
644
|
+
* Listen for the store's status on `relays`: gift wraps to the buyer key from
|
|
645
|
+
* the order's date back two days and the skew (NIP-59 back-dates wraps), each
|
|
646
|
+
* checked by `statusFor` before it is handed on. It listens with its own client
|
|
647
|
+
* that answers AUTH with the buyer key (inbox relays often ask for it before
|
|
648
|
+
* serving gift wraps), closed with the subscription.
|
|
649
|
+
*/
|
|
650
|
+
declare function listenForStatus(record: OrderRecord, relays: readonly string[], deps: Pick<OrderDeps, 'clientFor'>, onStatus: (message: OrderStatusMessage) => void): {
|
|
651
|
+
close(): void;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
type ReadyOffer = Extract<LoadedOffer, {
|
|
655
|
+
ok: true;
|
|
656
|
+
}>;
|
|
657
|
+
/**
|
|
658
|
+
* A wallet account offering `solana:signTransaction`. The wallet signs the wire
|
|
659
|
+
* transaction the widget built and returns it; the widget sends it itself.
|
|
660
|
+
*/
|
|
661
|
+
interface SolanaWallet {
|
|
662
|
+
address: string;
|
|
663
|
+
signTransaction(transaction: Uint8Array): Promise<Uint8Array>;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* What one payment attempt may spend, as the caller's spend limits count it:
|
|
667
|
+
* the coin's amount (for a token), and the SOL that leaves the payer's wallet
|
|
668
|
+
* (the fee, the payee's token-account rent when missing, `increment_stats`'
|
|
669
|
+
* rent on a new asset, and the amount itself for native SOL). The payer's own
|
|
670
|
+
* rent floor is only kept, never spent, so it is not counted.
|
|
671
|
+
*/
|
|
672
|
+
interface PaymentCosts {
|
|
673
|
+
asset: Asset;
|
|
674
|
+
/** Subunits of `asset` when it is a token; 0 for native SOL (counted in `lamports`). */
|
|
675
|
+
tokenAmount: bigint;
|
|
676
|
+
lamports: bigint;
|
|
677
|
+
/**
|
|
678
|
+
* The network fee inside `lamports`: the one part an attempt that never paid
|
|
679
|
+
* may still have spent (a transaction that landed and failed).
|
|
680
|
+
*/
|
|
681
|
+
feeLamports: bigint;
|
|
682
|
+
}
|
|
683
|
+
interface SolanaPayDeps extends OrderDeps {
|
|
684
|
+
/** The widget's own Solana RPC. */
|
|
685
|
+
rpc: Rpc<SolanaRpcApi>;
|
|
686
|
+
/** The device clock (seconds); the receipt's date and the marker's `setAt`. */
|
|
687
|
+
now?: () => number;
|
|
688
|
+
newAttemptId?: () => string;
|
|
689
|
+
/**
|
|
690
|
+
* Whether `rpc` can prove an attempt over: a full-history endpoint whose
|
|
691
|
+
* "no payment" can be trusted. Without it (`false`) the watch never answers
|
|
692
|
+
* `over`, so nothing is retried or ended on an answer that could be wrong.
|
|
693
|
+
* The widget always has one (true, the default).
|
|
694
|
+
*/
|
|
695
|
+
canProveOver?: boolean;
|
|
696
|
+
/**
|
|
697
|
+
* Called once an attempt's costs are known and before its marker is written;
|
|
698
|
+
* a throw refuses the payment with `spend_limit` before anything is recorded.
|
|
699
|
+
*/
|
|
700
|
+
reserve?: (costs: PaymentCosts, attemptId: string) => void;
|
|
701
|
+
/** Called when the attempt `attemptId` was refused after `reserve`, before any broadcast. */
|
|
702
|
+
release?: (attemptId: string) => void;
|
|
703
|
+
}
|
|
704
|
+
interface PayInput {
|
|
705
|
+
/** A FRESH verified offer (at most two minutes old): the payout and price are checked against it. */
|
|
706
|
+
fresh: ReadyOffer;
|
|
707
|
+
/** Chain time read just before (seconds). */
|
|
708
|
+
chainTime: number;
|
|
709
|
+
}
|
|
710
|
+
type SolanaPayRefusal =
|
|
711
|
+
/** The record cannot take a payment now (state, no request, the store closed it). */
|
|
712
|
+
'not_payable'
|
|
713
|
+
/** The offer snapshot is older than two minutes: verify again. */
|
|
714
|
+
| 'stale_offer'
|
|
715
|
+
/** The store no longer offers this payout at this price: end this order and start a new one. */
|
|
716
|
+
| 'offer_changed'
|
|
717
|
+
/** Too close to the end of the merchant's catch-up: a new order instead. */
|
|
718
|
+
| 'too_late'
|
|
719
|
+
/** The payer is the payout address itself (a transfer to oneself pays nothing). */
|
|
720
|
+
| 'self_payment' | 'insufficient_token' | 'insufficient_sol'
|
|
721
|
+
/** A chain read failed; nothing was requested. */
|
|
722
|
+
| 'rpc_error'
|
|
723
|
+
/** Another order for this product holds a live payment. */
|
|
724
|
+
| 'exclusion'
|
|
725
|
+
/** The record changed meanwhile; read it again. */
|
|
726
|
+
| 'conflict'
|
|
727
|
+
/** The wallet did not sign. The attempt stays live until its blockhash expires. */
|
|
728
|
+
| 'wallet_failed'
|
|
729
|
+
/** The wallet returned a transaction the widget does not send (below). Same wait. */
|
|
730
|
+
| 'wallet_unsupported'
|
|
731
|
+
/** A retry before the last attempt provably ended. */
|
|
732
|
+
| 'still_waiting' | 'already_paid'
|
|
733
|
+
/** The caller's spend limit refused the attempt's costs (`reserve` threw). Nothing was recorded. */
|
|
734
|
+
| 'spend_limit';
|
|
735
|
+
type SolanaPayResult = {
|
|
736
|
+
ok: true;
|
|
737
|
+
record: OrderRecord;
|
|
738
|
+
signature: string;
|
|
739
|
+
} | {
|
|
740
|
+
ok: false;
|
|
741
|
+
reason: SolanaPayRefusal;
|
|
742
|
+
record?: OrderRecord;
|
|
743
|
+
/** For `exclusion`: the order holding it. */
|
|
744
|
+
holder?: string;
|
|
745
|
+
/** For `insufficient_*`: what the payment needs and what the payer has, in subunits / lamports. */
|
|
746
|
+
needed?: bigint;
|
|
747
|
+
available?: bigint;
|
|
748
|
+
/** For `wallet_unsupported`: what was wrong with the returned transaction. */
|
|
749
|
+
detail?: SignedRefusal;
|
|
750
|
+
};
|
|
751
|
+
/** The request stored on the record, as the schema reads it. */
|
|
752
|
+
declare function storedSolanaRequest(record: OrderRecord): PaymentRequestData | undefined;
|
|
753
|
+
/**
|
|
754
|
+
* Compose the order's payment request once, right after the order was
|
|
755
|
+
* acknowledged, from the order's own snapshot; resume and every verdict use this
|
|
756
|
+
* stored request, never a recomposed one.
|
|
757
|
+
*/
|
|
758
|
+
declare function composeOrderPayment(record: OrderRecord, store: OrderStore): Promise<StoreWrite>;
|
|
759
|
+
/** What the payer holds and what the payment needs besides the fee itself. */
|
|
760
|
+
interface Funds {
|
|
761
|
+
/** The payer's SOL, read before the marker. */
|
|
762
|
+
lamports: bigint;
|
|
763
|
+
/**
|
|
764
|
+
* Everything but the transaction fee the payer's SOL must cover: the payee's
|
|
765
|
+
* token-account rent when missing, `increment_stats`' rent on a new asset, the
|
|
766
|
+
* payer's own rent floor, and the amount itself for native SOL.
|
|
767
|
+
*/
|
|
768
|
+
otherLamports: bigint;
|
|
769
|
+
/** The compute-unit price the widget asks (micro-lamports), for the payment's own accounts. */
|
|
770
|
+
priceMicroLamports: bigint;
|
|
771
|
+
/** What the attempt spends, for the caller's spend limits. */
|
|
772
|
+
costs: PaymentCosts;
|
|
773
|
+
}
|
|
774
|
+
type Checked = {
|
|
775
|
+
ok: true;
|
|
776
|
+
request: PaymentRequestData;
|
|
777
|
+
asset: Asset;
|
|
778
|
+
network: Network;
|
|
779
|
+
funds: Funds;
|
|
780
|
+
} | Extract<SolanaPayResult, {
|
|
781
|
+
ok: false;
|
|
782
|
+
}>;
|
|
783
|
+
/**
|
|
784
|
+
* Every check that runs BEFORE the marker: nothing was requested if one fails.
|
|
785
|
+
* The fresh offer must still hold this payout at exactly the stored amount (a
|
|
786
|
+
* higher price cannot be accepted, a lower one would overpay).
|
|
787
|
+
*/
|
|
788
|
+
declare function checkBeforePaying(record: OrderRecord, payer: string, input: PayInput, deps: Pick<SolanaPayDeps, 'rpc' | 'now'>): Promise<Checked>;
|
|
789
|
+
type SignedRefusal =
|
|
790
|
+
/** Not a transaction the widget can read. */
|
|
791
|
+
'unreadable'
|
|
792
|
+
/** Another blockhash, or a durable nonce (no expiry at all). */
|
|
793
|
+
| 'lifetime_changed'
|
|
794
|
+
/** Another fee payer than the wallet account. */
|
|
795
|
+
| 'wrong_payer'
|
|
796
|
+
/** A signature is missing or does not verify. */
|
|
797
|
+
| 'unsigned'
|
|
798
|
+
/** The bound transfer is gone or pays another amount. */
|
|
799
|
+
| 'not_bound'
|
|
800
|
+
/** A compute-budget setting twice: the runtime refuses the whole transaction. */
|
|
801
|
+
| 'duplicate_compute_budget';
|
|
802
|
+
/**
|
|
803
|
+
* What the widget sends of a transaction the wallet returned: only one whose
|
|
804
|
+
* lifetime is exactly the attempt's blockhash (no durable nonce), paid by the
|
|
805
|
+
* wallet account, fully signed, with each compute-budget setting at most once,
|
|
806
|
+
* and whose bound transfer still pays exactly the stored amount - the same
|
|
807
|
+
* binding the merchant and "found" judge. It also returns the fee the
|
|
808
|
+
* transaction pays: a wallet may raise the price the widget set.
|
|
809
|
+
*/
|
|
810
|
+
declare function checkSignedTransaction(bytes: Uint8Array, expected: {
|
|
811
|
+
payer: string;
|
|
812
|
+
blockhash: string;
|
|
813
|
+
request: PaymentRequestData;
|
|
814
|
+
asset: Asset;
|
|
815
|
+
}): Promise<{
|
|
816
|
+
ok: true;
|
|
817
|
+
signature: string;
|
|
818
|
+
wire: string;
|
|
819
|
+
feeLamports: bigint;
|
|
820
|
+
} | {
|
|
821
|
+
ok: false;
|
|
822
|
+
reason: SignedRefusal;
|
|
823
|
+
}>;
|
|
824
|
+
/**
|
|
825
|
+
* The first payment of an acknowledged order: checks, the transaction, then the
|
|
826
|
+
* marker (test-and-set with the product's other orders) and only then the wallet.
|
|
827
|
+
*/
|
|
828
|
+
declare function payWithSolana(record: OrderRecord, wallet: SolanaWallet, input: PayInput, deps: SolanaPayDeps): Promise<SolanaPayResult>;
|
|
829
|
+
type SolanaWatch =
|
|
830
|
+
/** The payment was found and recorded. */
|
|
831
|
+
{
|
|
832
|
+
state: 'paid';
|
|
833
|
+
record: OrderRecord;
|
|
834
|
+
}
|
|
835
|
+
/** The attempt may still land, or the chain could not be read in full: keep watching. */
|
|
836
|
+
| {
|
|
837
|
+
state: 'waiting';
|
|
838
|
+
record: OrderRecord;
|
|
839
|
+
}
|
|
840
|
+
/** The attempt's blockhash expired at `finalized` and a full pass found no payment. */
|
|
841
|
+
| {
|
|
842
|
+
state: 'over';
|
|
843
|
+
record: OrderRecord;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* The store delivered or refunded (a terminal record): its answer stands and
|
|
847
|
+
* nothing is left to watch. Says nothing about which transaction paid.
|
|
848
|
+
*/
|
|
849
|
+
| {
|
|
850
|
+
state: 'closed';
|
|
851
|
+
record: OrderRecord;
|
|
852
|
+
};
|
|
853
|
+
/**
|
|
854
|
+
* One reconciliation step for a record with a Solana attempt: found -> paid;
|
|
855
|
+
* otherwise the signed bytes are sent again while the blockhash can land, and
|
|
856
|
+
* once it expired at `finalized` - read BEFORE the pass, together with its slot
|
|
857
|
+
* (the pass reads at least that far), so a pass that finds nothing proves it -
|
|
858
|
+
* a complete pass that finds nothing ends the attempt.
|
|
859
|
+
*/
|
|
860
|
+
declare function watchSolanaPayment(record: OrderRecord, deps: SolanaPayDeps): Promise<SolanaWatch>;
|
|
861
|
+
/**
|
|
862
|
+
* A new attempt for the same order, reference and request, once the last one
|
|
863
|
+
* provably ended: the pass runs here, and the marker is replaced only at the
|
|
864
|
+
* version it judged (a payment found meanwhile is never overrun).
|
|
865
|
+
*/
|
|
866
|
+
declare function retryWithSolana(record: OrderRecord, wallet: SolanaWallet, input: PayInput, deps: SolanaPayDeps): Promise<SolanaPayResult>;
|
|
867
|
+
/**
|
|
868
|
+
* End an order that will not be paid (the offer changed, the buyer starts over):
|
|
869
|
+
* at once when nothing was requested; with an attempt, only once it provably
|
|
870
|
+
* ended. `ended-unpaid` keeps the marker for later reconciliation.
|
|
871
|
+
*/
|
|
872
|
+
declare function endSolanaOrder(record: OrderRecord, deps: SolanaPayDeps): Promise<{
|
|
873
|
+
ended: boolean;
|
|
874
|
+
record: OrderRecord;
|
|
875
|
+
}>;
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* A wallet backed by a key in this process (the MCP's agent keypair): it signs
|
|
879
|
+
* exactly the wire transaction it is handed, and nothing else - the core checks
|
|
880
|
+
* the signed bytes as it does a browser wallet's.
|
|
881
|
+
*/
|
|
882
|
+
declare function localSolanaWallet(signer: KeyPairSigner): SolanaWallet;
|
|
883
|
+
/** Chain time from a finalized block (the newest may not have its time yet), in seconds. */
|
|
884
|
+
declare function readChainTime(rpc: Rpc<SolanaRpcApi>): Promise<number>;
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Decisions a purchase makes about the records it holds, shared by the widget's
|
|
888
|
+
* session and the MCP's tools. Each caller sequences them with its own screens
|
|
889
|
+
* or answers; the rules are the same.
|
|
890
|
+
*/
|
|
891
|
+
/** The store cancelled an order no payment is known for (the attempt, if any, is over). */
|
|
892
|
+
declare function cancelledUnpaid(record: OrderRecord): boolean;
|
|
893
|
+
/** Ended with no payment found (by this caller or another): no longer this product's purchase. */
|
|
894
|
+
declare function gone(record: OrderRecord): boolean;
|
|
895
|
+
/**
|
|
896
|
+
* Whether `record` can still be paid on `payout`: an order is placed for one
|
|
897
|
+
* payout and price, so an order on other terms, or one the store cancelled
|
|
898
|
+
* while unpaid, must end before a new one is placed.
|
|
899
|
+
*/
|
|
900
|
+
declare function onOtherTerms(record: OrderRecord, payout: PricedPayout): boolean;
|
|
901
|
+
type EndDeps = Pick<SolanaPayDeps, 'store' | 'readClient' | 'clientFor' | 'now' | 'canProveOver'> & {
|
|
902
|
+
/** The order's own network's RPC, or `undefined` when the caller has none for it. */
|
|
903
|
+
rpc: SolanaPayDeps['rpc'] | undefined;
|
|
904
|
+
store: OrderStore;
|
|
905
|
+
};
|
|
906
|
+
/**
|
|
907
|
+
* End an order that will not be paid, before a new one is placed beside it:
|
|
908
|
+
* - a `created` order was never acknowledged and holds nothing: it counts as
|
|
909
|
+
* ended at once (it is never moved; `created` only becomes `ordered`);
|
|
910
|
+
* - an acknowledged order with no attempt ends through the store alone;
|
|
911
|
+
* - an order with an attempt ends only once the attempt provably ended, which
|
|
912
|
+
* needs the order's own RPC and `canProveOver` (see `endSolanaOrder`).
|
|
913
|
+
* `ended: false` means an attempt may still land: follow it, never place a
|
|
914
|
+
* second order beside it.
|
|
915
|
+
*/
|
|
916
|
+
declare function endOrder(record: OrderRecord, deps: EndDeps): Promise<{
|
|
917
|
+
ended: boolean;
|
|
918
|
+
record: OrderRecord;
|
|
919
|
+
}>;
|
|
920
|
+
|
|
921
|
+
export { type AuthSigner, CONFIRM_WARNINGS, DEFAULT_RELAYS, type EndDeps, type LoadOfferOptions, type LoadedOffer, MAX_CLOCK_SKEW_SECS, MAX_RELAY_URL_LENGTH, MERCHANT_CATCH_UP_SECS, MemoryOrderBackend, OFFER_SNAPSHOT_MAX_AGE_SECS, ORDER_ACK_TARGET, type OrderBackend, type OrderDeps, type OrderRecord, type OrderState, type OrderStatus, OrderStore, PAYMENT_SCAN_MARGIN_SECS, PAY_CUTOFF_SECS, type PayInput, type PaymentCosts, type PaymentMarker, type PlaceOrderInput, type PlaceOrderResult, type PoolLike, type PricedPayout, type PublishResult, RELAY_CONNECT_MAX_WAIT_MS, RELAY_PUBLISH_DEADLINE_MS, RELAY_QUERY_DEADLINE_MS, RELAY_QUERY_MAX_WAIT_MS, type RecordPatch, type RelayClient, type RelayClientOptions, type RelayLike, SOLANA_COMPUTE_UNIT_LIMIT, STORE_RELAY_CAP, STORE_WRITE_ATTEMPTS, SUBSCRIBE_RETRY_MAX_MS, SUBSCRIBE_RETRY_MS, SUBSCRIBE_STABLE_MS, type SignedRefusal, type SolanaPayDeps, type SolanaPayRefusal, type SolanaPayResult, type SolanaWallet, type SolanaWatch, type StoreInbox, type StorePins, type StorePinsRecord, type StoreRelaySources, type StoreWrite, TERMINAL_STATES, WRAP_BACKDATE_SECS, type WidgetRefusal, 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 };
|