@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.
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/dist/account.d.ts +32 -0
- package/dist/account.js +113 -0
- package/dist/aster.d.ts +74 -0
- package/dist/aster.js +169 -0
- package/dist/book.d.ts +39 -0
- package/dist/book.js +64 -0
- package/dist/client.d.ts +92 -0
- package/dist/client.js +100 -0
- package/dist/delivery.d.ts +68 -0
- package/dist/delivery.js +118 -0
- package/dist/events.d.ts +30 -0
- package/dist/events.js +93 -0
- package/dist/funds.d.ts +19 -0
- package/dist/funds.js +17 -0
- package/dist/generated/api.d.ts +5707 -0
- package/dist/generated/api.js +281 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/lighter.d.ts +92 -0
- package/dist/lighter.js +206 -0
- package/dist/maker.d.ts +251 -0
- package/dist/maker.js +201 -0
- package/dist/protocol/consents.d.ts +82 -0
- package/dist/protocol/consents.js +78 -0
- package/dist/protocol/deployments.d.ts +54 -0
- package/dist/protocol/deployments.js +44 -0
- package/dist/protocol/index.d.ts +6 -0
- package/dist/protocol/index.js +6 -0
- package/dist/protocol/intents.d.ts +126 -0
- package/dist/protocol/intents.js +141 -0
- package/dist/protocol/lighter-constants.d.ts +4 -0
- package/dist/protocol/lighter-constants.js +49 -0
- package/dist/protocol/lighter.d.ts +52 -0
- package/dist/protocol/lighter.js +132 -0
- package/dist/protocol/protocol.d.ts +168 -0
- package/dist/protocol/protocol.js +317 -0
- package/dist/protocol/validation.d.ts +11 -0
- package/dist/protocol/validation.js +34 -0
- package/dist/retry-after.d.ts +2 -0
- package/dist/retry-after.js +7 -0
- package/dist/session.d.ts +14 -0
- package/dist/session.js +28 -0
- package/dist/signer.d.ts +43 -0
- package/dist/signer.js +28 -0
- package/dist/store.d.ts +21 -0
- package/dist/store.js +46 -0
- package/dist/take.d.ts +27 -0
- package/dist/take.js +81 -0
- package/dist/transfer.d.ts +63 -0
- package/dist/transfer.js +253 -0
- package/dist/withdraw.d.ts +67 -0
- package/dist/withdraw.js +122 -0
- package/package.json +57 -4
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
export const routes = {
|
|
2
|
+
"getSiteShareImage": {
|
|
3
|
+
"path": "/share/v1/{filename}",
|
|
4
|
+
"method": "GET"
|
|
5
|
+
},
|
|
6
|
+
"getTradeShareImage": {
|
|
7
|
+
"path": "/share/v1/trades/{id}/image.png",
|
|
8
|
+
"method": "GET"
|
|
9
|
+
},
|
|
10
|
+
"getTradeShareMetadata": {
|
|
11
|
+
"path": "/share/trade",
|
|
12
|
+
"method": "GET"
|
|
13
|
+
},
|
|
14
|
+
"getVenues": {
|
|
15
|
+
"path": "/v1/venues",
|
|
16
|
+
"method": "GET"
|
|
17
|
+
},
|
|
18
|
+
"postAsterSigningRpc": {
|
|
19
|
+
"path": "/v1/venues/aster/rpc",
|
|
20
|
+
"method": "POST"
|
|
21
|
+
},
|
|
22
|
+
"getBook": {
|
|
23
|
+
"path": "/v1/books",
|
|
24
|
+
"method": "GET"
|
|
25
|
+
},
|
|
26
|
+
"getRoute": {
|
|
27
|
+
"path": "/v1/routes",
|
|
28
|
+
"method": "GET"
|
|
29
|
+
},
|
|
30
|
+
"getTradeProof": {
|
|
31
|
+
"path": "/v1/trades/{id}/proof",
|
|
32
|
+
"method": "GET"
|
|
33
|
+
},
|
|
34
|
+
"getVolume": {
|
|
35
|
+
"path": "/v1/volume",
|
|
36
|
+
"method": "GET"
|
|
37
|
+
},
|
|
38
|
+
"getHealth": {
|
|
39
|
+
"path": "/health",
|
|
40
|
+
"method": "GET",
|
|
41
|
+
"accept": [
|
|
42
|
+
503
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"getV1Info": {
|
|
46
|
+
"path": "/v1/info",
|
|
47
|
+
"method": "GET"
|
|
48
|
+
},
|
|
49
|
+
"postAuthChallenge": {
|
|
50
|
+
"path": "/v1/auth/challenge",
|
|
51
|
+
"method": "POST"
|
|
52
|
+
},
|
|
53
|
+
"postAuthVerify": {
|
|
54
|
+
"path": "/v1/auth/verify",
|
|
55
|
+
"method": "POST"
|
|
56
|
+
},
|
|
57
|
+
"deleteV1AuthSessionsId": {
|
|
58
|
+
"path": "/v1/auth/sessions/{id}",
|
|
59
|
+
"method": "DELETE"
|
|
60
|
+
},
|
|
61
|
+
"getMe": {
|
|
62
|
+
"path": "/v1/me",
|
|
63
|
+
"method": "GET"
|
|
64
|
+
},
|
|
65
|
+
"postLinkChallenge": {
|
|
66
|
+
"path": "/v1/accounts/link/challenge",
|
|
67
|
+
"method": "POST"
|
|
68
|
+
},
|
|
69
|
+
"getVenueSetup": {
|
|
70
|
+
"path": "/v1/accounts/{venue}/setup",
|
|
71
|
+
"method": "GET"
|
|
72
|
+
},
|
|
73
|
+
"postVenueSetupStep": {
|
|
74
|
+
"path": "/v1/accounts/{venue}/setup/{step}",
|
|
75
|
+
"method": "POST"
|
|
76
|
+
},
|
|
77
|
+
"postLink": {
|
|
78
|
+
"path": "/v1/accounts/link",
|
|
79
|
+
"method": "POST"
|
|
80
|
+
},
|
|
81
|
+
"deleteLink": {
|
|
82
|
+
"path": "/v1/accounts/{venue}",
|
|
83
|
+
"method": "DELETE"
|
|
84
|
+
},
|
|
85
|
+
"getBalances": {
|
|
86
|
+
"path": "/v1/balances",
|
|
87
|
+
"method": "GET"
|
|
88
|
+
},
|
|
89
|
+
"getStats": {
|
|
90
|
+
"path": "/v1/me/stats",
|
|
91
|
+
"method": "GET"
|
|
92
|
+
},
|
|
93
|
+
"getV1MeExposure": {
|
|
94
|
+
"path": "/v1/me/exposure",
|
|
95
|
+
"method": "GET"
|
|
96
|
+
},
|
|
97
|
+
"prepareMaker": {
|
|
98
|
+
"path": "/v1/maker/{venue}/prepare",
|
|
99
|
+
"method": "POST"
|
|
100
|
+
},
|
|
101
|
+
"getMakerPreparation": {
|
|
102
|
+
"path": "/v1/maker/preparations/{id}",
|
|
103
|
+
"method": "GET"
|
|
104
|
+
},
|
|
105
|
+
"cancelMakerPreparation": {
|
|
106
|
+
"path": "/v1/maker/preparations/{id}",
|
|
107
|
+
"method": "DELETE"
|
|
108
|
+
},
|
|
109
|
+
"submitMakerSignatures": {
|
|
110
|
+
"path": "/v1/maker/preparations/{id}/signatures",
|
|
111
|
+
"method": "POST"
|
|
112
|
+
},
|
|
113
|
+
"getOrders": {
|
|
114
|
+
"path": "/v1/orders",
|
|
115
|
+
"method": "GET"
|
|
116
|
+
},
|
|
117
|
+
"getOrder": {
|
|
118
|
+
"path": "/v1/orders/{id}",
|
|
119
|
+
"method": "GET"
|
|
120
|
+
},
|
|
121
|
+
"putQuotes": {
|
|
122
|
+
"path": "/v1/orders/{id}/quotes",
|
|
123
|
+
"method": "PUT"
|
|
124
|
+
},
|
|
125
|
+
"deleteQuotes": {
|
|
126
|
+
"path": "/v1/orders/{id}/quotes",
|
|
127
|
+
"method": "DELETE"
|
|
128
|
+
},
|
|
129
|
+
"prepareQuotes": {
|
|
130
|
+
"path": "/v1/orders/{id}/quotes/prepare",
|
|
131
|
+
"method": "POST"
|
|
132
|
+
},
|
|
133
|
+
"postV1QuotesBatch": {
|
|
134
|
+
"path": "/v1/quotes/batch",
|
|
135
|
+
"method": "POST"
|
|
136
|
+
},
|
|
137
|
+
"putV1QuoteIdentitiesIdLease": {
|
|
138
|
+
"path": "/v1/quote-identities/{id}/lease",
|
|
139
|
+
"method": "PUT"
|
|
140
|
+
},
|
|
141
|
+
"deleteV1QuoteIdentitiesIdQuotes": {
|
|
142
|
+
"path": "/v1/quote-identities/{id}/quotes",
|
|
143
|
+
"method": "DELETE"
|
|
144
|
+
},
|
|
145
|
+
"postCloses": {
|
|
146
|
+
"path": "/v1/closes",
|
|
147
|
+
"method": "POST"
|
|
148
|
+
},
|
|
149
|
+
"postCloseSignatures": {
|
|
150
|
+
"path": "/v1/closes/{id}/signatures",
|
|
151
|
+
"method": "POST"
|
|
152
|
+
},
|
|
153
|
+
"getEscrow": {
|
|
154
|
+
"path": "/v1/escrow",
|
|
155
|
+
"method": "GET"
|
|
156
|
+
},
|
|
157
|
+
"postEscrowClosePrepare": {
|
|
158
|
+
"path": "/v1/escrow/close/prepare",
|
|
159
|
+
"method": "POST"
|
|
160
|
+
},
|
|
161
|
+
"postEscrowClose": {
|
|
162
|
+
"path": "/v1/escrow/close",
|
|
163
|
+
"method": "POST"
|
|
164
|
+
},
|
|
165
|
+
"postTransfer": {
|
|
166
|
+
"path": "/v1/transfers",
|
|
167
|
+
"method": "POST"
|
|
168
|
+
},
|
|
169
|
+
"getTransfers": {
|
|
170
|
+
"path": "/v1/transfers",
|
|
171
|
+
"method": "GET"
|
|
172
|
+
},
|
|
173
|
+
"getTransfer": {
|
|
174
|
+
"path": "/v1/transfers/{id}",
|
|
175
|
+
"method": "GET"
|
|
176
|
+
},
|
|
177
|
+
"cancelTransfer": {
|
|
178
|
+
"path": "/v1/transfers/{id}",
|
|
179
|
+
"method": "DELETE"
|
|
180
|
+
},
|
|
181
|
+
"prepareTransferWithdrawal": {
|
|
182
|
+
"path": "/v1/transfers/{id}/withdrawal/prepare",
|
|
183
|
+
"method": "POST"
|
|
184
|
+
},
|
|
185
|
+
"submitTransferWithdrawal": {
|
|
186
|
+
"path": "/v1/transfers/{id}/withdrawal",
|
|
187
|
+
"method": "POST"
|
|
188
|
+
},
|
|
189
|
+
"postTake": {
|
|
190
|
+
"path": "/v1/takes",
|
|
191
|
+
"method": "POST"
|
|
192
|
+
},
|
|
193
|
+
"getV1TakesId": {
|
|
194
|
+
"path": "/v1/takes/{id}",
|
|
195
|
+
"method": "GET"
|
|
196
|
+
},
|
|
197
|
+
"deleteTake": {
|
|
198
|
+
"path": "/v1/takes/{id}",
|
|
199
|
+
"method": "DELETE"
|
|
200
|
+
},
|
|
201
|
+
"postTakeSignatures": {
|
|
202
|
+
"path": "/v1/takes/{id}/signatures",
|
|
203
|
+
"method": "POST"
|
|
204
|
+
},
|
|
205
|
+
"preparePaymentRefund": {
|
|
206
|
+
"path": "/v1/fills/{id}/refund/prepare",
|
|
207
|
+
"method": "POST"
|
|
208
|
+
},
|
|
209
|
+
"submitPaymentRefund": {
|
|
210
|
+
"path": "/v1/fills/{id}/refund",
|
|
211
|
+
"method": "POST"
|
|
212
|
+
},
|
|
213
|
+
"submitDelivery": {
|
|
214
|
+
"path": "/v1/fills/{id}/delivery",
|
|
215
|
+
"method": "POST"
|
|
216
|
+
},
|
|
217
|
+
"getFills": {
|
|
218
|
+
"path": "/v1/fills",
|
|
219
|
+
"method": "GET"
|
|
220
|
+
},
|
|
221
|
+
"postWithdrawalPrepare": {
|
|
222
|
+
"path": "/v1/withdrawals/prepare",
|
|
223
|
+
"method": "POST"
|
|
224
|
+
},
|
|
225
|
+
"postWithdrawal": {
|
|
226
|
+
"path": "/v1/withdrawals",
|
|
227
|
+
"method": "POST"
|
|
228
|
+
},
|
|
229
|
+
"postV1Credentials": {
|
|
230
|
+
"path": "/v1/credentials",
|
|
231
|
+
"method": "POST"
|
|
232
|
+
},
|
|
233
|
+
"deleteV1CredentialsId": {
|
|
234
|
+
"path": "/v1/credentials/{id}",
|
|
235
|
+
"method": "DELETE"
|
|
236
|
+
},
|
|
237
|
+
"postV1CredentialsIdRotate": {
|
|
238
|
+
"path": "/v1/credentials/{id}/rotate",
|
|
239
|
+
"method": "POST"
|
|
240
|
+
},
|
|
241
|
+
"getV1OrdersHistory": {
|
|
242
|
+
"path": "/v1/orders/history",
|
|
243
|
+
"method": "GET"
|
|
244
|
+
},
|
|
245
|
+
"getTakeHistory": {
|
|
246
|
+
"path": "/v1/takes/history",
|
|
247
|
+
"method": "GET"
|
|
248
|
+
},
|
|
249
|
+
"getV1FillsHistory": {
|
|
250
|
+
"path": "/v1/fills/history",
|
|
251
|
+
"method": "GET"
|
|
252
|
+
},
|
|
253
|
+
"getV1WithdrawalsHistory": {
|
|
254
|
+
"path": "/v1/withdrawals/history",
|
|
255
|
+
"method": "GET"
|
|
256
|
+
},
|
|
257
|
+
"getV1TradesHistory": {
|
|
258
|
+
"path": "/v1/trades/history",
|
|
259
|
+
"method": "GET"
|
|
260
|
+
},
|
|
261
|
+
"getV1Events": {
|
|
262
|
+
"path": "/v1/events",
|
|
263
|
+
"method": "GET"
|
|
264
|
+
},
|
|
265
|
+
"getV1Snapshot": {
|
|
266
|
+
"path": "/v1/snapshot",
|
|
267
|
+
"method": "GET"
|
|
268
|
+
},
|
|
269
|
+
"getV1MeEvents": {
|
|
270
|
+
"path": "/v1/me/events",
|
|
271
|
+
"method": "GET"
|
|
272
|
+
},
|
|
273
|
+
"getV1MeSnapshot": {
|
|
274
|
+
"path": "/v1/me/snapshot",
|
|
275
|
+
"method": "GET"
|
|
276
|
+
},
|
|
277
|
+
"getV1Stream": {
|
|
278
|
+
"path": "/v1/stream",
|
|
279
|
+
"method": "GET"
|
|
280
|
+
}
|
|
281
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { createClient, exchange, LaMargeError, type Answer, type CallArguments, type CallOptions, type OperationAnswer, type OperationInput, type Client, type ClientConfig, type Request, type Transport } from "./client.ts";
|
|
2
|
+
export { retryAfterMs } from "./retry-after.ts";
|
|
3
|
+
export { checkDeployment, signIn, signOut } from "./session.ts";
|
|
4
|
+
export { signItems, signTyped, typedDataRequest, type Hex, type SignedItem, type Signer, type TypedDataRequest } from "./signer.ts";
|
|
5
|
+
export type { components, operations, paths } from "./generated/api.ts";
|
|
6
|
+
export { linkAccount, permitDone, setUpVenue, submitPermit } from "./account.ts";
|
|
7
|
+
export { changes, type Change, type ChangeOptions, type EventPage, type Snapshot, type SocketConstructor } from "./events.ts";
|
|
8
|
+
export { fresh, FRESH_MS, prefunded, sweepable } from "./funds.ts";
|
|
9
|
+
export { memoryStore, type SignatureStore } from "./store.ts";
|
|
10
|
+
export { checkQuote, MAX_PARTS, minimumTake, QuoteUnavailable, sweep, type Sweep } from "./book.ts";
|
|
11
|
+
export { take, type Balance, type Fill, type TakeInput, type TakeOptions } from "./take.ts";
|
|
12
|
+
export { transfer, TransferPending, unsignedReservation, type Funding, type TransferIntent, type TransferOptions, type TransferRequest, type TransferStage } from "./transfer.ts";
|
|
13
|
+
export { errorText, Refused, VenueDeliverer, type DeliveryJournal, type FillSource, type Log } from "./delivery.ts";
|
|
14
|
+
export { Maker, Quoter, type Credential, type DeliveryFill, type FeedFill, type Order, type QuoteSigner } from "./maker.ts";
|
|
15
|
+
export { checkEscrowLocks, refundPayment, withdraw, type ChainReaders, type RefundFill, type Withdrawal, type WithdrawalRequest } from "./withdraw.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createClient, exchange, LaMargeError } from "./client.js";
|
|
2
|
+
export { retryAfterMs } from "./retry-after.js";
|
|
3
|
+
export { checkDeployment, signIn, signOut } from "./session.js";
|
|
4
|
+
export { signItems, signTyped, typedDataRequest } from "./signer.js";
|
|
5
|
+
export { linkAccount, permitDone, setUpVenue, submitPermit } from "./account.js";
|
|
6
|
+
export { changes } from "./events.js";
|
|
7
|
+
export { fresh, FRESH_MS, prefunded, sweepable } from "./funds.js";
|
|
8
|
+
export { memoryStore } from "./store.js";
|
|
9
|
+
export { checkQuote, MAX_PARTS, minimumTake, QuoteUnavailable, sweep } from "./book.js";
|
|
10
|
+
export { take } from "./take.js";
|
|
11
|
+
export { transfer, TransferPending, unsignedReservation } from "./transfer.js";
|
|
12
|
+
export { errorText, Refused, VenueDeliverer } from "./delivery.js";
|
|
13
|
+
export { Maker, Quoter } from "./maker.js";
|
|
14
|
+
export { checkEscrowLocks, refundPayment, withdraw } from "./withdraw.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { VenueDeliverer, type DeliveryJournal, type FillSource, type Log } from "./delivery.ts";
|
|
2
|
+
import type { DeliveryFill } from "./maker.ts";
|
|
3
|
+
import type { Signer } from "./signer.ts";
|
|
4
|
+
/** `max_fee`: the most Lighter may charge per transfer, in USDC base units; 0 unless set. */
|
|
5
|
+
export type LighterConfig = {
|
|
6
|
+
api: string;
|
|
7
|
+
chain_id: number;
|
|
8
|
+
account_index: number;
|
|
9
|
+
api_key_index: number;
|
|
10
|
+
max_fee?: number;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A delivery's signed transaction, persisted before its first send. `attempted` is set before
|
|
14
|
+
* any send, `sent` once Lighter acknowledged or knows it. `outcome` once settled: `reported` to
|
|
15
|
+
* LaMarge; `failed`, LaMarge will not take it; `refused` by Lighter on the first send; `dropped`,
|
|
16
|
+
* it never executes (not attempted before the deadline, failed on Lighter, or expired).
|
|
17
|
+
* Refused and dropped transfers leave their nonce to Lighter's count.
|
|
18
|
+
*/
|
|
19
|
+
export type Delivery = {
|
|
20
|
+
fill_id: string;
|
|
21
|
+
tx_hash: string;
|
|
22
|
+
tx_type: number;
|
|
23
|
+
tx_info: string;
|
|
24
|
+
nonce: number;
|
|
25
|
+
expires_at: number;
|
|
26
|
+
amount: string;
|
|
27
|
+
fee: number;
|
|
28
|
+
to: number;
|
|
29
|
+
attempted: boolean;
|
|
30
|
+
sent: boolean;
|
|
31
|
+
outcome?: "reported" | "failed" | "dropped" | "refused";
|
|
32
|
+
created_at: string;
|
|
33
|
+
};
|
|
34
|
+
export type DeliveryStore = DeliveryJournal<Delivery>;
|
|
35
|
+
/** A transfer as Lighter's signers take it (`signTransfer` of lighter-ts-sdk's wallet, or Lighter's Go signer). */
|
|
36
|
+
export type LighterTransferParams = {
|
|
37
|
+
toAccountIndex: number;
|
|
38
|
+
asset_id: number;
|
|
39
|
+
is_spot_account: boolean;
|
|
40
|
+
from_is_spot_account: boolean;
|
|
41
|
+
usdcAmount: number;
|
|
42
|
+
fee: number;
|
|
43
|
+
memo: string;
|
|
44
|
+
skipNonce: number;
|
|
45
|
+
nonce: number;
|
|
46
|
+
apiKeyIndex: number;
|
|
47
|
+
accountIndex: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* The pool account's API key, held by the application: Lighter signs with its own scheme, in Go
|
|
51
|
+
* or WebAssembly, so the SDK never loads a signer nor sees the key.
|
|
52
|
+
*/
|
|
53
|
+
export interface LighterSigner {
|
|
54
|
+
/** A token authenticating the account's reads, as `createAuthTokenWithExpiry` returns it. */
|
|
55
|
+
authToken(): Promise<string>;
|
|
56
|
+
signTransfer(params: LighterTransferParams): Promise<{
|
|
57
|
+
txType: number;
|
|
58
|
+
txInfo: string;
|
|
59
|
+
txHash: string;
|
|
60
|
+
messageToSign?: string | undefined;
|
|
61
|
+
error?: string | undefined;
|
|
62
|
+
}>;
|
|
63
|
+
}
|
|
64
|
+
export declare class LighterDeliverer extends VenueDeliverer<Delivery> {
|
|
65
|
+
readonly config: LighterConfig;
|
|
66
|
+
private signer;
|
|
67
|
+
private owner;
|
|
68
|
+
/** When each pending transfer's status may be read again: Lighter allows 60 reads a minute. */
|
|
69
|
+
private readAt;
|
|
70
|
+
/** Transfers seen executed: their status is not read again. */
|
|
71
|
+
private executed;
|
|
72
|
+
/** Times of this bot's requests to Lighter within the last minute. */
|
|
73
|
+
private requests;
|
|
74
|
+
/** `signer` holds the pool account's API key; `owner`, its owner's key, signs each transfer to another owner's account. */
|
|
75
|
+
constructor(config: LighterConfig, signer: LighterSigner, owner: Pick<Signer, "signMessage">, store: DeliveryStore, now?: () => number);
|
|
76
|
+
/** One request to Lighter, within the minute's budget. */
|
|
77
|
+
private spend;
|
|
78
|
+
private get;
|
|
79
|
+
/** Lighter's status of a transaction; null only when Lighter answers that it does not know it. Any other answer throws: the delivery stays pending. */
|
|
80
|
+
status(txHash: string): Promise<number | null>;
|
|
81
|
+
/** Signs the fill's transfer at `nonce`: API key signature, then the owner's wallet signature of the message Lighter requires for another owner's account. */
|
|
82
|
+
sign(fill: DeliveryFill, nonce: number): Promise<Delivery>;
|
|
83
|
+
/** Sends persisted bytes; Lighter accepts a nonce once, so a resend after a crash cannot pay twice. */
|
|
84
|
+
send(delivery: Delivery): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Signs at most one new fill, and only while no transfer is of unknown outcome: one unresolved
|
|
87
|
+
* nonce at a time, never reused for another fill.
|
|
88
|
+
*/
|
|
89
|
+
protected deliver(quoter: FillSource, due: DeliveryFill[], deliveries: Record<string, Delivery>, log: Log): Promise<boolean>;
|
|
90
|
+
/** Moves one persisted delivery forward; false while its outcome on Lighter is unknown. */
|
|
91
|
+
protected settle(quoter: FillSource, delivery: Delivery, fill: DeliveryFill | undefined, deliveries: Record<string, Delivery>, log: Log): Promise<boolean>;
|
|
92
|
+
}
|
package/dist/lighter.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Delivery inside Lighter: one internal transfer per fill, signed with the account's API key
|
|
2
|
+
// and its owner's wallet key, persisted durably before it is sent and never signed twice.
|
|
3
|
+
import { errorText, Refused, VenueDeliverer } from "./delivery.js";
|
|
4
|
+
import { fillMemo, USDC_ASSET } from "./protocol/lighter.js";
|
|
5
|
+
import { insist } from "./protocol/protocol.js";
|
|
6
|
+
import { record } from "./protocol/validation.js";
|
|
7
|
+
// Lighter transaction status: 0 failed, 1 pending, 2 executed, 3 final (minutes after 2);
|
|
8
|
+
// above 3, failed.
|
|
9
|
+
const TRANSFER = 12, FAILED = 0, EXECUTED = 2, FINAL = 3;
|
|
10
|
+
/** No transfer is first sent closer than this to its deadline. This bot's signer cannot make the
|
|
11
|
+
* transaction expire at `deliver_by`, and an execution after the deadline is the maker's loss. */
|
|
12
|
+
const SEND_MARGIN_MS = 60_000;
|
|
13
|
+
/** A Standard Lighter account allows 60 requests a minute, reads and sends alike; this bot makes at most 50. */
|
|
14
|
+
const REQUESTS_PER_MINUTE = 50;
|
|
15
|
+
export class LighterDeliverer extends VenueDeliverer {
|
|
16
|
+
config;
|
|
17
|
+
signer;
|
|
18
|
+
owner;
|
|
19
|
+
/** When each pending transfer's status may be read again: Lighter allows 60 reads a minute. */
|
|
20
|
+
readAt = new Map();
|
|
21
|
+
/** Transfers seen executed: their status is not read again. */
|
|
22
|
+
executed = new Set();
|
|
23
|
+
/** Times of this bot's requests to Lighter within the last minute. */
|
|
24
|
+
requests = [];
|
|
25
|
+
/** `signer` holds the pool account's API key; `owner`, its owner's key, signs each transfer to another owner's account. */
|
|
26
|
+
constructor(config, signer, owner, store, now = () => Date.now()) {
|
|
27
|
+
super("lighter", SEND_MARGIN_MS, store, now);
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.signer = signer;
|
|
30
|
+
this.owner = owner;
|
|
31
|
+
}
|
|
32
|
+
/** One request to Lighter, within the minute's budget. */
|
|
33
|
+
spend() {
|
|
34
|
+
const now = this.now();
|
|
35
|
+
this.requests = this.requests.filter(at => now - at < 60_000);
|
|
36
|
+
if (this.requests.length >= REQUESTS_PER_MINUTE)
|
|
37
|
+
throw new Error("Lighter request budget spent for this minute");
|
|
38
|
+
this.requests.push(now);
|
|
39
|
+
}
|
|
40
|
+
async get(path, authorization) {
|
|
41
|
+
this.spend();
|
|
42
|
+
const response = await fetch(`${this.config.api}/api/v1/${path}`, { headers: { accept: "application/json", ...(authorization ? { authorization } : {}) }, signal: AbortSignal.timeout(10_000) });
|
|
43
|
+
const answer = await response.json();
|
|
44
|
+
const body = record(answer);
|
|
45
|
+
if (!response.ok && body.code !== 21500)
|
|
46
|
+
throw new Error(`Lighter ${path}: HTTP ${response.status} ${typeof body.message === "string" ? body.message : ""}`);
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
/** Lighter's status of a transaction; null only when Lighter answers that it does not know it. Any other answer throws: the delivery stays pending. */
|
|
50
|
+
async status(txHash) {
|
|
51
|
+
const tx = await this.get(`tx?by=hash&value=${encodeURIComponent(txHash)}`);
|
|
52
|
+
if (tx.code === 21500)
|
|
53
|
+
return null;
|
|
54
|
+
if (tx.code !== 200 || typeof tx.status !== "number" || !Number.isInteger(tx.status))
|
|
55
|
+
throw new Error(`Lighter answered no status for ${txHash}`);
|
|
56
|
+
return tx.status;
|
|
57
|
+
}
|
|
58
|
+
/** Signs the fill's transfer at `nonce`: API key signature, then the owner's wallet signature of the message Lighter requires for another owner's account. */
|
|
59
|
+
async sign(fill, nonce) {
|
|
60
|
+
// The taker's account index, in decimal.
|
|
61
|
+
const to = Number(fill.delivery_to);
|
|
62
|
+
insist(fill.to_venue === "lighter" && /^\d+$/.test(fill.delivery_to ?? "") && Number.isSafeInteger(to), "Not a Lighter delivery");
|
|
63
|
+
// The sending account's fee, in base units, charged on top of the amount; Lighter refuses
|
|
64
|
+
// a transfer whose fee differs. Read with the account's token, as Lighter documents it.
|
|
65
|
+
const token = await this.signer.authToken();
|
|
66
|
+
const fee = await this.get(`transferFeeInfo?account_index=${this.config.account_index}&to_account_index=${to}&auth=${encodeURIComponent(token)}`, token);
|
|
67
|
+
const usdcFee = Number(fee.transfer_fee_usdc);
|
|
68
|
+
insist(fee.code === 200 && Number.isSafeInteger(usdcFee) && usdcFee >= 0, "Unexpected Lighter transfer fee");
|
|
69
|
+
insist(usdcFee <= (this.config.max_fee ?? 0), `Lighter charges this account ${usdcFee} per transfer, above LIGHTER_MAX_FEE`);
|
|
70
|
+
const signed = await this.signer.signTransfer({ toAccountIndex: to, asset_id: USDC_ASSET, is_spot_account: false, from_is_spot_account: false, usdcAmount: Number(fill.amount_out), fee: usdcFee, memo: `0x${fillMemo(fill.id)}`, skipNonce: 0, nonce, apiKeyIndex: this.config.api_key_index, accountIndex: this.config.account_index });
|
|
71
|
+
if (signed.error)
|
|
72
|
+
throw new Error(`Lighter signer: ${signed.error}`);
|
|
73
|
+
insist(signed.txType === TRANSFER && signed.messageToSign, "Unexpected Lighter transaction");
|
|
74
|
+
const parsed = JSON.parse(signed.txInfo);
|
|
75
|
+
const info = record(parsed);
|
|
76
|
+
insist(info.ToAccountIndex === to && info.FromAccountIndex === this.config.account_index && info.Amount === Number(fill.amount_out) && info.USDCFee === usdcFee && info.AssetIndex === USDC_ASSET && info.ToRouteType === 0 && info.Nonce === nonce && typeof info.ExpiredAt === "number", "The signed transfer differs from the fill");
|
|
77
|
+
info.L1Sig = await this.owner.signMessage({ message: signed.messageToSign });
|
|
78
|
+
return { fill_id: fill.id, tx_hash: signed.txHash, tx_type: signed.txType, tx_info: JSON.stringify(info), nonce, expires_at: Number(info.ExpiredAt), amount: fill.amount_out, fee: usdcFee, to, attempted: false, sent: false, created_at: new Date(this.now()).toISOString() };
|
|
79
|
+
}
|
|
80
|
+
/** Sends persisted bytes; Lighter accepts a nonce once, so a resend after a crash cannot pay twice. */
|
|
81
|
+
async send(delivery) {
|
|
82
|
+
const params = new URLSearchParams({ tx_type: String(delivery.tx_type), tx_info: delivery.tx_info, account_index: String(this.config.account_index), api_key_index: String(this.config.api_key_index), price_protection: "false" });
|
|
83
|
+
this.spend();
|
|
84
|
+
const response = await fetch(`${this.config.api}/api/v1/sendTx`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, body: params, signal: AbortSignal.timeout(15_000) });
|
|
85
|
+
const answer = await response.json().catch(() => ({}));
|
|
86
|
+
const body = record(answer);
|
|
87
|
+
if (body.code !== 200) {
|
|
88
|
+
// Already known by hash: an earlier send landed.
|
|
89
|
+
if ((await this.status(delivery.tx_hash)) !== null)
|
|
90
|
+
return;
|
|
91
|
+
const reason = `Lighter refused the transfer: ${typeof body.message === "string" ? body.message : `HTTP ${response.status}`}`;
|
|
92
|
+
// An answered refusal is final; an overloaded or failing API may still take the bytes.
|
|
93
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 429 && typeof body.code === "number")
|
|
94
|
+
throw new Refused(reason);
|
|
95
|
+
throw new Error(reason);
|
|
96
|
+
}
|
|
97
|
+
insist(!body.tx_hash || body.tx_hash === delivery.tx_hash, "Lighter answered another transaction hash");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Signs at most one new fill, and only while no transfer is of unknown outcome: one unresolved
|
|
101
|
+
* nonce at a time, never reused for another fill.
|
|
102
|
+
*/
|
|
103
|
+
async deliver(quoter, due, deliveries, log) {
|
|
104
|
+
const next = due[0];
|
|
105
|
+
if (!next)
|
|
106
|
+
return true;
|
|
107
|
+
const { nonce: remote } = await this.get(`nextNonce?account_index=${this.config.account_index}&api_key_index=${this.config.api_key_index}`);
|
|
108
|
+
insist(typeof remote === "number" && Number.isSafeInteger(remote), "Lighter answered no nonce");
|
|
109
|
+
// Lighter's next nonce, or one past the last transfer it may know, whichever is higher.
|
|
110
|
+
const known = Object.values(deliveries).filter(delivery => (delivery.attempted || delivery.sent) && delivery.outcome !== "dropped" && delivery.outcome !== "refused").map(delivery => delivery.nonce + 1);
|
|
111
|
+
const delivery = await this.sign(next, Math.max(remote, ...known));
|
|
112
|
+
deliveries[next.id] = delivery;
|
|
113
|
+
await this.store.save(deliveries);
|
|
114
|
+
try {
|
|
115
|
+
return await this.settle(quoter, delivery, next, deliveries, log);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
log(`${next.id}: ${errorText(error)}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Moves one persisted delivery forward; false while its outcome on Lighter is unknown. */
|
|
123
|
+
async settle(quoter, delivery, fill, deliveries, log) {
|
|
124
|
+
if (!this.awaits(fill) || fill.delivery_hash === delivery.tx_hash) {
|
|
125
|
+
// The fill is final, or settled otherwise: nothing left to report for this transfer.
|
|
126
|
+
delivery.outcome = fill?.delivery_hash === delivery.tx_hash ? "reported" : "failed";
|
|
127
|
+
await this.store.save(deliveries);
|
|
128
|
+
log(`${delivery.fill_id}: fill no longer awaits this delivery`);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const id = delivery.fill_id;
|
|
132
|
+
if (!this.reportDue(id))
|
|
133
|
+
return true;
|
|
134
|
+
// Back off between reads and sends whatever the outcome, failures included.
|
|
135
|
+
if (!this.executed.has(id) && !this.turn(this.readAt, id))
|
|
136
|
+
return false;
|
|
137
|
+
let status = this.executed.has(id) ? EXECUTED : await this.status(delivery.tx_hash);
|
|
138
|
+
if (status !== null && !delivery.sent) {
|
|
139
|
+
// Lighter knows it, whatever became of the acknowledgement: its nonce is spent.
|
|
140
|
+
delivery.sent = true;
|
|
141
|
+
await this.store.save(deliveries);
|
|
142
|
+
}
|
|
143
|
+
if (status === null && !delivery.sent) {
|
|
144
|
+
const deadline = fill.deliver_by ? Date.parse(fill.deliver_by) : 0;
|
|
145
|
+
if (deadline - this.now() > SEND_MARGIN_MS) {
|
|
146
|
+
// Recorded before the request: a lost answer never lets these bytes be dropped as unsent.
|
|
147
|
+
const first = !delivery.attempted;
|
|
148
|
+
delivery.attempted = true;
|
|
149
|
+
await this.store.save(deliveries);
|
|
150
|
+
try {
|
|
151
|
+
await this.send(delivery);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
// Only a first send's refusal is final: after a lost answer, the earlier send may
|
|
155
|
+
// still execute, and the resend's refusal says nothing of it.
|
|
156
|
+
if (!(error instanceof Refused) || !first)
|
|
157
|
+
throw error;
|
|
158
|
+
// Never executed: the fill is left to its refund rather than signed again.
|
|
159
|
+
delivery.outcome = "refused";
|
|
160
|
+
await this.store.save(deliveries);
|
|
161
|
+
log(`${delivery.fill_id}: ${error.message}`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
delivery.sent = true;
|
|
165
|
+
await this.store.save(deliveries);
|
|
166
|
+
log(`${delivery.fill_id}: transfer ${delivery.tx_hash} sent`);
|
|
167
|
+
status = await this.status(delivery.tx_hash);
|
|
168
|
+
}
|
|
169
|
+
else if (!delivery.attempted) {
|
|
170
|
+
// Never sent in time: the bytes stay unsent and their nonce is free again.
|
|
171
|
+
delivery.outcome = "dropped";
|
|
172
|
+
await this.store.save(deliveries);
|
|
173
|
+
log(`${delivery.fill_id}: too close to the delivery deadline, transfer not sent`);
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (status !== null && status >= EXECUTED && status <= FINAL) {
|
|
178
|
+
this.executed.add(id);
|
|
179
|
+
this.readAt.delete(id);
|
|
180
|
+
}
|
|
181
|
+
if (status === FAILED || status !== null && status > FINAL) {
|
|
182
|
+
// Never executes; Lighter knows it, so its next nonce accounts for it.
|
|
183
|
+
delivery.outcome = "dropped";
|
|
184
|
+
await this.store.save(deliveries);
|
|
185
|
+
log(`${delivery.fill_id}: transfer ${delivery.tx_hash} failed (status ${status})`);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (status === null || status < EXECUTED) {
|
|
189
|
+
// Unknown to Lighter, or not executed, past its own expiry: it can no longer execute.
|
|
190
|
+
if (this.now() > delivery.expires_at + 60_000) {
|
|
191
|
+
delivery.outcome = "dropped";
|
|
192
|
+
await this.store.save(deliveries);
|
|
193
|
+
log(`${delivery.fill_id}: transfer expired without executing (status ${status})`);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
// Executed on Lighter, its nonce spent: reported until the server takes it or never will.
|
|
199
|
+
const outcome = await this.report(quoter, id, delivery.tx_hash, log);
|
|
200
|
+
if (outcome) {
|
|
201
|
+
delivery.outcome = outcome;
|
|
202
|
+
await this.store.save(deliveries);
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|