@lamarge/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -2
  3. package/dist/account.d.ts +32 -0
  4. package/dist/account.js +113 -0
  5. package/dist/aster.d.ts +74 -0
  6. package/dist/aster.js +169 -0
  7. package/dist/book.d.ts +39 -0
  8. package/dist/book.js +64 -0
  9. package/dist/client.d.ts +92 -0
  10. package/dist/client.js +100 -0
  11. package/dist/delivery.d.ts +68 -0
  12. package/dist/delivery.js +118 -0
  13. package/dist/events.d.ts +30 -0
  14. package/dist/events.js +93 -0
  15. package/dist/funds.d.ts +19 -0
  16. package/dist/funds.js +17 -0
  17. package/dist/generated/api.d.ts +5707 -0
  18. package/dist/generated/api.js +281 -0
  19. package/dist/index.d.ts +15 -0
  20. package/dist/index.js +14 -0
  21. package/dist/lighter.d.ts +92 -0
  22. package/dist/lighter.js +206 -0
  23. package/dist/maker.d.ts +251 -0
  24. package/dist/maker.js +201 -0
  25. package/dist/protocol/consents.d.ts +82 -0
  26. package/dist/protocol/consents.js +78 -0
  27. package/dist/protocol/deployments.d.ts +54 -0
  28. package/dist/protocol/deployments.js +44 -0
  29. package/dist/protocol/index.d.ts +6 -0
  30. package/dist/protocol/index.js +6 -0
  31. package/dist/protocol/intents.d.ts +126 -0
  32. package/dist/protocol/intents.js +141 -0
  33. package/dist/protocol/lighter-constants.d.ts +4 -0
  34. package/dist/protocol/lighter-constants.js +49 -0
  35. package/dist/protocol/lighter.d.ts +52 -0
  36. package/dist/protocol/lighter.js +132 -0
  37. package/dist/protocol/protocol.d.ts +168 -0
  38. package/dist/protocol/protocol.js +317 -0
  39. package/dist/protocol/validation.d.ts +11 -0
  40. package/dist/protocol/validation.js +34 -0
  41. package/dist/retry-after.d.ts +2 -0
  42. package/dist/retry-after.js +7 -0
  43. package/dist/session.d.ts +14 -0
  44. package/dist/session.js +28 -0
  45. package/dist/signer.d.ts +43 -0
  46. package/dist/signer.js +28 -0
  47. package/dist/store.d.ts +21 -0
  48. package/dist/store.js +46 -0
  49. package/dist/take.d.ts +27 -0
  50. package/dist/take.js +81 -0
  51. package/dist/transfer.d.ts +63 -0
  52. package/dist/transfer.js +253 -0
  53. package/dist/withdraw.d.ts +67 -0
  54. package/dist/withdraw.js +122 -0
  55. package/package.json +57 -4
