@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,54 @@
1
+ /** Addresses identify reviewed UUPS proxies, never implementations or values supplied by the API. */
2
+ export type EscrowDeployment = Readonly<{
3
+ version: 5;
4
+ chainId: number;
5
+ address: string;
6
+ }>;
7
+ export type DeploymentManifest = readonly EscrowDeployment[];
8
+ /** Reviewed deployments (contracts/README.md#deployments). */
9
+ export declare const ESCROW_DEPLOYMENTS: DeploymentManifest;
10
+ /** Retired v5 proxies are trusted for existing proofs, never new signatures. BSC's current proxy
11
+ * only lets owners close and withdraw, since Aster settles inside itself. */
12
+ export declare const RETIRED_V5_DEPLOYMENTS: readonly [{
13
+ readonly version: 5;
14
+ readonly chainId: 56;
15
+ readonly address: "0x3100b7b49c0f9631c6af25877b5a4e13a47afad6";
16
+ }, {
17
+ readonly version: 5;
18
+ readonly chainId: 999;
19
+ readonly address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734";
20
+ }, {
21
+ readonly version: 5;
22
+ readonly chainId: 137;
23
+ readonly address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734";
24
+ }, {
25
+ readonly version: 5;
26
+ readonly chainId: 56;
27
+ readonly address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734";
28
+ }];
29
+ export declare const ESCROW_PROOF_DEPLOYMENTS: DeploymentManifest;
30
+ export declare function escrowDeployment(chainId: number, address: string, manifest?: DeploymentManifest): EscrowDeployment | undefined;
31
+ /** Never derive or fund a receiver against an unpinned escrow, even if the API names it. */
32
+ export declare function requireV5Deployment(chainId: number, address: string, manifest?: DeploymentManifest): EscrowDeployment;
33
+ /** Signing domains of venues without escrow: Lighter's L2 chain id and zkLighter contract, BSC and Aster's vault. Quote sets of inventory held there are signed in them; nothing is ever sent to them. */
34
+ export declare const VENUE_DOMAINS: readonly [{
35
+ readonly venue: "lighter";
36
+ readonly chainId: 304;
37
+ readonly address: "0x3b4d794a66304f130a4db8f2551b0070dfcf5ca7";
38
+ }, {
39
+ readonly venue: "aster";
40
+ readonly chainId: 56;
41
+ readonly address: "0x128463a60784c4d3f46c23af3f65ed859ba87974";
42
+ }];
43
+ /** A quote set's domain: a reviewed escrow, or a pinned venue domain. */
44
+ export declare function requireSigningDomain(chainId: number, address: string, manifest?: DeploymentManifest): void;
45
+ /** A whole bundle is checked before its first prompt: each quote set's domain, and each funding or payment's escrow. */
46
+ export declare function requireSigningDomains(items: readonly {
47
+ kind: string;
48
+ typed_data: {
49
+ domain: {
50
+ chainId: number;
51
+ verifyingContract: string;
52
+ };
53
+ };
54
+ }[], manifest?: DeploymentManifest): void;
@@ -0,0 +1,44 @@
1
+ /** Reviewed deployments (contracts/README.md#deployments). */
2
+ export const ESCROW_DEPLOYMENTS = Object.freeze([
3
+ { version: 5, chainId: 999, address: "0x3100b7b49c0f9631c6af25877b5a4e13a47afad6" },
4
+ { version: 5, chainId: 137, address: "0x3100b7b49c0f9631c6af25877b5a4e13a47afad6" },
5
+ ]);
6
+ /** Retired v5 proxies are trusted for existing proofs, never new signatures. BSC's current proxy
7
+ * only lets owners close and withdraw, since Aster settles inside itself. */
8
+ export const RETIRED_V5_DEPLOYMENTS = Object.freeze([
9
+ { version: 5, chainId: 56, address: "0x3100b7b49c0f9631c6af25877b5a4e13a47afad6" },
10
+ { version: 5, chainId: 999, address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734" },
11
+ { version: 5, chainId: 137, address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734" },
12
+ { version: 5, chainId: 56, address: "0xea5ba9e1c0f5dde572bc08f3af6a3e8b9db73734" },
13
+ ]);
14
+ export const ESCROW_PROOF_DEPLOYMENTS = Object.freeze([...ESCROW_DEPLOYMENTS, ...RETIRED_V5_DEPLOYMENTS]);
15
+ export function escrowDeployment(chainId, address, manifest = ESCROW_DEPLOYMENTS) {
16
+ return manifest.find(deployment => deployment.chainId === chainId && deployment.address.toLowerCase() === address.toLowerCase());
17
+ }
18
+ /** Never derive or fund a receiver against an unpinned escrow, even if the API names it. */
19
+ export function requireV5Deployment(chainId, address, manifest = ESCROW_DEPLOYMENTS) {
20
+ const deployment = escrowDeployment(chainId, address, manifest);
21
+ if (deployment?.version !== 5)
22
+ throw new Error("The balance upgrade is not available on this venue yet");
23
+ return deployment;
24
+ }
25
+ /** Signing domains of venues without escrow: Lighter's L2 chain id and zkLighter contract, BSC and Aster's vault. Quote sets of inventory held there are signed in them; nothing is ever sent to them. */
26
+ export const VENUE_DOMAINS = Object.freeze([
27
+ { venue: "lighter", chainId: 304, address: "0x3b4d794a66304f130a4db8f2551b0070dfcf5ca7" },
28
+ { venue: "aster", chainId: 56, address: "0x128463a60784c4d3f46c23af3f65ed859ba87974" },
29
+ ]);
30
+ /** A quote set's domain: a reviewed escrow, or a pinned venue domain. */
31
+ export function requireSigningDomain(chainId, address, manifest = ESCROW_DEPLOYMENTS) {
32
+ if (VENUE_DOMAINS.some(domain => domain.chainId === chainId && domain.address === address.toLowerCase()))
33
+ return;
34
+ requireV5Deployment(chainId, address, manifest);
35
+ }
36
+ /** A whole bundle is checked before its first prompt: each quote set's domain, and each funding or payment's escrow. */
37
+ export function requireSigningDomains(items, manifest = ESCROW_DEPLOYMENTS) {
38
+ for (const { kind, typed_data: { domain } } of items) {
39
+ if (kind === "quote_set")
40
+ requireSigningDomain(domain.chainId, domain.verifyingContract, manifest);
41
+ else if (kind === "fund" || kind === "pay_take")
42
+ requireV5Deployment(domain.chainId, domain.verifyingContract, manifest);
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./consents.ts";
2
+ export * from "./deployments.ts";
3
+ export * from "./intents.ts";
4
+ export * from "./lighter.ts";
5
+ export * from "./protocol.ts";
6
+ export * from "./validation.ts";
@@ -0,0 +1,6 @@
1
+ export * from "./consents.js";
2
+ export * from "./deployments.js";
3
+ export * from "./intents.js";
4
+ export * from "./lighter.js";
5
+ export * from "./protocol.js";
6
+ export * from "./validation.js";
@@ -0,0 +1,126 @@
1
+ import { type Verified } from "./validation.ts";
2
+ import { type SignItem, type QuoteIntent } from "./protocol.ts";
3
+ /** `replace` names a position in the current set; without it, the quote adds a price level on `receive_venue`. */
4
+ export type MakerQuoteIntent = {
5
+ receive_venue: string;
6
+ receive: string;
7
+ min_fill: string;
8
+ quote_validity_seconds?: number | null | undefined;
9
+ replace?: {
10
+ sequence: number;
11
+ index: number;
12
+ } | undefined;
13
+ };
14
+ /** Added capital comes from the linked wallet alone, or from one margin move of the whole deposit. */
15
+ export type FundingSource = "wallet" | "venue";
16
+ type MakerIntent = {
17
+ venue: string;
18
+ amount: string;
19
+ quote?: MakerQuoteIntent | null;
20
+ quote_identity_id?: string | null | undefined;
21
+ client_request_id: string;
22
+ funding_source: FundingSource;
23
+ };
24
+ export type MakerOrderSnapshot = {
25
+ id: string;
26
+ venue: string;
27
+ owner_address: string;
28
+ quote_signer: string;
29
+ quote_identity_id: string;
30
+ state: string;
31
+ revision: string;
32
+ last_sequence: number;
33
+ available_to_quote: string;
34
+ quote_set: {
35
+ sequence: number;
36
+ active: boolean;
37
+ valid_until: string;
38
+ expires_at: string;
39
+ quotes: (QuoteIntent & {
40
+ available: string;
41
+ })[];
42
+ } | null;
43
+ };
44
+ export type PreparedMaker = {
45
+ id: string;
46
+ client_request_id: string;
47
+ state: string;
48
+ venue: string;
49
+ order: MakerOrderSnapshot;
50
+ expires_at: string;
51
+ expected_revision: string;
52
+ sequence: number | null;
53
+ to_sign: SignItem[];
54
+ intent: {
55
+ amount: string;
56
+ quote: MakerQuoteIntent | null;
57
+ quote_identity_id: string;
58
+ funding_source: string;
59
+ };
60
+ funding: {
61
+ existing_available: string;
62
+ added_amount: string;
63
+ wallet_amount: string;
64
+ venue_amount: string;
65
+ idle_amount: string;
66
+ };
67
+ };
68
+ type MakerAuthority = {
69
+ owner: string;
70
+ quoteSigner: string;
71
+ quoteIdentityId: string;
72
+ operator: string;
73
+ recipient?: string;
74
+ existingOrder: MakerOrderSnapshot | null;
75
+ };
76
+ /** Bind all new capital and the complete replacement quote set before any wallet request. */
77
+ export declare function checkMakerIntent<T extends PreparedMaker>(prepared: T, input: MakerIntent, authority: MakerAuthority, now?: number): asserts prepared is T & {
78
+ to_sign: Verified<SignItem[]>;
79
+ };
80
+ type TakeIntent = {
81
+ from: string;
82
+ to: string;
83
+ amount_in: string;
84
+ min_out: string;
85
+ max_parts?: number;
86
+ };
87
+ export type TakeFill = {
88
+ id: string;
89
+ order_id: string;
90
+ sequence: number;
91
+ role: string;
92
+ from_venue: string;
93
+ to_venue: string;
94
+ amount_in: string;
95
+ amount_out: string;
96
+ payer: string;
97
+ maker_recipient: string;
98
+ taker_recipient: string;
99
+ swap_id: string;
100
+ hashlock: string;
101
+ pay_by: string;
102
+ t1: string;
103
+ t2: string;
104
+ };
105
+ /** What a take from `venue` signs to pay: a transfer inside Aster or Lighter, an escrow `pay_take` elsewhere. */
106
+ export declare const takePaymentKind: (venue: string) => string;
107
+ type LighterTransfer = {
108
+ kind: string;
109
+ signer: string;
110
+ fill_id?: string | undefined;
111
+ typed_data: {
112
+ from_account_index: number;
113
+ amount: string;
114
+ memo: string;
115
+ };
116
+ };
117
+ /** A take's whole signing batch, as the API sends it. */
118
+ type TakeItems = readonly (SignItem | LighterTransfer)[];
119
+ /** A take paid from Lighter: the transfer the taker signs pays exactly its fill, from his linked account, named by its memo. */
120
+ export declare function checkLighterTakeIntent(items: TakeItems, fills: TakeFill[], input: TakeIntent, payer: string, recipient: string, account: number, now?: number): void;
121
+ /** A take paid from Aster: the taker's wallet signs a transfer of the fill's amount to the maker's
122
+ * account under the fill id, with a nonce far enough ahead for LaMarge to lock the maker and
123
+ * within Aster's nonce window. */
124
+ export declare function checkAsterTakeIntent(items: TakeItems, fills: TakeFill[], input: TakeIntent, payer: string, recipient: string, now?: number): void;
125
+ export declare function checkTakeIntent(items: SignItem[], fills: TakeFill[], input: TakeIntent, payer: string, recipient: string, now?: number): asserts items is Verified<SignItem[]>;
126
+ export {};
@@ -0,0 +1,141 @@
1
+ import { fillMemo } from "./lighter.js";
2
+ import { required } from "./validation.js";
3
+ import { asterTransfer, CHAINS, checkAsterTransfer, checkDeadline, checkEscrowTyped, checkSignedQuote, checkWithdrawal, insist, MAKER_TIMEOUT_SECONDS, NEVER_VALID_UNTIL, same, takePayments, TAKER_TIMEOUT_SECONDS, uint, venueUnits, withdrawalCost } from "./protocol.js";
4
+ /** Bind all new capital and the complete replacement quote set before any wallet request. */
5
+ export function checkMakerIntent(prepared, input, authority, now = Date.now() / 1000) {
6
+ const { owner, quoteSigner, quoteIdentityId, operator, existingOrder } = authority;
7
+ const { order, funding, intent, to_sign: items } = prepared;
8
+ insist(/^0x[0-9a-fA-F]{40}$/.test(operator), "The deployment operator has not been configured");
9
+ insist(prepared.state === "preparing" && prepared.id.length > 0 && prepared.client_request_id === input.client_request_id && input.client_request_id.length > 0 && prepared.venue === input.venue);
10
+ insist(order.venue === input.venue && same(order.owner_address, owner) && same(order.quote_signer, quoteSigner) && order.quote_identity_id === quoteIdentityId && (!input.quote_identity_id || input.quote_identity_id === quoteIdentityId));
11
+ insist(intent.amount === input.amount && intent.quote_identity_id === quoteIdentityId && intent.funding_source === input.funding_source && prepared.expected_revision === order.revision);
12
+ checkDeadline(Math.floor(Date.parse(prepared.expires_at) / 1000), 3600, now);
13
+ const asked = input.quote ?? null, echoed = intent.quote;
14
+ insist(asked === null ? echoed === null : echoed !== null && echoed.receive_venue === asked.receive_venue && echoed.receive === asked.receive && echoed.min_fill === asked.min_fill && (echoed.quote_validity_seconds ?? 0) === (asked.quote_validity_seconds ?? 0) && echoed.replace?.sequence === asked.replace?.sequence && echoed.replace?.index === asked.replace?.index);
15
+ insist(!asked?.replace || existingOrder?.quote_set?.sequence === asked.replace.sequence, "Quotes changed; review again");
16
+ if (existingOrder) {
17
+ insist(same(order.id, existingOrder.id) && existingOrder.venue === input.venue && same(existingOrder.owner_address, owner) && same(existingOrder.quote_signer, quoteSigner) && existingOrder.quote_identity_id === quoteIdentityId);
18
+ insist(order.last_sequence === existingOrder.last_sequence && order.available_to_quote === existingOrder.available_to_quote && uint(order.revision) >= uint(existingOrder.revision), "Maker capital changed; review again");
19
+ }
20
+ else
21
+ insist(order.last_sequence === 0 && uint(order.available_to_quote) === 0n);
22
+ const amount = uint(input.amount), available = existingOrder ? uint(existingOrder.available_to_quote) : 0n;
23
+ const added = uint(funding.added_amount), idle = uint(funding.idle_amount), wallet = uint(funding.wallet_amount), venue = uint(funding.venue_amount), existing = uint(funding.existing_available);
24
+ const deposit = wallet + venue;
25
+ insist(amount > 0n && existing >= available && added === idle + deposit && (input.funding_source === "venue" ? wallet : venue) === 0n);
26
+ const kept = [];
27
+ // A deposit for a delegated quoter does not publish prices on its behalf.
28
+ const prior = existingOrder && !["closed", "failed"].includes(existingOrder.state) ? existingOrder.quote_set : null;
29
+ if ((asked || !input.quote_identity_id) && prior && Date.parse(prior.valid_until) / 1000 > now) {
30
+ for (const [index, q] of prior.quotes.entries()) {
31
+ const remaining = uint(q.available), supply = uint(q.supply), receive = uint(q.receive);
32
+ insist(remaining <= supply && remaining <= available);
33
+ if (remaining < uint(q.min_fill) || remaining === 0n)
34
+ continue;
35
+ kept.push({ venue: q.venue, recipient: q.recipient, supply: String(remaining), receive: String((remaining * receive + supply - 1n) / supply), min_fill: q.min_fill, index });
36
+ }
37
+ }
38
+ if (asked) {
39
+ // A venue's levels need capital for their sum; other venues share it. A change adds only
40
+ // the amount by which it raises that sum above both the available capital and the current sum.
41
+ const levels = kept.filter(q => q.venue === asked.receive_venue);
42
+ const before = levels.reduce((sum, q) => sum + uint(q.supply), 0n);
43
+ const after = levels.filter(q => q.index !== asked.replace?.index).reduce((sum, q) => sum + uint(q.supply), amount);
44
+ const covered = before > available ? before : available, spare = existing - available;
45
+ insist(added === (after > covered ? after - covered : 0n) && deposit === (added > spare ? added - spare : 0n));
46
+ }
47
+ else
48
+ insist(added === amount && idle === 0n);
49
+ const quotes = kept.filter(q => q.index !== asked?.replace?.index).map(({ index: _, ...q }) => q);
50
+ if (asked)
51
+ quotes.push({ venue: asked.receive_venue, recipient: required(authority.recipient), supply: input.amount, receive: asked.receive, min_fill: asked.min_fill });
52
+ // The server's stable order: by venue, a venue's levels as they were, a new one last.
53
+ quotes.sort((a, b) => a.venue < b.venue ? -1 : a.venue > b.venue ? 1 : 0);
54
+ const kinds = items.map(item => item.kind);
55
+ insist(new Set(kinds).size === kinds.length && kinds.includes("fund") === (added > 0n) && kinds.includes("quote_set") === (quotes.length > 0) && kinds.includes("margin_move") === (venue > 0n));
56
+ const sequence = prepared.sequence === null ? null : uint(prepared.sequence);
57
+ insist(quotes.length ? sequence !== null && sequence > BigInt(order.last_sequence) : sequence === null);
58
+ for (const item of items) {
59
+ const typed = item.typed_data, m = typed.message;
60
+ if (item.kind === "margin_move") {
61
+ // The wallet keeps what the move rounds up to whole millionths.
62
+ const moved = venueUnits(venue, input.venue);
63
+ insist(same(item.signer, owner) && withdrawalCost(input.venue, typed) === moved);
64
+ checkWithdrawal(typed, input.venue, owner, owner, moved, now);
65
+ }
66
+ else if (item.kind === "fund") {
67
+ checkEscrowTyped(typed, input.venue);
68
+ const floor = uint(m.minQuoteSequence);
69
+ insist(typed.primaryType === "FundOffer" && same(item.signer, owner) && same(m.owner, owner) && same(m.offerId, order.id) && uint(m.amount) === added && uint(m.deposit) === deposit && same(m.quoteSigner, quoteSigner) && same(m.operator, operator));
70
+ insist(floor > BigInt(order.last_sequence) && floor <= BigInt(Number.MAX_SAFE_INTEGER) && (sequence === null || floor === sequence));
71
+ checkDeadline(m.deadline, 3600, now);
72
+ }
73
+ else if (item.kind === "quote_set") {
74
+ const expiry = Number(uint(m.expiresAt)), validity = asked?.quote_validity_seconds ?? 0;
75
+ insist(Number.isSafeInteger(validity) && validity >= 0 && validity <= 300);
76
+ insist(!asked ? prior !== null && prior !== undefined && expiry === Math.floor(Date.parse(prior.expires_at) / 1000) : validity === 0 ? expiry === NEVER_VALID_UNTIL + MAKER_TIMEOUT_SECONDS + 1 : expiry > now + MAKER_TIMEOUT_SECONDS && expiry <= now + validity + MAKER_TIMEOUT_SECONDS + 31);
77
+ insist(sequence !== null && sequence <= BigInt(Number.MAX_SAFE_INTEGER));
78
+ checkSignedQuote(item, order, quotes, Number(sequence), expiry);
79
+ }
80
+ else
81
+ throw new Error("Unexpected maker signing operation");
82
+ }
83
+ }
84
+ /** What a take from `venue` signs to pay: a transfer inside Aster or Lighter, an escrow `pay_take` elsewhere. */
85
+ export const takePaymentKind = (venue) => venue === "aster" ? "aster_transfer" : venue === "lighter" ? "lighter_transfer" : "pay_take";
86
+ /** A take paid inside its venue buys exactly one fill, paid by one signed transfer that is the whole batch. */
87
+ function checkVenueTake(items, kind, fills, input, payer, recipient, now) {
88
+ insist(takePaymentKind(input.from) === kind, "The signing request differs from the take's venue");
89
+ const item = items[0];
90
+ insist(items.length === 1 && fills.length === 1 && item?.kind === kind && same(item.signer, payer), "A take paid inside a venue is one transfer");
91
+ const fill = required(fills[0]);
92
+ insist(fill.role === "taker" && fill.from_venue === input.from && fill.to_venue === input.to && same(fill.payer, payer) && same(fill.taker_recipient, recipient) && item.fill_id === fill.id);
93
+ insist(BigInt(fill.amount_in) > 0n && BigInt(fill.amount_in) <= BigInt(input.amount_in) && BigInt(fill.amount_out) >= BigInt(input.min_out));
94
+ const payBy = Date.parse(fill.pay_by) / 1000, t1 = Date.parse(fill.t1) / 1000;
95
+ insist(payBy > now && payBy <= now + 90 && t1 > payBy && t1 <= now + MAKER_TIMEOUT_SECONDS + 30);
96
+ return { terms: item.typed_data, fill };
97
+ }
98
+ /** A take paid from Lighter: the transfer the taker signs pays exactly its fill, from his linked account, named by its memo. */
99
+ export function checkLighterTakeIntent(items, fills, input, payer, recipient, account, now = Date.now() / 1000) {
100
+ const { terms, fill } = checkVenueTake(items, "lighter_transfer", fills, input, payer, recipient, now);
101
+ insist("memo" in terms && terms.from_account_index === account && terms.amount === fill.amount_in && terms.memo === fillMemo(fill.id), "The Lighter transfer differs from the take");
102
+ }
103
+ /** A take paid from Aster: the taker's wallet signs a transfer of the fill's amount to the maker's
104
+ * account under the fill id, with a nonce far enough ahead for LaMarge to lock the maker and
105
+ * within Aster's nonce window. */
106
+ export function checkAsterTakeIntent(items, fills, input, payer, recipient, now = Date.now() / 1000) {
107
+ const { terms, fill } = checkVenueTake(items, "aster_transfer", fills, input, payer, recipient, now);
108
+ insist("domain" in terms, "Unexpected Aster transfer");
109
+ const transfer = asterTransfer(terms);
110
+ checkAsterTransfer(transfer, fill);
111
+ insist(transfer.nonce >= now + 20 && transfer.nonce <= now + 70, "The Aster transfer's nonce differs from the take");
112
+ }
113
+ export function checkTakeIntent(items, fills, input, payer, recipient, now = Date.now() / 1000) {
114
+ insist(takePaymentKind(input.from) === "pay_take", "The signing request differs from the take's venue");
115
+ insist(fills.length > 0 && fills.length <= (input.max_parts ?? 8) && new Set(fills.map(fill => fill.id)).size === fills.length && new Set(fills.map(fill => fill.swap_id.toLowerCase())).size === fills.length);
116
+ const totalIn = fills.reduce((sum, fill) => sum + BigInt(fill.amount_in), 0n);
117
+ const totalOut = fills.reduce((sum, fill) => sum + BigInt(fill.amount_out), 0n);
118
+ insist(totalIn > 0n && totalIn <= BigInt(input.amount_in) && totalOut >= BigInt(input.min_out));
119
+ const takes = items.filter(item => item.kind === "pay_take");
120
+ insist(takes.length === 1 && items.length === 1, "A reserved take must already be funded and authorized");
121
+ const take = required(takes[0]), typed = take.typed_data, m = typed.message;
122
+ checkEscrowTyped(typed, input.from);
123
+ insist(typed.primaryType === "PayTake" && same(take.signer, payer) && same(m.payer, payer) && take.fill_id !== undefined && fills.some(fill => fill.id === take.fill_id));
124
+ const payments = takePayments(typed);
125
+ insist(payments.length === fills.length);
126
+ const receive = CHAINS[input.to];
127
+ for (const fill of fills) {
128
+ insist(fill.role === "taker" && fill.from_venue === input.from && fill.to_venue === input.to && same(fill.payer, payer) && same(fill.taker_recipient, recipient) && BigInt(fill.amount_in) > 0n && BigInt(fill.amount_out) > 0n);
129
+ const t1 = Date.parse(fill.t1) / 1000, t2 = Date.parse(fill.t2) / 1000, payBy = Date.parse(fill.pay_by) / 1000;
130
+ insist(payBy > now && payBy <= now + 90 && t1 > payBy && t1 <= now + MAKER_TIMEOUT_SECONDS + 30 && t2 > t1 && t2 <= now + TAKER_TIMEOUT_SECONDS + 30);
131
+ const matching = payments.filter(payment => same(payment.swapId, fill.swap_id));
132
+ insist(matching.length === 1);
133
+ const p = required(matching[0]);
134
+ insist(same(p.payer, payer) && same(p.recipient, fill.maker_recipient) && same(p.hashlock, fill.hashlock) && /^0x[0-9a-f]{64}$/i.test(fill.hashlock) && uint(p.amount) === BigInt(fill.amount_in) && uint(p.refundAt) === BigInt(Math.floor(t2)) && uint(p.deadline) === BigInt(Math.floor(payBy)));
135
+ insist(uint(p.dex) === 0n || input.from === "hyperliquid" && uint(p.dex) === 4294967295n);
136
+ insist(same(p.orderId, fill.order_id) && uint(p.sequence) === BigInt(fill.sequence) && uint(p.amountOut) === BigInt(fill.amount_out) && same(p.receiveRecipient, recipient) && receive && uint(p.receiveChainId) === BigInt(receive.id) && same(p.receiveToken, receive.token) && same(p.receiveEscrow, receive.escrow));
137
+ }
138
+ // Only the shortfall is pulled from the payer. The allowance is reusable, but no unrelated operation is signed.
139
+ const deposit = uint(m.deposit);
140
+ insist(deposit <= totalIn);
141
+ }
@@ -0,0 +1,4 @@
1
+ /** Poseidon2 round constants of Lighter's Goldilocks width-12 permutation (poseidon_crypto v0.0.15). */
2
+ export declare const EXTERNAL: readonly (readonly bigint[])[];
3
+ export declare const INTERNAL: readonly bigint[];
4
+ export declare const DIAG: readonly bigint[];
@@ -0,0 +1,49 @@
1
+ /** Poseidon2 round constants of Lighter's Goldilocks width-12 permutation (poseidon_crypto v0.0.15). */
2
+ export const EXTERNAL = [
3
+ [0xd70193d17ab3b7d6n, 0xa2c3662a78a9162bn, 0x7a9fda827556ad44n, 0xe8d5501818c99643n, 0x4c7a8fced4d5fd38n, 0x55ab38985c0c513dn, 0x28a17bd016210b0bn, 0x8f8277679ec32fa8n, 0x768b3c3d68a460e9n, 0x872a022eb559d941n, 0xd1316dd4b3b97973n, 0xa7b608e578321000n],
4
+ [0x3fa02c87b0bee026n, 0x7a38f0022e13c31en, 0x00c054f3c5e8d20dn, 0x439f50f4bca7242fn, 0x4d0938aa57cd517fn, 0xb2e03ac5fb6b9a7dn, 0xe29d1f4237bedca8n, 0x05b7c844bc99b848n, 0x91cc0b73f34e17edn, 0x876e4427694bd755n, 0x67002ae0725c612dn, 0x05351f20e0b6315fn],
5
+ [0x2e3b9ef5457eb60bn, 0xd9ac17618c3783ddn, 0x0807528ad8874bcfn, 0xc78d546a455d2a0en, 0xf8b930c81e2481f0n, 0x712707d8dff3b041n, 0xdcb8c0aa0b9d34c3n, 0x9baddbdf2ee3a468n, 0x2dd16d50c5176c78n, 0x89eac5cfbc075cd3n, 0x2a741dea181587f3n, 0x1a4d6aa85a113d84n],
6
+ [0x4d736286a2387e34n, 0x8bad5dfc4fcb3ee3n, 0x84fbd03adb77c56an, 0x8d5cdd1a23ec53a2n, 0x036f08f08fff28ecn, 0xb717a3f4dbdfb443n, 0x58a074b5509d645cn, 0xf92bf834e4b87718n, 0x1541c3a0baa5ac4bn, 0x22149e6783e67692n, 0x9be8b5d9e112476fn, 0x41e0969f62babb76n],
7
+ [0xbc585ad3b9443dbbn, 0xf28dd3206975cbb1n, 0xdd8815e53ca045e0n, 0xde82c416b9e701ban, 0xc5cb875233afa025n, 0x7212697cd897ffa9n, 0x67844790aa63cfd7n, 0xdc0b9cfa97fe65c3n, 0xe8fe091869a82070n, 0x62902bb2e413c6d1n, 0x29f9f5001fb84f57n, 0xbe1014796ef5f8ben],
8
+ [0x71feb53e9bdba19cn, 0x251054f592ebb71cn, 0xe1a57643a4bb284bn, 0xa4ba6f87a45b739bn, 0x2c1fcade0b958c49n, 0xbbb424cda9a3e360n, 0x2ca647354c5f3f54n, 0xc9277b64d152e084n, 0xdbc9ac97445eff17n, 0x6f6cdf3198969f70n, 0x1de29d14fa76d8f1n, 0x73337458a8cc1d19n],
9
+ [0xb87e775e2fb3ab23n, 0xf166a1c7a565c80bn, 0xb24be06f426c747fn, 0xc281e8c49482ce00n, 0x51974c3b3b726c2dn, 0x87444cf8caf7d619n, 0x7c362f827a580cedn, 0x9567af14667647a0n, 0xcbf0473cbec54e37n, 0xe3209dedeff4f620n, 0xd43ad94e45a4c4een, 0x976981ee73f41768n],
10
+ [0xef707a224e207258n, 0x2fc779e10e6362een, 0x29b5ee60ad8c891fn, 0x96b37b39d8bfd667n, 0x877df68a8b22e733n, 0x5c41746f562c8d9fn, 0x0c9d76751052b71an, 0xfb3465341bf1c087n, 0xa0d14dc614d15eb1n, 0xdc27d17136906fa6n, 0x482e163b05ec397fn, 0x0273a462992366efn],
11
+ ];
12
+ export const INTERNAL = [
13
+ 0xa571418d95897b60n,
14
+ 0x8f32676574fcf6d3n,
15
+ 0x731102d4e3fb1bben,
16
+ 0x0330f08328a82d2bn,
17
+ 0x7f0449b6557f785dn,
18
+ 0x62f06210658dcbcbn,
19
+ 0xd5a98af9f89c458bn,
20
+ 0x77ec69083a346385n,
21
+ 0xef7ca48bbc27f890n,
22
+ 0x53e9652f61eac532n,
23
+ 0xa71c634abff4f0ccn,
24
+ 0xb16f5f0d7e28ea29n,
25
+ 0xc9dde31d0a003ab2n,
26
+ 0x2ddadf9775902533n,
27
+ 0xe4fa73fb16408b47n,
28
+ 0x90242ebc00d2ee59n,
29
+ 0xbb02dffd9f381982n,
30
+ 0xdea328364c50907cn,
31
+ 0x1395d3b924857cf8n,
32
+ 0x7d3ead0d5aec04e6n,
33
+ 0xc2f12be3fed74668n,
34
+ 0x0ba3c338f8c3d285n,
35
+ ];
36
+ export const DIAG = [
37
+ 0xc3b6c08e23ba9300n,
38
+ 0xd84b5de94a324fb6n,
39
+ 0x0d0c371c5b35b84fn,
40
+ 0x7964f570e7188037n,
41
+ 0x5daf18bbd996604bn,
42
+ 0x6743bc47b9595257n,
43
+ 0x5528b9362c59bb70n,
44
+ 0xac45e25b7127b68bn,
45
+ 0xa2077d7dfbb606b5n,
46
+ 0xf3faac6faee378aen,
47
+ 0x0c6388b51545e883n,
48
+ 0xd27dbb6944917b60n,
49
+ ];
@@ -0,0 +1,52 @@
1
+ /** Lighter's L2 chain id, which every transaction hash names. */
2
+ export declare const LIGHTER_CHAIN_ID: number;
3
+ /** The API key index of LaMarge's key on a taker's account. */
4
+ export declare const TAKER_API_KEY = 254;
5
+ /** A maker bot's API key slot, `LIGHTER_API_KEY_INDEX`: 4 unless set, below LaMarge's taker key. */
6
+ export declare function makerKeySlot(value?: string): number;
7
+ /** A taker's transfer moves USDC between collateral routes. */
8
+ export declare const USDC_ASSET = 3;
9
+ export declare const COLLATERAL_ROUTE = 0;
10
+ /** Lighter's public API, read directly so account data does not depend on LaMarge. */
11
+ export declare const LIGHTER_API = "https://mainnet.zklighter.elliot.ai";
12
+ /** The address owning Lighter account `index`, as Lighter's public API reads it. */
13
+ export declare function lighterAccountOwner(index: number, signal?: AbortSignal, fetcher?: (url: string, init: RequestInit) => Promise<Response>): Promise<string>;
14
+ /** Hashes field elements to five, without padding: each block of 8 overwrites the rate. */
15
+ export declare function hashToFp5(input: readonly bigint[]): bigint[];
16
+ /** An internal transfer, as LaMarge's key and the account's owner sign it. */
17
+ export interface LighterTransfer {
18
+ from: number;
19
+ apiKey: number;
20
+ to: number;
21
+ asset: number;
22
+ fromRoute: number;
23
+ toRoute: number;
24
+ amount: bigint;
25
+ fee: bigint;
26
+ memo: Uint8Array;
27
+ expiredAt: number;
28
+ nonce: number;
29
+ }
30
+ /** An API key registration, which LaMarge's key and the account's owner sign. */
31
+ interface LighterKeyRegistration {
32
+ account: number;
33
+ apiKey: number;
34
+ publicKey: Uint8Array;
35
+ nonce: number;
36
+ }
37
+ /** The 40-byte hash a transfer's API key signs, also its Lighter transaction hash. */
38
+ export declare function transferHash(t: LighterTransfer, chainId?: number): Uint8Array;
39
+ export declare const toHex: (bytes: Uint8Array) => string;
40
+ export declare const fromHex: (hex: string) => Uint8Array<ArrayBuffer>;
41
+ /** The memo naming a fill: its id's 16 bytes then 16 zero bytes, as hex. */
42
+ export declare const fillMemo: (fill: string) => string;
43
+ /** The text the owner signs with `personal_sign` for a transfer to another owner. */
44
+ export declare const transferMessage: (t: LighterTransfer, chainId?: number) => string;
45
+ /** The text the owner signs with `personal_sign` to register a key. */
46
+ export declare const registrationMessage: (r: LighterKeyRegistration) => string;
47
+ /** A sent transfer read back from its `tx_info`, without transaction attributes. */
48
+ export declare function readTransferTxInfo(info: Record<string, unknown>): {
49
+ transfer: LighterTransfer;
50
+ l1Signature: string;
51
+ };
52
+ export {};
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Lighter transaction hashes and owner messages for the browser. The taker's wallet signs the
3
+ * owner message (`L1Sig`) for what LaMarge's API key on its account signs; trade proofs are
4
+ * checked by the transfer's Poseidon2 hash. Port of `server/src/lighter_sig`, itself ported
5
+ * from Lighter's Go libraries and checked against their vectors.
6
+ */
7
+ import { DIAG, EXTERNAL, INTERNAL } from "./lighter-constants.js";
8
+ import { CHAINS } from "./protocol.js";
9
+ import { required } from "./validation.js";
10
+ /** Lighter's L2 chain id, which every transaction hash names. */
11
+ export const LIGHTER_CHAIN_ID = required(CHAINS.lighter).id;
12
+ /** The API key index of LaMarge's key on a taker's account. */
13
+ export const TAKER_API_KEY = 254;
14
+ /** A maker bot's API key slot, `LIGHTER_API_KEY_INDEX`: 4 unless set, below LaMarge's taker key. */
15
+ export function makerKeySlot(value = "4") {
16
+ const slot = Number(value);
17
+ if (!Number.isInteger(slot) || slot < 4 || slot >= TAKER_API_KEY)
18
+ throw new Error(`LIGHTER_API_KEY_INDEX is a key slot from 4 to ${TAKER_API_KEY - 1}: ${TAKER_API_KEY} is LaMarge's taker key`);
19
+ return slot;
20
+ }
21
+ /** A taker's transfer moves USDC between collateral routes. */
22
+ export const USDC_ASSET = 3;
23
+ export const COLLATERAL_ROUTE = 0;
24
+ /** Lighter's public API, read directly so account data does not depend on LaMarge. */
25
+ export const LIGHTER_API = "https://mainnet.zklighter.elliot.ai";
26
+ /** The address owning Lighter account `index`, as Lighter's public API reads it. */
27
+ export async function lighterAccountOwner(index, signal, fetcher = fetch) {
28
+ const response = await fetcher(`${LIGHTER_API}/api/v1/account?by=index&value=${index}`, signal ? { signal } : {});
29
+ const body = response.ok ? await response.json() : undefined;
30
+ const accounts = typeof body === "object" && body !== null ? Reflect.get(body, "accounts") : undefined;
31
+ const account = Array.isArray(accounts) && accounts.length === 1 ? accounts[0] : undefined;
32
+ const owner = typeof account === "object" && account !== null && Reflect.get(account, "index") === index ? Reflect.get(account, "l1_address") : undefined;
33
+ if (typeof owner !== "string")
34
+ throw new Error(`Lighter account #${index} is unreadable`);
35
+ return owner;
36
+ }
37
+ /** An element of a fixed-size array; out of range is a bug. */
38
+ function at(values, index) {
39
+ const value = values[index];
40
+ if (value === undefined)
41
+ throw new RangeError(`index ${index} out of range`);
42
+ return value;
43
+ }
44
+ const P = 0xffffffff00000001n;
45
+ const add = (a, b) => (a + b) % P;
46
+ const mul = (a, b) => (a * b) % P;
47
+ /** Five field elements as 40 little-endian bytes. */
48
+ const toBytes = (x) => {
49
+ const out = new Uint8Array(40);
50
+ x.forEach((limb, i) => { for (let j = 0; j < 8; j++)
51
+ out[i * 8 + j] = Number((limb >> BigInt(8 * j)) & 0xffn); });
52
+ return out;
53
+ };
54
+ // Poseidon2 over Goldilocks, width 12, rate 8, as Lighter's plonky2 variant.
55
+ function externalLayer(state) {
56
+ const s = [...state];
57
+ for (let i = 0; i < 12; i += 4) {
58
+ const t01 = at(s, i) + at(s, i + 1), t23 = at(s, i + 2) + at(s, i + 3), t0123 = t01 + t23;
59
+ const x0 = at(s, i), x2 = at(s, i + 2);
60
+ s[i] = t0123 + t01 + at(s, i + 1);
61
+ s[i + 1] = t0123 + at(s, i + 1) + 2n * x2;
62
+ s[i + 2] = t0123 + t23 + at(s, i + 3);
63
+ s[i + 3] = t0123 + at(s, i + 3) + 2n * x0;
64
+ }
65
+ const sums = [0, 1, 2, 3].map(i => at(s, i) + at(s, i + 4) + at(s, i + 8));
66
+ for (let i = 0; i < 12; i++)
67
+ state[i] = (at(s, i) + at(sums, i % 4)) % P;
68
+ }
69
+ const sbox = (x) => { const x3 = mul(mul(x, x), x); return mul(mul(x3, x3), x); };
70
+ function permute(state) {
71
+ externalLayer(state);
72
+ for (let round = 0; round < 8; round++) {
73
+ // The partial rounds sit between the two halves of the full rounds.
74
+ if (round === 4)
75
+ for (const constant of INTERNAL) {
76
+ state[0] = sbox(add(at(state, 0), constant));
77
+ const sum = state.reduce(add, 0n);
78
+ for (let i = 0; i < 12; i++)
79
+ state[i] = add(sum, mul(at(state, i), at(DIAG, i)));
80
+ }
81
+ for (let i = 0; i < 12; i++)
82
+ state[i] = sbox(add(at(state, i), at(at(EXTERNAL, round), i)));
83
+ externalLayer(state);
84
+ }
85
+ }
86
+ /** Hashes field elements to five, without padding: each block of 8 overwrites the rate. */
87
+ export function hashToFp5(input) {
88
+ const state = Array(12).fill(0n);
89
+ for (let i = 0; i < input.length; i += 8) {
90
+ input.slice(i, i + 8).forEach((x, j) => { state[j] = x; });
91
+ permute(state);
92
+ }
93
+ return state.slice(0, 5);
94
+ }
95
+ const MASK32 = 0xffffffffn;
96
+ /** The 40-byte hash a transfer's API key signs, also its Lighter transaction hash. */
97
+ export function transferHash(t, chainId = LIGHTER_CHAIN_ID) {
98
+ return toBytes(hashToFp5([BigInt(chainId), 12n, BigInt(t.nonce), BigInt(t.expiredAt), BigInt(t.from), BigInt(t.apiKey), BigInt(t.to),
99
+ BigInt(t.asset), BigInt(t.fromRoute), BigInt(t.toRoute), t.amount & MASK32, t.amount >> 32n, t.fee & MASK32, t.fee >> 32n]));
100
+ }
101
+ const hex10 = (value) => `0x${BigInt(value).toString(16).padStart(16, "0")}`;
102
+ export const toHex = (bytes) => Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
103
+ export const fromHex = (hex) => Uint8Array.from(hex.match(/../g) ?? [], byte => parseInt(byte, 16));
104
+ /** The memo naming a fill: its id's 16 bytes then 16 zero bytes, as hex. */
105
+ export const fillMemo = (fill) => fill.replaceAll("-", "").toLowerCase().padEnd(64, "0");
106
+ /** The text the owner signs with `personal_sign` for a transfer to another owner. */
107
+ export const transferMessage = (t, chainId = LIGHTER_CHAIN_ID) => `Transfer\n\nnonce: ${hex10(t.nonce)}\nfrom: ${hex10(t.from)} (route ${hex10(t.fromRoute)})\napi key: ${hex10(t.apiKey)}\nto: ${hex10(t.to)} (route ${hex10(t.toRoute)})\nasset: ${hex10(t.asset)}\namount: ${hex10(t.amount)}\nfee: ${hex10(t.fee)}\nchainId: ${hex10(chainId)}\nmemo: ${toHex(t.memo)}\nOnly sign this message for a trusted client!`;
108
+ /** The text the owner signs with `personal_sign` to register a key. */
109
+ export const registrationMessage = (r) => `Register Lighter Account\n\npubkey: 0x${toHex(r.publicKey)}\nnonce: ${hex10(r.nonce)}\naccount index: ${hex10(r.account)}\napi key index: ${hex10(r.apiKey)}\nOnly sign this message for a trusted client!`;
110
+ /** A sent transfer read back from its `tx_info`, without transaction attributes. */
111
+ export function readTransferTxInfo(info) {
112
+ const integer = (name) => {
113
+ const value = info[name];
114
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
115
+ throw new Error(`Lighter transfer ${name} is unreadable`);
116
+ return value;
117
+ };
118
+ const attributes = info.L2TxAttributes;
119
+ if (attributes !== null && attributes !== undefined && Object.keys(attributes).length > 0)
120
+ throw new Error("Lighter transfer attributes are not accepted");
121
+ const memo = info.Memo, l1 = info.L1Sig;
122
+ if (!Array.isArray(memo) || memo.length !== 32 || !memo.every(byte => Number.isInteger(byte) && byte >= 0 && byte < 256))
123
+ throw new Error("Lighter transfer memo is unreadable");
124
+ if (typeof l1 !== "string")
125
+ throw new Error("Lighter transfer owner signature is unreadable");
126
+ return {
127
+ transfer: { from: integer("FromAccountIndex"), apiKey: integer("ApiKeyIndex"), to: integer("ToAccountIndex"), asset: integer("AssetIndex"),
128
+ fromRoute: integer("FromRouteType"), toRoute: integer("ToRouteType"), amount: BigInt(integer("Amount")), fee: BigInt(integer("USDCFee")),
129
+ memo: Uint8Array.from(memo), expiredAt: integer("ExpiredAt"), nonce: integer("Nonce") },
130
+ l1Signature: l1,
131
+ };
132
+ }