@derivexyz/derive-ts 3.0.4
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/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/codecs/package.json +7 -0
- package/dist/chunk-WR2IIVUC.js +289 -0
- package/dist/chunk-WR2IIVUC.js.map +1 -0
- package/dist/codecs/index.cjs +319 -0
- package/dist/codecs/index.cjs.map +1 -0
- package/dist/codecs/index.d.cts +264 -0
- package/dist/codecs/index.d.ts +264 -0
- package/dist/codecs/index.js +37 -0
- package/dist/codecs/index.js.map +1 -0
- package/dist/endpointMap-BBTkarIm.d.cts +45 -0
- package/dist/endpointMap-BOOWiqXf.d.ts +45 -0
- package/dist/generated-DwMaydIF.d.cts +5930 -0
- package/dist/generated-DwMaydIF.d.ts +5930 -0
- package/dist/index.cjs +2729 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1463 -0
- package/dist/index.d.ts +1463 -0
- package/dist/index.js +2405 -0
- package/dist/index.js.map +1 -0
- package/dist/scopes-RzGb3xH6.d.cts +108 -0
- package/dist/scopes-RzGb3xH6.d.ts +108 -0
- package/dist/types/endpoints.cjs +19 -0
- package/dist/types/endpoints.cjs.map +1 -0
- package/dist/types/endpoints.d.cts +2 -0
- package/dist/types/endpoints.d.ts +2 -0
- package/dist/types/endpoints.js +1 -0
- package/dist/types/endpoints.js.map +1 -0
- package/dist/types/index.cjs +19 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +7 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2405 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_SIGNATURE_EXPIRY_SEC,
|
|
3
|
+
encodeCreateSessionKeyActionData,
|
|
4
|
+
encodeExternalTransfer,
|
|
5
|
+
encodeRfqExecute,
|
|
6
|
+
encodeRfqQuote,
|
|
7
|
+
encodeTradeData,
|
|
8
|
+
encodeTransfer,
|
|
9
|
+
encodeUpdateWhitelistedRecipients,
|
|
10
|
+
encodeVaultBurnShares,
|
|
11
|
+
encodeVaultCancel,
|
|
12
|
+
encodeVaultCreate,
|
|
13
|
+
encodeVaultDeposit,
|
|
14
|
+
encodeVaultMintShares,
|
|
15
|
+
encodeVaultWithdraw,
|
|
16
|
+
encodeWithdrawal,
|
|
17
|
+
expiresIn,
|
|
18
|
+
randomNonce,
|
|
19
|
+
sortRfqLegs,
|
|
20
|
+
toE18,
|
|
21
|
+
toScaled
|
|
22
|
+
} from "./chunk-WR2IIVUC.js";
|
|
23
|
+
|
|
24
|
+
// src/version.ts
|
|
25
|
+
var SDK_VERSION = "3.0.0";
|
|
26
|
+
|
|
27
|
+
// src/client.ts
|
|
28
|
+
import { Wallet } from "ethers";
|
|
29
|
+
|
|
30
|
+
// src/api/deposits.ts
|
|
31
|
+
import { Contract, getAddress } from "ethers";
|
|
32
|
+
|
|
33
|
+
// src/abis/actionManager.ts
|
|
34
|
+
var ACTION_MANAGER_ABI = [
|
|
35
|
+
"function deposit(address asset, uint256 amount, uint64 subaccountId, address fallbackRecipient) returns (uint256 actionId)",
|
|
36
|
+
"function depositToNewSubaccount(address asset, uint256 amount, uint32 managerId, address owner) returns (uint256 actionId)"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// src/abis/erc20.ts
|
|
40
|
+
var ERC20_ABI = [
|
|
41
|
+
"function approve(address spender, uint256 amount) returns (bool)",
|
|
42
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
43
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
44
|
+
"function decimals() view returns (uint8)"
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// src/errors.ts
|
|
48
|
+
var DeriveRpcError = class extends Error {
|
|
49
|
+
code;
|
|
50
|
+
data;
|
|
51
|
+
method;
|
|
52
|
+
constructor(method, error) {
|
|
53
|
+
super(
|
|
54
|
+
`${method}: [${error.code}] ${error.message}${error.data !== void 0 ? ` \u2014 ${JSON.stringify(error.data)}` : ""}`
|
|
55
|
+
);
|
|
56
|
+
this.name = "DeriveRpcError";
|
|
57
|
+
this.method = method;
|
|
58
|
+
this.code = error.code;
|
|
59
|
+
this.data = error.data;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var DeriveTimeoutError = class extends Error {
|
|
63
|
+
constructor(message) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.name = "DeriveTimeoutError";
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
var DeriveConnectionError = class extends Error {
|
|
69
|
+
constructor(message, options) {
|
|
70
|
+
super(message, options);
|
|
71
|
+
this.name = "DeriveConnectionError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/api/deposits.ts
|
|
76
|
+
var DepositsApi = class {
|
|
77
|
+
constructor(ctx) {
|
|
78
|
+
this.ctx = ctx;
|
|
79
|
+
this.contractCall = new ContractCallDeposits(ctx);
|
|
80
|
+
this.depositAddress = new DepositAddressDeposits(ctx);
|
|
81
|
+
}
|
|
82
|
+
ctx;
|
|
83
|
+
contractCall;
|
|
84
|
+
depositAddress;
|
|
85
|
+
/**
|
|
86
|
+
* Deposit history rows (up to 1000), regardless of which flow funded
|
|
87
|
+
* them. Scoped to one subaccount when `subaccountId` is given,
|
|
88
|
+
* otherwise to the whole owner wallet. Timestamps are unix milliseconds.
|
|
89
|
+
*/
|
|
90
|
+
async getHistory(options = {}) {
|
|
91
|
+
return this.ctx.send("private/get_deposit_history", {
|
|
92
|
+
subaccount_id: options.subaccountId,
|
|
93
|
+
wallet: options.subaccountId === void 0 ? this.ctx.credentials().ownerAddress : void 0,
|
|
94
|
+
start_timestamp: options.startTimestampMs,
|
|
95
|
+
end_timestamp: options.endTimestampMs
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Polls `private/get_subaccounts` until a subaccount id outside
|
|
100
|
+
* `knownSubaccountIds` appears — the signal that a deposit creating a
|
|
101
|
+
* new subaccount (either flow) was credited — and returns the new id.
|
|
102
|
+
* Snapshot the ids BEFORE initiating the deposit.
|
|
103
|
+
*/
|
|
104
|
+
async awaitCredited(params) {
|
|
105
|
+
const { knownSubaccountIds, timeoutMs = 12e4, pollIntervalMs = 2e3 } = params;
|
|
106
|
+
const known = new Set(knownSubaccountIds);
|
|
107
|
+
const wallet = this.ctx.credentials().ownerAddress;
|
|
108
|
+
const deadline = Date.now() + timeoutMs;
|
|
109
|
+
for (; ; ) {
|
|
110
|
+
const { subaccount_ids } = await this.ctx.send("private/get_subaccounts", { wallet });
|
|
111
|
+
const credited = subaccount_ids.find((id) => !known.has(id));
|
|
112
|
+
if (credited !== void 0) return credited;
|
|
113
|
+
if (Date.now() >= deadline) {
|
|
114
|
+
throw new DeriveTimeoutError(`deposit not credited within ${timeoutMs}ms \u2014 no new subaccount for ${wallet}`);
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var ContractCallDeposits = class {
|
|
121
|
+
constructor(ctx) {
|
|
122
|
+
this.ctx = ctx;
|
|
123
|
+
}
|
|
124
|
+
ctx;
|
|
125
|
+
/**
|
|
126
|
+
* Approves (if needed) and deposits into an existing subaccount.
|
|
127
|
+
* `fallbackRecipient` (default: the signer) receives the funds into
|
|
128
|
+
* its fallback subaccount if the deposit cannot be applied.
|
|
129
|
+
* Resolves once the transaction is mined — crediting happens
|
|
130
|
+
* asynchronously once the deposit is observed on-chain.
|
|
131
|
+
*/
|
|
132
|
+
async deposit(params) {
|
|
133
|
+
const { actionManager, amountRaw } = await this.approveForDeposit(params);
|
|
134
|
+
const fallback = getAddress(params.fallbackRecipient ?? await params.signer.getAddress());
|
|
135
|
+
const tx = await actionManager.getFunction("deposit")(
|
|
136
|
+
getAddress(params.asset),
|
|
137
|
+
amountRaw,
|
|
138
|
+
params.subaccountId,
|
|
139
|
+
fallback
|
|
140
|
+
);
|
|
141
|
+
await tx.wait();
|
|
142
|
+
return { txHash: tx.hash };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Approves (if needed) and deposits into a NEW subaccount under
|
|
146
|
+
* `managerId`, owned by `owner` (default: the signer). The exchange
|
|
147
|
+
* assigns the subaccount id asynchronously — discover it with
|
|
148
|
+
* `deposits.awaitCredited` against a pre-deposit snapshot of the
|
|
149
|
+
* owner's ids.
|
|
150
|
+
*/
|
|
151
|
+
async depositToNewSubaccount(params) {
|
|
152
|
+
const { actionManager, amountRaw } = await this.approveForDeposit(params);
|
|
153
|
+
const owner = getAddress(params.owner ?? await params.signer.getAddress());
|
|
154
|
+
const tx = await actionManager.getFunction("depositToNewSubaccount")(
|
|
155
|
+
getAddress(params.asset),
|
|
156
|
+
amountRaw,
|
|
157
|
+
params.managerId,
|
|
158
|
+
owner
|
|
159
|
+
);
|
|
160
|
+
await tx.wait();
|
|
161
|
+
return { txHash: tx.hash };
|
|
162
|
+
}
|
|
163
|
+
/** Scales the amount, then ensures the ActionManager may pull it from the ERC-20. */
|
|
164
|
+
async approveForDeposit(params) {
|
|
165
|
+
const managerAddress = this.ctx.network.contracts.actionManager;
|
|
166
|
+
if (!managerAddress) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`network '${this.ctx.network.name}' has no actionManager contract configured \u2014 contract-call deposits need a NetworkConfig with contracts.actionManager set`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const erc20Address = params.erc20 ?? this.ctx.network.contracts.usdc;
|
|
172
|
+
if (!erc20Address) {
|
|
173
|
+
throw new Error(`network '${this.ctx.network.name}' has no usdc contract configured \u2014 pass erc20 explicitly`);
|
|
174
|
+
}
|
|
175
|
+
const token = new Contract(getAddress(erc20Address), ERC20_ABI, params.signer);
|
|
176
|
+
const amountRaw = typeof params.amount === "bigint" ? params.amount : toScaled(params.amount, Number(await token.getFunction("decimals")()));
|
|
177
|
+
if (amountRaw <= 0n) throw new Error("deposit amount must be positive");
|
|
178
|
+
const holder = await params.signer.getAddress();
|
|
179
|
+
const allowance = await token.getFunction("allowance")(holder, managerAddress);
|
|
180
|
+
if (allowance < amountRaw) {
|
|
181
|
+
this.ctx.logger("debug", `approving ${amountRaw} of ${erc20Address} to ActionManager`);
|
|
182
|
+
const approval = await token.getFunction("approve")(managerAddress, amountRaw);
|
|
183
|
+
await approval.wait();
|
|
184
|
+
}
|
|
185
|
+
return { actionManager: new Contract(managerAddress, ACTION_MANAGER_ABI, params.signer), amountRaw };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var DepositAddressDeposits = class {
|
|
189
|
+
constructor(ctx) {
|
|
190
|
+
this.ctx = ctx;
|
|
191
|
+
}
|
|
192
|
+
ctx;
|
|
193
|
+
/**
|
|
194
|
+
* Registers (or re-fetches — the address is deterministic per wallet/
|
|
195
|
+
* subaccount/manager) the deposit address the exchange watches and
|
|
196
|
+
* sweeps, routing funds to an existing subaccount or, with
|
|
197
|
+
* `managerId`, a new one. `wallet` defaults to the client's owner.
|
|
198
|
+
*/
|
|
199
|
+
async register(options = {}) {
|
|
200
|
+
if (!options.subaccountId && !options.managerId) {
|
|
201
|
+
throw new Error("provide subaccountId (existing) or managerId (to route deposits into a new subaccount)");
|
|
202
|
+
}
|
|
203
|
+
return this.ctx.send("public/register_deposit_address", {
|
|
204
|
+
wallet: getAddress(options.wallet ?? this.ctx.credentials().ownerAddress),
|
|
205
|
+
subaccount_id: options.subaccountId,
|
|
206
|
+
manager_id: options.managerId
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/api/marketData.ts
|
|
212
|
+
var toWireCollateral = (c) => ({ asset_name: c.assetName, amount: c.amount });
|
|
213
|
+
var toWirePosition = (p) => ({
|
|
214
|
+
instrument_name: p.instrumentName,
|
|
215
|
+
amount: p.amount,
|
|
216
|
+
entry_price: p.entryPrice ?? null
|
|
217
|
+
});
|
|
218
|
+
var MarketDataApi = class {
|
|
219
|
+
constructor(ctx) {
|
|
220
|
+
this.ctx = ctx;
|
|
221
|
+
}
|
|
222
|
+
ctx;
|
|
223
|
+
getInstrument(instrumentName) {
|
|
224
|
+
return this.ctx.send("public/get_instrument", { instrument_name: instrumentName });
|
|
225
|
+
}
|
|
226
|
+
/** One page of instruments; the response carries pagination info. */
|
|
227
|
+
getInstruments(query) {
|
|
228
|
+
return this.ctx.send("public/get_all_instruments", {
|
|
229
|
+
instrument_type: query.instrumentType,
|
|
230
|
+
expired: query.expired ?? false,
|
|
231
|
+
currency: query.currency ?? null,
|
|
232
|
+
page: query.page ?? null,
|
|
233
|
+
page_size: query.pageSize ?? null
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** Every currently tradeable instrument name across all currencies. */
|
|
237
|
+
getAllLiveInstruments() {
|
|
238
|
+
return this.ctx.send("public/get_all_live_instruments", null);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Latest ticker snapshot: top of book (`b`/`a` prices, `B`/`A`
|
|
242
|
+
* amounts), mark (`M`) and index (`I`) prices, and match bounds.
|
|
243
|
+
*/
|
|
244
|
+
getTicker(instrumentName) {
|
|
245
|
+
return this.ctx.send("public/get_ticker", { instrument_name: instrumentName });
|
|
246
|
+
}
|
|
247
|
+
/** Ticker snapshots for every instrument of a type, keyed by instrument name. */
|
|
248
|
+
getTickers(params) {
|
|
249
|
+
return this.ctx.send("public/get_tickers", {
|
|
250
|
+
instrument_type: params.instrumentType,
|
|
251
|
+
currency: params.currency ?? null,
|
|
252
|
+
expiry_date: params.expiryDate
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
getAllCurrencies() {
|
|
256
|
+
return this.ctx.send("public/get_all_currencies", null);
|
|
257
|
+
}
|
|
258
|
+
getCurrency(currency) {
|
|
259
|
+
return this.ctx.send("public/get_currency", { currency });
|
|
260
|
+
}
|
|
261
|
+
/** Latest signed oracle feeds (spot/perp/option/forward), optionally filtered by currency/expiry. */
|
|
262
|
+
getLatestSignedFeeds(params = {}) {
|
|
263
|
+
return this.ctx.send("public/get_latest_signed_feeds", {
|
|
264
|
+
currency: params.currency ?? null,
|
|
265
|
+
expiry: params.expiry ?? null
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** Settlement prices per expiry for the currency's options. */
|
|
269
|
+
getOptionSettlementPrices(currency) {
|
|
270
|
+
return this.ctx.send("public/get_option_settlement_prices", { currency });
|
|
271
|
+
}
|
|
272
|
+
/** On-chain settlement status and hash for a single op by its uuid. */
|
|
273
|
+
getTransaction(opUuid) {
|
|
274
|
+
return this.ctx.send("public/get_transaction", { op_uuid: opUuid });
|
|
275
|
+
}
|
|
276
|
+
/** Referral fee-share and reward breakdown over a millisecond window. */
|
|
277
|
+
getReferralPerformance(params) {
|
|
278
|
+
return this.ctx.send("public/get_referral_performance", {
|
|
279
|
+
start_ms: params.startMs,
|
|
280
|
+
end_ms: params.endMs,
|
|
281
|
+
referral_code: params.referralCode ?? null,
|
|
282
|
+
wallet: params.wallet ?? null
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** Spot index OHLC candles for a currency over a UTC-seconds window; `period` is the bucket size in seconds. */
|
|
286
|
+
getIndexChartData(params) {
|
|
287
|
+
return this.ctx.send("public/get_index_chart_data", {
|
|
288
|
+
currency: params.currency,
|
|
289
|
+
start_timestamp: params.startTimestamp,
|
|
290
|
+
end_timestamp: params.endTimestamp,
|
|
291
|
+
period: params.period
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** Interest-rate candles for a currency's lending pools, optionally scoped to one risk universe. */
|
|
295
|
+
getInterestRateHistory(params) {
|
|
296
|
+
return this.ctx.send("public/get_interest_rate_history", {
|
|
297
|
+
currency: params.currency,
|
|
298
|
+
start_timestamp: params.startTimestamp ?? null,
|
|
299
|
+
end_timestamp: params.endTimestamp ?? null,
|
|
300
|
+
period: params.period ?? null,
|
|
301
|
+
risk_universe_id: params.riskUniverseId ?? null
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// The methods below pre-date the generated EndpointMap, so their method string and params
|
|
305
|
+
// are cast through `never`; results are untyped (`unknown`) unless a generated type exists.
|
|
306
|
+
/** 24h and lifetime volume/fee/trade statistics for an instrument name or type (`ALL`/`OPTION`/`PERP`/`SPOT`). */
|
|
307
|
+
getStatistics(params) {
|
|
308
|
+
return this.ctx.send(
|
|
309
|
+
"public/statistics",
|
|
310
|
+
{
|
|
311
|
+
instrument_name: params.instrumentName,
|
|
312
|
+
currency: params.currency ?? null,
|
|
313
|
+
end_time: params.endTime ?? null
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
/** All maker programs, including past/historical epochs. */
|
|
318
|
+
getMakerPrograms() {
|
|
319
|
+
return this.ctx.send("public/get_maker_programs", {});
|
|
320
|
+
}
|
|
321
|
+
/** Per-maker score breakdown for one program epoch. */
|
|
322
|
+
getMakerProgramScores(params) {
|
|
323
|
+
return this.ctx.send(
|
|
324
|
+
"public/get_maker_program_scores",
|
|
325
|
+
{
|
|
326
|
+
program_name: params.programName,
|
|
327
|
+
epoch_start_timestamp: params.epochStartTimestamp
|
|
328
|
+
}
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
/** Paginated liquidation-auction history across all subaccounts (or one, if `subaccountId` is given). */
|
|
332
|
+
getLiquidationHistory(params = {}) {
|
|
333
|
+
return this.ctx.send(
|
|
334
|
+
"public/get_liquidation_history",
|
|
335
|
+
{
|
|
336
|
+
subaccount_id: params.subaccountId ?? null,
|
|
337
|
+
start_timestamp: params.fromTimestamp,
|
|
338
|
+
end_timestamp: params.toTimestamp,
|
|
339
|
+
page: params.page,
|
|
340
|
+
page_size: params.pageSize
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
/** Mark-to-market value and maintenance margin for a subaccount. */
|
|
345
|
+
marginWatch(params) {
|
|
346
|
+
return this.ctx.send(
|
|
347
|
+
"public/margin_watch",
|
|
348
|
+
{
|
|
349
|
+
subaccount_id: params.subaccountId,
|
|
350
|
+
force_onchain: params.forceOnchain,
|
|
351
|
+
is_delayed_liquidation: params.isDelayedLiquidation
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
/** Margin requirement for a simulated portfolio and optional trade deltas; ignores open-order margin. */
|
|
356
|
+
simulateMargin(params) {
|
|
357
|
+
return this.ctx.send(
|
|
358
|
+
"public/get_margin",
|
|
359
|
+
{
|
|
360
|
+
margin_type: params.marginType,
|
|
361
|
+
market: params.market ?? null,
|
|
362
|
+
simulated_collaterals: params.simulatedCollaterals.map(toWireCollateral),
|
|
363
|
+
simulated_positions: params.simulatedPositions.map(toWirePosition),
|
|
364
|
+
simulated_collateral_changes: params.simulatedCollateralChanges?.map(toWireCollateral) ?? null,
|
|
365
|
+
simulated_position_changes: params.simulatedPositionChanges?.map(toWirePosition) ?? null
|
|
366
|
+
}
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// src/api/orders.ts
|
|
372
|
+
import { formatUnits } from "ethers";
|
|
373
|
+
|
|
374
|
+
// src/signing/action.ts
|
|
375
|
+
import { AbiCoder as AbiCoder2, concat, getAddress as getAddress3, isHexString, keccak256 as keccak2562 } from "ethers";
|
|
376
|
+
|
|
377
|
+
// src/signing/eip712.ts
|
|
378
|
+
import { AbiCoder, getAddress as getAddress2, keccak256, toUtf8Bytes } from "ethers";
|
|
379
|
+
var ACTION_TYPEHASH = "0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17";
|
|
380
|
+
var DOMAIN_TYPEHASH = keccak256(
|
|
381
|
+
toUtf8Bytes("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
|
|
382
|
+
);
|
|
383
|
+
var NAME_HASH = keccak256(toUtf8Bytes("Matching"));
|
|
384
|
+
var VERSION_HASH = keccak256(toUtf8Bytes("1.0"));
|
|
385
|
+
var MATCHING_VERIFYING_CONTRACT = "0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD";
|
|
386
|
+
function domainSeparator(network) {
|
|
387
|
+
return keccak256(
|
|
388
|
+
AbiCoder.defaultAbiCoder().encode(
|
|
389
|
+
["bytes32", "bytes32", "bytes32", "uint256", "address"],
|
|
390
|
+
[DOMAIN_TYPEHASH, NAME_HASH, VERSION_HASH, network.chainId, getAddress2(MATCHING_VERIFYING_CONTRACT)]
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/signing/action.ts
|
|
396
|
+
var SignedAction = class {
|
|
397
|
+
fields;
|
|
398
|
+
domainSeparator;
|
|
399
|
+
signature;
|
|
400
|
+
constructor(fields, domainSeparator2) {
|
|
401
|
+
if (!isHexString(fields.data)) throw new Error("action data must be a 0x-prefixed hex string");
|
|
402
|
+
if (!/^\d+$/.test(fields.nonce)) throw new Error("action nonce must be a decimal string");
|
|
403
|
+
if (!isHexString(domainSeparator2, 32)) throw new Error("domainSeparator must be 32 bytes of hex");
|
|
404
|
+
this.fields = Object.freeze({
|
|
405
|
+
...fields,
|
|
406
|
+
module: getAddress3(fields.module),
|
|
407
|
+
owner: getAddress3(fields.owner),
|
|
408
|
+
signer: getAddress3(fields.signer)
|
|
409
|
+
});
|
|
410
|
+
this.domainSeparator = domainSeparator2;
|
|
411
|
+
}
|
|
412
|
+
actionHash() {
|
|
413
|
+
const f = this.fields;
|
|
414
|
+
return keccak2562(
|
|
415
|
+
AbiCoder2.defaultAbiCoder().encode(
|
|
416
|
+
["bytes32", "uint256", "uint256", "address", "bytes32", "uint256", "address", "address"],
|
|
417
|
+
[ACTION_TYPEHASH, f.subaccountId, f.nonce, f.module, keccak2562(f.data), f.expirySec, f.owner, f.signer]
|
|
418
|
+
)
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
digest() {
|
|
422
|
+
return keccak2562(concat(["0x1901", this.domainSeparator, this.actionHash()]));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Signs the digest with a raw signing key. The wallet must be the
|
|
426
|
+
* declared `signer` (the owner itself, or a registered session key).
|
|
427
|
+
*/
|
|
428
|
+
sign(wallet) {
|
|
429
|
+
if (getAddress3(wallet.address) !== this.fields.signer) {
|
|
430
|
+
throw new Error(`signing wallet ${wallet.address} does not match declared signer ${this.fields.signer}`);
|
|
431
|
+
}
|
|
432
|
+
this.signature = wallet.signingKey.sign(this.digest()).serialized;
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/api/orders.ts
|
|
438
|
+
var OrdersApi = class {
|
|
439
|
+
constructor(ctx, marketData) {
|
|
440
|
+
this.ctx = ctx;
|
|
441
|
+
this.marketData = marketData;
|
|
442
|
+
}
|
|
443
|
+
ctx;
|
|
444
|
+
marketData;
|
|
445
|
+
/** Asset address/subId per instrument name are immutable — cache the lookups. */
|
|
446
|
+
instruments = /* @__PURE__ */ new Map();
|
|
447
|
+
/**
|
|
448
|
+
* Builds the signed wire payload shared by `place` and `quote`. The trade
|
|
449
|
+
* action commits only to asset/price/amount/fee/recipient — trigger,
|
|
450
|
+
* referral, algo and the other flags below are wire-only and never signed.
|
|
451
|
+
*/
|
|
452
|
+
async buildOrderPayload(params) {
|
|
453
|
+
const amount = toE18(params.amount);
|
|
454
|
+
if (amount <= 0n) throw new Error("order amount must be positive");
|
|
455
|
+
const limitPrice = toE18(params.limitPrice);
|
|
456
|
+
const maxFee = toE18(params.maxFee ?? await this.defaultMaxFee(params.instrumentName, params.limitPrice));
|
|
457
|
+
if (maxFee < 0n) throw new Error("maxFee must not be negative");
|
|
458
|
+
const instrument = await this.instrument(params.instrumentName);
|
|
459
|
+
const nonce = params.nonce ?? randomNonce();
|
|
460
|
+
const expirySec = params.signatureExpirySec ?? expiresIn(DEFAULT_SIGNATURE_EXPIRY_SEC);
|
|
461
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
462
|
+
const action = new SignedAction(
|
|
463
|
+
{
|
|
464
|
+
subaccountId: params.subaccountId,
|
|
465
|
+
nonce,
|
|
466
|
+
module: this.ctx.network.modules.trade,
|
|
467
|
+
data: encodeTradeData({
|
|
468
|
+
assetAddress: instrument.base_asset_address,
|
|
469
|
+
subId: instrument.base_asset_sub_id,
|
|
470
|
+
limitPrice,
|
|
471
|
+
amount,
|
|
472
|
+
maxFee,
|
|
473
|
+
recipientSubaccountId: params.subaccountId,
|
|
474
|
+
isBid: params.direction === "buy"
|
|
475
|
+
}),
|
|
476
|
+
expirySec,
|
|
477
|
+
owner: ownerAddress,
|
|
478
|
+
signer: signer.address
|
|
479
|
+
},
|
|
480
|
+
domainSeparator(this.ctx.network)
|
|
481
|
+
).sign(signer);
|
|
482
|
+
return {
|
|
483
|
+
subaccount_id: params.subaccountId,
|
|
484
|
+
instrument_name: params.instrumentName,
|
|
485
|
+
direction: params.direction,
|
|
486
|
+
// Wire decimals are formatted from the signed e18 words so the exchange re-derives the exact signed bytes.
|
|
487
|
+
limit_price: formatUnits(limitPrice, 18),
|
|
488
|
+
amount: formatUnits(amount, 18),
|
|
489
|
+
max_fee: formatUnits(maxFee, 18),
|
|
490
|
+
nonce,
|
|
491
|
+
signer: signer.address,
|
|
492
|
+
signature: action.signature,
|
|
493
|
+
signature_expiry_sec: expirySec,
|
|
494
|
+
order_type: params.orderType ?? "limit",
|
|
495
|
+
time_in_force: params.timeInForce,
|
|
496
|
+
label: params.label,
|
|
497
|
+
mmp: params.mmp,
|
|
498
|
+
reduce_only: params.reduceOnly,
|
|
499
|
+
trigger_type: params.triggerType,
|
|
500
|
+
trigger_price: params.triggerPrice === void 0 ? void 0 : formatUnits(toE18(params.triggerPrice), 18),
|
|
501
|
+
trigger_price_type: params.triggerPriceType,
|
|
502
|
+
referral_code: params.referralCode,
|
|
503
|
+
reject_post_only: params.rejectPostOnly,
|
|
504
|
+
is_atomic_signing: params.isAtomicSigning,
|
|
505
|
+
client: params.clientOrderId,
|
|
506
|
+
extra_fee: params.extraFee === void 0 ? void 0 : formatUnits(toE18(params.extraFee), 18),
|
|
507
|
+
reject_timestamp: params.rejectTimestamp,
|
|
508
|
+
algo_type: params.algoType,
|
|
509
|
+
algo_duration_sec: params.algoDurationSec,
|
|
510
|
+
algo_num_slices: params.algoNumSlices
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async place(params) {
|
|
514
|
+
return this.ctx.send("private/order", await this.buildOrderPayload(params));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Prices an order without placing it. Signs like `place` (pass
|
|
518
|
+
* `{ public: true }` to hit the unauthenticated public estimate instead).
|
|
519
|
+
*/
|
|
520
|
+
async getOrderQuote(params, options = {}) {
|
|
521
|
+
const payload = await this.buildOrderPayload(params);
|
|
522
|
+
return this.ctx.send(options.public ? "public/order_quote" : "private/order_quote", payload);
|
|
523
|
+
}
|
|
524
|
+
/** Cancels one order. Needs only authentication, not a signature. */
|
|
525
|
+
cancel(params) {
|
|
526
|
+
return this.ctx.send("private/cancel", {
|
|
527
|
+
subaccount_id: params.subaccountId,
|
|
528
|
+
order_id: params.orderId,
|
|
529
|
+
instrument_name: params.instrumentName
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
cancelAll(subaccountId, options) {
|
|
533
|
+
return this.ctx.send("private/cancel_all", {
|
|
534
|
+
subaccount_id: subaccountId,
|
|
535
|
+
cancel_trigger_orders: options?.cancelTriggerOrders ?? null,
|
|
536
|
+
cancel_algo_orders: options?.cancelAlgoOrders ?? null
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
cancelByLabel(params) {
|
|
540
|
+
return this.ctx.send("private/cancel_by_label", {
|
|
541
|
+
subaccount_id: params.subaccountId,
|
|
542
|
+
label: params.label,
|
|
543
|
+
instrument_name: params.instrumentName ?? null
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
getOrder(params) {
|
|
547
|
+
return this.ctx.send("private/get_order", {
|
|
548
|
+
subaccount_id: params.subaccountId,
|
|
549
|
+
order_id: params.orderId
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
async getOpenOrders(subaccountId) {
|
|
553
|
+
const result = await this.ctx.send("private/get_open_orders", { subaccount_id: subaccountId });
|
|
554
|
+
return result.orders;
|
|
555
|
+
}
|
|
556
|
+
/** Paginated closed/open order history; defaults to all of the wallet's subaccounts. */
|
|
557
|
+
getOrderHistory(query = {}) {
|
|
558
|
+
return this.ctx.send("private/get_order_history", {
|
|
559
|
+
subaccount_id: query.subaccountId ?? null,
|
|
560
|
+
from_timestamp: query.fromTimestamp ?? null,
|
|
561
|
+
to_timestamp: query.toTimestamp ?? null,
|
|
562
|
+
page: query.page ?? null,
|
|
563
|
+
page_size: query.pageSize ?? null
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/** Sets the market-maker-protection limits for a currency; overwrites any existing config. */
|
|
567
|
+
setMmpConfig(params) {
|
|
568
|
+
return this.ctx.send("private/set_mmp_config", {
|
|
569
|
+
subaccount_id: params.subaccountId,
|
|
570
|
+
currency: params.currency,
|
|
571
|
+
mmp_frozen_time: params.mmpFrozenTime,
|
|
572
|
+
mmp_interval: params.mmpInterval,
|
|
573
|
+
mmp_amount_limit: params.mmpAmountLimit === void 0 ? void 0 : String(params.mmpAmountLimit),
|
|
574
|
+
mmp_delta_limit: params.mmpDeltaLimit === void 0 ? void 0 : String(params.mmpDeltaLimit)
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/** Clears a tripped market-maker-protection freeze; scoped to one currency, or all when omitted. */
|
|
578
|
+
resetMmp(params) {
|
|
579
|
+
return this.ctx.send("private/reset_mmp", {
|
|
580
|
+
subaccount_id: params.subaccountId,
|
|
581
|
+
currency: params.currency ?? null
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
instrument(name) {
|
|
585
|
+
let lookup = this.instruments.get(name);
|
|
586
|
+
if (!lookup) {
|
|
587
|
+
lookup = this.marketData.getInstrument(name);
|
|
588
|
+
lookup.catch(() => this.instruments.delete(name));
|
|
589
|
+
this.instruments.set(name, lookup);
|
|
590
|
+
}
|
|
591
|
+
return lookup;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The signed max fee must cover whatever the exchange charges at match
|
|
595
|
+
* time or the order is rejected. The exchange bounds the per-unit fee at
|
|
596
|
+
* `2 x taker_fee_rate x max(index, limit) + base_fee`, where `index` is
|
|
597
|
+
* the underlying SPOT feed (`ticker.I`) — NOT the option mark
|
|
598
|
+
* (`ticker.M`). We mirror that and apply a 3x headroom for feed moves
|
|
599
|
+
* between signing and matching.
|
|
600
|
+
*/
|
|
601
|
+
async defaultMaxFee(instrumentName, limitPrice) {
|
|
602
|
+
const [instrument, ticker] = await Promise.all([
|
|
603
|
+
this.instrument(instrumentName),
|
|
604
|
+
this.marketData.getTicker(instrumentName)
|
|
605
|
+
]);
|
|
606
|
+
const feeBasis = Math.max(Number(ticker.I), Number(limitPrice));
|
|
607
|
+
const perUnitTakerCost = feeBasis * Number(instrument.taker_fee_rate) + Number(instrument.base_fee);
|
|
608
|
+
if (!Number.isFinite(perUnitTakerCost) || perUnitTakerCost < 0) {
|
|
609
|
+
throw new Error(`cannot derive a default max fee for ${instrumentName} \u2014 pass maxFee explicitly`);
|
|
610
|
+
}
|
|
611
|
+
return (perUnitTakerCost * 3).toFixed(6);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/api/positionTransfers.ts
|
|
616
|
+
import { formatUnits as formatUnits2 } from "ethers";
|
|
617
|
+
var PositionTransfersApi = class {
|
|
618
|
+
constructor(ctx) {
|
|
619
|
+
this.ctx = ctx;
|
|
620
|
+
}
|
|
621
|
+
ctx;
|
|
622
|
+
instruments = /* @__PURE__ */ new Map();
|
|
623
|
+
async transferPositions(params) {
|
|
624
|
+
if (params.makerSubaccountId === params.takerSubaccountId) {
|
|
625
|
+
throw new Error("makerSubaccountId and takerSubaccountId must be different");
|
|
626
|
+
}
|
|
627
|
+
if (params.legs.length === 0) {
|
|
628
|
+
throw new Error("transferPositions needs at least one leg");
|
|
629
|
+
}
|
|
630
|
+
const makerNonce = params.makerNonce ?? randomNonce();
|
|
631
|
+
const takerNonce = params.takerNonce ?? randomNonce();
|
|
632
|
+
if (makerNonce === takerNonce) {
|
|
633
|
+
throw new Error("makerNonce and takerNonce must be different");
|
|
634
|
+
}
|
|
635
|
+
const expirySec = params.signatureExpirySec ?? expiresIn(700);
|
|
636
|
+
const takerDirection = params.makerDirection === "buy" ? "sell" : "buy";
|
|
637
|
+
const legs = await this.resolveLegs(params.legs);
|
|
638
|
+
const maker = this.signAction(
|
|
639
|
+
params.makerSubaccountId,
|
|
640
|
+
makerNonce,
|
|
641
|
+
expirySec,
|
|
642
|
+
encodeRfqQuote({ maxFee: 0n, direction: params.makerDirection, legs })
|
|
643
|
+
);
|
|
644
|
+
const taker = this.signAction(
|
|
645
|
+
params.takerSubaccountId,
|
|
646
|
+
takerNonce,
|
|
647
|
+
expirySec,
|
|
648
|
+
encodeRfqExecute({ maxFee: 0n, direction: takerDirection, legs })
|
|
649
|
+
);
|
|
650
|
+
const wireLegs = legs.map(wireLeg);
|
|
651
|
+
return this.ctx.send("private/transfer_positions", {
|
|
652
|
+
wallet: this.ctx.credentials().ownerAddress,
|
|
653
|
+
maker_params: {
|
|
654
|
+
subaccount_id: params.makerSubaccountId,
|
|
655
|
+
direction: params.makerDirection,
|
|
656
|
+
legs: wireLegs,
|
|
657
|
+
max_fee: formatUnits2(0n, 18),
|
|
658
|
+
nonce: makerNonce,
|
|
659
|
+
signer: maker.signer,
|
|
660
|
+
signature: maker.signature,
|
|
661
|
+
signature_expiry_sec: expirySec
|
|
662
|
+
},
|
|
663
|
+
taker_params: {
|
|
664
|
+
subaccount_id: params.takerSubaccountId,
|
|
665
|
+
direction: takerDirection,
|
|
666
|
+
legs: wireLegs,
|
|
667
|
+
max_fee: formatUnits2(0n, 18),
|
|
668
|
+
nonce: takerNonce,
|
|
669
|
+
signer: taker.signer,
|
|
670
|
+
signature: taker.signature,
|
|
671
|
+
signature_expiry_sec: expirySec
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async resolveLegs(legs) {
|
|
676
|
+
return sortRfqLegs(
|
|
677
|
+
await Promise.all(
|
|
678
|
+
legs.map(async (leg) => {
|
|
679
|
+
const instrument = await this.instrument(leg.instrumentName);
|
|
680
|
+
return {
|
|
681
|
+
instrumentName: leg.instrumentName,
|
|
682
|
+
assetAddress: instrument.base_asset_address,
|
|
683
|
+
subId: instrument.base_asset_sub_id,
|
|
684
|
+
price: toE18(leg.price),
|
|
685
|
+
amount: toE18(leg.amount),
|
|
686
|
+
direction: leg.direction
|
|
687
|
+
};
|
|
688
|
+
})
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
instrument(name) {
|
|
693
|
+
let lookup = this.instruments.get(name);
|
|
694
|
+
if (!lookup) {
|
|
695
|
+
lookup = this.ctx.send("public/get_instrument", { instrument_name: name });
|
|
696
|
+
lookup.catch(() => this.instruments.delete(name));
|
|
697
|
+
this.instruments.set(name, lookup);
|
|
698
|
+
}
|
|
699
|
+
return lookup;
|
|
700
|
+
}
|
|
701
|
+
signAction(subaccountId, nonce, expirySec, data) {
|
|
702
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
703
|
+
const action = new SignedAction(
|
|
704
|
+
{
|
|
705
|
+
subaccountId,
|
|
706
|
+
nonce,
|
|
707
|
+
module: this.ctx.network.modules.rfq,
|
|
708
|
+
data,
|
|
709
|
+
expirySec,
|
|
710
|
+
owner: ownerAddress,
|
|
711
|
+
signer: signer.address
|
|
712
|
+
},
|
|
713
|
+
domainSeparator(this.ctx.network)
|
|
714
|
+
).sign(signer);
|
|
715
|
+
return { signer: action.fields.signer, signature: action.signature };
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
function wireLeg(leg) {
|
|
719
|
+
return {
|
|
720
|
+
amount: formatUnits2(leg.amount, 18),
|
|
721
|
+
direction: leg.direction,
|
|
722
|
+
instrument_name: leg.instrumentName,
|
|
723
|
+
price: formatUnits2(leg.price, 18)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/api/rfq.ts
|
|
728
|
+
import { formatUnits as formatUnits3, getAddress as getAddress4 } from "ethers";
|
|
729
|
+
var DEFAULT_QUOTE_EXPIRY_SEC = 700;
|
|
730
|
+
var RfqApi = class {
|
|
731
|
+
constructor(ctx) {
|
|
732
|
+
this.ctx = ctx;
|
|
733
|
+
}
|
|
734
|
+
ctx;
|
|
735
|
+
/** Asset address/subId per instrument name are immutable — cache the lookups. */
|
|
736
|
+
instruments = /* @__PURE__ */ new Map();
|
|
737
|
+
// ── Taker ──────────────────────────────────────────────────────────
|
|
738
|
+
/** Requests quotes for a package of legs. Unsigned — only the eventual execute is. */
|
|
739
|
+
async sendRfq(params) {
|
|
740
|
+
return this.ctx.send("private/send_rfq", {
|
|
741
|
+
subaccount_id: params.subaccountId,
|
|
742
|
+
legs: unpricedWireLegs(params.legs),
|
|
743
|
+
label: params.label,
|
|
744
|
+
max_total_cost: params.maxTotalCost === void 0 ? void 0 : wireDecimal(params.maxTotalCost),
|
|
745
|
+
min_total_cost: params.minTotalCost === void 0 ? void 0 : wireDecimal(params.minTotalCost),
|
|
746
|
+
counterparties: params.counterparties?.map((wallet) => getAddress4(wallet)),
|
|
747
|
+
partial_fill_step: params.partialFillStep === void 0 ? void 0 : wireDecimal(params.partialFillStep)
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
/** Polls quotes received for the taker's RFQs (e.g. `status: 'open'`). */
|
|
751
|
+
async pollQuotes(params) {
|
|
752
|
+
return this.ctx.send("private/poll_quotes", {
|
|
753
|
+
subaccount_id: params.subaccountId,
|
|
754
|
+
rfq_id: params.rfqId,
|
|
755
|
+
quote_id: params.quoteId,
|
|
756
|
+
status: params.status,
|
|
757
|
+
from_timestamp: params.fromTimestamp,
|
|
758
|
+
to_timestamp: params.toTimestamp,
|
|
759
|
+
page: params.page,
|
|
760
|
+
page_size: params.pageSize
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/** The exchange's pick of the best open quote for an RFQ, with margin/cost estimates. */
|
|
764
|
+
async getBestQuote(params) {
|
|
765
|
+
return this.ctx.send("private/rfq_get_best_quote", {
|
|
766
|
+
subaccount_id: params.subaccountId,
|
|
767
|
+
rfq_id: params.rfqId,
|
|
768
|
+
direction: params.direction,
|
|
769
|
+
legs: unpricedWireLegs(params.legs)
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Executes a maker quote (from `pollQuotes` / `getBestQuote`). The taker
|
|
774
|
+
* trades opposite the quote's direction and signs a commitment to the
|
|
775
|
+
* maker's exact leg bundle, so legs and prices come from the quote itself.
|
|
776
|
+
*/
|
|
777
|
+
async executeQuote(params) {
|
|
778
|
+
return this.ctx.send("private/execute_quote", await this.buildExecutePayload(params));
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Debug helper: signs the execute exactly like `executeQuote` but submits it
|
|
782
|
+
* to `public/execute_quote_debug`, returning the server-computed encoded data
|
|
783
|
+
* and hashes instead of filling. Use it to verify your client-side signing.
|
|
784
|
+
*/
|
|
785
|
+
async executeQuoteDebug(params) {
|
|
786
|
+
return this.ctx.send("public/execute_quote_debug", await this.buildExecutePayload(params));
|
|
787
|
+
}
|
|
788
|
+
async buildExecutePayload(params) {
|
|
789
|
+
const direction = params.quote.direction === "buy" ? "sell" : "buy";
|
|
790
|
+
const maxFee = toE18(params.maxFee);
|
|
791
|
+
const legs = await this.resolveLegs(
|
|
792
|
+
params.quote.legs.map((leg) => ({
|
|
793
|
+
instrumentName: leg.instrument_name,
|
|
794
|
+
amount: leg.amount,
|
|
795
|
+
price: leg.price,
|
|
796
|
+
direction: leg.direction
|
|
797
|
+
}))
|
|
798
|
+
);
|
|
799
|
+
const nonce = params.nonce ?? randomNonce();
|
|
800
|
+
const expirySec = params.signatureExpirySec ?? expiresIn(DEFAULT_QUOTE_EXPIRY_SEC);
|
|
801
|
+
const signed = this.signRfqAction(
|
|
802
|
+
params.subaccountId,
|
|
803
|
+
encodeRfqExecute({ maxFee, direction, legs }),
|
|
804
|
+
nonce,
|
|
805
|
+
expirySec
|
|
806
|
+
);
|
|
807
|
+
return {
|
|
808
|
+
subaccount_id: params.subaccountId,
|
|
809
|
+
rfq_id: params.quote.rfq_id,
|
|
810
|
+
quote_id: params.quote.quote_id,
|
|
811
|
+
direction,
|
|
812
|
+
legs: legs.map(pricedWireLeg),
|
|
813
|
+
max_fee: formatUnits3(maxFee, 18),
|
|
814
|
+
nonce,
|
|
815
|
+
signer: signed.signer,
|
|
816
|
+
signature: signed.signature,
|
|
817
|
+
signature_expiry_sec: expirySec,
|
|
818
|
+
enable_taker_protection: params.enableTakerProtection,
|
|
819
|
+
label: params.label
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
async cancelRfq(params) {
|
|
823
|
+
return this.ctx.send("private/cancel_rfq", {
|
|
824
|
+
subaccount_id: params.subaccountId,
|
|
825
|
+
rfq_id: params.rfqId
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
// ── Maker ──────────────────────────────────────────────────────────
|
|
829
|
+
/** Polls RFQs open for quoting (e.g. `status: 'open'`). */
|
|
830
|
+
async pollRfqs(params) {
|
|
831
|
+
return this.ctx.send("private/poll_rfqs", {
|
|
832
|
+
subaccount_id: params.subaccountId,
|
|
833
|
+
rfq_id: params.rfqId,
|
|
834
|
+
status: params.status,
|
|
835
|
+
from_timestamp: params.fromTimestamp,
|
|
836
|
+
to_timestamp: params.toTimestamp,
|
|
837
|
+
page: params.page,
|
|
838
|
+
page_size: params.pageSize
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Signs and sends a quote against an open RFQ. Legs must match the RFQ's
|
|
843
|
+
* legs with prices added; `direction: 'sell'` quotes the package to a
|
|
844
|
+
* buying taker.
|
|
845
|
+
*/
|
|
846
|
+
async sendQuote(params) {
|
|
847
|
+
return this.ctx.send("private/send_quote", await this.buildSendQuotePayload(params));
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Debug helper: signs the quote exactly like `sendQuote` but submits it to
|
|
851
|
+
* `public/send_quote_debug`, returning the server-computed encoded data and
|
|
852
|
+
* hashes instead of posting. Use it to verify your client-side signing.
|
|
853
|
+
*/
|
|
854
|
+
async sendQuoteDebug(params) {
|
|
855
|
+
return this.ctx.send("public/send_quote_debug", await this.buildSendQuotePayload(params));
|
|
856
|
+
}
|
|
857
|
+
async buildSendQuotePayload(params) {
|
|
858
|
+
const maxFee = toE18(params.maxFee);
|
|
859
|
+
const legs = await this.resolveLegs(params.legs);
|
|
860
|
+
const nonce = params.nonce ?? randomNonce();
|
|
861
|
+
const expirySec = params.signatureExpirySec ?? expiresIn(DEFAULT_QUOTE_EXPIRY_SEC);
|
|
862
|
+
const signed = this.signRfqAction(
|
|
863
|
+
params.subaccountId,
|
|
864
|
+
encodeRfqQuote({ maxFee, direction: params.direction, legs }),
|
|
865
|
+
nonce,
|
|
866
|
+
expirySec
|
|
867
|
+
);
|
|
868
|
+
return {
|
|
869
|
+
subaccount_id: params.subaccountId,
|
|
870
|
+
rfq_id: params.rfqId,
|
|
871
|
+
direction: params.direction,
|
|
872
|
+
legs: legs.map(pricedWireLeg),
|
|
873
|
+
max_fee: formatUnits3(maxFee, 18),
|
|
874
|
+
// The generated stub types nonce as number; the wire value is the decimal-string nanosecond nonce (> 2^53).
|
|
875
|
+
nonce,
|
|
876
|
+
signer: signed.signer,
|
|
877
|
+
signature: signed.signature,
|
|
878
|
+
signature_expiry_sec: expirySec,
|
|
879
|
+
mmp: params.mmp,
|
|
880
|
+
label: params.label
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async cancelQuote(params) {
|
|
884
|
+
return this.ctx.send("private/cancel_quote", {
|
|
885
|
+
subaccount_id: params.subaccountId,
|
|
886
|
+
quote_id: params.quoteId,
|
|
887
|
+
rfq_id: params.rfqId
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
/** Cancels all the subaccount's open quotes, optionally scoped to one RFQ. */
|
|
891
|
+
async cancelBatchQuotes(params) {
|
|
892
|
+
return this.ctx.send("private/cancel_batch_quotes", {
|
|
893
|
+
subaccount_id: params.subaccountId,
|
|
894
|
+
rfq_id: params.rfqId
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/** Cancels all the subaccount's open RFQs, optionally scoped to one RFQ id. */
|
|
898
|
+
async cancelBatchRfqs(params) {
|
|
899
|
+
return this.ctx.send("private/cancel_batch_rfqs", {
|
|
900
|
+
subaccount_id: params.subaccountId,
|
|
901
|
+
rfq_id: params.rfqId
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
/** Paginated quote history/read for the subaccount, filterable by rfq/quote/status. */
|
|
905
|
+
async getQuotes(params) {
|
|
906
|
+
return this.ctx.send("private/get_quotes", {
|
|
907
|
+
subaccount_id: params.subaccountId,
|
|
908
|
+
rfq_id: params.rfqId,
|
|
909
|
+
quote_id: params.quoteId,
|
|
910
|
+
status: params.status,
|
|
911
|
+
page: params.page,
|
|
912
|
+
page_size: params.pageSize,
|
|
913
|
+
from_timestamp: params.fromTimestamp,
|
|
914
|
+
to_timestamp: params.toTimestamp
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Atomically cancels an existing quote and signs+sends a replacement on the
|
|
919
|
+
* same RFQ. Legs/price/maxFee sign the new quote exactly like `sendQuote`;
|
|
920
|
+
* identify the quote to cancel by `quoteIdToCancel` or `nonceToCancel`.
|
|
921
|
+
*/
|
|
922
|
+
async replaceQuote(params) {
|
|
923
|
+
const maxFee = toE18(params.maxFee);
|
|
924
|
+
const legs = await this.resolveLegs(params.legs);
|
|
925
|
+
const nonce = params.nonce ?? randomNonce();
|
|
926
|
+
const expirySec = params.signatureExpirySec ?? expiresIn(DEFAULT_QUOTE_EXPIRY_SEC);
|
|
927
|
+
const signed = this.signRfqAction(
|
|
928
|
+
params.subaccountId,
|
|
929
|
+
encodeRfqQuote({ maxFee, direction: params.direction, legs }),
|
|
930
|
+
nonce,
|
|
931
|
+
expirySec
|
|
932
|
+
);
|
|
933
|
+
return this.ctx.send("private/replace_quote", {
|
|
934
|
+
subaccount_id: params.subaccountId,
|
|
935
|
+
rfq_id: params.rfqId,
|
|
936
|
+
direction: params.direction,
|
|
937
|
+
legs: legs.map(pricedWireLeg),
|
|
938
|
+
max_fee: formatUnits3(maxFee, 18),
|
|
939
|
+
nonce,
|
|
940
|
+
signer: signed.signer,
|
|
941
|
+
signature: signed.signature,
|
|
942
|
+
signature_expiry_sec: expirySec,
|
|
943
|
+
quote_id_to_cancel: params.quoteIdToCancel,
|
|
944
|
+
nonce_to_cancel: params.nonceToCancel,
|
|
945
|
+
mmp: params.mmp,
|
|
946
|
+
label: params.label
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
// ── Shared signing plumbing ────────────────────────────────────────
|
|
950
|
+
/** Attaches each leg's protocol asset address + sub id and sorts into the canonical signed order. */
|
|
951
|
+
async resolveLegs(legs) {
|
|
952
|
+
return sortRfqLegs(
|
|
953
|
+
await Promise.all(
|
|
954
|
+
legs.map(async (leg) => {
|
|
955
|
+
const instrument = await this.instrument(leg.instrumentName);
|
|
956
|
+
return {
|
|
957
|
+
instrumentName: leg.instrumentName,
|
|
958
|
+
assetAddress: instrument.base_asset_address,
|
|
959
|
+
subId: instrument.base_asset_sub_id,
|
|
960
|
+
price: toE18(leg.price),
|
|
961
|
+
amount: toE18(leg.amount),
|
|
962
|
+
direction: leg.direction
|
|
963
|
+
};
|
|
964
|
+
})
|
|
965
|
+
)
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
instrument(name) {
|
|
969
|
+
let lookup = this.instruments.get(name);
|
|
970
|
+
if (!lookup) {
|
|
971
|
+
lookup = this.ctx.send("public/get_instrument", { instrument_name: name });
|
|
972
|
+
lookup.catch(() => this.instruments.delete(name));
|
|
973
|
+
this.instruments.set(name, lookup);
|
|
974
|
+
}
|
|
975
|
+
return lookup;
|
|
976
|
+
}
|
|
977
|
+
signRfqAction(subaccountId, data, nonce, expirySec) {
|
|
978
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
979
|
+
const action = new SignedAction(
|
|
980
|
+
{
|
|
981
|
+
subaccountId,
|
|
982
|
+
nonce,
|
|
983
|
+
module: this.ctx.network.modules.rfq,
|
|
984
|
+
data,
|
|
985
|
+
expirySec,
|
|
986
|
+
owner: ownerAddress,
|
|
987
|
+
signer: signer.address
|
|
988
|
+
},
|
|
989
|
+
domainSeparator(this.ctx.network)
|
|
990
|
+
).sign(signer);
|
|
991
|
+
return { signer: signer.address, signature: action.signature };
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
function wireDecimal(value) {
|
|
995
|
+
return formatUnits3(toE18(value), 18);
|
|
996
|
+
}
|
|
997
|
+
function unpricedWireLegs(legs) {
|
|
998
|
+
return sortRfqLegs(legs).map((leg) => {
|
|
999
|
+
const amount = toE18(leg.amount);
|
|
1000
|
+
if (amount <= 0n) {
|
|
1001
|
+
throw new Error(`rfq leg amount must be positive (direction carries the sign): ${leg.instrumentName}`);
|
|
1002
|
+
}
|
|
1003
|
+
return { amount: formatUnits3(amount, 18), direction: leg.direction, instrument_name: leg.instrumentName };
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function pricedWireLeg(leg) {
|
|
1007
|
+
return {
|
|
1008
|
+
amount: formatUnits3(leg.amount, 18),
|
|
1009
|
+
direction: leg.direction,
|
|
1010
|
+
instrument_name: leg.instrumentName,
|
|
1011
|
+
price: formatUnits3(leg.price, 18)
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/api/sessionKeys.ts
|
|
1016
|
+
import { getAddress as getAddress5 } from "ethers";
|
|
1017
|
+
|
|
1018
|
+
// src/auth/scopes.ts
|
|
1019
|
+
var ProtocolScopeCode = {
|
|
1020
|
+
/** Superuser grant — allows every protocol action. Grant deliberately. */
|
|
1021
|
+
Admin: 0,
|
|
1022
|
+
/** Withdrawals, restricted to the account's whitelisted recipients. */
|
|
1023
|
+
Withdraw: 1,
|
|
1024
|
+
TradeAll: 2,
|
|
1025
|
+
TradeOrderbookAll: 3,
|
|
1026
|
+
TradeOrderbookSpot: 4,
|
|
1027
|
+
TradeOrderbookPerp: 5,
|
|
1028
|
+
TradeOrderbookOption: 6,
|
|
1029
|
+
TradeRfqAll: 7,
|
|
1030
|
+
TradeRfqSpot: 8,
|
|
1031
|
+
TradeRfqPerp: 9,
|
|
1032
|
+
TradeRfqOption: 10,
|
|
1033
|
+
TransferAll: 11,
|
|
1034
|
+
/** Between subaccounts of the same owner (spot, and positions via RFQ). */
|
|
1035
|
+
TransferExistingSubaccount: 12,
|
|
1036
|
+
/** Spot transfer that creates a new subaccount under the owner. */
|
|
1037
|
+
TransferNewSubaccount: 13,
|
|
1038
|
+
/** Spot transfer to a whitelisted different-owner wallet. */
|
|
1039
|
+
TransferDifferentOwnerSubaccount: 14,
|
|
1040
|
+
/** Create child session keys; the child must be a subset of this key. */
|
|
1041
|
+
CreateSessionKey: 15,
|
|
1042
|
+
Liquidate: 16,
|
|
1043
|
+
VaultAll: 17,
|
|
1044
|
+
VaultCuratorCreate: 18,
|
|
1045
|
+
VaultCuratorMintAndBurn: 19,
|
|
1046
|
+
VaultUserDeposit: 20,
|
|
1047
|
+
VaultUserWithdraw: 21,
|
|
1048
|
+
VaultUserCancel: 22
|
|
1049
|
+
};
|
|
1050
|
+
var ProtocolScopeWireString = {
|
|
1051
|
+
[ProtocolScopeCode.Admin]: "admin",
|
|
1052
|
+
[ProtocolScopeCode.Withdraw]: "withdraw",
|
|
1053
|
+
[ProtocolScopeCode.TradeAll]: "trade:all",
|
|
1054
|
+
[ProtocolScopeCode.TradeOrderbookAll]: "trade:orderbook:all",
|
|
1055
|
+
[ProtocolScopeCode.TradeOrderbookSpot]: "trade:orderbook:spot",
|
|
1056
|
+
[ProtocolScopeCode.TradeOrderbookPerp]: "trade:orderbook:perp",
|
|
1057
|
+
[ProtocolScopeCode.TradeOrderbookOption]: "trade:orderbook:option",
|
|
1058
|
+
[ProtocolScopeCode.TradeRfqAll]: "trade:rfq:all",
|
|
1059
|
+
[ProtocolScopeCode.TradeRfqSpot]: "trade:rfq:spot",
|
|
1060
|
+
[ProtocolScopeCode.TradeRfqPerp]: "trade:rfq:perp",
|
|
1061
|
+
[ProtocolScopeCode.TradeRfqOption]: "trade:rfq:option",
|
|
1062
|
+
[ProtocolScopeCode.TransferAll]: "transfer:all",
|
|
1063
|
+
[ProtocolScopeCode.TransferExistingSubaccount]: "transfer:existing_subaccount",
|
|
1064
|
+
[ProtocolScopeCode.TransferNewSubaccount]: "transfer:new_subaccount",
|
|
1065
|
+
[ProtocolScopeCode.TransferDifferentOwnerSubaccount]: "transfer:different_owner_subaccount",
|
|
1066
|
+
[ProtocolScopeCode.CreateSessionKey]: "create_session_key",
|
|
1067
|
+
[ProtocolScopeCode.Liquidate]: "liquidate",
|
|
1068
|
+
[ProtocolScopeCode.VaultAll]: "vault:all",
|
|
1069
|
+
[ProtocolScopeCode.VaultCuratorCreate]: "vault:curator_create",
|
|
1070
|
+
[ProtocolScopeCode.VaultCuratorMintAndBurn]: "vault:curator_mint_and_burn",
|
|
1071
|
+
[ProtocolScopeCode.VaultUserDeposit]: "vault:user_deposit",
|
|
1072
|
+
[ProtocolScopeCode.VaultUserWithdraw]: "vault:user_withdraw",
|
|
1073
|
+
[ProtocolScopeCode.VaultUserCancel]: "vault:user_cancel"
|
|
1074
|
+
};
|
|
1075
|
+
var OffchainScope = {
|
|
1076
|
+
AccountInfo: "account_info",
|
|
1077
|
+
DeleteSessionKey: "delete_session_key"
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
// src/api/sessionKeys.ts
|
|
1081
|
+
var SessionKeysApi = class {
|
|
1082
|
+
constructor(ctx) {
|
|
1083
|
+
this.ctx = ctx;
|
|
1084
|
+
}
|
|
1085
|
+
ctx;
|
|
1086
|
+
/**
|
|
1087
|
+
* Registers a scoped session key. The action must be authorized by the
|
|
1088
|
+
* account owner or by a registered session key holding the
|
|
1089
|
+
* `create_session_key` scope — a child key must then be a subset of its
|
|
1090
|
+
* creator (scopes, subaccounts, and expiry no later than the creator's).
|
|
1091
|
+
* Signs with the client's action signer, so a client configured with an
|
|
1092
|
+
* unscoped session key will be rejected server-side.
|
|
1093
|
+
*/
|
|
1094
|
+
async create(params) {
|
|
1095
|
+
const publicSessionKey = getAddress5(
|
|
1096
|
+
typeof params.publicSessionKey === "string" ? params.publicSessionKey : params.publicSessionKey.address
|
|
1097
|
+
);
|
|
1098
|
+
const scopes = params.protocolScopes ?? [];
|
|
1099
|
+
const offchainScopes = params.offchainScopes ?? [OffchainScope.AccountInfo];
|
|
1100
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
1101
|
+
const action = new SignedAction(
|
|
1102
|
+
{
|
|
1103
|
+
subaccountId: 0,
|
|
1104
|
+
nonce: params.nonce ?? randomNonce(),
|
|
1105
|
+
module: this.ctx.network.modules.createSessionKey,
|
|
1106
|
+
data: encodeCreateSessionKeyActionData({
|
|
1107
|
+
sessionKey: publicSessionKey,
|
|
1108
|
+
expirySec: params.expirySec,
|
|
1109
|
+
scopes,
|
|
1110
|
+
subaccountIds: params.subaccountIds ?? []
|
|
1111
|
+
}),
|
|
1112
|
+
expirySec: params.signatureExpirySec ?? expiresIn(600),
|
|
1113
|
+
owner: ownerAddress,
|
|
1114
|
+
signer: signer.address
|
|
1115
|
+
},
|
|
1116
|
+
domainSeparator(this.ctx.network)
|
|
1117
|
+
).sign(signer);
|
|
1118
|
+
return this.ctx.send("private/create_session_key", {
|
|
1119
|
+
wallet: ownerAddress,
|
|
1120
|
+
public_session_key: publicSessionKey,
|
|
1121
|
+
expiry_sec: params.expirySec,
|
|
1122
|
+
subaccount_ids: params.subaccountIds ?? null,
|
|
1123
|
+
nonce: action.fields.nonce,
|
|
1124
|
+
signer: action.fields.signer,
|
|
1125
|
+
// sign() above guarantees the signature is set
|
|
1126
|
+
signature: action.signature,
|
|
1127
|
+
signature_expiry_sec: action.fields.expirySec,
|
|
1128
|
+
protocol_scopes: scopes.map((code) => ProtocolScopeWireString[code]),
|
|
1129
|
+
offchain_scopes: offchainScopes,
|
|
1130
|
+
label: params.label,
|
|
1131
|
+
ip_whitelist: params.ipWhitelist
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
/** All registered session keys of the owner wallet. */
|
|
1135
|
+
async list() {
|
|
1136
|
+
const result = await this.ctx.send("private/session_keys", {
|
|
1137
|
+
wallet: this.ctx.credentials().ownerAddress
|
|
1138
|
+
});
|
|
1139
|
+
return result.public_session_keys;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Patches a key's OFF-CHAIN attributes. Protocol scopes, subaccounts,
|
|
1143
|
+
* and expiry are committed in the signed registration and cannot be
|
|
1144
|
+
* edited — register a replacement key instead. The public API has no
|
|
1145
|
+
* revoke endpoint: a key stops working at its signed `expiry_sec`.
|
|
1146
|
+
*/
|
|
1147
|
+
async edit(params) {
|
|
1148
|
+
return this.ctx.send("private/edit_session_key", {
|
|
1149
|
+
wallet: this.ctx.credentials().ownerAddress,
|
|
1150
|
+
public_session_key: getAddress5(params.publicSessionKey),
|
|
1151
|
+
label: params.label,
|
|
1152
|
+
ip_whitelist: params.ipWhitelist,
|
|
1153
|
+
offchain_scopes: params.offchainScopes
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// src/api/spotTransfers.ts
|
|
1159
|
+
import { formatUnits as formatUnits4, getAddress as getAddress6 } from "ethers";
|
|
1160
|
+
async function resolveSpotAsset(ctx, asset) {
|
|
1161
|
+
const wanted = asset.trim();
|
|
1162
|
+
const wantedAddress = wanted.startsWith("0x") ? getAddress6(wanted) : void 0;
|
|
1163
|
+
const currencies = await ctx.send("public/get_all_currencies", null);
|
|
1164
|
+
for (const currency of currencies) {
|
|
1165
|
+
const enabled = currency.spot.filter((s) => s.erc20.underlying_erc20 != null || s.erc20.decimals > 0);
|
|
1166
|
+
let match;
|
|
1167
|
+
if (wantedAddress) {
|
|
1168
|
+
match = enabled.find((s) => getAddress6(s.address) === wantedAddress);
|
|
1169
|
+
} else if (currency.currency.toUpperCase() === wanted.toUpperCase()) {
|
|
1170
|
+
if (enabled.length > 1) {
|
|
1171
|
+
throw new Error(
|
|
1172
|
+
`currency ${currency.currency} has multiple deposit-enabled spot assets \u2014 pass the protocol asset address`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
match = enabled[0];
|
|
1176
|
+
if (!match) throw new Error(`currency ${currency.currency} has no deposit-enabled spot asset`);
|
|
1177
|
+
}
|
|
1178
|
+
if (match) {
|
|
1179
|
+
return { name: currency.currency, address: getAddress6(match.address), decimals: match.erc20.decimals };
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
throw new Error(
|
|
1183
|
+
`unknown asset ${asset} \u2014 expected a currency name (e.g. "USDC") or the address of a deposit-enabled spot asset`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
function decimalString(value, scale = 18) {
|
|
1187
|
+
return typeof value === "bigint" ? formatUnits4(value, scale) : String(value).trim();
|
|
1188
|
+
}
|
|
1189
|
+
var SpotTransfersApi = class {
|
|
1190
|
+
constructor(ctx) {
|
|
1191
|
+
this.ctx = ctx;
|
|
1192
|
+
}
|
|
1193
|
+
ctx;
|
|
1194
|
+
/**
|
|
1195
|
+
* Moves a spot balance between the owner's OWN subaccounts
|
|
1196
|
+
* (`private/transfer_spot`). Destination is either an existing
|
|
1197
|
+
* `toSubaccountId` or, when `newSubaccountManager` is set, a freshly
|
|
1198
|
+
* created sender-owned subaccount under that manager. For a transfer
|
|
1199
|
+
* to another wallet use `transferExternal`.
|
|
1200
|
+
*/
|
|
1201
|
+
async transferInternal(params) {
|
|
1202
|
+
const toSubaccountId = params.toSubaccountId ?? 0;
|
|
1203
|
+
const newSubaccountManager = params.newSubaccountManager ?? 0;
|
|
1204
|
+
if (toSubaccountId === 0 && newSubaccountManager === 0) {
|
|
1205
|
+
throw new Error("specify toSubaccountId or, to create a new subaccount, newSubaccountManager");
|
|
1206
|
+
}
|
|
1207
|
+
const subId = params.subId ?? 0;
|
|
1208
|
+
const maxFeeUsd = params.maxFeeUsd ?? "0";
|
|
1209
|
+
if (newSubaccountManager !== 0 && toE18(maxFeeUsd) === 0n) {
|
|
1210
|
+
throw new Error(
|
|
1211
|
+
"creating a subaccount charges a transfer fee \u2014 set maxFeeUsd (>= 1) when newSubaccountManager is used"
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const asset = await resolveSpotAsset(this.ctx, params.asset);
|
|
1215
|
+
const action = this.signAction(
|
|
1216
|
+
params.subaccountId,
|
|
1217
|
+
this.ctx.network.modules.transfer,
|
|
1218
|
+
encodeTransfer({
|
|
1219
|
+
toSubaccountId,
|
|
1220
|
+
newSubaccountManager,
|
|
1221
|
+
asset: asset.address,
|
|
1222
|
+
subId,
|
|
1223
|
+
amount: params.amount,
|
|
1224
|
+
maxFeeUsd
|
|
1225
|
+
}),
|
|
1226
|
+
params
|
|
1227
|
+
);
|
|
1228
|
+
return this.ctx.send("private/transfer_spot", {
|
|
1229
|
+
subaccount_id: params.subaccountId,
|
|
1230
|
+
to_subaccount_id: toSubaccountId,
|
|
1231
|
+
new_subaccount_manager: newSubaccountManager,
|
|
1232
|
+
asset_name: asset.name,
|
|
1233
|
+
sub_id: subId,
|
|
1234
|
+
amount: decimalString(params.amount),
|
|
1235
|
+
max_fee_usd: decimalString(maxFeeUsd),
|
|
1236
|
+
// Nonces are UTC-nanosecond decimal strings beyond 2^53; the API accepts string-or-number despite the generated `number`.
|
|
1237
|
+
nonce: action.fields.nonce,
|
|
1238
|
+
signer: action.fields.signer,
|
|
1239
|
+
signature: action.signature,
|
|
1240
|
+
signature_expiry_sec: action.fields.expirySec
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Adds/removes recipient wallets on the owner's external-transfer
|
|
1245
|
+
* whitelist (`private/update_whitelisted_recipients`). The resulting
|
|
1246
|
+
* list is `(current ∪ add) \ remove`. Required before `transferExternal`
|
|
1247
|
+
* can send to a given wallet.
|
|
1248
|
+
*/
|
|
1249
|
+
async updateWhitelistedRecipients(params) {
|
|
1250
|
+
const add = params.add ?? [];
|
|
1251
|
+
const remove = params.remove ?? [];
|
|
1252
|
+
if (add.length === 0 && remove.length === 0) {
|
|
1253
|
+
throw new Error("updateWhitelistedRecipients needs at least one address to add or remove");
|
|
1254
|
+
}
|
|
1255
|
+
const { ownerAddress } = this.ctx.credentials();
|
|
1256
|
+
const action = this.signAction(
|
|
1257
|
+
0,
|
|
1258
|
+
this.ctx.network.modules.whitelistedRecipient,
|
|
1259
|
+
encodeUpdateWhitelistedRecipients({ add, remove }),
|
|
1260
|
+
params
|
|
1261
|
+
);
|
|
1262
|
+
return this.ctx.send("private/update_whitelisted_recipients", {
|
|
1263
|
+
wallet: ownerAddress,
|
|
1264
|
+
add: add.map((a) => getAddress6(a)),
|
|
1265
|
+
remove: remove.map((a) => getAddress6(a)),
|
|
1266
|
+
// Nonces are UTC-nanosecond decimal strings beyond 2^53; the API accepts string-or-number despite the generated `number`.
|
|
1267
|
+
nonce: action.fields.nonce,
|
|
1268
|
+
signer: action.fields.signer,
|
|
1269
|
+
signature: action.signature,
|
|
1270
|
+
signature_expiry_sec: action.fields.expirySec
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Moves a spot balance to another owner's wallet
|
|
1275
|
+
* (`private/transfer_spot_external`). The recipient must already be on
|
|
1276
|
+
* the sender's whitelist — call `updateWhitelistedRecipients` first;
|
|
1277
|
+
* otherwise the exchange rejects with RPC error 11033 "Transfer
|
|
1278
|
+
* recipient not whitelisted".
|
|
1279
|
+
*/
|
|
1280
|
+
async transferExternal(params) {
|
|
1281
|
+
const recipient = getAddress6(params.recipient);
|
|
1282
|
+
const toSubaccountId = params.toSubaccountId ?? 0;
|
|
1283
|
+
const newSubaccountManager = params.newSubaccountManager ?? 0;
|
|
1284
|
+
if (toSubaccountId === 0 && newSubaccountManager === 0) {
|
|
1285
|
+
throw new Error("specify the recipient's toSubaccountId or, to create one for them, newSubaccountManager");
|
|
1286
|
+
}
|
|
1287
|
+
const subId = params.subId ?? 0;
|
|
1288
|
+
const asset = await resolveSpotAsset(this.ctx, params.asset);
|
|
1289
|
+
const action = this.signAction(
|
|
1290
|
+
params.subaccountId,
|
|
1291
|
+
this.ctx.network.modules.externalTransfer,
|
|
1292
|
+
encodeExternalTransfer({
|
|
1293
|
+
toSubaccountId,
|
|
1294
|
+
newSubaccountManager,
|
|
1295
|
+
asset: asset.address,
|
|
1296
|
+
subId,
|
|
1297
|
+
amount: params.amount,
|
|
1298
|
+
maxFeeUsd: params.maxFeeUsd,
|
|
1299
|
+
recipient
|
|
1300
|
+
}),
|
|
1301
|
+
params
|
|
1302
|
+
);
|
|
1303
|
+
return this.ctx.send("private/transfer_spot_external", {
|
|
1304
|
+
subaccount_id: params.subaccountId,
|
|
1305
|
+
recipient_address: recipient,
|
|
1306
|
+
to_subaccount_id: toSubaccountId,
|
|
1307
|
+
new_subaccount_manager: newSubaccountManager,
|
|
1308
|
+
asset_name: asset.name,
|
|
1309
|
+
sub_id: subId,
|
|
1310
|
+
amount: decimalString(params.amount),
|
|
1311
|
+
max_fee_usd: decimalString(params.maxFeeUsd),
|
|
1312
|
+
// Nonces are UTC-nanosecond decimal strings beyond 2^53; the API accepts string-or-number despite the generated `number`.
|
|
1313
|
+
nonce: action.fields.nonce,
|
|
1314
|
+
signer: action.fields.signer,
|
|
1315
|
+
signature: action.signature,
|
|
1316
|
+
signature_expiry_sec: action.fields.expirySec
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
signAction(subaccountId, module, data, overrides) {
|
|
1320
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
1321
|
+
return new SignedAction(
|
|
1322
|
+
{
|
|
1323
|
+
subaccountId,
|
|
1324
|
+
nonce: overrides.nonce ?? randomNonce(),
|
|
1325
|
+
module,
|
|
1326
|
+
data,
|
|
1327
|
+
expirySec: overrides.signatureExpirySec ?? expiresIn(DEFAULT_SIGNATURE_EXPIRY_SEC),
|
|
1328
|
+
owner: ownerAddress,
|
|
1329
|
+
signer: signer.address
|
|
1330
|
+
},
|
|
1331
|
+
domainSeparator(this.ctx.network)
|
|
1332
|
+
).sign(signer);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
// src/api/subaccounts.ts
|
|
1337
|
+
var SubaccountsApi = class {
|
|
1338
|
+
constructor(ctx) {
|
|
1339
|
+
this.ctx = ctx;
|
|
1340
|
+
}
|
|
1341
|
+
ctx;
|
|
1342
|
+
/** Ids of all subaccounts owned by the authenticated wallet. */
|
|
1343
|
+
async list() {
|
|
1344
|
+
const result = await this.ctx.send("private/get_subaccounts", {
|
|
1345
|
+
wallet: this.ctx.credentials().ownerAddress
|
|
1346
|
+
});
|
|
1347
|
+
return result.subaccount_ids;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Full portfolio snapshot: collaterals, positions, open orders, and
|
|
1351
|
+
* the margin figures (there is no separate margin endpoint).
|
|
1352
|
+
*/
|
|
1353
|
+
get(subaccountId) {
|
|
1354
|
+
return this.ctx.send("private/get_subaccount", { subaccount_id: subaccountId });
|
|
1355
|
+
}
|
|
1356
|
+
/** Account-level info for the authenticated wallet: fee tiers, subaccount ids, and rate limits. */
|
|
1357
|
+
getAccount() {
|
|
1358
|
+
return this.ctx.send("private/get_account", {
|
|
1359
|
+
wallet: this.ctx.credentials().ownerAddress
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
/** Full portfolio snapshot for every subaccount owned by the authenticated wallet. */
|
|
1363
|
+
getAllPortfolios() {
|
|
1364
|
+
return this.ctx.send("private/get_all_portfolios", {
|
|
1365
|
+
wallet: this.ctx.credentials().ownerAddress
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
/** Open positions for a subaccount. */
|
|
1369
|
+
getPositions(subaccountId) {
|
|
1370
|
+
return this.ctx.send("private/get_positions", { subaccount_id: subaccountId });
|
|
1371
|
+
}
|
|
1372
|
+
/** Rename a subaccount. */
|
|
1373
|
+
changeLabel(params) {
|
|
1374
|
+
return this.ctx.send("private/change_subaccount_label", {
|
|
1375
|
+
subaccount_id: params.subaccountId,
|
|
1376
|
+
label: params.label
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
/** Filtered and paginated fills; defaults to all of the wallet's subaccounts. */
|
|
1380
|
+
getTradeHistory(query = {}) {
|
|
1381
|
+
return this.ctx.send("private/get_trade_history", {
|
|
1382
|
+
subaccount_id: query.subaccountId ?? null,
|
|
1383
|
+
instrument_name: query.instrumentName ?? null,
|
|
1384
|
+
order_id: query.orderId ?? null,
|
|
1385
|
+
quote_id: query.quoteId ?? null,
|
|
1386
|
+
from_timestamp: query.fromTimestamp ?? null,
|
|
1387
|
+
to_timestamp: query.toTimestamp ?? null,
|
|
1388
|
+
page: query.page ?? null,
|
|
1389
|
+
page_size: query.pageSize ?? null
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
/** Realized interest settlements, newest first; defaults to the whole wallet. Capped at 1000 rows. */
|
|
1393
|
+
getInterestHistory(query = {}) {
|
|
1394
|
+
return this.ctx.send("private/get_interest_history", {
|
|
1395
|
+
wallet: query.subaccountId == null ? this.ctx.credentials().ownerAddress : void 0,
|
|
1396
|
+
subaccount_id: query.subaccountId ?? void 0,
|
|
1397
|
+
start_timestamp: query.startTimestamp ?? void 0,
|
|
1398
|
+
end_timestamp: query.endTimestamp ?? void 0
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
/** Internal ERC-20 transfers touching the wallet's subaccounts; defaults to the whole wallet. Capped at 1000 rows. */
|
|
1402
|
+
getErc20TransferHistory(query = {}) {
|
|
1403
|
+
return this.ctx.send("private/get_erc20_transfer_history", {
|
|
1404
|
+
wallet: query.subaccountId == null ? this.ctx.credentials().ownerAddress : void 0,
|
|
1405
|
+
subaccount_id: query.subaccountId ?? void 0,
|
|
1406
|
+
start_timestamp: query.startTimestamp ?? void 0,
|
|
1407
|
+
end_timestamp: query.endTimestamp ?? void 0
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
/** Settled option positions; defaults to the whole wallet, pass `subaccountId` to scope to one. */
|
|
1411
|
+
getOptionSettlementHistory(query = {}) {
|
|
1412
|
+
return this.ctx.send("private/get_option_settlement_history", {
|
|
1413
|
+
wallet: query.subaccountId == null ? this.ctx.credentials().ownerAddress : void 0,
|
|
1414
|
+
subaccount_id: query.subaccountId ?? void 0
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Margin figures for a subaccount, optionally under simulated position/collateral changes.
|
|
1419
|
+
* Predates the schema, so it is absent from the generated EndpointMap and sent untyped.
|
|
1420
|
+
*/
|
|
1421
|
+
getMargin(query) {
|
|
1422
|
+
return this.ctx.send(
|
|
1423
|
+
"private/get_margin",
|
|
1424
|
+
{
|
|
1425
|
+
subaccount_id: query.subaccountId,
|
|
1426
|
+
simulated_position_changes: query.simulatedPositionChanges?.map((p) => ({
|
|
1427
|
+
instrument_name: p.instrumentName,
|
|
1428
|
+
amount: p.amount,
|
|
1429
|
+
entry_price: p.entryPrice ?? null
|
|
1430
|
+
})) ?? null,
|
|
1431
|
+
simulated_collateral_changes: query.simulatedCollateralChanges?.map((c) => ({
|
|
1432
|
+
asset_name: c.assetName,
|
|
1433
|
+
amount: c.amount
|
|
1434
|
+
})) ?? null
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Auctions in which a subaccount (or the whole wallet) was liquidated.
|
|
1440
|
+
* Predates the schema, so it is absent from the generated EndpointMap and sent untyped.
|
|
1441
|
+
*/
|
|
1442
|
+
getLiquidationHistory(query = {}) {
|
|
1443
|
+
return this.ctx.send(
|
|
1444
|
+
"private/get_liquidation_history",
|
|
1445
|
+
{
|
|
1446
|
+
wallet: query.subaccountId == null ? this.ctx.credentials().ownerAddress : void 0,
|
|
1447
|
+
subaccount_id: query.subaccountId ?? void 0,
|
|
1448
|
+
start_timestamp: query.startTimestamp ?? void 0,
|
|
1449
|
+
end_timestamp: query.endTimestamp ?? void 0
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Paginated auctions a subaccount participated in as the liquidator.
|
|
1455
|
+
* Predates the schema, so it is absent from the generated EndpointMap and sent untyped.
|
|
1456
|
+
*/
|
|
1457
|
+
getLiquidatorHistory(query) {
|
|
1458
|
+
return this.ctx.send(
|
|
1459
|
+
"private/get_liquidator_history",
|
|
1460
|
+
{
|
|
1461
|
+
subaccount_id: query.subaccountId,
|
|
1462
|
+
start_timestamp: query.startTimestamp ?? null,
|
|
1463
|
+
end_timestamp: query.endTimestamp ?? null,
|
|
1464
|
+
page: query.page ?? null,
|
|
1465
|
+
page_size: query.pageSize ?? null
|
|
1466
|
+
}
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
// src/api/vaults.ts
|
|
1472
|
+
import { formatUnits as formatUnits5, getAddress as getAddress7 } from "ethers";
|
|
1473
|
+
var SHAREHOLDER_INTENT_TTL_SEC = 600;
|
|
1474
|
+
function signVaultAction(ctx, subaccountId, data, overrides, defaultTtlSec) {
|
|
1475
|
+
const { ownerAddress, signer } = ctx.credentials();
|
|
1476
|
+
const action = new SignedAction(
|
|
1477
|
+
{
|
|
1478
|
+
subaccountId,
|
|
1479
|
+
nonce: overrides.nonce ?? randomNonce(),
|
|
1480
|
+
module: ctx.network.modules.vault,
|
|
1481
|
+
data,
|
|
1482
|
+
expirySec: overrides.signatureExpirySec ?? expiresIn(defaultTtlSec),
|
|
1483
|
+
owner: ownerAddress,
|
|
1484
|
+
signer: signer.address
|
|
1485
|
+
},
|
|
1486
|
+
domainSeparator(ctx.network)
|
|
1487
|
+
);
|
|
1488
|
+
return action.sign(signer);
|
|
1489
|
+
}
|
|
1490
|
+
function signedEnvelope(action) {
|
|
1491
|
+
return {
|
|
1492
|
+
subaccount_id: Number(action.fields.subaccountId),
|
|
1493
|
+
// The API deserializes nonce via string_or_number; nanosecond nonces
|
|
1494
|
+
// exceed 2^53, so send the decimal string despite the generated type.
|
|
1495
|
+
nonce: action.fields.nonce,
|
|
1496
|
+
signature_expiry_sec: action.fields.expirySec,
|
|
1497
|
+
signer: action.fields.signer,
|
|
1498
|
+
signature: action.signature
|
|
1499
|
+
// set by sign() above
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
var ShareholderVaultsApi = class {
|
|
1503
|
+
constructor(ctx) {
|
|
1504
|
+
this.ctx = ctx;
|
|
1505
|
+
}
|
|
1506
|
+
ctx;
|
|
1507
|
+
/** The vault subaccount ids the wallet holds shares in. */
|
|
1508
|
+
async getShareholderVaults() {
|
|
1509
|
+
return this.ctx.send("private/get_shareholder_vaults", { wallet: this.ctx.credentials().ownerAddress });
|
|
1510
|
+
}
|
|
1511
|
+
/** The wallet's share balance plus the full vault row for every vault it holds shares in. */
|
|
1512
|
+
async getShares() {
|
|
1513
|
+
return this.ctx.send("private/get_vault_shares", { wallet: this.ctx.credentials().ownerAddress });
|
|
1514
|
+
}
|
|
1515
|
+
/** The wallet's queued deposit/withdraw intents still awaiting curator settlement. */
|
|
1516
|
+
async getLiveRequests() {
|
|
1517
|
+
return this.ctx.send("private/get_live_vault_requests", { wallet: this.ctx.credentials().ownerAddress });
|
|
1518
|
+
}
|
|
1519
|
+
/** The wallet's full vault action history (every lifecycle status). */
|
|
1520
|
+
async getRequestHistory(options = {}) {
|
|
1521
|
+
return this.ctx.send("private/get_vault_request_history", {
|
|
1522
|
+
wallet: this.ctx.credentials().ownerAddress,
|
|
1523
|
+
page: options.page,
|
|
1524
|
+
page_size: options.pageSize
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Queues a deposit into a vault. The curator later mints shares at a
|
|
1529
|
+
* quoted price committed to this exact signed payload; funds are held
|
|
1530
|
+
* from the source subaccount until then (or until cancelled).
|
|
1531
|
+
*/
|
|
1532
|
+
async requestDeposit(request) {
|
|
1533
|
+
const amountE18 = toE18(request.amount);
|
|
1534
|
+
const data = encodeVaultDeposit({
|
|
1535
|
+
vaultSubaccountId: request.vaultSubaccountId,
|
|
1536
|
+
depositSpotAsset: request.depositSpotAsset,
|
|
1537
|
+
amount: amountE18
|
|
1538
|
+
});
|
|
1539
|
+
const action = signVaultAction(this.ctx, request.subaccountId, data, request, SHAREHOLDER_INTENT_TTL_SEC);
|
|
1540
|
+
return this.ctx.send("private/request_vault_deposit", {
|
|
1541
|
+
...signedEnvelope(action),
|
|
1542
|
+
vault_subaccount_id: request.vaultSubaccountId,
|
|
1543
|
+
deposit_spot_asset: getAddress7(request.depositSpotAsset),
|
|
1544
|
+
amount: formatUnits5(amountE18, 18)
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
/** Queues a share redemption; the curator later burns the shares at a quoted price. */
|
|
1548
|
+
async requestWithdraw(request) {
|
|
1549
|
+
const sharesE18 = toE18(request.sharesToBurn);
|
|
1550
|
+
const data = encodeVaultWithdraw({
|
|
1551
|
+
vaultSubaccountId: request.vaultSubaccountId,
|
|
1552
|
+
sharesToBurn: sharesE18
|
|
1553
|
+
});
|
|
1554
|
+
const action = signVaultAction(this.ctx, request.subaccountId, data, request, SHAREHOLDER_INTENT_TTL_SEC);
|
|
1555
|
+
return this.ctx.send("private/request_vault_withdraw", {
|
|
1556
|
+
...signedEnvelope(action),
|
|
1557
|
+
vault_subaccount_id: request.vaultSubaccountId,
|
|
1558
|
+
shares_to_burn: formatUnits5(sharesE18, 18)
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
/** Cancels ALL of the wallet's pending intents for a vault (deposits and withdrawals). */
|
|
1562
|
+
async cancelAllRequests(request) {
|
|
1563
|
+
const data = encodeVaultCancel(request.vaultSubaccountId);
|
|
1564
|
+
const action = signVaultAction(this.ctx, request.subaccountId, data, request, SHAREHOLDER_INTENT_TTL_SEC);
|
|
1565
|
+
return this.ctx.send("private/cancel_all_vault_requests", {
|
|
1566
|
+
...signedEnvelope(action),
|
|
1567
|
+
vault_subaccount_id: request.vaultSubaccountId
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
var CuratorVaultsApi = class {
|
|
1572
|
+
constructor(ctx) {
|
|
1573
|
+
this.ctx = ctx;
|
|
1574
|
+
}
|
|
1575
|
+
ctx;
|
|
1576
|
+
/** The vault subaccount ids this wallet curates. */
|
|
1577
|
+
async getCuratedVaults() {
|
|
1578
|
+
return this.ctx.send("private/get_curated_vaults", { wallet: this.ctx.credentials().ownerAddress });
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Creates a vault funded from the signer's subaccount; the signer
|
|
1582
|
+
* becomes its curator. Fee rates and max slippage are immutable
|
|
1583
|
+
* afterwards.
|
|
1584
|
+
*/
|
|
1585
|
+
async createVault(request) {
|
|
1586
|
+
const initialDepositE18 = toE18(request.initialDeposit);
|
|
1587
|
+
const maxFeeUsdE18 = toE18(request.maxFeeUsd);
|
|
1588
|
+
const sharePriceUsdE18 = toE18(request.initialSharePriceUsd);
|
|
1589
|
+
const benchmarkAsset = request.benchmarkAsset === void 0 ? void 0 : getAddress7(request.benchmarkAsset);
|
|
1590
|
+
const data = encodeVaultCreate({
|
|
1591
|
+
managerId: request.managerId,
|
|
1592
|
+
depositSpotAsset: request.depositSpotAsset,
|
|
1593
|
+
initialDeposit: initialDepositE18,
|
|
1594
|
+
managementFeeBps: request.managementFeeBps,
|
|
1595
|
+
performanceFeeBps: request.performanceFeeBps,
|
|
1596
|
+
maxSlippageBps: request.maxSlippageBps,
|
|
1597
|
+
cooldownSec: request.cooldownSec,
|
|
1598
|
+
maxFeeUsd: maxFeeUsdE18,
|
|
1599
|
+
initialSharePriceUsd: sharePriceUsdE18,
|
|
1600
|
+
benchmarkAsset
|
|
1601
|
+
});
|
|
1602
|
+
const action = signVaultAction(this.ctx, request.subaccountId, data, request, DEFAULT_SIGNATURE_EXPIRY_SEC);
|
|
1603
|
+
return this.ctx.send("private/create_vault", {
|
|
1604
|
+
...signedEnvelope(action),
|
|
1605
|
+
manager_id: request.managerId,
|
|
1606
|
+
deposit_spot_asset: getAddress7(request.depositSpotAsset),
|
|
1607
|
+
initial_deposit: formatUnits5(initialDepositE18, 18),
|
|
1608
|
+
management_fee_bps: request.managementFeeBps,
|
|
1609
|
+
performance_fee_bps: request.performanceFeeBps,
|
|
1610
|
+
max_slippage_bps: request.maxSlippageBps,
|
|
1611
|
+
cooldown_sec: request.cooldownSec,
|
|
1612
|
+
max_fee_usd: formatUnits5(maxFeeUsdE18, 18),
|
|
1613
|
+
initial_share_price_usd: formatUnits5(sharePriceUsdE18, 18),
|
|
1614
|
+
// Presence drives the signed hasBenchmark flag; the exchange derives it the same way.
|
|
1615
|
+
benchmark_asset: benchmarkAsset ?? null
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/** Settles one queued shareholder deposit, minting shares at the quoted price. */
|
|
1619
|
+
async mintShares(request) {
|
|
1620
|
+
const sharePriceE18 = toE18(request.sharePrice);
|
|
1621
|
+
const data = encodeVaultMintShares({ sharePrice: sharePriceE18, depositHash: request.depositHash });
|
|
1622
|
+
const action = signVaultAction(this.ctx, request.vaultSubaccountId, data, request, DEFAULT_SIGNATURE_EXPIRY_SEC);
|
|
1623
|
+
return this.ctx.send("private/mint_vault_shares", {
|
|
1624
|
+
...signedEnvelope(action),
|
|
1625
|
+
share_price: formatUnits5(sharePriceE18, 18),
|
|
1626
|
+
deposit_hash: request.depositHash,
|
|
1627
|
+
request_id: request.requestId
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
/** Settles one queued shareholder withdrawal, burning shares at the quoted price. */
|
|
1631
|
+
async burnShares(request) {
|
|
1632
|
+
const sharePriceE18 = toE18(request.sharePrice);
|
|
1633
|
+
const data = encodeVaultBurnShares({ sharePrice: sharePriceE18, withdrawHash: request.withdrawHash });
|
|
1634
|
+
const action = signVaultAction(this.ctx, request.vaultSubaccountId, data, request, DEFAULT_SIGNATURE_EXPIRY_SEC);
|
|
1635
|
+
return this.ctx.send("private/burn_vault_shares", {
|
|
1636
|
+
...signedEnvelope(action),
|
|
1637
|
+
share_price: formatUnits5(sharePriceE18, 18),
|
|
1638
|
+
withdraw_hash: request.withdrawHash,
|
|
1639
|
+
request_id: request.requestId
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
/** Updates the vault's advisory metadata (unsigned; an ownership check gates it to the curator). */
|
|
1643
|
+
async updateInfo(request) {
|
|
1644
|
+
return this.ctx.send("private/update_vault_info", {
|
|
1645
|
+
subaccount_id: request.vaultSubaccountId,
|
|
1646
|
+
name: request.name,
|
|
1647
|
+
description: request.description,
|
|
1648
|
+
mtm_cap: request.mtmCap === void 0 ? void 0 : formatUnits5(toE18(request.mtmCap), 18),
|
|
1649
|
+
whitelist_only: request.whitelistOnly
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
/** The vault's queued deposit intents awaiting a `mintShares` settlement, FIFO-ordered with the queue total. */
|
|
1653
|
+
async getLiveMintRequests(vaultSubaccountId, limit = 100) {
|
|
1654
|
+
return this.ctx.send("private/get_live_mint_requests", { subaccount_id: vaultSubaccountId, limit });
|
|
1655
|
+
}
|
|
1656
|
+
/** The vault's queued withdraw intents awaiting a `burnShares` settlement, FIFO-ordered with the queue total. */
|
|
1657
|
+
async getLiveBurnRequests(vaultSubaccountId, limit = 100) {
|
|
1658
|
+
return this.ctx.send("private/get_live_burn_requests", { subaccount_id: vaultSubaccountId, limit });
|
|
1659
|
+
}
|
|
1660
|
+
/** Rejects a queued deposit intent, releasing the holder's funds without minting shares. */
|
|
1661
|
+
async rejectDepositRequest(requestId, reason) {
|
|
1662
|
+
return this.ctx.send("private/reject_deposit_request", {
|
|
1663
|
+
request_id: requestId,
|
|
1664
|
+
reason,
|
|
1665
|
+
wallet: this.ctx.credentials().ownerAddress
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Force-exits a holder at mark-to-market with no curator price quote (unsigned;
|
|
1670
|
+
* an ownership check gates it to the curator of `vaultSubaccountId`).
|
|
1671
|
+
*/
|
|
1672
|
+
async forceBurn(vaultSubaccountId, holder) {
|
|
1673
|
+
return this.ctx.send("private/force_burn", { subaccount_id: vaultSubaccountId, holder });
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
var VaultsApi = class {
|
|
1677
|
+
constructor(ctx) {
|
|
1678
|
+
this.ctx = ctx;
|
|
1679
|
+
this.shareholder = new ShareholderVaultsApi(ctx);
|
|
1680
|
+
this.curator = new CuratorVaultsApi(ctx);
|
|
1681
|
+
}
|
|
1682
|
+
ctx;
|
|
1683
|
+
shareholder;
|
|
1684
|
+
curator;
|
|
1685
|
+
async getVault(vaultSubaccountId) {
|
|
1686
|
+
return this.ctx.send("public/get_vault", { subaccount_id: vaultSubaccountId });
|
|
1687
|
+
}
|
|
1688
|
+
async listVaults(options = {}) {
|
|
1689
|
+
return this.ctx.send("public/get_vaults", { page: options.page, page_size: options.pageSize });
|
|
1690
|
+
}
|
|
1691
|
+
async getActionHistory(vaultSubaccountId, options = {}) {
|
|
1692
|
+
return this.ctx.send("public/get_vault_action_history", {
|
|
1693
|
+
subaccount_id: vaultSubaccountId,
|
|
1694
|
+
event_type: options.eventType,
|
|
1695
|
+
page: options.page,
|
|
1696
|
+
page_size: options.pageSize
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
async getPerformanceHistory(vaultSubaccountId, resolution, options = {}) {
|
|
1700
|
+
return this.ctx.send("public/get_vault_performance_history", {
|
|
1701
|
+
subaccount_id: vaultSubaccountId,
|
|
1702
|
+
resolution,
|
|
1703
|
+
...options
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
// src/api/withdrawals.ts
|
|
1709
|
+
import { getAddress as getAddress8 } from "ethers";
|
|
1710
|
+
var WithdrawalsApi = class {
|
|
1711
|
+
constructor(ctx) {
|
|
1712
|
+
this.ctx = ctx;
|
|
1713
|
+
}
|
|
1714
|
+
ctx;
|
|
1715
|
+
/** Builds the signed wire payload shared by `withdraw` and `withdrawDebug`. */
|
|
1716
|
+
async buildWithdrawPayload(params) {
|
|
1717
|
+
const { ownerAddress, signer } = this.ctx.credentials();
|
|
1718
|
+
const recipient = getAddress8(params.recipient ?? signer.address);
|
|
1719
|
+
if (recipient !== getAddress8(signer.address)) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`the exchange pays withdrawals out to the action signer (${signer.address}); recipient ${recipient} cannot be honored`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
const asset = await resolveSpotAsset(this.ctx, params.asset);
|
|
1725
|
+
if (params.decimals !== void 0 && params.decimals !== asset.decimals) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
`decimals ${params.decimals} does not match the exchange's metadata for ${asset.name} (${asset.decimals}) \u2014 the signature would be rejected`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
const forceBatch = params.forceBatch ?? false;
|
|
1731
|
+
const maxFeeUsd = params.maxFeeUsd ?? (forceBatch ? "10" : "1");
|
|
1732
|
+
const data = encodeWithdrawal({
|
|
1733
|
+
protocolAsset: asset.address,
|
|
1734
|
+
maxFeeUsd,
|
|
1735
|
+
recipient,
|
|
1736
|
+
amount: params.amount,
|
|
1737
|
+
decimals: asset.decimals,
|
|
1738
|
+
forceBatch
|
|
1739
|
+
});
|
|
1740
|
+
const action = new SignedAction(
|
|
1741
|
+
{
|
|
1742
|
+
subaccountId: params.subaccountId,
|
|
1743
|
+
nonce: params.nonce ?? randomNonce(),
|
|
1744
|
+
module: this.ctx.network.modules.withdrawal,
|
|
1745
|
+
data,
|
|
1746
|
+
expirySec: params.signatureExpirySec ?? expiresIn(DEFAULT_SIGNATURE_EXPIRY_SEC),
|
|
1747
|
+
owner: ownerAddress,
|
|
1748
|
+
signer: signer.address
|
|
1749
|
+
},
|
|
1750
|
+
domainSeparator(this.ctx.network)
|
|
1751
|
+
).sign(signer);
|
|
1752
|
+
return {
|
|
1753
|
+
subaccount_id: params.subaccountId,
|
|
1754
|
+
asset_name: asset.name,
|
|
1755
|
+
amount_in_underlying: decimalString(params.amount, asset.decimals),
|
|
1756
|
+
max_fee_usd: decimalString(maxFeeUsd),
|
|
1757
|
+
force_batch: forceBatch,
|
|
1758
|
+
// Nonces are UTC-nanosecond decimal strings beyond 2^53; the API accepts string-or-number despite the generated `number`.
|
|
1759
|
+
nonce: action.fields.nonce,
|
|
1760
|
+
signer: action.fields.signer,
|
|
1761
|
+
signature: action.signature,
|
|
1762
|
+
signature_expiry_sec: action.fields.expirySec
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
async withdraw(params) {
|
|
1766
|
+
return this.ctx.send("private/withdraw", await this.buildWithdrawPayload(params));
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Debug helper: signs the withdrawal exactly like `withdraw` but submits it
|
|
1770
|
+
* to `public/withdraw_debug`, which returns the server-computed encoded data
|
|
1771
|
+
* and action/typed-data hashes instead of executing. Use it to verify your
|
|
1772
|
+
* client-side signing matches the exchange's.
|
|
1773
|
+
*/
|
|
1774
|
+
async withdrawDebug(params) {
|
|
1775
|
+
return this.ctx.send("public/withdraw_debug", await this.buildWithdrawPayload(params));
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Withdrawal history rows (up to 1000). Scoped to one subaccount when
|
|
1779
|
+
* `subaccountId` is given, otherwise to the whole owner wallet.
|
|
1780
|
+
* Timestamps are unix milliseconds.
|
|
1781
|
+
*/
|
|
1782
|
+
async getHistory(options = {}) {
|
|
1783
|
+
return this.ctx.send("private/get_withdrawal_history", {
|
|
1784
|
+
subaccount_id: options.subaccountId,
|
|
1785
|
+
wallet: options.subaccountId === void 0 ? this.ctx.credentials().ownerAddress : void 0,
|
|
1786
|
+
start_timestamp: options.startTimestampMs,
|
|
1787
|
+
end_timestamp: options.endTimestampMs
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/auth/auth.ts
|
|
1793
|
+
async function authHeaders(credentials) {
|
|
1794
|
+
const timestamp = Date.now().toString();
|
|
1795
|
+
const signature = await credentials.signer.signMessage(timestamp);
|
|
1796
|
+
return {
|
|
1797
|
+
"X-LyraWallet": credentials.ownerAddress,
|
|
1798
|
+
"X-LyraTimestamp": timestamp,
|
|
1799
|
+
"X-LyraSignature": signature
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
async function loginParams(credentials) {
|
|
1803
|
+
const timestamp = Date.now().toString();
|
|
1804
|
+
return {
|
|
1805
|
+
wallet: credentials.ownerAddress,
|
|
1806
|
+
timestamp,
|
|
1807
|
+
signature: await credentials.signer.signMessage(timestamp)
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/signing/modules.ts
|
|
1812
|
+
var V3_MODULE_ADDRESSES = {
|
|
1813
|
+
trade: "0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b",
|
|
1814
|
+
transfer: "0x01259207A40925b794C8ac320456F7F6c8FE2636",
|
|
1815
|
+
withdrawal: "0x9d0E8f5b25384C7310CB8C6aE32C8fbeb645d083",
|
|
1816
|
+
rfq: "0x9371352CCef6f5b36EfDFE90942fFE622Ab77F1D",
|
|
1817
|
+
externalTransfer: "0x8F9B8f12ddA05FB1F0DDDDe8f5af8cECF54f8aC9",
|
|
1818
|
+
whitelistedRecipient: "0xB86D6DE1b76c9839e4BA860848CD98A1dABd6B54",
|
|
1819
|
+
vault: "0x2885c174ebf5524aED9c721d60c12b1537685186",
|
|
1820
|
+
liquidation: "0x66d23e59DaEEF13904eFA2D4B8658aeD05f59a92",
|
|
1821
|
+
createSessionKey: "0xe330CF64ff6EbF41699aad344Cb21d78db1D2bb6"
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
// src/config/networks.ts
|
|
1825
|
+
var NETWORKS = {
|
|
1826
|
+
mainnet: {
|
|
1827
|
+
name: "mainnet",
|
|
1828
|
+
httpUrl: "https://api.derive.xyz/v3",
|
|
1829
|
+
wsUrl: "wss://api.derive.xyz/v3/ws",
|
|
1830
|
+
chainId: 1,
|
|
1831
|
+
// Ethereum L1
|
|
1832
|
+
modules: { ...V3_MODULE_ADDRESSES },
|
|
1833
|
+
contracts: {
|
|
1834
|
+
// Placeholder — set the mainnet ActionManager address once published.
|
|
1835
|
+
actionManager: "0x0000000000000000000000000000000000000000",
|
|
1836
|
+
usdc: "0x6879287835A86F50f784313dBEd5E5cCC5bb8481",
|
|
1837
|
+
cash: "0x57B03E14d409ADC7fAb6CFc44b5886CAD2D5f02b"
|
|
1838
|
+
}
|
|
1839
|
+
},
|
|
1840
|
+
testnet: {
|
|
1841
|
+
name: "testnet",
|
|
1842
|
+
httpUrl: "https://testnet.api.derive.xyz/v3",
|
|
1843
|
+
wsUrl: "wss://testnet.api.derive.xyz/v3/ws",
|
|
1844
|
+
chainId: 11155111,
|
|
1845
|
+
// Sepolia
|
|
1846
|
+
modules: { ...V3_MODULE_ADDRESSES },
|
|
1847
|
+
contracts: {
|
|
1848
|
+
actionManager: "0xcd84E4CC2996787D2F6794Ce99FeCda5Edb10E86",
|
|
1849
|
+
usdc: "0x7F4B9B80863b937340202f26Ad555CC7D3ABD2BA"
|
|
1850
|
+
}
|
|
1851
|
+
},
|
|
1852
|
+
local: {
|
|
1853
|
+
name: "local",
|
|
1854
|
+
httpUrl: "http://localhost:8080",
|
|
1855
|
+
wsUrl: "ws://localhost:3000/ws",
|
|
1856
|
+
chainId: 31337,
|
|
1857
|
+
// anvil
|
|
1858
|
+
modules: { ...V3_MODULE_ADDRESSES },
|
|
1859
|
+
contracts: {
|
|
1860
|
+
actionManager: "0x0165878A594ca255338adfa4d48449f69242Eb8F",
|
|
1861
|
+
usdc: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0"
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
function resolveNetwork(network) {
|
|
1866
|
+
if (typeof network !== "string") return network;
|
|
1867
|
+
const preset = NETWORKS[network];
|
|
1868
|
+
if (!preset) {
|
|
1869
|
+
throw new Error(`unknown network preset '${network}' \u2014 pass a full NetworkConfig instead`);
|
|
1870
|
+
}
|
|
1871
|
+
return preset;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/subscriptions/subscriptions.ts
|
|
1875
|
+
var STREAM_BUFFER_LIMIT = 1e4;
|
|
1876
|
+
var Subscriptions = class {
|
|
1877
|
+
constructor(ctx, ws) {
|
|
1878
|
+
this.ctx = ctx;
|
|
1879
|
+
this.ws = ws;
|
|
1880
|
+
ws.onNotification = (channelName, data) => this.dispatch(channelName, data);
|
|
1881
|
+
}
|
|
1882
|
+
ctx;
|
|
1883
|
+
ws;
|
|
1884
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1885
|
+
/**
|
|
1886
|
+
* Subscribes to a channel and registers `handler` for its
|
|
1887
|
+
* notifications. Multiple handlers share one wire subscription per
|
|
1888
|
+
* channel: the `subscribe` RPC is sent only for the first handler on a
|
|
1889
|
+
* channel, and `unsubscribe` only when the last is removed.
|
|
1890
|
+
*/
|
|
1891
|
+
async subscribe(ch, handler) {
|
|
1892
|
+
const entry = (data) => handler(data);
|
|
1893
|
+
const existing = this.handlers.get(ch.name);
|
|
1894
|
+
if (existing) {
|
|
1895
|
+
existing.add(entry);
|
|
1896
|
+
} else {
|
|
1897
|
+
const set = /* @__PURE__ */ new Set([entry]);
|
|
1898
|
+
this.handlers.set(ch.name, set);
|
|
1899
|
+
try {
|
|
1900
|
+
await this.sendChannelRpc("subscribe", [ch.name]);
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
set.delete(entry);
|
|
1903
|
+
if (set.size === 0) this.handlers.delete(ch.name);
|
|
1904
|
+
throw error;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
return {
|
|
1908
|
+
channel: ch.name,
|
|
1909
|
+
unsubscribe: async () => {
|
|
1910
|
+
const current = this.handlers.get(ch.name);
|
|
1911
|
+
if (!current?.delete(entry)) return;
|
|
1912
|
+
if (current.size === 0) {
|
|
1913
|
+
this.handlers.delete(ch.name);
|
|
1914
|
+
await this.sendChannelRpc("unsubscribe", [ch.name]);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
/**
|
|
1920
|
+
* The channel as an async iterator: subscribes on the first `next()`,
|
|
1921
|
+
* buffers notifications the consumer has not yet read (dropping the
|
|
1922
|
+
* oldest beyond `STREAM_BUFFER_LIMIT`), and unsubscribes when the
|
|
1923
|
+
* loop ends (`break` / `return()` / `throw()`).
|
|
1924
|
+
*/
|
|
1925
|
+
stream(ch) {
|
|
1926
|
+
const queue = [];
|
|
1927
|
+
const waiters = [];
|
|
1928
|
+
let subscribing;
|
|
1929
|
+
let done = false;
|
|
1930
|
+
let overflowWarned = false;
|
|
1931
|
+
const onData = (data) => {
|
|
1932
|
+
if (done) return;
|
|
1933
|
+
const waiter = waiters.shift();
|
|
1934
|
+
if (waiter) {
|
|
1935
|
+
waiter({ value: data, done: false });
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
queue.push(data);
|
|
1939
|
+
if (queue.length > STREAM_BUFFER_LIMIT) {
|
|
1940
|
+
queue.shift();
|
|
1941
|
+
if (!overflowWarned) {
|
|
1942
|
+
overflowWarned = true;
|
|
1943
|
+
this.ctx.logger(
|
|
1944
|
+
"warn",
|
|
1945
|
+
`stream ${ch.name}: buffer exceeded ${STREAM_BUFFER_LIMIT} notifications; dropping oldest`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
const finish = async () => {
|
|
1951
|
+
if (done) return;
|
|
1952
|
+
done = true;
|
|
1953
|
+
for (const waiter of waiters.splice(0)) waiter({ value: void 0, done: true });
|
|
1954
|
+
const subscription = await subscribing?.catch(() => void 0);
|
|
1955
|
+
await subscription?.unsubscribe();
|
|
1956
|
+
};
|
|
1957
|
+
return {
|
|
1958
|
+
[Symbol.asyncIterator]() {
|
|
1959
|
+
return this;
|
|
1960
|
+
},
|
|
1961
|
+
next: async () => {
|
|
1962
|
+
if (done) return { value: void 0, done: true };
|
|
1963
|
+
subscribing ??= this.subscribe(ch, onData);
|
|
1964
|
+
await subscribing;
|
|
1965
|
+
if (done) return { value: void 0, done: true };
|
|
1966
|
+
if (queue.length > 0) return { value: queue.shift(), done: false };
|
|
1967
|
+
return new Promise((resolve) => waiters.push(resolve));
|
|
1968
|
+
},
|
|
1969
|
+
return: async () => {
|
|
1970
|
+
await finish();
|
|
1971
|
+
return { value: void 0, done: true };
|
|
1972
|
+
},
|
|
1973
|
+
throw: async (error) => {
|
|
1974
|
+
await finish();
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
/** Re-sends `subscribe` for every active channel; the client calls this after a websocket reconnect. */
|
|
1980
|
+
async resubscribeAll() {
|
|
1981
|
+
const channels = [...this.handlers.keys()];
|
|
1982
|
+
if (channels.length === 0) return;
|
|
1983
|
+
const result = await this.ws.send("subscribe", { channels });
|
|
1984
|
+
for (const name of channels) {
|
|
1985
|
+
const status = result.status[name];
|
|
1986
|
+
if (status !== "ok" && status !== "already subscribed") {
|
|
1987
|
+
this.ctx.logger("error", `resubscribe to ${name} failed`, status);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* `subscribe`/`unsubscribe` exist only over the websocket, so they go
|
|
1993
|
+
* straight to the ws transport (never the client's HTTP fallback). A
|
|
1994
|
+
* per-channel `status` other than 'ok'/'already subscribed' throws.
|
|
1995
|
+
*/
|
|
1996
|
+
async sendChannelRpc(method, channels) {
|
|
1997
|
+
const result = await this.ws.send(method, { channels });
|
|
1998
|
+
for (const name of channels) {
|
|
1999
|
+
const status = result.status[name];
|
|
2000
|
+
if (status !== "ok" && status !== "already subscribed") {
|
|
2001
|
+
throw new Error(`${method} ${name} failed: ${String(status)}`);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
dispatch(channelName, data) {
|
|
2006
|
+
const set = this.handlers.get(channelName);
|
|
2007
|
+
if (!set || set.size === 0) {
|
|
2008
|
+
this.ctx.logger("debug", `dropping notification for unhandled channel ${channelName}`);
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
for (const handler of set) {
|
|
2012
|
+
try {
|
|
2013
|
+
handler(data);
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
this.ctx.logger("error", `subscription handler for ${channelName} threw`, error);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
// src/methodGuard.ts
|
|
2022
|
+
var PUBLIC_METHOD_PREFIXES = ["public/", "private/"];
|
|
2023
|
+
var WS_CONTROL_METHODS = /* @__PURE__ */ new Set(["subscribe", "unsubscribe"]);
|
|
2024
|
+
function isPublicApiMethod(method) {
|
|
2025
|
+
return PUBLIC_METHOD_PREFIXES.some((prefix) => method.startsWith(prefix));
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/transport/jsonrpc.ts
|
|
2029
|
+
var METHOD_FORMAT = /^[a-z]+\/[a-z0-9_]+$/;
|
|
2030
|
+
function assertPublicMethod(method) {
|
|
2031
|
+
if (WS_CONTROL_METHODS.has(method)) return;
|
|
2032
|
+
if (!METHOD_FORMAT.test(method)) {
|
|
2033
|
+
throw new Error(`invalid method '${method}': expected the form namespace/name`);
|
|
2034
|
+
}
|
|
2035
|
+
if (!isPublicApiMethod(method)) {
|
|
2036
|
+
throw new Error(`${method} is not part of the public Derive API`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
function unwrapResponse(method, raw) {
|
|
2040
|
+
if (raw.error) throw new DeriveRpcError(method, raw.error);
|
|
2041
|
+
if (!("result" in raw)) throw new Error(`${method}: malformed response \u2014 neither result nor error present`);
|
|
2042
|
+
return raw.result;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/transport/http.ts
|
|
2046
|
+
var HttpTransport = class {
|
|
2047
|
+
constructor(options) {
|
|
2048
|
+
this.options = options;
|
|
2049
|
+
}
|
|
2050
|
+
options;
|
|
2051
|
+
async send(method, params) {
|
|
2052
|
+
assertPublicMethod(method);
|
|
2053
|
+
const { baseUrl, timeoutMs, logger, authHeaders: authHeaders2 } = this.options;
|
|
2054
|
+
const headers = {
|
|
2055
|
+
"content-type": "application/json",
|
|
2056
|
+
// The API rejects UA-less requests; browsers ignore this
|
|
2057
|
+
// (the browser UA is a forbidden header) and send their own.
|
|
2058
|
+
"user-agent": `derive-ts/${SDK_VERSION}`
|
|
2059
|
+
};
|
|
2060
|
+
if (method.startsWith("private/")) {
|
|
2061
|
+
if (!authHeaders2) {
|
|
2062
|
+
throw new Error(`${method} requires authentication \u2014 construct the client with a wallet or session key`);
|
|
2063
|
+
}
|
|
2064
|
+
Object.assign(headers, await authHeaders2());
|
|
2065
|
+
}
|
|
2066
|
+
logger("debug", `HTTP ${method}`, params);
|
|
2067
|
+
let response;
|
|
2068
|
+
try {
|
|
2069
|
+
response = await fetch(`${baseUrl}/${method}`, {
|
|
2070
|
+
method: "POST",
|
|
2071
|
+
headers,
|
|
2072
|
+
body: JSON.stringify(params),
|
|
2073
|
+
// Never follow a redirect: it would forward the signed X-Lyra*
|
|
2074
|
+
// auth headers to the redirect target.
|
|
2075
|
+
redirect: "error",
|
|
2076
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2077
|
+
});
|
|
2078
|
+
} catch (cause) {
|
|
2079
|
+
if (cause instanceof DOMException && cause.name === "TimeoutError") {
|
|
2080
|
+
throw new DeriveTimeoutError(`${method}: no response within ${timeoutMs}ms`);
|
|
2081
|
+
}
|
|
2082
|
+
throw cause;
|
|
2083
|
+
}
|
|
2084
|
+
let body;
|
|
2085
|
+
try {
|
|
2086
|
+
body = await response.json();
|
|
2087
|
+
} catch {
|
|
2088
|
+
throw new Error(`${method}: non-JSON response with HTTP status ${response.status}`);
|
|
2089
|
+
}
|
|
2090
|
+
return unwrapResponse(method, body);
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
|
|
2094
|
+
// src/transport/ws.ts
|
|
2095
|
+
var WS_OPEN = 1;
|
|
2096
|
+
var RECONNECT_MAX_ATTEMPTS = 5;
|
|
2097
|
+
var RECONNECT_BASE_DELAY_MS = 500;
|
|
2098
|
+
var RECONNECT_MAX_DELAY_MS = 8e3;
|
|
2099
|
+
async function defaultWebSocketFactory(url) {
|
|
2100
|
+
const globalWs = globalThis.WebSocket;
|
|
2101
|
+
if (globalWs) return new globalWs(url);
|
|
2102
|
+
const { WebSocket: nodeWs } = await import("ws");
|
|
2103
|
+
return new nodeWs(url);
|
|
2104
|
+
}
|
|
2105
|
+
var WsTransport = class {
|
|
2106
|
+
constructor(options) {
|
|
2107
|
+
this.options = options;
|
|
2108
|
+
}
|
|
2109
|
+
options;
|
|
2110
|
+
/** Set by the subscriptions layer; receives every pub/sub notification. */
|
|
2111
|
+
onNotification;
|
|
2112
|
+
/** Set by the client; runs after an automatic reconnect succeeds. */
|
|
2113
|
+
onReconnected;
|
|
2114
|
+
socket;
|
|
2115
|
+
nextId = 0;
|
|
2116
|
+
pending = /* @__PURE__ */ new Map();
|
|
2117
|
+
intentionallyClosed = false;
|
|
2118
|
+
connecting;
|
|
2119
|
+
get connected() {
|
|
2120
|
+
return this.socket?.readyState === WS_OPEN;
|
|
2121
|
+
}
|
|
2122
|
+
async connect() {
|
|
2123
|
+
if (this.connected) return;
|
|
2124
|
+
this.connecting ??= (async () => {
|
|
2125
|
+
this.intentionallyClosed = false;
|
|
2126
|
+
this.socket = await this.open();
|
|
2127
|
+
})().finally(() => {
|
|
2128
|
+
this.connecting = void 0;
|
|
2129
|
+
});
|
|
2130
|
+
return this.connecting;
|
|
2131
|
+
}
|
|
2132
|
+
async open() {
|
|
2133
|
+
const factory = this.options.wsFactory ?? defaultWebSocketFactory;
|
|
2134
|
+
const socket = await factory(this.options.url);
|
|
2135
|
+
await new Promise((resolve, reject) => {
|
|
2136
|
+
if (socket.readyState === WS_OPEN) return resolve();
|
|
2137
|
+
socket.onopen = () => resolve();
|
|
2138
|
+
socket.onerror = (event) => reject(new DeriveConnectionError(`failed to connect to ${this.options.url}`, { cause: event }));
|
|
2139
|
+
});
|
|
2140
|
+
socket.onerror = (event) => this.options.logger("warn", "websocket error", event);
|
|
2141
|
+
socket.onmessage = (event) => this.handleMessage(event.data);
|
|
2142
|
+
socket.onclose = (event) => this.handleClose(event.code);
|
|
2143
|
+
return socket;
|
|
2144
|
+
}
|
|
2145
|
+
async send(method, params) {
|
|
2146
|
+
assertPublicMethod(method);
|
|
2147
|
+
if (!this.connected) {
|
|
2148
|
+
throw new DeriveConnectionError("websocket is not connected \u2014 call connect() first");
|
|
2149
|
+
}
|
|
2150
|
+
const id = ++this.nextId;
|
|
2151
|
+
const raw = await new Promise((resolve, reject) => {
|
|
2152
|
+
const timer = setTimeout(() => {
|
|
2153
|
+
this.pending.delete(id);
|
|
2154
|
+
reject(new DeriveTimeoutError(`${method}: no response within ${this.options.timeoutMs}ms`));
|
|
2155
|
+
}, this.options.timeoutMs);
|
|
2156
|
+
this.pending.set(id, { method, resolve, reject, timer });
|
|
2157
|
+
this.options.logger("debug", `WS ${method}`, params);
|
|
2158
|
+
try {
|
|
2159
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
2160
|
+
} catch (cause) {
|
|
2161
|
+
clearTimeout(timer);
|
|
2162
|
+
this.pending.delete(id);
|
|
2163
|
+
reject(new DeriveConnectionError("websocket send failed", { cause }));
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
return unwrapResponse(method, raw);
|
|
2167
|
+
}
|
|
2168
|
+
async close() {
|
|
2169
|
+
this.intentionallyClosed = true;
|
|
2170
|
+
const socket = this.socket;
|
|
2171
|
+
if (!socket || socket.readyState !== WS_OPEN) return;
|
|
2172
|
+
this.rejectAllPending(new DeriveConnectionError("websocket closed by client"));
|
|
2173
|
+
await new Promise((resolve) => {
|
|
2174
|
+
const timer = setTimeout(() => {
|
|
2175
|
+
socket.onclose = null;
|
|
2176
|
+
socket.onerror = null;
|
|
2177
|
+
resolve();
|
|
2178
|
+
}, 1e3);
|
|
2179
|
+
socket.onclose = () => {
|
|
2180
|
+
clearTimeout(timer);
|
|
2181
|
+
resolve();
|
|
2182
|
+
};
|
|
2183
|
+
socket.close();
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
handleMessage(payload) {
|
|
2187
|
+
let message;
|
|
2188
|
+
try {
|
|
2189
|
+
message = JSON.parse(String(payload));
|
|
2190
|
+
} catch {
|
|
2191
|
+
this.options.logger("warn", "ignoring non-JSON websocket message");
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (typeof message.id === "number" && this.pending.has(message.id)) {
|
|
2195
|
+
const request = this.pending.get(message.id);
|
|
2196
|
+
this.pending.delete(message.id);
|
|
2197
|
+
clearTimeout(request.timer);
|
|
2198
|
+
request.resolve(message);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (message.method === "subscription" && message.params?.channel) {
|
|
2202
|
+
this.onNotification?.(message.params.channel, message.params.data);
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
this.options.logger("debug", "unmatched websocket message", message);
|
|
2206
|
+
}
|
|
2207
|
+
handleClose(code) {
|
|
2208
|
+
this.rejectAllPending(new DeriveTimeoutError("websocket closed before a response was received"));
|
|
2209
|
+
if (this.intentionallyClosed) return;
|
|
2210
|
+
this.options.logger("warn", `websocket closed unexpectedly (code ${code}); reconnecting`);
|
|
2211
|
+
void this.reconnect();
|
|
2212
|
+
}
|
|
2213
|
+
rejectAllPending(error) {
|
|
2214
|
+
for (const request of this.pending.values()) {
|
|
2215
|
+
clearTimeout(request.timer);
|
|
2216
|
+
request.reject(error);
|
|
2217
|
+
}
|
|
2218
|
+
this.pending.clear();
|
|
2219
|
+
}
|
|
2220
|
+
async reconnect() {
|
|
2221
|
+
for (let attempt = 1; attempt <= RECONNECT_MAX_ATTEMPTS; attempt++) {
|
|
2222
|
+
const delay = Math.min(RECONNECT_BASE_DELAY_MS * 2 ** (attempt - 1), RECONNECT_MAX_DELAY_MS);
|
|
2223
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2224
|
+
if (this.intentionallyClosed) return;
|
|
2225
|
+
let socket;
|
|
2226
|
+
try {
|
|
2227
|
+
socket = await this.open();
|
|
2228
|
+
this.socket = socket;
|
|
2229
|
+
await this.onReconnected?.();
|
|
2230
|
+
this.options.logger("info", `websocket reconnected after ${attempt} attempt(s)`);
|
|
2231
|
+
return;
|
|
2232
|
+
} catch (error) {
|
|
2233
|
+
try {
|
|
2234
|
+
socket?.close();
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
this.options.logger("warn", `reconnect attempt ${attempt}/${RECONNECT_MAX_ATTEMPTS} failed`, error);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
this.options.logger("error", "websocket reconnection failed; connection abandoned");
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
// src/client.ts
|
|
2245
|
+
var noopLogger = () => {
|
|
2246
|
+
};
|
|
2247
|
+
function toWallet(input) {
|
|
2248
|
+
return typeof input === "string" ? new Wallet(input) : input;
|
|
2249
|
+
}
|
|
2250
|
+
var DeriveClient = class {
|
|
2251
|
+
network;
|
|
2252
|
+
ws;
|
|
2253
|
+
http;
|
|
2254
|
+
marketData;
|
|
2255
|
+
subaccounts;
|
|
2256
|
+
orders;
|
|
2257
|
+
rfq;
|
|
2258
|
+
spotTransfers;
|
|
2259
|
+
positionTransfers;
|
|
2260
|
+
withdrawals;
|
|
2261
|
+
deposits;
|
|
2262
|
+
vaults;
|
|
2263
|
+
sessionKeys;
|
|
2264
|
+
subscriptions;
|
|
2265
|
+
logger;
|
|
2266
|
+
owner;
|
|
2267
|
+
sessionKey;
|
|
2268
|
+
accountOwner;
|
|
2269
|
+
loggedIn = false;
|
|
2270
|
+
constructor(options) {
|
|
2271
|
+
this.network = resolveNetwork(options.network);
|
|
2272
|
+
this.logger = options.logger ?? noopLogger;
|
|
2273
|
+
this.owner = toWallet(options.wallet);
|
|
2274
|
+
this.sessionKey = toWallet(options.sessionKey);
|
|
2275
|
+
this.accountOwner = options.ownerAddress ?? this.owner?.address;
|
|
2276
|
+
if (this.sessionKey && !this.accountOwner) {
|
|
2277
|
+
throw new Error("a session key acts for an account owner \u2014 also pass `wallet` or `ownerAddress`");
|
|
2278
|
+
}
|
|
2279
|
+
const timeoutMs = options.requestTimeoutMs ?? 2e4;
|
|
2280
|
+
this.http = new HttpTransport({
|
|
2281
|
+
baseUrl: this.network.httpUrl,
|
|
2282
|
+
timeoutMs,
|
|
2283
|
+
logger: this.logger,
|
|
2284
|
+
authHeaders: () => authHeaders(this.credentials())
|
|
2285
|
+
});
|
|
2286
|
+
this.ws = new WsTransport({
|
|
2287
|
+
url: this.network.wsUrl,
|
|
2288
|
+
timeoutMs,
|
|
2289
|
+
logger: this.logger,
|
|
2290
|
+
wsFactory: options.wsFactory
|
|
2291
|
+
});
|
|
2292
|
+
this.ws.onReconnected = async () => {
|
|
2293
|
+
if (this.loggedIn) await this.login();
|
|
2294
|
+
await this.subscriptions.resubscribeAll();
|
|
2295
|
+
};
|
|
2296
|
+
this.marketData = new MarketDataApi(this);
|
|
2297
|
+
this.subaccounts = new SubaccountsApi(this);
|
|
2298
|
+
this.orders = new OrdersApi(this, this.marketData);
|
|
2299
|
+
this.rfq = new RfqApi(this);
|
|
2300
|
+
this.spotTransfers = new SpotTransfersApi(this);
|
|
2301
|
+
this.positionTransfers = new PositionTransfersApi(this);
|
|
2302
|
+
this.withdrawals = new WithdrawalsApi(this);
|
|
2303
|
+
this.deposits = new DepositsApi(this);
|
|
2304
|
+
this.vaults = new VaultsApi(this);
|
|
2305
|
+
this.sessionKeys = new SessionKeysApi(this);
|
|
2306
|
+
this.subscriptions = new Subscriptions(this, this.ws);
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* The identity requests act as: the owner address plus whichever
|
|
2310
|
+
* wallet signs (session key when configured, otherwise the owner).
|
|
2311
|
+
*/
|
|
2312
|
+
credentials() {
|
|
2313
|
+
const signer = this.sessionKey ?? this.owner;
|
|
2314
|
+
if (!signer || !this.accountOwner) {
|
|
2315
|
+
throw new Error("this operation requires authentication \u2014 construct DeriveClient with a wallet or session key");
|
|
2316
|
+
}
|
|
2317
|
+
return { ownerAddress: this.accountOwner, signer };
|
|
2318
|
+
}
|
|
2319
|
+
/** Opens the websocket. REST-only usage can skip this. */
|
|
2320
|
+
async connect() {
|
|
2321
|
+
await this.ws.connect();
|
|
2322
|
+
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Authenticates the websocket connection (REST requests instead carry
|
|
2325
|
+
* signed headers per request). Re-runs automatically after reconnects.
|
|
2326
|
+
*/
|
|
2327
|
+
async login() {
|
|
2328
|
+
if (!this.ws.connected) {
|
|
2329
|
+
throw new Error("login runs over the websocket \u2014 call connect() first");
|
|
2330
|
+
}
|
|
2331
|
+
const result = await this.ws.send("public/login", await loginParams(this.credentials()));
|
|
2332
|
+
this.loggedIn = true;
|
|
2333
|
+
return result;
|
|
2334
|
+
}
|
|
2335
|
+
async close() {
|
|
2336
|
+
this.loggedIn = false;
|
|
2337
|
+
await this.ws.close();
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Typed escape hatch for any public RPC method: uses the websocket
|
|
2341
|
+
* when connected, REST otherwise.
|
|
2342
|
+
*/
|
|
2343
|
+
send(method, params) {
|
|
2344
|
+
return this.ws.connected ? this.ws.send(method, params) : this.http.send(method, params);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
// src/subscriptions/channels.ts
|
|
2349
|
+
function channel(template, params) {
|
|
2350
|
+
const values = params;
|
|
2351
|
+
const used = /* @__PURE__ */ new Set();
|
|
2352
|
+
const name = template.replace(/\{(\w+)\}/g, (_token, key) => {
|
|
2353
|
+
const value = values[key];
|
|
2354
|
+
if (value === void 0 || value === null || String(value) === "") {
|
|
2355
|
+
throw new Error(`channel ${template}: missing value for {${key}}`);
|
|
2356
|
+
}
|
|
2357
|
+
used.add(key);
|
|
2358
|
+
return String(value);
|
|
2359
|
+
});
|
|
2360
|
+
for (const key of Object.keys(values)) {
|
|
2361
|
+
if (!used.has(key)) {
|
|
2362
|
+
throw new Error(`channel ${template}: unknown param '${key}'`);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return { name };
|
|
2366
|
+
}
|
|
2367
|
+
export {
|
|
2368
|
+
ACTION_TYPEHASH,
|
|
2369
|
+
ContractCallDeposits,
|
|
2370
|
+
CuratorVaultsApi,
|
|
2371
|
+
DepositAddressDeposits,
|
|
2372
|
+
DepositsApi,
|
|
2373
|
+
DeriveClient,
|
|
2374
|
+
DeriveConnectionError,
|
|
2375
|
+
DeriveRpcError,
|
|
2376
|
+
DeriveTimeoutError,
|
|
2377
|
+
MarketDataApi,
|
|
2378
|
+
NETWORKS,
|
|
2379
|
+
OffchainScope,
|
|
2380
|
+
OrdersApi,
|
|
2381
|
+
PositionTransfersApi,
|
|
2382
|
+
ProtocolScopeCode,
|
|
2383
|
+
ProtocolScopeWireString,
|
|
2384
|
+
RfqApi,
|
|
2385
|
+
SDK_VERSION,
|
|
2386
|
+
SessionKeysApi,
|
|
2387
|
+
ShareholderVaultsApi,
|
|
2388
|
+
SignedAction,
|
|
2389
|
+
SpotTransfersApi,
|
|
2390
|
+
SubaccountsApi,
|
|
2391
|
+
Subscriptions,
|
|
2392
|
+
V3_MODULE_ADDRESSES,
|
|
2393
|
+
VaultsApi,
|
|
2394
|
+
WithdrawalsApi,
|
|
2395
|
+
authHeaders,
|
|
2396
|
+
channel,
|
|
2397
|
+
domainSeparator,
|
|
2398
|
+
expiresIn,
|
|
2399
|
+
loginParams,
|
|
2400
|
+
randomNonce,
|
|
2401
|
+
resolveNetwork,
|
|
2402
|
+
toE18,
|
|
2403
|
+
toScaled
|
|
2404
|
+
};
|
|
2405
|
+
//# sourceMappingURL=index.js.map
|