@@ -0,0 +1,92 @@
1
+ import { type operations } from "./generated/api.ts";
2
+ /** An API refusal or an unusable answer. `code` and `reason` are stable; `message` is for people. */
3
+ export declare class LaMargeError extends Error {
4
+ readonly status: number;
5
+ readonly code: string;
6
+ readonly name = "LaMargeError";
7
+ readonly requestId: string | undefined;
8
+ readonly retryable: boolean | undefined;
9
+ readonly retryAfterMs: number | undefined;
10
+ readonly reason: string | undefined;
11
+ /** With `SETUP_REQUIRED`: the venue and the setup step to do first. */
12
+ readonly venue: string | undefined;
13
+ readonly step: string | undefined;
14
+ constructor(status: number, code: string, message: string, details?: {
15
+ requestId?: string | undefined;
16
+ retryable?: boolean | undefined;
17
+ retryAfterMs?: number | undefined;
18
+ reason?: string | undefined;
19
+ venue?: string | undefined;
20
+ step?: string | undefined;
21
+ });
22
+ }
23
+ export interface Request {
24
+ method: string;
25
+ /** Path template, such as `/v1/takes/{id}`. */
26
+ path: string;
27
+ params?: Record<string, string | number> | undefined;
28
+ query?: Record<string, string | number | boolean | undefined> | undefined;
29
+ body?: unknown;
30
+ token?: string | null | undefined;
31
+ signal?: AbortSignal | undefined;
32
+ /** Retry a 429, a 5xx or a network failure with the identical request, at most twice. Only for an
33
+ * operation that repeating cannot change: a read, or an exact replay of a client id or signed operation. */
34
+ retry?: boolean | undefined;
35
+ /** Statuses whose JSON body is the answer, as `/health`'s 503 while it is not ready. */
36
+ accept?: readonly number[] | undefined;
37
+ }
38
+ export interface Transport {
39
+ apiUrl: string;
40
+ fetch?: typeof fetch | undefined;
41
+ timeoutMs?: number | undefined;
42
+ }
43
+ /** Resolves after `ms`, or rejects as soon as `signal` aborts. */
44
+ export declare function wait(ms: number, signal?: AbortSignal): Promise<void>;
45
+ /** One HTTP exchange with the API: the answer's status and decoded JSON, or a `LaMargeError`. */
46
+ export declare function exchange(transport: Transport, request: Request): Promise<{
47
+ status: number;
48
+ body: unknown;
49
+ }>;
50
+ /** An operation's path and query parameters, and its body, as a generated `operations` entry types them. */
51
+ export type OperationInput<Op> = (Op extends {
52
+ parameters: infer P;
53
+ } ? Pick<P, Extract<keyof P, "path" | "query">> : never) & (Op extends {
54
+ requestBody: {
55
+ content: {
56
+ "application/json": infer B;
57
+ };
58
+ };
59
+ } ? {
60
+ body: B;
61
+ } : {
62
+ body?: never;
63
+ });
64
+ type SuccessBody<R> = R extends {
65
+ content: {
66
+ "application/json": infer B;
67
+ };
68
+ } ? B : null;
69
+ /** An operation's success answer, as a generated `operations` entry types it. */
70
+ export type OperationAnswer<Op> = Op extends {
71
+ responses: infer R;
72
+ } ? SuccessBody<R[Extract<keyof R, 200 | 201 | 202 | 204>]> : never;
73
+ export type Answer<O extends keyof operations> = OperationAnswer<operations[O]>;
74
+ export type CallOptions<O extends keyof operations> = OperationInput<operations[O]> & {
75
+ signal?: AbortSignal | undefined;
76
+ retry?: boolean | undefined;
77
+ };
78
+ /** The options, required when the operation has a path parameter, a required query or a body. */
79
+ export type CallArguments<O extends keyof operations> = object extends CallOptions<O> ? [options?: CallOptions<O>] : [options: CallOptions<O>];
80
+ export interface ClientConfig extends Transport {
81
+ /** A session token or a maker credential (`mk_…`). */
82
+ token?: string | undefined;
83
+ }
84
+ export interface Client {
85
+ readonly apiUrl: string;
86
+ token: string | undefined;
87
+ /** Any public operation by its `operationId`, typed by the API reference. Answers are typed, not validated:
88
+ * what gets signed is checked by `@lamarge/sdk/protocol`. */
89
+ call<O extends keyof operations>(operation: O, ...options: CallArguments<O>): Promise<Answer<O>>;
90
+ }
91
+ export declare function createClient(config: ClientConfig): Client;
92
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,100 @@
1
+ import { routes } from "./generated/api.js";
2
+ import { isRecord, required } from "./protocol/validation.js";
3
+ import { retryAfterMs } from "./retry-after.js";
4
+ /** An API refusal or an unusable answer. `code` and `reason` are stable; `message` is for people. */
5
+ export class LaMargeError extends Error {
6
+ status;
7
+ code;
8
+ name = "LaMargeError";
9
+ requestId;
10
+ retryable;
11
+ retryAfterMs;
12
+ reason;
13
+ /** With `SETUP_REQUIRED`: the venue and the setup step to do first. */
14
+ venue;
15
+ step;
16
+ constructor(status, code, message, details = {}) {
17
+ super(message);
18
+ this.status = status;
19
+ this.code = code;
20
+ this.requestId = details.requestId;
21
+ this.retryable = details.retryable;
22
+ this.retryAfterMs = details.retryAfterMs;
23
+ this.reason = details.reason;
24
+ this.venue = details.venue;
25
+ this.step = details.step;
26
+ }
27
+ }
28
+ const text = (value) => typeof value === "string" ? value : undefined;
29
+ function refusal(status, raw, headers) {
30
+ const error = isRecord(raw) && isRecord(raw.error) ? raw.error : undefined;
31
+ const code = text(error?.code);
32
+ const message = text(error?.message);
33
+ return new LaMargeError(status, code && message !== undefined ? code : "UNKNOWN", code && message !== undefined ? message : `HTTP ${status}`, {
34
+ requestId: text(error?.request_id) ?? headers.get("x-request-id") ?? undefined,
35
+ retryable: typeof error?.retryable === "boolean" ? error.retryable : undefined,
36
+ retryAfterMs: retryAfterMs(headers.get("retry-after")),
37
+ reason: text(error?.reason),
38
+ venue: text(error?.venue),
39
+ step: text(error?.step),
40
+ });
41
+ }
42
+ /** Resolves after `ms`, or rejects as soon as `signal` aborts. */
43
+ export function wait(ms, signal) {
44
+ return new Promise((resolve, reject) => {
45
+ const abort = () => { clearTimeout(timer); reject(signal?.reason); };
46
+ const timer = setTimeout(() => { signal?.removeEventListener("abort", abort); resolve(); }, ms);
47
+ signal?.addEventListener("abort", abort, { once: true });
48
+ });
49
+ }
50
+ /** One HTTP exchange with the API: the answer's status and decoded JSON, or a `LaMargeError`. */
51
+ export async function exchange(transport, request) {
52
+ const path = request.path.replace(/\{([^}]+)\}/g, (_match, key) => encodeURIComponent(required(request.params?.[key], `Missing path parameter ${key}`)));
53
+ const query = new URLSearchParams();
54
+ for (const [key, value] of Object.entries(request.query ?? {}))
55
+ if (value !== undefined)
56
+ query.set(key, String(value));
57
+ const headers = { accept: "application/json" };
58
+ if (request.body !== undefined)
59
+ headers["content-type"] = "application/json";
60
+ if (request.token)
61
+ headers.authorization = `Bearer ${request.token}`;
62
+ const url = `${transport.apiUrl.replace(/\/+$/, "")}${path}${query.size ? `?${query}` : ""}`;
63
+ const body = request.body === undefined ? undefined : JSON.stringify(request.body);
64
+ const timeoutMs = transport.timeoutMs ?? 30_000;
65
+ for (let attempt = 0;; attempt++) {
66
+ request.signal?.throwIfAborted();
67
+ try {
68
+ const signal = request.signal ? AbortSignal.any([request.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
69
+ const response = await (transport.fetch ?? globalThis.fetch)(url, { method: request.method, headers, signal, ...(body === undefined ? {} : { body }) });
70
+ const raw = response.status === 204 ? null : await response.json().catch(() => null);
71
+ if (!response.ok && !request.accept?.includes(response.status))
72
+ throw refusal(response.status, raw, response.headers);
73
+ return { status: response.status, body: raw };
74
+ }
75
+ catch (error) {
76
+ request.signal?.throwIfAborted();
77
+ // The server says what may be retried; an answer without its envelope, from a proxy, only for a 429 or 5xx.
78
+ const transient = !(error instanceof LaMargeError) || (error.retryable ?? (error.status === 429 || error.status >= 500));
79
+ if (!request.retry || attempt >= 2 || !transient)
80
+ throw error;
81
+ const delay = error instanceof LaMargeError && error.retryAfterMs !== undefined ? error.retryAfterMs : 250 * 2 ** attempt;
82
+ await wait(delay, request.signal);
83
+ }
84
+ }
85
+ }
86
+ export function createClient(config) {
87
+ const url = new URL(config.apiUrl);
88
+ if (url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
89
+ throw new Error("Use HTTPS outside localhost");
90
+ async function call(operation, options = {}) {
91
+ const route = routes[operation];
92
+ const answer = await exchange(config, {
93
+ method: route.method, path: route.path, params: options.path, query: options.query, body: options.body, token: client.token,
94
+ signal: options.signal, retry: options.retry ?? route.method === "GET", accept: route.accept,
95
+ });
96
+ return answer.body;
97
+ }
98
+ const client = { apiUrl: config.apiUrl, token: config.token, call };
99
+ return client;
100
+ }
@@ -0,0 +1,68 @@
1
+ import type { DeliveryFill } from "./maker.ts";
2
+ /** The deliveries a deliverer persisted, by fill: read whole, saved whole; nothing is sent before a save resolves, and a failed save sends nothing. */
3
+ export type DeliveryJournal<T> = {
4
+ load: () => Record<string, T> | Promise<Record<string, T>>;
5
+ save: (deliveries: Record<string, T>) => void | Promise<void>;
6
+ };
7
+ /** Where the deliverer reads the identity's fills and reports deliveries: a `Quoter` or its `fillFeed`. */
8
+ export interface FillSource {
9
+ fills(): Promise<DeliveryFill[]>;
10
+ deliver(id: string, hash: string): Promise<{
11
+ fill: {
12
+ delivery_hash: string | null;
13
+ state: string;
14
+ };
15
+ }>;
16
+ }
17
+ export type Log = (line: string) => void;
18
+ export declare const errorText: (error: unknown) => string;
19
+ /** The venue answered the send and refused it: these bytes never execute. */
20
+ export declare class Refused extends Error {
21
+ }
22
+ export declare abstract class VenueDeliverer<D extends {
23
+ fill_id: string;
24
+ outcome?: string | undefined;
25
+ }> {
26
+ private venue;
27
+ protected sendMargin: number;
28
+ protected store: DeliveryJournal<D>;
29
+ protected now: () => number;
30
+ private running;
31
+ private stuckSince;
32
+ /** When each fill's report may be retried. */
33
+ private reportAt;
34
+ /** `sendMargin`: no transfer is first sent closer than this to `deliver_by`. */
35
+ constructor(venue: string, sendMargin: number, store: DeliveryJournal<D>, now: () => number);
36
+ /** Moves one persisted delivery forward; false while its outcome on the venue is unknown. */
37
+ protected abstract settle(quoter: FillSource, delivery: D, fill: DeliveryFill | undefined, deliveries: Record<string, D>, log: Log): Promise<boolean>;
38
+ /** Delivers fills of `due`, the earliest deadline first; false while one's outcome on the venue is unknown. */
39
+ protected abstract deliver(quoter: FillSource, due: DeliveryFill[], deliveries: Record<string, D>, log: Log): Promise<boolean>;
40
+ /** One pass at a time: overlapping calls share the pass in progress. */
41
+ run(quoter: FillSource, log?: Log): Promise<void>;
42
+ /** True while deliveries have been stuck for a while: the pool must stop taking fills. */
43
+ get blocked(): boolean;
44
+ /**
45
+ * Settles persisted deliveries first, then, only if no outcome is unknown, delivers paid fills
46
+ * whose payment the server has re-read. Returns true when the pass left no outcome unknown.
47
+ */
48
+ private pass;
49
+ /**
50
+ * Whether the server still accepts a delivery for `fill`: until `report_by`, while the payment
51
+ * is locked, even after the maker leg was abandoned for lack of a report.
52
+ */
53
+ protected awaits(fill: DeliveryFill | undefined): fill is DeliveryFill;
54
+ /** False while `id` waits for its next try; otherwise schedules the next one with a growing backoff. */
55
+ protected turn(map: Map<string, {
56
+ at: number;
57
+ wait: number;
58
+ }>, id: string): boolean;
59
+ /** False while the fill's report waits for its retry. */
60
+ protected reportDue(id: string): boolean;
61
+ /**
62
+ * Reports `hash` as the fill's delivery. Returns `failed` when the server will never take it,
63
+ * or undefined to retry, less often while the server says the venue does not show it final yet.
64
+ */
65
+ protected report(quoter: FillSource, id: string, hash: string, log: Log): Promise<"reported" | "failed" | undefined>;
66
+ /** Unsettled deliveries plus fills this bot can still send; a shutdown waits for zero. */
67
+ outstanding(quoter: FillSource): Promise<number>;
68
+ }
@@ -0,0 +1,118 @@
1
+ // Shared by deliveries inside a venue: one pass at a time, persisted deliveries settled before new
2
+ // fills are signed, stuck pools stop quoting, delivery reports and shutdown draining. The journal's
3
+ // storage is the caller's: a file synced on each save, a database.
4
+ import { LaMargeError } from "./client.js";
5
+ import { insist } from "./protocol/protocol.js";
6
+ /** Deliveries stuck this long stop the pool's quotes: a new fill could not be delivered. */
7
+ const STUCK_MS = 15_000;
8
+ export const errorText = (error) => error instanceof Error ? error.message : String(error);
9
+ /** The venue answered the send and refused it: these bytes never execute. */
10
+ export class Refused extends Error {
11
+ }
12
+ export class VenueDeliverer {
13
+ venue;
14
+ sendMargin;
15
+ store;
16
+ now;
17
+ running = null;
18
+ stuckSince = null;
19
+ /** When each fill's report may be retried. */
20
+ reportAt = new Map();
21
+ /** `sendMargin`: no transfer is first sent closer than this to `deliver_by`. */
22
+ constructor(venue, sendMargin, store, now) {
23
+ this.venue = venue;
24
+ this.sendMargin = sendMargin;
25
+ this.store = store;
26
+ this.now = now;
27
+ }
28
+ /** One pass at a time: overlapping calls share the pass in progress. */
29
+ run(quoter, log = console.log) {
30
+ this.running ??= this.pass(quoter, log).then(progressed => { this.stuckSince = progressed ? null : this.stuckSince ?? this.now(); }, error => { this.stuckSince ??= this.now(); throw error; }).finally(() => { this.running = null; });
31
+ return this.running;
32
+ }
33
+ /** True while deliveries have been stuck for a while: the pool must stop taking fills. */
34
+ get blocked() {
35
+ return this.stuckSince !== null && this.now() - this.stuckSince >= STUCK_MS;
36
+ }
37
+ /**
38
+ * Settles persisted deliveries first, then, only if no outcome is unknown, delivers paid fills
39
+ * whose payment the server has re-read. Returns true when the pass left no outcome unknown.
40
+ */
41
+ async pass(quoter, log) {
42
+ const deliveries = await this.store.load();
43
+ const fills = (await quoter.fills()).filter(fill => fill.role === "maker" && fill.to_venue === this.venue && fill.delivery_to !== null);
44
+ const byId = new Map(fills.map(fill => [fill.id, fill]));
45
+ let unknown = false;
46
+ for (const delivery of Object.values(deliveries).filter(delivery => !delivery.outcome)) {
47
+ try {
48
+ unknown = !(await this.settle(quoter, delivery, byId.get(delivery.fill_id), deliveries, log)) || unknown;
49
+ }
50
+ catch (error) {
51
+ unknown = true;
52
+ log(`${delivery.fill_id}: ${errorText(error)}`);
53
+ }
54
+ }
55
+ if (unknown)
56
+ return false;
57
+ const due = fills
58
+ .filter(fill => !deliveries[fill.id] && fill.state === "PAID" && fill.maker_leg === "NONE" && !fill.delivery_hash && fill.payment_verified_at && fill.deliver_by && Date.parse(fill.deliver_by) - this.now() > this.sendMargin)
59
+ .sort((a, b) => Date.parse(a.deliver_by ?? "") - Date.parse(b.deliver_by ?? ""));
60
+ return this.deliver(quoter, due, deliveries, log);
61
+ }
62
+ /**
63
+ * Whether the server still accepts a delivery for `fill`: until `report_by`, while the payment
64
+ * is locked, even after the maker leg was abandoned for lack of a report.
65
+ */
66
+ awaits(fill) {
67
+ return fill !== undefined && fill.payment_leg === "LOCKED" && ["NONE", "ABANDONED"].includes(fill.maker_leg)
68
+ && (!fill.report_by || Date.parse(fill.report_by) > this.now());
69
+ }
70
+ /** False while `id` waits for its next try; otherwise schedules the next one with a growing backoff. */
71
+ turn(map, id) {
72
+ const last = map.get(id);
73
+ if (last && last.at > this.now())
74
+ return false;
75
+ const wait = Math.min((last?.wait ?? 250) * 2, 5_000);
76
+ map.set(id, { at: this.now() + wait, wait });
77
+ return true;
78
+ }
79
+ /** False while the fill's report waits for its retry. */
80
+ reportDue(id) {
81
+ const retry = this.reportAt.get(id);
82
+ return !retry || retry.at <= this.now();
83
+ }
84
+ /**
85
+ * Reports `hash` as the fill's delivery. Returns `failed` when the server will never take it,
86
+ * or undefined to retry, less often while the server says the venue does not show it final yet.
87
+ */
88
+ async report(quoter, id, hash, log) {
89
+ try {
90
+ const { fill } = await quoter.deliver(id, hash);
91
+ insist(fill.delivery_hash === hash, "The server recorded another delivery");
92
+ this.reportAt.delete(id);
93
+ log(`${id}: delivered, ${fill.state}`);
94
+ return "reported";
95
+ }
96
+ catch (error) {
97
+ const code = error instanceof LaMargeError ? error.code : undefined;
98
+ const reason = error instanceof LaMargeError ? error.reason : undefined;
99
+ if (code === "VALIDATION_ERROR" || code === "STATE_CONFLICT") {
100
+ // The server will never take this transfer as the delivery: `reason` says why.
101
+ log(`${id}: delivery refused (${String(reason ?? code)}): ${errorText(error)}`);
102
+ return "failed";
103
+ }
104
+ const retry = this.reportAt.get(id);
105
+ const wait = code === "DELIVERY_PENDING" ? Math.min((retry?.wait ?? 500) * 2, 30_000) : 1_000;
106
+ this.reportAt.set(id, { at: this.now() + wait, wait });
107
+ log(`${id}: report pending: ${errorText(error)}`);
108
+ return undefined;
109
+ }
110
+ }
111
+ /** Unsettled deliveries plus fills this bot can still send; a shutdown waits for zero. */
112
+ async outstanding(quoter) {
113
+ const open = Object.values(await this.store.load()).filter(delivery => !delivery.outcome).length;
114
+ const fills = (await quoter.fills()).filter(fill => fill.role === "maker" && fill.to_venue === this.venue && fill.maker_leg === "NONE"
115
+ && ["RESERVED", "PAID"].includes(fill.state) && fill.deliver_by && Date.parse(fill.deliver_by) - this.sendMargin > this.now());
116
+ return open + fills.length;
117
+ }
118
+ }
@@ -0,0 +1,30 @@
1
+ import { type Answer, type Client } from "./client.ts";
2
+ import type { components } from "./generated/api.ts";
3
+ export type EventPage = components["schemas"]["EventPage"];
4
+ /** `private` snapshots add the signed-in user's or credential's orders, fills and leases. */
5
+ export type Snapshot = Answer<"getV1Snapshot"> | Answer<"getV1MeSnapshot">;
6
+ export type Change = EventPage | {
7
+ resync: Snapshot;
8
+ };
9
+ /** What the feed needs of a WebSocket: the browser's, Bun's and Node 22's global, or `ws`. */
10
+ export type SocketConstructor = new (url: string) => {
11
+ onopen: (() => void) | null;
12
+ onmessage: ((event: {
13
+ data: unknown;
14
+ }) => void) | null;
15
+ onclose: (() => void) | null;
16
+ send(data: string): void;
17
+ close(): void;
18
+ };
19
+ export interface ChangeOptions {
20
+ /** `private`: the signed-in user's or credential's orders, fills, leases, balances and withdrawals. */
21
+ scope: "public" | "private";
22
+ signal: AbortSignal;
23
+ WebSocket?: SocketConstructor | undefined;
24
+ }
25
+ /**
26
+ * What changed, durably: a snapshot first, then each page of events after it, pushed over the
27
+ * stream; between connections, HTTP replay catches up from the last page yielded. `{ resync }`
28
+ * replaces all local state. Events say what to read again; they are no settlement ledger.
29
+ */
30
+ export declare function changes(client: Client, options: ChangeOptions): AsyncGenerator<Change>;
package/dist/events.js ADDED
@@ -0,0 +1,93 @@
1
+ import { LaMargeError, wait } from "./client.js";
2
+ import { insist } from "./protocol/protocol.js";
3
+ function decode(frame) {
4
+ const value = JSON.parse(frame);
5
+ return value;
6
+ }
7
+ /**
8
+ * The journal after `after` over the WebSocket stream: each page as the server publishes it, at
9
+ * least every half second, or a control frame. Ends with the connection, or after five seconds
10
+ * without a frame.
11
+ */
12
+ async function* stream(client, after, scope, signal, Socket) {
13
+ const socket = new Socket(`${client.apiUrl.replace(/\/+$/, "").replace(/^http/, "ws")}/v1/stream`);
14
+ const frames = [];
15
+ let wake = () => { };
16
+ const push = (frame) => { frames.push(frame); wake(); };
17
+ // The token travels in the first frame, never in the URL.
18
+ socket.onopen = () => socket.send(JSON.stringify(scope === "private" ? { token: client.token, after } : { after }));
19
+ socket.onmessage = (event) => push(typeof event.data === "string" ? event.data : null);
20
+ socket.onclose = () => push(null);
21
+ const close = () => socket.close();
22
+ signal.addEventListener("abort", close, { once: true });
23
+ try {
24
+ for (;;) {
25
+ if (!frames.length) {
26
+ let timer;
27
+ await new Promise(resolve => { wake = resolve; timer = setTimeout(resolve, 5_000); });
28
+ clearTimeout(timer);
29
+ }
30
+ const frame = frames.shift();
31
+ if (frame === undefined || frame === null)
32
+ return;
33
+ yield decode(frame);
34
+ }
35
+ }
36
+ finally {
37
+ signal.removeEventListener("abort", close);
38
+ socket.close();
39
+ }
40
+ }
41
+ /**
42
+ * What changed, durably: a snapshot first, then each page of events after it, pushed over the
43
+ * stream; between connections, HTTP replay catches up from the last page yielded. `{ resync }`
44
+ * replaces all local state. Events say what to read again; they are no settlement ledger.
45
+ */
46
+ export async function* changes(client, options) {
47
+ const { scope, signal } = options;
48
+ const Socket = options.WebSocket ?? globalThis.WebSocket;
49
+ const snapshot = () => scope === "private" ? client.call("getV1MeSnapshot", { signal }) : client.call("getV1Snapshot", { signal });
50
+ const replay = (after) => scope === "private"
51
+ ? client.call("getV1MeEvents", { query: { after, limit: 500 }, signal })
52
+ : client.call("getV1Events", { query: { after, limit: 500 }, signal });
53
+ const initial = await snapshot();
54
+ let cursor = initial.cursor;
55
+ yield { resync: initial };
56
+ const page = (next) => {
57
+ insist(BigInt(next.next_cursor) >= BigInt(cursor), "Event cursor moved backwards");
58
+ cursor = next.next_cursor;
59
+ return next;
60
+ };
61
+ while (!signal.aborted) {
62
+ let resync = false;
63
+ for await (const frame of stream(client, cursor, scope, signal, Socket)) {
64
+ if ("type" in frame) {
65
+ if (frame.type === "authentication_required")
66
+ throw new LaMargeError(401, "UNAUTHENTICATED", frame.reason);
67
+ resync = frame.type === "resync_required";
68
+ break;
69
+ }
70
+ yield page(frame);
71
+ }
72
+ if (signal.aborted)
73
+ break;
74
+ try {
75
+ if (resync)
76
+ throw new LaMargeError(410, "RESYNC_REQUIRED", "The stream asked for a snapshot");
77
+ for (;;) {
78
+ const next = page(await replay(cursor));
79
+ yield next;
80
+ if (!next.has_more)
81
+ break;
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (!(error instanceof LaMargeError && error.code === "RESYNC_REQUIRED"))
86
+ throw error;
87
+ const fresh = await snapshot();
88
+ cursor = fresh.cursor;
89
+ yield { resync: fresh };
90
+ }
91
+ await wait(500, signal).catch(() => undefined);
92
+ }
93
+ }
@@ -0,0 +1,19 @@
1
+ /** A balance read under two minutes old; a missing or unparsable time is stale. */
2
+ export declare const FRESH_MS = 120000;
3
+ export declare const fresh: (at: string | null | undefined, now?: number) => boolean;
4
+ type Held = {
5
+ venue: string;
6
+ chain_balance: string | null;
7
+ chain_error?: string | null | undefined;
8
+ chain_read_at: string | null;
9
+ escrow_balance: string | null;
10
+ deposit_balance?: string | null | undefined;
11
+ };
12
+ /** What the deposit address holds that the relayer sweeps without a withdrawal to it. */
13
+ export declare function sweepable(balance: Held | undefined): bigint;
14
+ /**
15
+ * What a take pays without moving margin, from a recent, successful escrow-chain read: the
16
+ * available balance, and the linked address, whose funds the take deposits. Null when unknown.
17
+ */
18
+ export declare function prefunded(balance: Held | undefined, now?: number): bigint | null;
19
+ export {};
package/dist/funds.js ADDED
@@ -0,0 +1,17 @@
1
+ import { sweepMinimum } from "./protocol/protocol.js";
2
+ /** A balance read under two minutes old; a missing or unparsable time is stale. */
3
+ export const FRESH_MS = 120_000;
4
+ export const fresh = (at, now = Date.now()) => Boolean(at) && now - Date.parse(at ?? "") < FRESH_MS;
5
+ /** What the deposit address holds that the relayer sweeps without a withdrawal to it. */
6
+ export function sweepable(balance) {
7
+ const held = BigInt(balance?.deposit_balance ?? 0);
8
+ return balance && held >= sweepMinimum(balance.venue) ? held : 0n;
9
+ }
10
+ /**
11
+ * What a take pays without moving margin, from a recent, successful escrow-chain read: the
12
+ * available balance, and the linked address, whose funds the take deposits. Null when unknown.
13
+ */
14
+ export function prefunded(balance, now = Date.now()) {
15
+ return balance?.chain_balance !== null && balance?.chain_balance !== undefined && !balance.chain_error && fresh(balance.chain_read_at, now)
16
+ ? BigInt(balance.chain_balance) + BigInt(balance.escrow_balance ?? 0) : null;
17
+ }