@reyaxyz/sdk 0.151.4 → 0.152.1
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/README.md +1 -1
- package/dist/services/orders/index.js +1 -1
- package/dist/services/orders/index.js.map +1 -1
- package/dist/services/orders/nonce.js +37 -0
- package/dist/services/orders/nonce.js.map +1 -0
- package/dist/services/orders/orderV2.js +416 -89
- package/dist/services/orders/orderV2.js.map +1 -1
- package/dist/services/orders/triggerSettlementHeadroom.js +51 -0
- package/dist/services/orders/triggerSettlementHeadroom.js.map +1 -0
- package/dist/services/orders/types.js.map +1 -1
- package/dist/types/services/orders/index.d.ts +1 -1
- package/dist/types/services/orders/index.d.ts.map +1 -1
- package/dist/types/services/orders/nonce.d.ts +2 -0
- package/dist/types/services/orders/nonce.d.ts.map +1 -0
- package/dist/types/services/orders/orderV2.d.ts +194 -14
- package/dist/types/services/orders/orderV2.d.ts.map +1 -1
- package/dist/types/services/orders/triggerSettlementHeadroom.d.ts +33 -0
- package/dist/types/services/orders/triggerSettlementHeadroom.d.ts.map +1 -0
- package/dist/types/services/orders/types.d.ts +1 -11
- package/dist/types/services/orders/types.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/services/orders/index.ts +1 -1
- package/src/services/orders/nonce.ts +34 -0
- package/src/services/orders/orderV2.ts +731 -122
- package/src/services/orders/triggerSettlementHeadroom.ts +56 -0
- package/src/services/orders/types.ts +0 -15
- package/dist/services/orders/order.js +0 -231
- package/dist/services/orders/order.js.map +0 -1
- package/dist/types/services/orders/order.d.ts +0 -4
- package/dist/types/services/orders/order.d.ts.map +0 -1
- package/src/services/orders/order.ts +0 -214
|
@@ -1,100 +1,292 @@
|
|
|
1
|
-
import { Signer, JsonRpcSigner,
|
|
1
|
+
import { Signer, JsonRpcSigner, parseUnits } from 'ethers';
|
|
2
2
|
import {
|
|
3
3
|
OrderEntryApi,
|
|
4
4
|
Configuration,
|
|
5
|
-
CancelOrderResponse,
|
|
6
5
|
CreateOrderResponse,
|
|
7
|
-
MassCancelResponse,
|
|
8
6
|
OrderType,
|
|
9
7
|
TimeInForce,
|
|
8
|
+
CancelOrderResponse,
|
|
9
|
+
MassCancelResponse,
|
|
10
|
+
CancelAllAfterResponse,
|
|
11
|
+
ModifyOrderResponse,
|
|
12
|
+
ModifyOrderRequest,
|
|
10
13
|
} from '@reyaxyz/api-v2-sdk';
|
|
11
14
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
signOrder,
|
|
16
|
+
signMEOrderCancel,
|
|
17
|
+
signMEMassCancel,
|
|
18
|
+
signMECancelAllAfter,
|
|
19
|
+
OrderType as OnChainOrderType,
|
|
20
|
+
fullPositionStopQuantity,
|
|
18
21
|
} from '@reyaxyz/common';
|
|
19
22
|
import { getReyaNetwork } from '../../utils/network';
|
|
20
23
|
import { getSdkConfig } from '../../config';
|
|
24
|
+
import { nextNonce } from './nonce';
|
|
25
|
+
import { assertSettlementHeadroom } from './triggerSettlementHeadroom';
|
|
26
|
+
|
|
27
|
+
// A provided, non-zero order id. '' / whitespace / '0' / all-zeros are the
|
|
28
|
+
// engine's "not provided" sentinels; normalize them consistently across cancel
|
|
29
|
+
// + modify so a caller passing orderId:'0' (or '') can't sign/target the
|
|
30
|
+
// sentinel or slip past the "requires an id" guard.
|
|
31
|
+
const isProvidedNonZeroId = (value: string | undefined): value is string => {
|
|
32
|
+
const trimmed = value?.trim();
|
|
33
|
+
return !!trimmed && !/^0+$/.test(trimmed);
|
|
34
|
+
};
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
// Maps the REST-surface `OrderType` (string: 'LIMIT' / 'STOP_LOSS' / 'TAKE_PROFIT',
|
|
37
|
+
// from @reyaxyz/api-v2-sdk) to the on-chain numeric enum signed into
|
|
38
|
+
// `OrderDetails.orderType` (0 / 1 / 2, from @reyaxyz/common).
|
|
39
|
+
export const toOnChainOrderType = (orderType: OrderType): OnChainOrderType => {
|
|
40
|
+
switch (orderType) {
|
|
41
|
+
case 'LIMIT':
|
|
42
|
+
return OnChainOrderType.Limit;
|
|
43
|
+
case 'STOP_LOSS':
|
|
44
|
+
return OnChainOrderType.StopLoss;
|
|
45
|
+
case 'TAKE_PROFIT':
|
|
46
|
+
return OnChainOrderType.TakeProfit;
|
|
47
|
+
default:
|
|
48
|
+
throw new Error(`Unsupported orderType: ${orderType}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
23
51
|
|
|
24
|
-
|
|
52
|
+
// Maps the REST-surface `TimeInForce` to the numeric value signed into
|
|
53
|
+
// `OrderDetails.timeInForce`, matching the off-chain verifier's
|
|
54
|
+
// REST_TIME_IN_FORCE_TO_NUMERIC (common-backend signature-validation):
|
|
55
|
+
// GTC=0, IOC=1, GTT=2.
|
|
56
|
+
export const toOnChainTimeInForce = (timeInForce: TimeInForce): number => {
|
|
57
|
+
switch (timeInForce) {
|
|
58
|
+
case 'GTC':
|
|
59
|
+
return 0;
|
|
60
|
+
case 'IOC':
|
|
61
|
+
return 1;
|
|
62
|
+
case 'GTT':
|
|
63
|
+
return 2;
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`Unsupported timeInForce: ${timeInForce}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
25
68
|
|
|
26
|
-
export type
|
|
69
|
+
export type CreateOrderParams = {
|
|
27
70
|
signer: Signer | JsonRpcSigner;
|
|
28
71
|
accountId: number;
|
|
29
72
|
exchangeId: number;
|
|
73
|
+
/** Unified market id: core_id for perp, core_id + 1e10 for spot. */
|
|
30
74
|
marketId: number;
|
|
31
75
|
isBuy: boolean;
|
|
32
76
|
limitPx: string;
|
|
33
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Order quantity. Type-conditional, mirroring the modify-trigger path:
|
|
79
|
+
* - LIMIT: REQUIRED — signed as ±qty (positive buy, negative sell) and sent
|
|
80
|
+
* on the wire.
|
|
81
|
+
* - STOP_LOSS / TAKE_PROFIT: OMIT — the signed quantity is the ±int256.max
|
|
82
|
+
* full-position sentinel (sign from `isBuy`; protect the whole position)
|
|
83
|
+
* and qty is dropped from the wire payload. Passing a qty on a trigger
|
|
84
|
+
* create throws (the backend rejects it).
|
|
85
|
+
*/
|
|
86
|
+
qty?: string;
|
|
34
87
|
symbol: string;
|
|
88
|
+
orderType: OrderType;
|
|
89
|
+
/**
|
|
90
|
+
* REQUIRED for every order class — signed and sent on the wire. On a
|
|
91
|
+
* STOP_LOSS / TAKE_PROFIT it chooses what the stop becomes when it fires:
|
|
92
|
+
* IOC fills what the book offers and cancels the rest, GTC rests the
|
|
93
|
+
* remainder until cancelled, GTT rests it until one settlement headroom
|
|
94
|
+
* before `expiresAfter`.
|
|
95
|
+
*/
|
|
35
96
|
timeInForce: TimeInForce;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
/** Required for STOP_LOSS / TAKE_PROFIT; omit for LIMIT. */
|
|
98
|
+
triggerPx?: string;
|
|
99
|
+
/** Perp only; spot markets must set this to false / omit. */
|
|
100
|
+
reduceOnly?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Maker-only intent. Signed into OrderDetails. Valid on GTC/GTT; the API
|
|
103
|
+
* rejects it on IOC + TP/SL. Omit / false for a normal order.
|
|
104
|
+
*/
|
|
105
|
+
postOnly?: boolean;
|
|
106
|
+
/** Off-chain correlation id. Signed but not on-chain-enforced. */
|
|
107
|
+
clientOrderId?: string;
|
|
108
|
+
/** Unix seconds. On-chain order lifetime; omit for no-expiry orders. */
|
|
109
|
+
expiresAfter?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Unix seconds. EIP-712 signature deadline (envelope). API-enforced at entry.
|
|
112
|
+
* Defaults to now + 30s when omitted.
|
|
113
|
+
*/
|
|
114
|
+
deadline?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Seconds of settlement headroom a GTT `expiresAfter` must outlast. Defaults
|
|
117
|
+
* to the production value; set it only for a deployment running a different
|
|
118
|
+
* one.
|
|
119
|
+
*/
|
|
120
|
+
settlementHeadroomSeconds?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Optional explicit nonce. Must be strictly greater than the signer's last
|
|
123
|
+
* accepted nonce in the matching engine. Defaults to a per-signer monotonic
|
|
124
|
+
* µs value.
|
|
125
|
+
*/
|
|
126
|
+
nonce?: bigint;
|
|
52
127
|
};
|
|
53
128
|
|
|
54
|
-
export
|
|
55
|
-
|
|
129
|
+
export type CancelMEOrderParams = {
|
|
130
|
+
signer: Signer | JsonRpcSigner;
|
|
131
|
+
accountId: number;
|
|
132
|
+
marketId: number;
|
|
133
|
+
symbol: string;
|
|
134
|
+
orderId?: string;
|
|
135
|
+
clientOrderId?: string;
|
|
136
|
+
/** Unix seconds; EIP-712 signature deadline. Defaults to now + 30s. */
|
|
137
|
+
deadline?: number;
|
|
138
|
+
/** Optional explicit nonce; otherwise per-signer monotonic µs. */
|
|
139
|
+
nonce?: bigint;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type MassCancelMEOrdersParams = {
|
|
143
|
+
signer: Signer | JsonRpcSigner;
|
|
144
|
+
accountId: number;
|
|
145
|
+
marketId?: number;
|
|
146
|
+
symbol?: string;
|
|
147
|
+
/** Unix seconds; EIP-712 signature deadline. Defaults to now + 30s. */
|
|
148
|
+
deadline?: number;
|
|
149
|
+
/** Optional explicit nonce; otherwise per-signer monotonic µs. */
|
|
150
|
+
nonce?: bigint;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const DEFAULT_DEADLINE_SECONDS = 30;
|
|
154
|
+
|
|
155
|
+
const isZeroClientOrderId = (clientOrderId: string | undefined): boolean =>
|
|
156
|
+
clientOrderId !== undefined && /^0+$/.test(clientOrderId.trim());
|
|
157
|
+
|
|
158
|
+
const assertNonZeroClientOrderId = (
|
|
159
|
+
clientOrderId: string | undefined,
|
|
160
|
+
): void => {
|
|
161
|
+
if (isZeroClientOrderId(clientOrderId)) {
|
|
162
|
+
throw new Error('clientOrderId must be omitted rather than set to 0');
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Spot unified market ids are offset by 1e10 (see CreateOrderParams.marketId).
|
|
167
|
+
// Spot markets don't support reduceOnly, so reject it at the SDK boundary
|
|
168
|
+
// rather than sign a request the backend will reject.
|
|
169
|
+
const SPOT_MARKET_ID_OFFSET = 10_000_000_000;
|
|
170
|
+
|
|
171
|
+
export const createOrder = async (
|
|
172
|
+
params: CreateOrderParams,
|
|
56
173
|
): Promise<CreateOrderResponse> => {
|
|
174
|
+
assertNonZeroClientOrderId(params.clientOrderId);
|
|
175
|
+
|
|
57
176
|
const reyaChainId = getReyaNetwork();
|
|
58
177
|
const config = getSdkConfig();
|
|
59
178
|
|
|
60
|
-
const
|
|
61
|
-
params.
|
|
62
|
-
params.
|
|
63
|
-
params.qty,
|
|
64
|
-
);
|
|
179
|
+
const isTrigger =
|
|
180
|
+
params.orderType === OrderType.STOP_LOSS ||
|
|
181
|
+
params.orderType === OrderType.TAKE_PROFIT;
|
|
65
182
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
183
|
+
if (
|
|
184
|
+
isTrigger &&
|
|
185
|
+
(params.reduceOnly !== undefined || params.postOnly !== undefined)
|
|
186
|
+
) {
|
|
187
|
+
throw new Error('reduceOnly and postOnly must be omitted for TP/SL orders');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isTrigger && params.triggerPx === undefined) {
|
|
191
|
+
throw new Error(`triggerPx is required for ${params.orderType} orders`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// qty is type-conditional (mirrors the modify-trigger path):
|
|
195
|
+
// - STOP_LOSS / TAKE_PROFIT: qty MUST be omitted; the signed quantity is
|
|
196
|
+
// the ±int256.max full-position sentinel (sign from `isBuy`) and qty is
|
|
197
|
+
// dropped from the wire below. The backend rejects a trigger create that
|
|
198
|
+
// carries qty.
|
|
199
|
+
// - LIMIT: qty is REQUIRED and signed as ±qty.
|
|
200
|
+
// Validated up front (before a nonce is minted), like the triggerPx guard.
|
|
201
|
+
if (isTrigger && params.qty !== undefined) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`qty must be omitted for ${params.orderType} orders (the signed quantity is the full-position sentinel)`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (!isTrigger && params.qty === undefined) {
|
|
207
|
+
throw new Error('qty is required for a LIMIT order');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Convert once while validation is still side-effect free. timeInForce is
|
|
211
|
+
// required for every order class — on a trigger it chooses what the fired
|
|
212
|
+
// child becomes, so there is no value the SDK could supply on the caller's
|
|
213
|
+
// behalf. Runtime callers can bypass the TypeScript type; reject a missing or
|
|
214
|
+
// unrecognised value before resolving the signer or advancing its nonce.
|
|
215
|
+
const onChainTimeInForce = toOnChainTimeInForce(params.timeInForce);
|
|
216
|
+
|
|
217
|
+
// Only GTT carries a lifetime, for triggers as for book orders. On a trigger
|
|
218
|
+
// the one timestamp bounds the armed window AND is the fired child's on-chain
|
|
219
|
+
// settlement deadline; GTC and IOC triggers have no lifetime of their own.
|
|
220
|
+
const hasExpiry =
|
|
221
|
+
params.expiresAfter !== undefined && params.expiresAfter !== 0;
|
|
222
|
+
if (params.timeInForce === TimeInForce.GTT && !hasExpiry) {
|
|
223
|
+
throw new Error('expiresAfter is required for GTT orders');
|
|
224
|
+
}
|
|
225
|
+
if (params.timeInForce !== TimeInForce.GTT && hasExpiry) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'expiresAfter must be omitted for IOC / GTC orders — only GTT carries a lifetime',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (params.reduceOnly && params.marketId >= SPOT_MARKET_ID_OFFSET) {
|
|
232
|
+
throw new Error('reduceOnly is not supported for spot markets');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Venue admission rules the SDK can evaluate itself, checked while validation
|
|
236
|
+
// is still side-effect free — a nonce burnt on an order the engine refuses
|
|
237
|
+
// has to be resynced before the next one can be signed.
|
|
238
|
+
if (hasExpiry) {
|
|
239
|
+
assertSettlementHeadroom(
|
|
240
|
+
params.expiresAfter!,
|
|
241
|
+
params.settlementHeadroomSeconds,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
96
244
|
|
|
97
245
|
const signerAddress = await params.signer.getAddress();
|
|
246
|
+
const defaultDeadline =
|
|
247
|
+
Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS;
|
|
248
|
+
const deadline = params.deadline ?? defaultDeadline;
|
|
249
|
+
const expiresAfter = params.expiresAfter ?? 0;
|
|
250
|
+
const nonce = params.nonce ?? nextNonce(signerAddress);
|
|
251
|
+
|
|
252
|
+
// On-chain `OrderDetails.quantity` is signed int256 (E18 for real sizes): a
|
|
253
|
+
// trigger signs the ±int256.max full-position sentinel — sign from `isBuy`
|
|
254
|
+
// (−max sells out a long, +max buys back a short; reya-network #738 enforces
|
|
255
|
+
// SL/TP <=> ±sentinel, and quantity 0 now reverts) — while a LIMIT signs ±qty
|
|
256
|
+
// (positive buy, negative sell). The REST surface keeps `isBuy + qty`
|
|
257
|
+
// (unsigned) for symmetry with the rest of the API; reconstruct the signed
|
|
258
|
+
// value here.
|
|
259
|
+
//
|
|
260
|
+
// Use `parseUnits(str, 18)` to match the off-chain verifier exactly
|
|
261
|
+
// (signature-validation/index.ts). The previous `scale(18)(parseFloat(str))`
|
|
262
|
+
// diverged for |x| < 1e-6 or >= 1e21 (parseFloat exponential form) → a
|
|
263
|
+
// signature mismatch on extreme sizes.
|
|
264
|
+
const quantity = isTrigger
|
|
265
|
+
? fullPositionStopQuantity(params.isBuy)
|
|
266
|
+
: params.isBuy
|
|
267
|
+
? parseUnits(params.qty!, 18)
|
|
268
|
+
: -parseUnits(params.qty!, 18);
|
|
269
|
+
const limitPrice = parseUnits(params.limitPx, 18);
|
|
270
|
+
const triggerPrice = params.triggerPx
|
|
271
|
+
? parseUnits(params.triggerPx, 18)
|
|
272
|
+
: BigInt(0);
|
|
273
|
+
|
|
274
|
+
const { serializedSignature } = await signOrder(params.signer, reyaChainId, {
|
|
275
|
+
accountId: params.accountId,
|
|
276
|
+
marketId: params.marketId,
|
|
277
|
+
exchangeId: params.exchangeId,
|
|
278
|
+
orderType: toOnChainOrderType(params.orderType),
|
|
279
|
+
quantity,
|
|
280
|
+
limitPrice,
|
|
281
|
+
triggerPrice,
|
|
282
|
+
timeInForce: onChainTimeInForce,
|
|
283
|
+
clientOrderId: BigInt(params.clientOrderId ?? 0),
|
|
284
|
+
reduceOnly: isTrigger ? false : params.reduceOnly ?? false,
|
|
285
|
+
postOnly: isTrigger ? false : params.postOnly ?? false,
|
|
286
|
+
expiresAfter: BigInt(expiresAfter),
|
|
287
|
+
nonce,
|
|
288
|
+
deadline,
|
|
289
|
+
});
|
|
98
290
|
|
|
99
291
|
const apiConfig = new Configuration({
|
|
100
292
|
basePath: `${config.apiEndpoint}/v2`,
|
|
@@ -109,51 +301,72 @@ export const createSpotOrder = async (
|
|
|
109
301
|
accountId: params.accountId,
|
|
110
302
|
isBuy: params.isBuy,
|
|
111
303
|
limitPx: params.limitPx,
|
|
112
|
-
qty
|
|
113
|
-
|
|
304
|
+
// qty is omitted on a trigger create (guaranteed undefined by the
|
|
305
|
+
// isTrigger guard above); sent for a LIMIT create.
|
|
306
|
+
...(params.qty !== undefined && { qty: params.qty }),
|
|
307
|
+
orderType: params.orderType,
|
|
114
308
|
timeInForce: params.timeInForce,
|
|
115
|
-
|
|
309
|
+
...(params.triggerPx !== undefined && { triggerPx: params.triggerPx }),
|
|
310
|
+
...(params.reduceOnly !== undefined && { reduceOnly: params.reduceOnly }),
|
|
311
|
+
...(params.postOnly !== undefined && { postOnly: params.postOnly }),
|
|
312
|
+
...(params.clientOrderId !== undefined && {
|
|
313
|
+
clientOrderId: params.clientOrderId,
|
|
314
|
+
}),
|
|
116
315
|
signature: serializedSignature,
|
|
117
316
|
nonce: nonce.toString(),
|
|
118
317
|
signerWallet: signerAddress,
|
|
119
|
-
|
|
318
|
+
deadline,
|
|
319
|
+
...(params.expiresAfter !== undefined &&
|
|
320
|
+
params.expiresAfter !== 0 && { expiresAfter }),
|
|
120
321
|
},
|
|
121
322
|
});
|
|
122
323
|
|
|
123
324
|
return response;
|
|
124
325
|
};
|
|
125
326
|
|
|
126
|
-
export
|
|
127
|
-
|
|
128
|
-
accountId: number;
|
|
129
|
-
marketId: number;
|
|
130
|
-
symbol: string;
|
|
131
|
-
orderId?: string;
|
|
132
|
-
clientOrderId?: number;
|
|
133
|
-
expiresAfterSeconds?: number;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export const cancelSpotOrder = async (
|
|
137
|
-
params: CancelSpotOrderParams,
|
|
327
|
+
export const cancelMEOrder = async (
|
|
328
|
+
params: CancelMEOrderParams,
|
|
138
329
|
): Promise<CancelOrderResponse> => {
|
|
139
|
-
|
|
140
|
-
throw new Error('Either orderId or clientOrderId must be provided');
|
|
141
|
-
}
|
|
330
|
+
assertNonZeroClientOrderId(params.clientOrderId);
|
|
142
331
|
|
|
143
332
|
const reyaChainId = getReyaNetwork();
|
|
144
333
|
const config = getSdkConfig();
|
|
145
334
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
335
|
+
// Validation: at least one of orderId or clientOrderId must be provided.
|
|
336
|
+
// Without this check the SDK would sign for `cancel(orderId=0, clOrdId=0)`
|
|
337
|
+
// and the API would either reject with a cryptic signature-mismatch error
|
|
338
|
+
// or — worse — accept and look up a sentinel "order zero" that doesn't
|
|
339
|
+
// exist. Better to surface the misuse at the SDK boundary. (PRO-81 C.2.26)
|
|
340
|
+
// Treat '0' as "not provided" for both ids — '0' is the engine's
|
|
341
|
+
// not-provided sentinel, so a caller passing clientOrderId:'0' (or
|
|
342
|
+
// orderId:'0') must NOT slip past this guard and sign the sentinel
|
|
343
|
+
// cancel(0,0) it is meant to block. (PRO-81 C.2.26)
|
|
344
|
+
const orderIdProvided = isProvidedNonZeroId(params.orderId);
|
|
345
|
+
const clientOrderIdProvided = isProvidedNonZeroId(params.clientOrderId);
|
|
346
|
+
if (!orderIdProvided && !clientOrderIdProvided) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
'cancelMEOrder requires either `orderId` or `clientOrderId`',
|
|
349
|
+
);
|
|
350
|
+
}
|
|
151
351
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
352
|
+
// When orderId is set, it is the canonical handle — drop clientOrderId
|
|
353
|
+
// from both the signature and the request body. Forwarding both creates
|
|
354
|
+
// ambiguity in server logging and would let a malicious / buggy caller
|
|
355
|
+
// sign one order id while requesting cancellation by a different
|
|
356
|
+
// (client-supplied) id. (PRO-81 C.2.26)
|
|
357
|
+
const signedOrderId = orderIdProvided ? BigInt(params.orderId!) : BigInt(0);
|
|
358
|
+
const signedClOrdId =
|
|
359
|
+
!orderIdProvided && clientOrderIdProvided
|
|
360
|
+
? BigInt(params.clientOrderId!)
|
|
361
|
+
: BigInt(0);
|
|
155
362
|
|
|
156
|
-
const
|
|
363
|
+
const signerAddress = await params.signer.getAddress();
|
|
364
|
+
const defaultDeadline =
|
|
365
|
+
Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS;
|
|
366
|
+
const deadline = params.deadline ?? defaultDeadline;
|
|
367
|
+
const nonce = params.nonce ?? nextNonce(signerAddress);
|
|
368
|
+
|
|
369
|
+
const signature = await signMEOrderCancel(
|
|
157
370
|
params.signer,
|
|
158
371
|
reyaChainId,
|
|
159
372
|
params.accountId,
|
|
@@ -161,7 +374,7 @@ export const cancelSpotOrder = async (
|
|
|
161
374
|
signedOrderId,
|
|
162
375
|
signedClOrdId,
|
|
163
376
|
nonce,
|
|
164
|
-
|
|
377
|
+
deadline,
|
|
165
378
|
);
|
|
166
379
|
|
|
167
380
|
const apiConfig = new Configuration({
|
|
@@ -172,54 +385,54 @@ export const cancelSpotOrder = async (
|
|
|
172
385
|
|
|
173
386
|
const response = await orderEntryApi.cancelOrder({
|
|
174
387
|
cancelOrderRequest: {
|
|
175
|
-
symbol: params.symbol,
|
|
176
388
|
accountId: params.accountId,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
389
|
+
symbol: params.symbol,
|
|
390
|
+
orderId: orderIdProvided ? params.orderId!.trim() : undefined,
|
|
391
|
+
// See validation block above: clientOrderId is suppressed in the body
|
|
392
|
+
// whenever orderId is the canonical handle.
|
|
393
|
+
clientOrderId: orderIdProvided ? undefined : params.clientOrderId?.trim(),
|
|
394
|
+
signature: signature,
|
|
180
395
|
nonce: nonce.toString(),
|
|
181
|
-
|
|
396
|
+
deadline,
|
|
182
397
|
},
|
|
183
398
|
});
|
|
184
399
|
|
|
185
400
|
return response;
|
|
186
401
|
};
|
|
187
402
|
|
|
188
|
-
export
|
|
189
|
-
|
|
190
|
-
accountId: number;
|
|
191
|
-
symbol?: string;
|
|
192
|
-
marketId?: number;
|
|
193
|
-
expiresAfterSeconds?: number;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
export const massCancelSpotOrders = async (
|
|
197
|
-
params: MassCancelSpotOrdersParams,
|
|
403
|
+
export const massCancelMEOrders = async (
|
|
404
|
+
params: MassCancelMEOrdersParams,
|
|
198
405
|
): Promise<MassCancelResponse> => {
|
|
406
|
+
const reyaChainId = getReyaNetwork();
|
|
407
|
+
const config = getSdkConfig();
|
|
408
|
+
|
|
409
|
+
// Validation: symbol and marketId must be provided together or not at all.
|
|
410
|
+
// The API resolves a symbol → marketId server-side; the signature locks
|
|
411
|
+
// marketId at sign time. If only one is provided we'd ship a body whose
|
|
412
|
+
// server-derived marketId doesn't match the signed marketId (=> opaque
|
|
413
|
+
// signature-validation failure), or — worse — a body that silently
|
|
414
|
+
// cancels the wrong market. (PRO-81 C.2.26)
|
|
199
415
|
const hasSymbol = params.symbol !== undefined;
|
|
200
416
|
const hasMarketId = params.marketId !== undefined;
|
|
201
417
|
if (hasSymbol !== hasMarketId) {
|
|
202
418
|
throw new Error(
|
|
203
|
-
'symbol and marketId
|
|
419
|
+
'massCancelMEOrders requires both `symbol` and `marketId` together, or neither (to mass-cancel across all markets)',
|
|
204
420
|
);
|
|
205
421
|
}
|
|
206
422
|
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
const
|
|
212
|
-
Math.floor(nowMs / 1000) +
|
|
213
|
-
(params.expiresAfterSeconds ?? DEFAULT_CANCEL_DEADLINE_SECONDS);
|
|
214
|
-
const nonce = BigInt(nowMs);
|
|
423
|
+
const signerAddress = await params.signer.getAddress();
|
|
424
|
+
const defaultDeadline =
|
|
425
|
+
Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS;
|
|
426
|
+
const deadline = params.deadline ?? defaultDeadline;
|
|
427
|
+
const nonce = params.nonce ?? nextNonce(signerAddress);
|
|
215
428
|
|
|
216
|
-
const
|
|
429
|
+
const signature = await signMEMassCancel(
|
|
217
430
|
params.signer,
|
|
218
431
|
reyaChainId,
|
|
219
432
|
params.accountId,
|
|
220
433
|
params.marketId ?? 0,
|
|
221
434
|
nonce,
|
|
222
|
-
|
|
435
|
+
deadline,
|
|
223
436
|
);
|
|
224
437
|
|
|
225
438
|
const apiConfig = new Configuration({
|
|
@@ -230,11 +443,407 @@ export const massCancelSpotOrders = async (
|
|
|
230
443
|
|
|
231
444
|
const response = await orderEntryApi.cancelAll({
|
|
232
445
|
massCancelRequest: {
|
|
446
|
+
accountId: params.accountId,
|
|
447
|
+
symbol: params.symbol,
|
|
448
|
+
signature: signature,
|
|
449
|
+
nonce: nonce.toString(),
|
|
450
|
+
deadline,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return response;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Convenience wrapper: LIMIT order with IOC timeInForce. UI teams may call
|
|
458
|
+
// createOrder directly with `timeInForce: TimeInForce.IOC` instead.
|
|
459
|
+
export const createIOCOrder = async (
|
|
460
|
+
params: Omit<CreateOrderParams, 'timeInForce'>,
|
|
461
|
+
): Promise<CreateOrderResponse> => {
|
|
462
|
+
return createOrder({
|
|
463
|
+
...params,
|
|
464
|
+
timeInForce: TimeInForce.IOC,
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
export type ClosePositionParams = {
|
|
469
|
+
signer: Signer | JsonRpcSigner;
|
|
470
|
+
accountId: number;
|
|
471
|
+
exchangeId: number;
|
|
472
|
+
marketId: number;
|
|
473
|
+
/** Closing direction — opposite of the current position side. */
|
|
474
|
+
isBuy: boolean;
|
|
475
|
+
/** Slippage gate. */
|
|
476
|
+
limitPx: string;
|
|
477
|
+
/** Position size to close. */
|
|
478
|
+
qty: string;
|
|
479
|
+
symbol: string;
|
|
480
|
+
clientOrderId?: string;
|
|
481
|
+
/** Unix seconds; EIP-712 signature deadline. Defaults to now + 30s. */
|
|
482
|
+
deadline?: number;
|
|
483
|
+
/** Optional explicit nonce override; otherwise generated per-signer. */
|
|
484
|
+
nonce?: bigint;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Convenience wrapper: close a perp position by submitting a reduce-only LIMIT
|
|
488
|
+
// IOC. The ME will bust fills that overflow the position at the clearing layer.
|
|
489
|
+
export const closePosition = async (
|
|
490
|
+
params: ClosePositionParams,
|
|
491
|
+
): Promise<CreateOrderResponse> => {
|
|
492
|
+
return createOrder({
|
|
493
|
+
signer: params.signer,
|
|
494
|
+
accountId: params.accountId,
|
|
495
|
+
exchangeId: params.exchangeId,
|
|
496
|
+
marketId: params.marketId,
|
|
497
|
+
isBuy: params.isBuy,
|
|
498
|
+
limitPx: params.limitPx,
|
|
499
|
+
qty: params.qty,
|
|
500
|
+
symbol: params.symbol,
|
|
501
|
+
orderType: OrderType.LIMIT,
|
|
502
|
+
timeInForce: TimeInForce.IOC,
|
|
503
|
+
reduceOnly: true,
|
|
504
|
+
clientOrderId: params.clientOrderId,
|
|
505
|
+
deadline: params.deadline,
|
|
506
|
+
nonce: params.nonce,
|
|
507
|
+
});
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
export type CancelAllAfterParams = {
|
|
511
|
+
signer: Signer | JsonRpcSigner;
|
|
512
|
+
accountId: number;
|
|
513
|
+
/**
|
|
514
|
+
* Countdown duration in milliseconds. `0` disarms; any non-zero value must
|
|
515
|
+
* be within [5000, 60000] and arms a fresh countdown of that duration,
|
|
516
|
+
* replacing any previously armed one (re-arming = the refresh/heartbeat).
|
|
517
|
+
* Recommended pattern: arm with 30000 and refresh every ~10-15s.
|
|
518
|
+
*/
|
|
519
|
+
timeoutMs: number;
|
|
520
|
+
deadline?: number;
|
|
521
|
+
/**
|
|
522
|
+
* Optional explicit nonce; otherwise a per-signer monotonic µs value. Must be
|
|
523
|
+
* strictly greater than the signer's last accepted nonce in the matching
|
|
524
|
+
* engine. See `cancelAllAfter` below for retry semantics; the default
|
|
525
|
+
* `nextNonce` advances automatically.
|
|
526
|
+
*/
|
|
527
|
+
nonce?: bigint;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const CANCEL_ALL_AFTER_TIMEOUT_MIN_MS = 5000;
|
|
531
|
+
const CANCEL_ALL_AFTER_TIMEOUT_MAX_MS = 60000;
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Arm, refresh, or disarm the account-scoped cancel-on-disconnect
|
|
535
|
+
* dead-man's-switch. The countdown lives in the matching engine; when it
|
|
536
|
+
* elapses, all of the account's open orders (all markets) are mass-cancelled.
|
|
537
|
+
*
|
|
538
|
+
* The matching engine owns nonce replay protection. A request rejected by
|
|
539
|
+
* local validation before it is forwarded does not consume its nonce. Once a
|
|
540
|
+
* request is sent, a transport failure may be outcome-unknown, and a request
|
|
541
|
+
* that reaches the matching engine may record its nonce in the WAL. Retry with
|
|
542
|
+
* a strictly higher nonce after an ME response or transport uncertainty.
|
|
543
|
+
* Reusing an explicit nonce is safe only after a definite local pre-forward
|
|
544
|
+
* rejection. When `nonce` is omitted, `nextNonce` advances automatically.
|
|
545
|
+
*/
|
|
546
|
+
export const cancelAllAfter = async (
|
|
547
|
+
params: CancelAllAfterParams,
|
|
548
|
+
): Promise<CancelAllAfterResponse> => {
|
|
549
|
+
const reyaChainId = getReyaNetwork();
|
|
550
|
+
const config = getSdkConfig();
|
|
551
|
+
|
|
552
|
+
// Fail fast before signing or sending a request the server will reject with
|
|
553
|
+
// INPUT_VALIDATION_ERROR anyway.
|
|
554
|
+
if (
|
|
555
|
+
!Number.isInteger(params.timeoutMs) ||
|
|
556
|
+
params.timeoutMs < 0 ||
|
|
557
|
+
(params.timeoutMs !== 0 &&
|
|
558
|
+
(params.timeoutMs < CANCEL_ALL_AFTER_TIMEOUT_MIN_MS ||
|
|
559
|
+
params.timeoutMs > CANCEL_ALL_AFTER_TIMEOUT_MAX_MS))
|
|
560
|
+
) {
|
|
561
|
+
throw new Error(
|
|
562
|
+
`cancelAllAfter timeoutMs must be 0 (disarm) or between ${CANCEL_ALL_AFTER_TIMEOUT_MIN_MS} and ${CANCEL_ALL_AFTER_TIMEOUT_MAX_MS} milliseconds`,
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const signerAddress = await params.signer.getAddress();
|
|
567
|
+
const defaultDeadline =
|
|
568
|
+
Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS;
|
|
569
|
+
const deadline = params.deadline ?? defaultDeadline;
|
|
570
|
+
const nonce = params.nonce ?? nextNonce(signerAddress);
|
|
571
|
+
|
|
572
|
+
const signature = await signMECancelAllAfter(
|
|
573
|
+
params.signer,
|
|
574
|
+
reyaChainId,
|
|
575
|
+
params.accountId,
|
|
576
|
+
params.timeoutMs,
|
|
577
|
+
nonce,
|
|
578
|
+
deadline,
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const apiConfig = new Configuration({
|
|
582
|
+
basePath: `${config.apiEndpoint}/v2`,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const orderEntryApi = new OrderEntryApi(apiConfig);
|
|
586
|
+
|
|
587
|
+
const response = await orderEntryApi.cancelAllAfter({
|
|
588
|
+
cancelAllAfterRequest: {
|
|
589
|
+
accountId: params.accountId,
|
|
590
|
+
timeoutMs: params.timeoutMs,
|
|
591
|
+
signature: signature,
|
|
592
|
+
nonce: nonce.toString(),
|
|
593
|
+
signerWallet: signerAddress,
|
|
594
|
+
deadline,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
return response;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
type ModifyMEOrderCommonParams = {
|
|
602
|
+
signer: Signer | JsonRpcSigner;
|
|
603
|
+
accountId: number;
|
|
604
|
+
exchangeId: number;
|
|
605
|
+
/** Unified market id: core_id for perp, core_id + 1e10 for spot. */
|
|
606
|
+
marketId: number;
|
|
607
|
+
symbol: string;
|
|
608
|
+
/** Targeting: provide `orderId`, or a non-zero `clientOrderId`; both may be provided. */
|
|
609
|
+
orderId?: string;
|
|
610
|
+
clientOrderId?: string;
|
|
611
|
+
/**
|
|
612
|
+
* Full post-modify order state. The SDK does not fetch the resting order —
|
|
613
|
+
* the caller supplies every field, and the EIP-712 signature commits to
|
|
614
|
+
* all of them. Modifiable fields are limitPx, qty, postOnly, expiresAfter,
|
|
615
|
+
* and triggerPx. There is no omitted-means-inherited shorthand, so unchanged
|
|
616
|
+
* values must be restated explicitly; LIMIT orders omit triggerPx and sign
|
|
617
|
+
* triggerPrice = 0. Non-modifiable fields (isBuy, orderType, timeInForce,
|
|
618
|
+
* reduceOnly) must match the resting order's values.
|
|
619
|
+
*/
|
|
620
|
+
isBuy: boolean;
|
|
621
|
+
limitPx: string;
|
|
622
|
+
/**
|
|
623
|
+
* TOTAL order quantity (not remaining). Type-conditional, mirroring the
|
|
624
|
+
* create-trigger path:
|
|
625
|
+
* - LIMIT: REQUIRED — must exceed the order's cumQty; signed as the total
|
|
626
|
+
* post-modify quantity.
|
|
627
|
+
* - STOP_LOSS / TAKE_PROFIT: OMIT — the signed quantity restates the
|
|
628
|
+
* ±int256.max full-position sentinel (sign from `isBuy`; protect the whole
|
|
629
|
+
* position) and qty is dropped from the wire payload. Passing a qty on a
|
|
630
|
+
* trigger modify throws (the backend rejects it).
|
|
631
|
+
*/
|
|
632
|
+
qty?: string;
|
|
633
|
+
/** Immutable; restate the resting order's value. */
|
|
634
|
+
timeInForce: TimeInForce;
|
|
635
|
+
triggerPx?: string;
|
|
636
|
+
/** Unix seconds. Post-modify on-chain order lifetime; omit for no-expiry orders. */
|
|
637
|
+
expiresAfter?: number;
|
|
638
|
+
/**
|
|
639
|
+
* Resting order's clientOrderId, signed into OrderDetails.clientOrderId.
|
|
640
|
+
* If orderId is absent this is also the lookup target and must be non-zero.
|
|
641
|
+
* If orderId is present, orderId targets and clientOrderId only restates the
|
|
642
|
+
* immutable; omit it when the resting order has no client id.
|
|
643
|
+
*/
|
|
644
|
+
/** Unix seconds; EIP-712 signature deadline. Defaults to now + 30s. */
|
|
645
|
+
deadline?: number;
|
|
646
|
+
/**
|
|
647
|
+
* Seconds of settlement headroom a GTT `expiresAfter` must outlast. Defaults
|
|
648
|
+
* to the production value; set it only for a deployment running a different
|
|
649
|
+
* one.
|
|
650
|
+
*/
|
|
651
|
+
settlementHeadroomSeconds?: number;
|
|
652
|
+
/** Optional explicit nonce; otherwise per-signer monotonic µs. */
|
|
653
|
+
nonce?: bigint;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Keep the helper's discriminant/flag rules tied to the generated wire types.
|
|
657
|
+
// LIMIT reduceOnly retains the helper's false default; raw requests require it.
|
|
658
|
+
type LimitModifyMEOrderFields = Pick<
|
|
659
|
+
Extract<ModifyOrderRequest, { orderType: 'LIMIT' }>,
|
|
660
|
+
'orderType' | 'postOnly'
|
|
661
|
+
> & { reduceOnly?: boolean };
|
|
662
|
+
type TriggerModifyMEOrderFields = Pick<
|
|
663
|
+
Extract<ModifyOrderRequest, { orderType: 'STOP_LOSS' | 'TAKE_PROFIT' }>,
|
|
664
|
+
'orderType' | 'postOnly' | 'reduceOnly'
|
|
665
|
+
>;
|
|
666
|
+
|
|
667
|
+
export type ModifyMEOrderParams = ModifyMEOrderCommonParams &
|
|
668
|
+
(LimitModifyMEOrderFields | TriggerModifyMEOrderFields);
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Modify a resting order in place. The order keeps its orderId and
|
|
672
|
+
* clientOrderId; queue priority is preserved only for a qty decrease at an
|
|
673
|
+
* unchanged limitPx. The params carry the COMPLETE post-modify state; a request
|
|
674
|
+
* restating the resting order's exact current state is rejected with
|
|
675
|
+
* EMPTY_MODIFY_ERROR.
|
|
676
|
+
*/
|
|
677
|
+
export const modifyMEOrder = async (
|
|
678
|
+
params: ModifyMEOrderParams,
|
|
679
|
+
): Promise<ModifyOrderResponse> => {
|
|
680
|
+
assertNonZeroClientOrderId(params.clientOrderId);
|
|
681
|
+
|
|
682
|
+
const reyaChainId = getReyaNetwork();
|
|
683
|
+
const config = getSdkConfig();
|
|
684
|
+
|
|
685
|
+
// Validate targeting before signing so misuse doesn't burn a nonce. `orderId`
|
|
686
|
+
// wins when both are present; `clientOrderId` still rides along as the
|
|
687
|
+
// restated signed immutable.
|
|
688
|
+
const hasOrderId = isProvidedNonZeroId(params.orderId);
|
|
689
|
+
const hasClientOrderIdTarget = isProvidedNonZeroId(params.clientOrderId);
|
|
690
|
+
if (!hasOrderId && !hasClientOrderIdTarget) {
|
|
691
|
+
throw new Error(
|
|
692
|
+
'modifyMEOrder requires `orderId` or a non-zero `clientOrderId`',
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const isTrigger =
|
|
697
|
+
params.orderType === OrderType.STOP_LOSS ||
|
|
698
|
+
params.orderType === OrderType.TAKE_PROFIT;
|
|
699
|
+
|
|
700
|
+
if (
|
|
701
|
+
isTrigger &&
|
|
702
|
+
(params.reduceOnly !== undefined || params.postOnly !== undefined)
|
|
703
|
+
) {
|
|
704
|
+
throw new Error('reduceOnly and postOnly must be omitted for TP/SL orders');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (!isTrigger && typeof params.postOnly !== 'boolean') {
|
|
708
|
+
throw new Error('postOnly is required for LIMIT modifications');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Convert once while validation is still side-effect free. Runtime callers
|
|
712
|
+
// can bypass the TypeScript enum; reject those values before resolving the
|
|
713
|
+
// signer or advancing its nonce.
|
|
714
|
+
const onChainTimeInForce = toOnChainTimeInForce(params.timeInForce);
|
|
715
|
+
|
|
716
|
+
// triggerPx is required for trigger orders — without it we would silently
|
|
717
|
+
// sign triggerPrice=0. Fail at the SDK boundary (mirrors the cancel/mass-
|
|
718
|
+
// cancel SDK-boundary validation) instead of signing a 0-trigger order.
|
|
719
|
+
if (isTrigger && params.triggerPx === undefined) {
|
|
720
|
+
throw new Error(`triggerPx is required for ${params.orderType} orders`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// qty is type-conditional (mirrors the create-trigger path):
|
|
724
|
+
// - STOP_LOSS / TAKE_PROFIT: qty MUST be omitted; the signed quantity is
|
|
725
|
+
// the ±int256.max full-position sentinel (sign from `isBuy`) and qty is
|
|
726
|
+
// dropped from the wire below. The backend rejects a trigger modify that
|
|
727
|
+
// carries qty.
|
|
728
|
+
// - LIMIT: qty is REQUIRED and signed as the total post-modify quantity.
|
|
729
|
+
// Validated up front (before a nonce is minted), like the triggerPx guard.
|
|
730
|
+
if (isTrigger && params.qty !== undefined) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`qty must be omitted for ${params.orderType} orders (the signed quantity is the full-position sentinel)`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (!isTrigger && params.qty === undefined) {
|
|
736
|
+
throw new Error('qty is required when modifying a LIMIT order');
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Only GTT carries a lifetime, for triggers as for book orders. The modify
|
|
740
|
+
// restates that pair immutably, so an inconsistent one can only be refused by
|
|
741
|
+
// the backend — catch it here, before a nonce is minted and burnt.
|
|
742
|
+
const hasExpiry =
|
|
743
|
+
params.expiresAfter !== undefined && params.expiresAfter !== 0;
|
|
744
|
+
if (params.timeInForce === TimeInForce.GTT && !hasExpiry) {
|
|
745
|
+
throw new Error('expiresAfter is required for GTT orders');
|
|
746
|
+
}
|
|
747
|
+
if (params.timeInForce !== TimeInForce.GTT && hasExpiry) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
'expiresAfter must be omitted for IOC / GTC orders — only GTT carries a lifetime',
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (params.reduceOnly && params.marketId >= SPOT_MARKET_ID_OFFSET) {
|
|
754
|
+
throw new Error('reduceOnly is not supported for spot markets');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Same venue admission rule as the create path: a restated GTT lifetime must
|
|
758
|
+
// still outlast the settlement headroom.
|
|
759
|
+
if (hasExpiry) {
|
|
760
|
+
assertSettlementHeadroom(
|
|
761
|
+
params.expiresAfter!,
|
|
762
|
+
params.settlementHeadroomSeconds,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const signerAddress = await params.signer.getAddress();
|
|
767
|
+
const defaultDeadline =
|
|
768
|
+
Math.floor(Date.now() / 1000) + DEFAULT_DEADLINE_SECONDS;
|
|
769
|
+
const deadline = params.deadline ?? defaultDeadline;
|
|
770
|
+
const nonce = params.nonce ?? nextNonce(signerAddress);
|
|
771
|
+
|
|
772
|
+
// Signed quantity: a trigger restates the ±int256.max full-position sentinel
|
|
773
|
+
// (sign from `isBuy`; reya-network #738); a LIMIT signs ±qty. Sign over the
|
|
774
|
+
// exact integers the off-chain verifier reconstructs
|
|
775
|
+
// with `parseUnits(str, 18)` (signature-validation/index.ts). The previous
|
|
776
|
+
// `scale(18)(parseFloat(str))` diverged for |x| < 1e-6 or >= 1e21 (parseFloat
|
|
777
|
+
// renders those in exponential form, which `scale` mishandles) → a signature
|
|
778
|
+
// mismatch on extreme sizes. `parseUnits` is exact.
|
|
779
|
+
const quantity = isTrigger
|
|
780
|
+
? fullPositionStopQuantity(params.isBuy)
|
|
781
|
+
: params.isBuy
|
|
782
|
+
? parseUnits(params.qty!, 18)
|
|
783
|
+
: -parseUnits(params.qty!, 18);
|
|
784
|
+
const limitPrice = parseUnits(params.limitPx, 18);
|
|
785
|
+
const triggerPrice = params.triggerPx
|
|
786
|
+
? parseUnits(params.triggerPx, 18)
|
|
787
|
+
: BigInt(0);
|
|
788
|
+
|
|
789
|
+
const { serializedSignature } = await signOrder(params.signer, reyaChainId, {
|
|
790
|
+
accountId: params.accountId,
|
|
791
|
+
marketId: params.marketId,
|
|
792
|
+
exchangeId: params.exchangeId,
|
|
793
|
+
orderType: toOnChainOrderType(params.orderType),
|
|
794
|
+
quantity,
|
|
795
|
+
limitPrice,
|
|
796
|
+
triggerPrice,
|
|
797
|
+
timeInForce: onChainTimeInForce,
|
|
798
|
+
clientOrderId: BigInt(params.clientOrderId ?? '0'),
|
|
799
|
+
reduceOnly: isTrigger ? false : params.reduceOnly ?? false,
|
|
800
|
+
postOnly: isTrigger ? false : params.postOnly!,
|
|
801
|
+
expiresAfter: BigInt(params.expiresAfter ?? 0),
|
|
802
|
+
nonce,
|
|
803
|
+
deadline,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const apiConfig = new Configuration({
|
|
807
|
+
basePath: `${config.apiEndpoint}/v2`,
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const orderEntryApi = new OrderEntryApi(apiConfig);
|
|
811
|
+
|
|
812
|
+
// Full restate: the body must carry the COMPLETE signed OrderDetails so it
|
|
813
|
+
// reproduces the signed digest exactly. The off-chain verifier
|
|
814
|
+
// `isModifyOrderSignatureValid` reconstructs from these fields with
|
|
815
|
+
// `timeInForce` required and never defaulted, so an omitted one cannot
|
|
816
|
+
// verify at all. The immutables are restated at the resting order's values;
|
|
817
|
+
// the ME rejects a mismatch with MODIFY_IMMUTABLE_MISMATCH.
|
|
818
|
+
const response = await orderEntryApi.modifyOrder({
|
|
819
|
+
modifyOrderRequest: {
|
|
820
|
+
...(hasOrderId ? { orderId: params.orderId!.trim() } : {}),
|
|
821
|
+
...(hasClientOrderIdTarget
|
|
822
|
+
? { clientOrderId: params.clientOrderId!.trim() }
|
|
823
|
+
: {}),
|
|
233
824
|
symbol: params.symbol,
|
|
234
825
|
accountId: params.accountId,
|
|
826
|
+
exchangeId: params.exchangeId,
|
|
827
|
+
isBuy: params.isBuy,
|
|
828
|
+
...(params.orderType === OrderType.LIMIT
|
|
829
|
+
? {
|
|
830
|
+
orderType: params.orderType,
|
|
831
|
+
reduceOnly: params.reduceOnly ?? false,
|
|
832
|
+
postOnly: params.postOnly,
|
|
833
|
+
}
|
|
834
|
+
: { orderType: params.orderType }),
|
|
835
|
+
timeInForce: params.timeInForce,
|
|
836
|
+
...(params.triggerPx !== undefined && { triggerPx: params.triggerPx }),
|
|
837
|
+
limitPx: params.limitPx,
|
|
838
|
+
// qty is omitted on a trigger modify (guaranteed undefined by the
|
|
839
|
+
// isTrigger guard above); sent for a LIMIT modify.
|
|
840
|
+
...(params.qty !== undefined && { qty: params.qty }),
|
|
841
|
+
...(params.expiresAfter !== undefined &&
|
|
842
|
+
params.expiresAfter !== 0 && { expiresAfter: params.expiresAfter }),
|
|
235
843
|
signature: serializedSignature,
|
|
236
844
|
nonce: nonce.toString(),
|
|
237
|
-
|
|
845
|
+
signerWallet: signerAddress,
|
|
846
|
+
deadline,
|
|
238
847
|
},
|
|
239
848
|
});
|
|
240
849
|
|