@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,251 @@
1
+ import { type Client } from "./client.ts";
2
+ import type { components } from "./generated/api.ts";
3
+ import { type DeploymentManifest } from "./protocol/deployments.ts";
4
+ import { type FundingSource } from "./protocol/intents.ts";
5
+ import { type QuoteIntent } from "./protocol/protocol.ts";
6
+ import { type Signer } from "./signer.ts";
7
+ import { type SignatureStore } from "./store.ts";
8
+ export type Order = components["schemas"]["Order"];
9
+ export type Credential = components["schemas"]["CredentialCreated"];
10
+ /** An EOA key, or a deployed ERC-1271 delegate whose signature is collected elsewhere (`prepareUnsigned`). */
11
+ export type QuoteSigner = Pick<Signer, "address" | "signTypedData">;
12
+ /** A fill as the event journal shows it, its venues named as in REST fills. */
13
+ export type FeedFill = Omit<components["schemas"]["FillPreview"], "from" | "to"> & {
14
+ from_venue: string;
15
+ to_venue: string;
16
+ };
17
+ /** A fill as delivery needs it, from the REST list or the event feed alike. */
18
+ export type DeliveryFill = Pick<components["schemas"]["Fill"], "id" | "role" | "state" | "payment_leg" | "maker_leg" | "to_venue" | "amount_out" | "deliver_by" | "report_by" | "delivery_to" | "delivery_hash" | "payment_verified_at">;
19
+ /** The financial key: provisioning and release of capital only. Never give it to the quoting host. */
20
+ export declare class Maker {
21
+ readonly client: Client;
22
+ private account;
23
+ private operator;
24
+ private appOrigin;
25
+ private manifest;
26
+ private sessionId;
27
+ /** `operator` and `appOrigin` come from reviewed deployment configuration, never from the API. */
28
+ constructor(client: Client, account: Signer, operator: string, appOrigin: string, manifest?: DeploymentManifest);
29
+ login(): Promise<void>;
30
+ logout(): Promise<void>;
31
+ balances(): Promise<{
32
+ setup: components["schemas"]["SetupSteps"];
33
+ holdings: components["schemas"]["Holdings"];
34
+ portfolio: components["schemas"]["PortfolioObservation"];
35
+ venue: components["schemas"]["VenueId"];
36
+ address: components["schemas"]["Address"];
37
+ venue_wallet: string | null;
38
+ equity: string | null;
39
+ withdrawable: string | null;
40
+ withdrawal_fee: string | null;
41
+ withdrawable_estimated: boolean;
42
+ mode: string | null;
43
+ has_account: boolean;
44
+ wallet: string | null;
45
+ chain_balance: string | null;
46
+ escrow_balance: string | null;
47
+ deposit_balance: string | null;
48
+ deposit_address: components["schemas"]["Address"] | null;
49
+ read_at: string | null;
50
+ error: string | null;
51
+ chain_read_at: components["schemas"]["Timestamp"] | null;
52
+ chain_error: string | null;
53
+ }[]>;
54
+ order(id: string): Promise<{
55
+ preparation_id: components["schemas"]["Uuid"] | null;
56
+ capital_pool: boolean;
57
+ held_in: components["schemas"]["HeldIn"];
58
+ id: components["schemas"]["Bytes32"];
59
+ venue: components["schemas"]["VenueId"];
60
+ receive_venue: components["schemas"]["VenueId"] | null;
61
+ owner_address: components["schemas"]["Address"];
62
+ state: components["schemas"]["OrderState"];
63
+ error: string | null;
64
+ amount: components["schemas"]["Amount"];
65
+ held: components["schemas"]["Amount"];
66
+ escrow_available: components["schemas"]["Amount"];
67
+ delivery_fee: components["schemas"]["Amount"];
68
+ revision: components["schemas"]["Cursor"];
69
+ available_to_quote: components["schemas"]["Amount"];
70
+ received_by_venue: {
71
+ venue: components["schemas"]["VenueId"];
72
+ amount: components["schemas"]["Amount"];
73
+ }[];
74
+ quotable: components["schemas"]["Amount"];
75
+ filled: components["schemas"]["Amount"];
76
+ last_sequence: number;
77
+ quote_set: components["schemas"]["QuoteSet"] | null;
78
+ margin_move: components["schemas"]["MarginMove"] | null;
79
+ transactions: components["schemas"]["Transaction"][];
80
+ created_at: components["schemas"]["Timestamp"];
81
+ updated_at: components["schemas"]["Timestamp"];
82
+ client_order_id: components["schemas"]["ClientId"];
83
+ quote_identity_id: components["schemas"]["Uuid"];
84
+ quote_signer: components["schemas"]["Address"];
85
+ }>;
86
+ /**
87
+ * Links the financial key's own account on `venue`, or `owner`'s, which signs the link message,
88
+ * then the setup step a maker pool needs: on Aster, approving LaMarge's agent.
89
+ */
90
+ link(venue: string, owner?: Signer): Promise<{
91
+ setup: components["schemas"]["SetupSteps"];
92
+ venue: components["schemas"]["VenueId"];
93
+ address: components["schemas"]["Address"];
94
+ venue_wallet: string | null;
95
+ linked_at: components["schemas"]["Timestamp"];
96
+ }>;
97
+ createCredential(name: string, quoteSigner: string, days?: number): Promise<Credential>;
98
+ /**
99
+ * Persist `client_request_id`, and keep `store`: each signed preparation is saved there before its first submission. `owner`,
100
+ * for a pool held inside its venue (Lighter, Aster), is the dedicated address linked there;
101
+ * escrow funding always comes from the financial key.
102
+ */
103
+ addCapital(input: {
104
+ venue: string;
105
+ amount: string;
106
+ client_request_id: string;
107
+ funding_source: FundingSource;
108
+ }, credential: Pick<Credential, "id" | "quote_signer">, store?: SignatureStore, expectedOwner?: string): Promise<Order>;
109
+ closeOrders(ids: string[]): Promise<Order[]>;
110
+ waitFor(id: string, states: Order["state"][], timeoutMs?: number): Promise<Order>;
111
+ }
112
+ /** A scoped credential (`mk_`) quotes and cancels only its own orders; this key cannot withdraw capital. */
113
+ export declare class Quoter {
114
+ readonly client: Client;
115
+ readonly identity: string;
116
+ private account;
117
+ private manifest;
118
+ private lastRtt;
119
+ constructor(client: Client, identity: string, account: QuoteSigner, manifest?: DeploymentManifest);
120
+ /** Each answer's round trip, reported with the next lease. */
121
+ private timed;
122
+ order(id: string): Promise<{
123
+ preparation_id: components["schemas"]["Uuid"] | null;
124
+ capital_pool: boolean;
125
+ held_in: components["schemas"]["HeldIn"];
126
+ id: components["schemas"]["Bytes32"];
127
+ venue: components["schemas"]["VenueId"];
128
+ receive_venue: components["schemas"]["VenueId"] | null;
129
+ owner_address: components["schemas"]["Address"];
130
+ state: components["schemas"]["OrderState"];
131
+ error: string | null;
132
+ amount: components["schemas"]["Amount"];
133
+ held: components["schemas"]["Amount"];
134
+ escrow_available: components["schemas"]["Amount"];
135
+ delivery_fee: components["schemas"]["Amount"];
136
+ revision: components["schemas"]["Cursor"];
137
+ available_to_quote: components["schemas"]["Amount"];
138
+ received_by_venue: {
139
+ venue: components["schemas"]["VenueId"];
140
+ amount: components["schemas"]["Amount"];
141
+ }[];
142
+ quotable: components["schemas"]["Amount"];
143
+ filled: components["schemas"]["Amount"];
144
+ last_sequence: number;
145
+ quote_set: components["schemas"]["QuoteSet"] | null;
146
+ margin_move: components["schemas"]["MarginMove"] | null;
147
+ transactions: components["schemas"]["Transaction"][];
148
+ created_at: components["schemas"]["Timestamp"];
149
+ updated_at: components["schemas"]["Timestamp"];
150
+ client_order_id: components["schemas"]["ClientId"];
151
+ quote_identity_id: components["schemas"]["Uuid"];
152
+ quote_signer: components["schemas"]["Address"];
153
+ }>;
154
+ lease(ttlSeconds?: number): Promise<{
155
+ identity_id: components["schemas"]["Uuid"];
156
+ generation: components["schemas"]["Cursor"];
157
+ expires_at: components["schemas"]["Timestamp"];
158
+ }>;
159
+ cancelAll(): Promise<{
160
+ identity_id: components["schemas"]["Uuid"];
161
+ generation: components["schemas"]["Cursor"];
162
+ orders: number;
163
+ }>;
164
+ /** Prepares `order`'s quote set and checks it; signing is the caller's, as for a contract delegate. */
165
+ prepareUnsigned(order: Order, quotes: QuoteIntent[], size: bigint, validUntil: number): Promise<{
166
+ expected_revision: components["schemas"]["Cursor"] | null;
167
+ draft_id: components["schemas"]["Uuid"];
168
+ sequence: number;
169
+ valid_until: components["schemas"]["UnixTime"];
170
+ expires_at: components["schemas"]["UnixTime"];
171
+ quote_size: components["schemas"]["Amount"];
172
+ quotes: components["schemas"]["Quote"][];
173
+ to_sign: components["schemas"]["ToSign"][];
174
+ } & import("./protocol/protocol.ts").PreparedQuote & {
175
+ to_sign: import("./protocol/validation.ts").Verified<import("./protocol/protocol.ts").SignItem[]>;
176
+ }>;
177
+ prepare(order: Order, quotes: QuoteIntent[], size: bigint, validUntil: number): Promise<{
178
+ order_id: string;
179
+ draft_id: string;
180
+ signature: `0x${string}`;
181
+ }>;
182
+ /** All distinct order updates and cancels commit atomically; a cancel is never replayed, as it would void the sequence that follows. */
183
+ batch(updates: {
184
+ order_id: string;
185
+ draft_id: string;
186
+ signature: string;
187
+ }[], cancels?: string[]): Promise<{
188
+ results: {
189
+ order_id: components["schemas"]["Bytes32"];
190
+ cancel_epoch: components["schemas"]["Cursor"];
191
+ sequence: number | null;
192
+ }[];
193
+ }>;
194
+ /** The identity's fills, current and the last final ones. */
195
+ fills(): Promise<{
196
+ id: string;
197
+ take_id: string;
198
+ role: components["schemas"]["FillRole"];
199
+ order_id: components["schemas"]["Bytes32"];
200
+ sequence: number;
201
+ quote_index: number;
202
+ swap_id: components["schemas"]["Bytes32"];
203
+ hashlock: components["schemas"]["Bytes32"];
204
+ from_venue: components["schemas"]["VenueId"];
205
+ to_venue: components["schemas"]["VenueId"];
206
+ amount_out: components["schemas"]["Amount"];
207
+ amount_in: components["schemas"]["Amount"];
208
+ payer: components["schemas"]["Address"];
209
+ maker_owner: components["schemas"]["Address"];
210
+ maker_recipient: components["schemas"]["Address"];
211
+ taker_recipient: components["schemas"]["Address"];
212
+ payment_leg: components["schemas"]["PaymentLeg"];
213
+ payment_destination: "venue" | "escrow" | null;
214
+ maker_destination: "venue" | "escrow" | null;
215
+ signed_at?: string | null;
216
+ payment_locked_at?: string | null;
217
+ maker_claimed_at?: string | null;
218
+ maker_leg: components["schemas"]["MakerLeg"];
219
+ state: components["schemas"]["FillState"];
220
+ reason: string | null;
221
+ pay_by: components["schemas"]["Timestamp"];
222
+ reveal_by: components["schemas"]["Timestamp"];
223
+ t1: components["schemas"]["Timestamp"];
224
+ t2: components["schemas"]["Timestamp"];
225
+ deliver_by: string | null;
226
+ report_by: string | null;
227
+ delivery_to: string | null;
228
+ delivery_hash: string | null;
229
+ delivery_fee: components["schemas"]["Amount"];
230
+ payment_verified_at: string | null;
231
+ created_at: components["schemas"]["Timestamp"];
232
+ updated_at: components["schemas"]["Timestamp"];
233
+ settled_at: string | null;
234
+ transactions: components["schemas"]["Transaction"][];
235
+ }[]>;
236
+ /** Reports the venue transaction delivering a fill (a Lighter hash or an Aster transfer id); the server verifies it before paying the maker. Exact retries return the fill. */
237
+ deliver(fillId: string, txHash: string): Promise<{
238
+ fill: components["schemas"]["Fill"];
239
+ }>;
240
+ /**
241
+ * The identity's open fills kept current from its event journal: a snapshot, then only the
242
+ * changes, pushed over the stream. `fills()` throws until the first snapshot is in.
243
+ */
244
+ fillFeed(signal: AbortSignal, log?: (line: string) => void): {
245
+ done: Promise<void>;
246
+ fills: () => Promise<FeedFill[]>;
247
+ deliver: (id: string, hash: string) => Promise<{
248
+ fill: components["schemas"]["Fill"];
249
+ }>;
250
+ };
251
+ }
package/dist/maker.js ADDED
@@ -0,0 +1,201 @@
1
+ import { linkAccount, setUpVenue } from "./account.js";
2
+ import { wait } from "./client.js";
3
+ import { errorText } from "./delivery.js";
4
+ import { changes } from "./events.js";
5
+ import { ESCROW_DEPLOYMENTS, requireSigningDomains } from "./protocol/deployments.js";
6
+ import { checkMakerIntent } from "./protocol/intents.js";
7
+ import { checkCloseOrders, checkPreparedQuote, insist, same, VENUE_SETTLED } from "./protocol/protocol.js";
8
+ import { signIn, signOut } from "./session.js";
9
+ import { signItems } from "./signer.js";
10
+ import { signedItems } from "./store.js";
11
+ /** The financial key: provisioning and release of capital only. Never give it to the quoting host. */
12
+ export class Maker {
13
+ client;
14
+ account;
15
+ operator;
16
+ appOrigin;
17
+ manifest;
18
+ sessionId = "";
19
+ /** `operator` and `appOrigin` come from reviewed deployment configuration, never from the API. */
20
+ constructor(client, account, operator, appOrigin, manifest = ESCROW_DEPLOYMENTS) {
21
+ this.client = client;
22
+ this.account = account;
23
+ this.operator = operator;
24
+ this.appOrigin = appOrigin;
25
+ this.manifest = manifest;
26
+ }
27
+ async login() { this.sessionId = (await signIn(this.client, this.account, this.appOrigin)).session_id; }
28
+ async logout() { if (this.sessionId)
29
+ await signOut(this.client, this.sessionId); }
30
+ balances() { return this.client.call("getBalances"); }
31
+ order(id) { return this.client.call("getOrder", { path: { id } }); }
32
+ /**
33
+ * Links the financial key's own account on `venue`, or `owner`'s, which signs the link message,
34
+ * then the setup step a maker pool needs: on Aster, approving LaMarge's agent.
35
+ */
36
+ async link(venue, owner) {
37
+ const linked = await linkAccount(this.client, this.account, venue, owner ?? this.account);
38
+ if (venue === "aster")
39
+ await setUpVenue(this.client, venue, owner ?? this.account, { steps: ["agent"] });
40
+ return linked;
41
+ }
42
+ async createCredential(name, quoteSigner, days = 30) {
43
+ const credential = await this.client.call("postV1Credentials", { body: { name, quote_signer: quoteSigner, scopes: ["read", "quote", "cancel"], expires_at: Math.floor(Date.now() / 1000) + days * 86_400 } });
44
+ insist(same(credential.quote_signer, quoteSigner) && credential.scopes.join(",") === "read,quote,cancel");
45
+ return credential;
46
+ }
47
+ /**
48
+ * Persist `client_request_id`, and keep `store`: each signed preparation is saved there before its first submission. `owner`,
49
+ * for a pool held inside its venue (Lighter, Aster), is the dedicated address linked there;
50
+ * escrow funding always comes from the financial key.
51
+ */
52
+ async addCapital(input, credential, store, expectedOwner = this.account.address) {
53
+ insist(Boolean(input.client_request_id), "Provide a persistent client_request_id");
54
+ insist(same(expectedOwner, this.account.address) || VENUE_SETTLED.has(input.venue), "Escrow capital is funded by the financial key's own account");
55
+ const [balances, orders] = await Promise.all([this.balances(), this.client.call("getOrders")]);
56
+ const owner = balances.find(balance => balance.venue === input.venue)?.address;
57
+ insist(owner && same(owner, expectedOwner), "Link the source account before provisioning");
58
+ const existingOrder = orders.find(order => order.capital_pool && order.venue === input.venue && order.quote_identity_id === credential.id && same(order.owner_address, owner)) ?? null;
59
+ const { venue, ...body } = input;
60
+ const asked = { ...body, quote_identity_id: credential.id };
61
+ const prepared = await this.client.call("prepareMaker", { path: { venue }, body: asked, retry: true });
62
+ if (prepared.state === "accepted" || prepared.state === "complete")
63
+ return prepared.order;
64
+ insist(prepared.state === "preparing", "Preparation ended; review a new request");
65
+ const key = `maker:${prepared.id}`;
66
+ let signatures = signedItems(await store?.load(key));
67
+ if (!signatures) {
68
+ checkMakerIntent(prepared, { ...asked, venue }, { owner, quoteSigner: credential.quote_signer, quoteIdentityId: credential.id, operator: this.operator, existingOrder });
69
+ signatures = await signItems(this.account, prepared.to_sign, this.manifest);
70
+ await store?.save(key, signatures);
71
+ }
72
+ const { order } = await this.client.call("submitMakerSignatures", { path: { id: prepared.id }, body: { signatures }, retry: true });
73
+ await store?.delete(key);
74
+ return order;
75
+ }
76
+ async closeOrders(ids) {
77
+ const result = [];
78
+ for (let start = 0; start < ids.length; start += 50) {
79
+ const chunk = ids.slice(start, start + 50);
80
+ const close = await this.client.call("postCloses", { body: { orders: chunk } });
81
+ if (!close.id)
82
+ continue;
83
+ checkCloseOrders(close.to_sign, chunk, this.account.address);
84
+ const signatures = await signItems(this.account, close.to_sign, this.manifest);
85
+ result.push(...(await this.client.call("postCloseSignatures", { path: { id: close.id }, body: { signatures }, retry: true })).orders);
86
+ }
87
+ return result;
88
+ }
89
+ async waitFor(id, states, timeoutMs = 600_000) {
90
+ const end = Date.now() + timeoutMs;
91
+ while (Date.now() < end) {
92
+ const order = await this.order(id);
93
+ if (states.includes(order.state))
94
+ return order;
95
+ if (order.state === "failed")
96
+ throw new Error(`Order failed: ${order.error}`);
97
+ await wait(2_000);
98
+ }
99
+ throw new Error(`Order ${id} still pending; recover it by the same client_request_id`);
100
+ }
101
+ }
102
+ /** A scoped credential (`mk_`) quotes and cancels only its own orders; this key cannot withdraw capital. */
103
+ export class Quoter {
104
+ client;
105
+ identity;
106
+ account;
107
+ manifest;
108
+ lastRtt;
109
+ constructor(client, identity, account, manifest = ESCROW_DEPLOYMENTS) {
110
+ this.client = client;
111
+ this.identity = identity;
112
+ this.account = account;
113
+ this.manifest = manifest;
114
+ insist(client.token?.startsWith("mk_"), "Use a scoped maker credential");
115
+ }
116
+ /** Each answer's round trip, reported with the next lease. */
117
+ async timed(run) {
118
+ const started = performance.now();
119
+ const answer = await run();
120
+ const completed = performance.now();
121
+ this.lastRtt = { ms: Math.round(completed - started), at: completed };
122
+ return answer;
123
+ }
124
+ order(id) { return this.timed(() => this.client.call("getOrder", { path: { id } })); }
125
+ lease(ttlSeconds = 30) {
126
+ const sample = this.lastRtt;
127
+ const age = sample ? Math.round(performance.now() - sample.at) : 0;
128
+ const telemetry = sample && sample.ms <= 60_000 && age <= 120_000 ? { api_rtt_ms: sample.ms, api_rtt_age_ms: age } : {};
129
+ return this.timed(() => this.client.call("putV1QuoteIdentitiesIdLease", { path: { id: this.identity }, body: { ttl_seconds: ttlSeconds, ...telemetry }, retry: true }));
130
+ }
131
+ cancelAll() { return this.timed(() => this.client.call("deleteV1QuoteIdentitiesIdQuotes", { path: { id: this.identity } })); }
132
+ /** Prepares `order`'s quote set and checks it; signing is the caller's, as for a contract delegate. */
133
+ async prepareUnsigned(order, quotes, size, validUntil) {
134
+ insist(order.quote_identity_id === this.identity && same(order.quote_signer, this.account.address) && order.state === "live" && size > 0n && size <= BigInt(order.available_to_quote));
135
+ insist(quotes.length > 0);
136
+ const asks = quotes.map(({ recipient: _, ...ask }) => ask);
137
+ const prepared = await this.timed(() => this.client.call("prepareQuotes", { path: { id: order.id }, body: { valid_until: validUntil, quote_size: String(size), quotes: asks } }));
138
+ checkPreparedQuote(prepared, order, quotes, validUntil, String(size));
139
+ requireSigningDomains(prepared.to_sign, this.manifest);
140
+ return prepared;
141
+ }
142
+ async prepare(order, quotes, size, validUntil) {
143
+ const prepared = await this.prepareUnsigned(order, quotes, size, validUntil);
144
+ const [signed] = await signItems(this.account, prepared.to_sign, this.manifest);
145
+ insist(signed, "Nothing to sign");
146
+ return { order_id: order.id, draft_id: prepared.draft_id, signature: signed.signature };
147
+ }
148
+ /** All distinct order updates and cancels commit atomically; a cancel is never replayed, as it would void the sequence that follows. */
149
+ batch(updates, cancels = []) {
150
+ insist(updates.length + cancels.length > 0 && updates.length + cancels.length <= 50);
151
+ return this.timed(() => this.client.call("postV1QuotesBatch", { body: { updates, cancels }, retry: cancels.length === 0 }));
152
+ }
153
+ /** The identity's fills, current and the last final ones. */
154
+ fills() { return this.timed(() => this.client.call("getFills")); }
155
+ /** Reports the venue transaction delivering a fill (a Lighter hash or an Aster transfer id); the server verifies it before paying the maker. Exact retries return the fill. */
156
+ deliver(fillId, txHash) {
157
+ return this.timed(() => this.client.call("submitDelivery", { path: { id: fillId }, body: { tx_hash: txHash }, retry: true }));
158
+ }
159
+ /**
160
+ * The identity's open fills kept current from its event journal: a snapshot, then only the
161
+ * changes, pushed over the stream. `fills()` throws until the first snapshot is in.
162
+ */
163
+ fillFeed(signal, log = console.error) {
164
+ const current = new Map();
165
+ let synced = false;
166
+ // The journal names the venues `from` and `to`; REST fills, `from_venue` and `to_venue`.
167
+ const put = (fill) => {
168
+ const { from, to, ...rest } = fill;
169
+ current.set(fill.id, { ...rest, from_venue: from, to_venue: to });
170
+ };
171
+ const done = (async () => {
172
+ while (!signal.aborted) {
173
+ try {
174
+ for await (const change of changes(this.client, { scope: "private", signal })) {
175
+ if ("resync" in change) {
176
+ current.clear();
177
+ if ("fills" in change.resync)
178
+ for (const fill of change.resync.fills)
179
+ put(fill);
180
+ }
181
+ else
182
+ for (const event of change.events)
183
+ if (event.kind === "fill.changed" && "role" in event.data)
184
+ put(event.data);
185
+ synced = true;
186
+ }
187
+ }
188
+ catch (error) {
189
+ synced = false;
190
+ log(`fills: ${errorText(error)}`);
191
+ await wait(2_000, signal).catch(() => undefined);
192
+ }
193
+ }
194
+ })();
195
+ return {
196
+ done,
197
+ fills: async () => { insist(synced, "Fills are not synced yet"); return [...current.values()]; },
198
+ deliver: (id, hash) => this.deliver(id, hash),
199
+ };
200
+ }
201
+ }
@@ -0,0 +1,82 @@
1
+ import { type SignItem } from "./protocol.ts";
2
+ import { type Verified } from "./validation.ts";
3
+ /** The message `address` signs to be linked under `master`: six lines, naming both, valid ten minutes at most. */
4
+ export declare function checkLinkChallenge(challenge: {
5
+ message: string;
6
+ nonce: string;
7
+ expires_at: string;
8
+ }, address: string, master: string, now?: number): void;
9
+ type FundingRequest = {
10
+ kind: string;
11
+ signer: string;
12
+ chain_id: number;
13
+ };
14
+ type FundingPlan = {
15
+ venue: string;
16
+ owner: string;
17
+ amount: string;
18
+ deposit: string;
19
+ withdrawal_amount: string;
20
+ withdrawal_to: string | null;
21
+ observed_at: string;
22
+ payment_setup: string;
23
+ requests: FundingRequest[];
24
+ };
25
+ /** What a transfer from wallet funds will ask its payer to sign: at most a permit, a margin withdrawal and the payment, on the pinned escrow chain, for this amount. */
26
+ export declare function checkFundingPlan<P extends FundingPlan>(intent: {
27
+ from: string;
28
+ payer: string;
29
+ amount_in: string;
30
+ funding_plan: P | null;
31
+ }, now?: number): P;
32
+ type PreparedClose = {
33
+ venue: string;
34
+ order_id: string;
35
+ to_margin: boolean;
36
+ dex: number;
37
+ nonce: string;
38
+ deadline: number;
39
+ to_sign: SignItem[];
40
+ };
41
+ /** One `close_offer` by the owner, in the pinned escrow domain, to the requested destination, good for an hour at most. */
42
+ export declare function checkCloseOffer<P extends PreparedClose>(prepared: P, order: {
43
+ venue: string;
44
+ owner: string;
45
+ order_id: string;
46
+ }, chain: {
47
+ id: number;
48
+ token: string;
49
+ decimals: number;
50
+ escrow: string;
51
+ }, now?: number, toMargin?: boolean): prepared is P & {
52
+ to_sign: Verified<P["to_sign"]>;
53
+ };
54
+ type RefundFill = {
55
+ id: string;
56
+ role: string;
57
+ maker_leg: string;
58
+ payment_leg: string;
59
+ t2: string;
60
+ from_venue: string;
61
+ maker_recipient: string;
62
+ payer: string;
63
+ swap_id: string;
64
+ amount_in: string;
65
+ hashlock: string;
66
+ deliver_by?: string | null | undefined;
67
+ delivery_hash?: string | null | undefined;
68
+ };
69
+ /** The maker of an abandoned fill may give the taker's locked payment back before `t2`. */
70
+ export declare const canRefundPayment: <F extends RefundFill>(fill: F) => boolean;
71
+ /** The one `refund_payment` consent of `fill`, signed by its payment's beneficiary, good until `t2` at most. */
72
+ export declare function checkPaymentRefund<F extends RefundFill, P extends {
73
+ deadline: number;
74
+ to_sign: SignItem[];
75
+ }>(fill: F, prepared: P, now?: number): asserts prepared is P & {
76
+ to_sign: Verified<P["to_sign"]>;
77
+ };
78
+ /** An escrow lock as `locks(owner, offerId, swapId)` returns it: amount, hashlock, recipient, refundAt, state, dex. */
79
+ export type EscrowLock = readonly [bigint, string, string, bigint, number, number];
80
+ /** The payment lock refunds `fill.amount_in` to its payer at `t2`, and the maker's lock, where the destination has one, was never made: consenting gives up nothing delivered. */
81
+ export declare function checkRefundLocks<F extends RefundFill>(fill: F, payment: EscrowLock, maker: EscrowLock | null): void;
82
+ export {};
@@ -0,0 +1,78 @@
1
+ import { requireV5Deployment } from "./deployments.js";
2
+ import { CHAINS, checkDeadline, checkEscrowTyped, checkRegistry, insist, same } from "./protocol.js";
3
+ import { required } from "./validation.js";
4
+ /** The message `address` signs to be linked under `master`: six lines, naming both, valid ten minutes at most. */
5
+ export function checkLinkChallenge(challenge, address, master, now = Date.now()) {
6
+ const lines = challenge.message.split("\n");
7
+ const expires = Date.parse(challenge.expires_at);
8
+ insist(lines.length === 6 && lines[0]?.startsWith("LaMarge: link a ") && same(lines[2], `Account: ${address}`) && same(lines[3], `Master account: ${master}`)
9
+ && lines[4] === `Nonce: ${challenge.nonce}` && lines[5] === `Expires: ${challenge.expires_at}` && expires > now && expires <= now + 10 * 60_000, "Unexpected account-link challenge");
10
+ }
11
+ /** What a transfer from wallet funds will ask its payer to sign: at most a permit, a margin withdrawal and the payment, on the pinned escrow chain, for this amount. */
12
+ export function checkFundingPlan(intent, now = Date.now()) {
13
+ const plan = required(intent.funding_plan, "Funding details unavailable; try again shortly");
14
+ const chain = required(CHAINS[intent.from], "Unknown funding venue");
15
+ requireV5Deployment(chain.id, chain.escrow);
16
+ insist(plan.venue === intent.from && same(plan.owner, intent.payer) && plan.amount === intent.amount_in
17
+ && BigInt(plan.deposit) <= BigInt(plan.amount) && BigInt(plan.withdrawal_amount) <= BigInt(plan.amount), "Funding details changed; review the transfer");
18
+ const age = now - Date.parse(plan.observed_at);
19
+ insist(age >= -5_000 && age < 120_000, "Funding details expired; review the transfer");
20
+ insist(plan.withdrawal_to === (BigInt(plan.withdrawal_amount) > 0n ? "signer" : null), "Funding destination differs from the transfer");
21
+ const kinds = plan.requests.map(request => request.kind);
22
+ const permit = plan.payment_setup === "required";
23
+ insist(plan.requests.every(request => same(request.signer, intent.payer) && request.chain_id === chain.id)
24
+ && kinds.filter(kind => kind === "pay_take").length === 1 && new Set(kinds).size === kinds.length
25
+ && kinds.every(kind => kind === "permit" || kind === "margin_move" || kind === "pay_take")
26
+ && kinds.includes("margin_move") === (BigInt(plan.withdrawal_amount) > 0n)
27
+ && kinds.includes("permit") === permit && (!permit || kinds[0] === "permit" && BigInt(plan.deposit) > 0n), "Funding requests differ from the transfer");
28
+ return plan;
29
+ }
30
+ /** How long a close signature may be good for, and the clock's margin. */
31
+ const CLOSE_TTL = 3_600 + 60;
32
+ /** Where a release may go on each venue: Hyperliquid's perps or spot, Polymarket's or Aster's account. */
33
+ const DEXES = { hyperliquid: [0, 4_294_967_295], polymarket: [0], aster: [0] };
34
+ /** One `close_offer` by the owner, in the pinned escrow domain, to the requested destination, good for an hour at most. */
35
+ export function checkCloseOffer(prepared, order, chain, now = Date.now() / 1000, toMargin = true) {
36
+ if (prepared.to_sign.length !== 1)
37
+ return false;
38
+ const [item] = prepared.to_sign;
39
+ if (!item)
40
+ return false;
41
+ try {
42
+ checkRegistry(order.venue, chain);
43
+ checkEscrowTyped(item.typed_data, order.venue);
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ const { primaryType, message } = item.typed_data;
49
+ return item.kind === "close_offer" && same(item.signer, order.owner) && primaryType === "CloseOffer" && same(message.owner, order.owner)
50
+ && same(message.offerId, order.order_id) && message.toMargin === toMargin && prepared.to_margin === toMargin
51
+ && (toMargin ? (DEXES[order.venue] ?? []).includes(prepared.dex) : prepared.dex === 0)
52
+ && String(message.dex) === String(prepared.dex) && String(message.nonce) === prepared.nonce && String(message.deadline) === String(prepared.deadline)
53
+ && same(prepared.order_id, order.order_id) && prepared.venue === order.venue && prepared.deadline > now && prepared.deadline <= now + CLOSE_TTL;
54
+ }
55
+ /** The maker of an abandoned fill may give the taker's locked payment back before `t2`. */
56
+ export const canRefundPayment = (fill) => fill.role === "maker" && fill.maker_leg === "ABANDONED" && fill.payment_leg === "LOCKED" && Date.parse(fill.t2) > Date.now();
57
+ /** The one `refund_payment` consent of `fill`, signed by its payment's beneficiary, good until `t2` at most. */
58
+ export function checkPaymentRefund(fill, prepared, now = Date.now() / 1000) {
59
+ insist(canRefundPayment(fill), "This payment cannot be refunded early");
60
+ insist(prepared.to_sign.length === 1, "Unexpected refund signatures");
61
+ const item = required(prepared.to_sign[0]);
62
+ checkEscrowTyped(item.typed_data, fill.from_venue);
63
+ const message = item.typed_data.message;
64
+ insist(item.kind === "refund_payment" && item.fill_id === fill.id && same(item.signer, fill.maker_recipient)
65
+ && item.typed_data.primaryType === "RefundPayment" && same(message.payer, fill.payer) && same(message.swapId, fill.swap_id)
66
+ && String(message.deadline) === String(prepared.deadline) && prepared.deadline <= Date.parse(fill.t2) / 1000, "Refund terms changed");
67
+ checkDeadline(message.deadline, 3600, now);
68
+ }
69
+ /** The payment lock refunds `fill.amount_in` to its payer at `t2`, and the maker's lock, where the destination has one, was never made: consenting gives up nothing delivered. */
70
+ export function checkRefundLocks(fill, payment, maker) {
71
+ // A fill keeps the settlement it was taken under: delivered inside the venue, it has a deadline.
72
+ const inVenue = Boolean(fill.deliver_by);
73
+ insist(inVenue === (maker === null), "Unsupported refund chain");
74
+ insist(!inVenue || !fill.delivery_hash, "This fill was delivered");
75
+ insist(payment[4] === 1 && payment[0] === BigInt(fill.amount_in) && same(payment[1], fill.hashlock)
76
+ && same(payment[2], fill.maker_recipient) && payment[3] === BigInt(Math.floor(Date.parse(fill.t2) / 1000))
77
+ && (maker === null || maker[4] === 0), "The onchain locks no longer permit this refund");
78
+ }