@switch-win/sdk 1.2.2 → 1.2.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.
@@ -0,0 +1,737 @@
1
+ # Switch Limit Orders — Integration Guide
2
+
3
+ > **Gasless EIP-712 signed limit orders on PulseChain and Robinhood Chain**
4
+
5
+ **Limit Order API:** `https://quote.switch.win`
6
+
7
+ | API network | Chain ID | Native currency | Current limit-order contract | Native flow contract |
8
+ |---|---:|---|---|---|
9
+ | `pulsechain` | 369 | PLS | `0x8e3881bdF81Fc0211383B2e576076B654F7aFD86` | `0x88c9e2C83b6B7c707602e548481e58E920694E64` |
10
+ | `robinhood` | 4663 | ETH | `0x752c50DDd3B426cAE3D7A995F313Ac74ac6B0230` | `0x029FfC6aF9112eA078f1D6f4a98826DDB2136cf6` |
11
+
12
+ Pass `network: "pulsechain"` or `network: "robinhood"` to every SDK API
13
+ helper. Call `fetchLimitOrderConfig({ network })` at startup and treat its
14
+ addresses and EIP-712 domain as the live source of truth.
15
+
16
+ ---
17
+
18
+ ## Table of Contents
19
+
20
+ 0. [Installation](#installation)
21
+ 1. [Overview](#overview)
22
+ 2. [How It Works](#how-it-works)
23
+ 3. [Creating a Limit Order](#creating-a-limit-order)
24
+ 4. [Native Currency Limit Orders (SwitchPLSFlow)](#native-currency-limit-orders-switchplsflow)
25
+ 5. [Choosing `feeOnOutput`](#choosing-feeonoutput)
26
+ 6. [Querying Limit Orders](#querying-limit-orders)
27
+ 7. [Cancelling a Limit Order](#cancelling-a-limit-order)
28
+ 8. [API Reference](#api-reference)
29
+ 9. [Types Reference](#types-reference)
30
+ 10. [Helper Functions](#helper-functions)
31
+ 11. [EIP-712 Signing Details](#eip-712-signing-details)
32
+ 12. [On-Chain Revert Errors](#on-chain-revert-errors)
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @switch-win/sdk
40
+ # or
41
+ yarn add @switch-win/sdk
42
+ # or
43
+ pnpm add @switch-win/sdk
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Overview
49
+
50
+ Switch Limit Orders let users place **gasless, signed orders** that are filled automatically when market conditions are met. Orders use **EIP-712 typed data signatures** — no tokens are deposited or locked.
51
+
52
+ **Key properties:**
53
+
54
+ - **No gas to create** — orders are signed off-chain and submitted to the Switch backend via REST API
55
+ - **No token deposit** — tokens stay in the maker's wallet until the order is filled
56
+ - **One approval per order type:**
57
+ - `feeOnOutput: false` (default) → approve **SwitchLimitOrder** (`getApprovalTarget()`)
58
+ - `feeOnOutput: true` → approve **SwitchRouter** (`getRouterApprovalTarget()`)
59
+ - **EIP-712 signed** — standard typed data signatures, supported by all major wallets
60
+ - **Nonce-based replay protection** — each order has a unique nonce per maker
61
+ - **Optional expiry** — set a deadline or make the order valid forever
62
+ - **Custom recipient** — output tokens can be sent to a different address
63
+ - **Native unwrap** — WPLS can be unwrapped to PLS and Robinhood WETH can be
64
+ unwrapped to ETH by setting `unwrapOutput`
65
+
66
+ > **⚠️ Important:** The `SWITCH_LIMIT_ORDER` address is the **current** default. The contract may be redeployed (e.g. when the router is upgraded). Each order returned by the API includes a `limitOrderContract` field — **always use the contract address from the order for on-chain interactions (approvals, cancellations), not a hardcoded constant.** This ensures your integration works seamlessly across contract versions without code changes.
67
+
68
+ ### Network-aware setup
69
+
70
+ ```ts
71
+ import {
72
+ fetchLimitOrderConfig,
73
+ getLimitOrderApprovalTarget,
74
+ getNetworkEIP712SigningParams,
75
+ submitLimitOrder,
76
+ } from "@switch-win/sdk/limit-orders";
77
+
78
+ // `signer` and a completed `order` are assumed below.
79
+ const network = "robinhood" as const;
80
+ const live = await fetchLimitOrderConfig({ network });
81
+ const { domain, types } = getNetworkEIP712SigningParams(
82
+ network,
83
+ live.limitOrderContract,
84
+ );
85
+ const approvalTarget = getLimitOrderApprovalTarget(network, order.feeOnOutput, {
86
+ limitOrderContract: live.limitOrderContract,
87
+ });
88
+ const signature = await signer.signTypedData(domain, types, order);
89
+ await submitLimitOrder(
90
+ { ...order, signature, limitOrderContract: live.limitOrderContract },
91
+ { network },
92
+ );
93
+ ```
94
+
95
+ ---
96
+
97
+ ## How It Works
98
+
99
+ ```
100
+ ┌─────────────────────────────────────────────────────────────────────────┐
101
+ │ 1. APPROVE — one-time per token + fee mode │
102
+ │ feeOnOutput=false → approve SwitchLimitOrder (getApprovalTarget) │
103
+ │ feeOnOutput=true → approve SwitchRouter (getRouterApprovalTarget)│
104
+ │ │
105
+ │ 2. SIGN — EIP-712 typed data (gasless, no transaction) │
106
+ │ │
107
+ │ 3. SUBMIT — POST /limit-orders to Switch backend │
108
+ │ ⚠️ CRITICAL: submit IMMEDIATELY after signing and AWAIT success │
109
+ │ The signature is off-chain only — if the POST never arrives, │
110
+ │ the order is permanently lost. See critical notes below. │
111
+ │ │
112
+ │ 4. FILL — automated by Switch operators when conditions are met │
113
+ │ Tokens are pulled from the maker's wallet and swapped. │
114
+ │ Output is sent to the maker (or custom recipient). │
115
+ │ │
116
+ │ 5. CANCEL (optional) │
117
+ │ invalidateNonce(nonce) on-chain — prevents execution │
118
+ │ The backend indexer observes the cancellation event. │
119
+ └─────────────────────────────────────────────────────────────────────────┘
120
+ ```
121
+
122
+ > **⚠️ Why the POST is critical (ERC-20 orders)**
123
+ >
124
+ > For standard ERC-20 limit orders, the EIP-712 signature is **purely off-chain**.
125
+ > Nothing is recorded on-chain until an operator fills the order. The backend
126
+ > only knows about the order because your integration POSTed it.
127
+ >
128
+ > If the POST never arrives — user closes the browser, network error, frontend
129
+ > navigates away — **the order is permanently lost**. No one can fill it.
130
+ >
131
+ > Your integration must:
132
+ > 1. Call `submitLimitOrder()` **immediately** after the user signs.
133
+ > 2. **Await** the response and confirm `success: true` before showing "order created".
134
+ > 3. **Retry** on transient failures — the backend is idempotent on `maker + nonce`.
135
+ > 4. Do **not** navigate away or close the signing flow until the backend confirms.
136
+ >
137
+ > *Native PLS/ETH flow orders are the exception — they are recorded on-chain*
138
+ > *first, so the backend discovers them via event indexing even if the POST*
139
+ > *never arrives.*
140
+
141
+ ---
142
+
143
+ ## Creating a Limit Order
144
+
145
+ The full lifecycle in code (ethers.js v6):
146
+
147
+ ```ts
148
+ import { ethers } from "ethers";
149
+ import {
150
+ buildLimitOrder,
151
+ fetchLimitOrderConfig,
152
+ getLimitOrderApprovalTarget,
153
+ getNetworkEIP712SigningParams,
154
+ submitLimitOrder,
155
+ } from "@switch-win/sdk/limit-orders";
156
+ import { ERC20_ABI } from "@switch-win/sdk/constants";
157
+
158
+ const network = "robinhood" as const;
159
+ const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
160
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
161
+ const maker = await signer.getAddress();
162
+ const live = await fetchLimitOrderConfig({ network });
163
+
164
+ // ── Step 1: Build the order ──
165
+ const order = buildLimitOrder({
166
+ maker,
167
+ tokenIn: "0x0Bd7D308f8E1639FAb988Df18A8011f41EAcAD73", // WETH
168
+ tokenOut: "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168", // USDG
169
+ amountIn: ethers.parseUnits("0.01", 18).toString(),
170
+ minAmountOut: ethers.parseUnits("30", 18).toString(),
171
+ deadline: Math.floor(Date.now() / 1000) + 86400, // 24h expiry
172
+ // nonce: auto-generated from Date.now()
173
+ // feeOnOutput: false (default — fee taken from input)
174
+ // recipient: maker (default — output goes to maker)
175
+ // unwrapOutput: false (default)
176
+ });
177
+
178
+ // ── Step 2: Approve the correct contract (one-time per token + fee mode) ──
179
+ // feeOnOutput=false (default) → approve this network's LO contract
180
+ // feeOnOutput=true → approve this network's SwitchRouter
181
+ const approvalTarget = getLimitOrderApprovalTarget(network, order.feeOnOutput, {
182
+ limitOrderContract: live.limitOrderContract,
183
+ });
184
+ const token = new ethers.Contract(order.tokenIn, ERC20_ABI, signer);
185
+ const allowance: bigint = await token.allowance(maker, approvalTarget);
186
+
187
+ if (allowance < BigInt(order.amountIn)) {
188
+ const tx = await token.approve(approvalTarget, ethers.MaxUint256);
189
+ await tx.wait();
190
+ }
191
+
192
+ // ── Step 3: Sign via EIP-712 (gasless!) ──
193
+ const { domain, types } = getNetworkEIP712SigningParams(
194
+ network,
195
+ live.limitOrderContract,
196
+ );
197
+ const signature = await signer.signTypedData(domain, types, order);
198
+
199
+ // ── Step 4: Submit to the Switch backend ──
200
+ // ⚠️ CRITICAL: The signature is off-chain only — if this POST fails,
201
+ // the order is lost. Submit IMMEDIATELY after signing, await the
202
+ // response, and retry on network failure.
203
+ const result = await submitLimitOrder(
204
+ { ...order, signature, limitOrderContract: live.limitOrderContract },
205
+ { network },
206
+ );
207
+
208
+ if ("error" in result) {
209
+ // Retry logic recommended here — the backend is idempotent on maker+nonce
210
+ console.error("Failed:", result.error);
211
+ } else {
212
+ console.log("Order created:", result.order.id);
213
+ console.log("Status:", result.order.status); // "ACTIVE"
214
+ }
215
+ ```
216
+
217
+ ### Order Parameters
218
+
219
+ | Parameter | Type | Required | Default | Description |
220
+ |---|---|---|---|---|
221
+ | `maker` | `string` | Yes | — | Maker (signer) address |
222
+ | `tokenIn` | `string` | Yes | — | Input token address (token being sold) |
223
+ | `tokenOut` | `string` | Yes | — | Output token address (token being bought) |
224
+ | `amountIn` | `string` | Yes | — | Amount of tokenIn in wei |
225
+ | `minAmountOut` | `string` | Yes | — | Minimum acceptable output in wei |
226
+ | `deadline` | `number` | No | `0` | Unix timestamp expiry. `0` = no expiry |
227
+ | `nonce` | `number` | No | `Date.now()` | Unique per maker. Auto-generated if omitted |
228
+ | `feeOnOutput` | `boolean` | No | `false` | Fee mode — see [Choosing `feeOnOutput`](#choosing-feeonoutput) below |
229
+ | `recipient` | `string` | No | `maker` | Address to receive output tokens |
230
+ | `unwrapOutput` | `boolean` | No | `false` | Unwrap wrapped native output to PLS/ETH on the selected network |
231
+
232
+ ---
233
+
234
+ ## Native Currency Limit Orders (SwitchPLSFlow)
235
+
236
+ When selling native **PLS** on PulseChain or native **ETH** on Robinhood
237
+ (rather than WPLS/WETH), use the selected network's native-flow contract
238
+ instead of the standard EIP-712 signing flow. The deployed contract retains
239
+ the legacy `SwitchPLSFlow` name on both networks, but its behavior is
240
+ native-currency neutral:
241
+
242
+ - **No approval needed** — users send the native currency directly to the contract
243
+ - **No EIP-712 signature** — the contract creates the order on-chain immediately
244
+ - **Single transaction** — wrap + approve + place order all in one tx
245
+ - **Fully indexed** — the backend discovers orders via `PLSOrderCreated` events
246
+
247
+ ### Native-flow contract address
248
+
249
+ ```ts
250
+ import { getNativeFlowAddress, PLS_FLOW_ABI } from "@switch-win/sdk";
251
+
252
+ const robinhoodNativeFlow = getNativeFlowAddress("robinhood");
253
+ // "0x029FfC6aF9112eA078f1D6f4a98826DDB2136cf6"
254
+ ```
255
+
256
+ ### Creating a native ETH limit order on Robinhood
257
+
258
+ ```ts
259
+ import { ethers } from "ethers";
260
+ import { getNativeFlowAddress, isNativeCurrency } from "@switch-win/sdk/limit-orders";
261
+ import { PLS_FLOW_ABI } from "@switch-win/sdk/constants";
262
+
263
+ const network = "robinhood" as const;
264
+ const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
265
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
266
+
267
+ const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
268
+ if (isNativeCurrency(NATIVE)) {
269
+ const nativeFlow = new ethers.Contract(
270
+ getNativeFlowAddress(network),
271
+ PLS_FLOW_ABI,
272
+ signer,
273
+ );
274
+
275
+ const tokenOut = "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168"; // USDG
276
+ const amountIn = ethers.parseEther("0.01");
277
+ const minAmountOut = ethers.parseUnits("30", 18);
278
+ const deadline = Math.floor(Date.now() / 1000) + 86400; // 24h expiry
279
+ const feeOnOutput = false;
280
+ const unwrapOutput = false;
281
+
282
+ // Single transaction — no approval, no signing
283
+ const tx = await nativeFlow.createOrder(
284
+ tokenOut,
285
+ minAmountOut,
286
+ deadline,
287
+ feeOnOutput,
288
+ unwrapOutput,
289
+ ethers.ZeroAddress, // partnerAddress (0x0 = no partner)
290
+ ethers.ZeroAddress, // recipient (0x0 = defaults to msg.sender)
291
+ { value: amountIn }
292
+ );
293
+
294
+ const receipt = await tx.wait();
295
+ console.log("Native ETH limit order created in tx:", receipt.hash);
296
+
297
+ // The backend will automatically detect this order via PLSOrderCreated event.
298
+ // No POST to /limit-orders required (though it's harmless if you do).
299
+ }
300
+ ```
301
+
302
+ ### Native flow vs EIP-712 orders
303
+
304
+ | Aspect | Native flow (PLS/ETH) | EIP-712 (ERC-20) |
305
+ |---|---|---|
306
+ | **Input token** | Native currency | Any ERC-20 (including WPLS/WETH) |
307
+ | **User experience** | Single transaction | Approve + Sign + Submit |
308
+ | **Gas cost** | User pays gas | Gasless signing (user pays on fill) |
309
+ | **Order discovery** | On-chain event | Requires successful POST |
310
+ | **Maker address** | Native-flow contract | User's wallet |
311
+ | **Recipient** | User's wallet (or custom) | User's wallet (or custom) |
312
+
313
+ ### Important: Order Discovery
314
+
315
+ For native-flow orders, the `maker` field in the order record is the
316
+ **native-flow contract address**, not the user's address. The actual user is
317
+ stored in the `recipient` field.
318
+
319
+ When querying orders for a user, use the `owner` parameter instead of `maker`:
320
+
321
+ ```ts
322
+ // ✅ Correct — finds both EIP-712 and native-flow orders
323
+ const { orders } = await fetchLimitOrders({
324
+ network: "robinhood",
325
+ owner: "0xUserAddress", // matches maker OR recipient
326
+ status: "ACTIVE",
327
+ });
328
+
329
+ // ❌ Won't find native-flow orders
330
+ const { orders } = await fetchLimitOrders({
331
+ network: "robinhood",
332
+ maker: "0xUserAddress", // only matches maker field
333
+ status: "ACTIVE",
334
+ });
335
+ ```
336
+
337
+ ### Cancelling a native-flow order
338
+
339
+ Native-flow orders can be cancelled by the original creator (`recipient`):
340
+
341
+ ```ts
342
+ const nativeFlow = new ethers.Contract(
343
+ getNativeFlowAddress("robinhood"),
344
+ PLS_FLOW_ABI,
345
+ signer,
346
+ );
347
+
348
+ // Cancel on-chain (only the original creator can call this)
349
+ const tx = await nativeFlow.cancelOrder(nonceToCancel);
350
+ await tx.wait();
351
+
352
+ // The backend will detect the NonceCancelled event and update the order status.
353
+ // No REST mutation is required.
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Choosing `feeOnOutput`
359
+
360
+ The `feeOnOutput` flag is **baked into the signed order** and cannot be changed after signing. It controls which contract the maker must approve **and** how flexibly operators (fillers) can execute the order. Choose carefully — it affects your order's fillability.
361
+
362
+ ### Approval target
363
+
364
+ | `feeOnOutput` | Maker approves | SDK helper |
365
+ |---|---|---|
366
+ | `false` (default) | **SwitchLimitOrder** — LO contract pulls tokens from maker, then routes internally | `getApprovalTarget()` |
367
+ | `true` | **SwitchRouter** — Router pulls tokens directly from maker to pool(s) | `getRouterApprovalTarget()` |
368
+
369
+ Only **one** approval is needed per fee mode, but the target is different.
370
+
371
+ ### Operator (filler) flexibility
372
+
373
+ Operators choose how to take their profit when filling an order: either from **excess input** (route less than `amountIn`, keep the unrouted tokens) or from **output surplus** (route all input, keep output above `minAmountOut`).
374
+
375
+ | `feeOnOutput` | Excess from input | Excess from output |
376
+ |---|---|---|
377
+ | `false` | ✅ Always works | ✅ Always works |
378
+ | `true` | ⚠️ Requires maker approved **both** LO + Router | ✅ Always works |
379
+
380
+ When `feeOnOutput=false`, the LO contract pulls all tokens to itself first — operators have full freedom to optimize their profit-taking strategy.
381
+
382
+ When `feeOnOutput=true`, the Router pulls tokens directly from the maker. If an operator also tries to take excess input via the LO contract, that requires a second approval the maker typically hasn't granted — the transaction would simply revert. The operator loses gas; the maker loses nothing and the order remains active for another operator. In practice, operators use output-side profit for `feeOnOutput=true` orders.
383
+
384
+ > **Impact on fillability:** `feeOnOutput=true` orders are still fully fillable, but operators have less flexibility. In tight-margin or low-liquidity situations, this *could* reduce the likelihood of a fill since operators cannot optimize their profit-taking strategy on both sides.
385
+
386
+ ### Tax token decision guide
387
+
388
+ | Scenario | Recommended `feeOnOutput` | Why |
389
+ |---|---|---|
390
+ | **Neither token is taxed** | `false` (default) | Maximum operator flexibility → best chance of fill |
391
+ | **Input token is taxed** | `true` | Router sends tokens directly from maker → pool in one transfer (one tax). Default mode would do maker → LO → pool (two transfers, two taxes). |
392
+ | **Output token is taxed** | `false` (default) | Output goes directly to recipient in one transfer. |
393
+ | **Both tokens taxed** | `false` | Supported through tax-safe routes; fee on input avoids another output-token transfer. |
394
+
395
+ > **Recommendation:** Use `feeOnOutput: false` (the default) unless the input token has a transfer tax. This gives operators the most flexibility and the best chance of filling your order.
396
+
397
+ ---
398
+
399
+ ## Querying Limit Orders
400
+
401
+ ```ts
402
+ import {
403
+ fetchLimitOrders,
404
+ fetchLimitOrder,
405
+ fetchLimitOrderPairs,
406
+ fetchLimitOrderStats,
407
+ } from "@switch-win/sdk/limit-orders";
408
+
409
+ // All active orders for a maker
410
+ const { orders, total } = await fetchLimitOrders({
411
+ maker: "0xYourAddress",
412
+ status: "ACTIVE",
413
+ });
414
+
415
+ // All orders for a user (includes native-PLS orders where user is recipient)
416
+ const { orders: allMyOrders } = await fetchLimitOrders({
417
+ owner: "0xYourAddress",
418
+ status: "ACTIVE",
419
+ });
420
+
421
+ // Single order by maker + nonce
422
+ const order = await fetchLimitOrder("0xYourAddress", 1717171717);
423
+
424
+ // Active trading pairs with order counts
425
+ const pairs = await fetchLimitOrderPairs();
426
+ // [{ pairKey: "0xwpls:0xplsx", tokenIn: "0x...", tokenOut: "0x...", activeOrders: 42 }]
427
+
428
+ // Global summary stats
429
+ const stats = await fetchLimitOrderStats();
430
+ // { active: 150, filled: 3200, cancelled: 45, expired: 12, total: 3407 }
431
+ ```
432
+
433
+ ### Query Filters
434
+
435
+ | Parameter | Type | Default | Description |
436
+ |---|---|---|---|
437
+ | `status` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` | `"ACTIVE"` | Filter by order status |
438
+ | `maker` | `string` | — | Filter by maker address |
439
+ | `owner` | `string` | — | Filter by owner (matches maker OR recipient) |
440
+ | `partnerAddress` | `string` | — | Filter by partner address stored on the order |
441
+ | `tokenIn` | `string` | — | Filter by input token |
442
+ | `tokenOut` | `string` | — | Filter by output token |
443
+ | `pair` | `string` | — | Filter by pair key (`tokenIn:tokenOut`, lowercased) |
444
+ | `limit` | `number` | `100` | Page size (1–500) |
445
+ | `offset` | `number` | `0` | Page offset |
446
+
447
+ > Each order record returned by the API includes the original `partnerAddress` used when the order was created, plus the `limitOrderContract` that should be used for on-chain cancellation/fill interactions.
448
+
449
+ ---
450
+
451
+ ## Cancelling a Limit Order
452
+
453
+ Cancellation is an **on-chain operation**:
454
+
455
+ 1. **On-chain:** Call `invalidateNonce(nonce)` on the **order's** SwitchLimitOrder contract. This is the authoritative cancellation — it prevents any operator from executing the order even if the backend hasn't been notified yet.
456
+
457
+ > **⚠️** Each order includes a `limitOrderContract` field. Always use that address — do not hardcode a single contract constant, as the contract may be redeployed across versions.
458
+
459
+ The backend indexer observes `NonceCancelled` and updates the orderbook. No
460
+ separate REST mutation is required.
461
+
462
+ ```ts
463
+ import { ethers } from "ethers";
464
+ import { LIMIT_ORDER_ABI } from "@switch-win/sdk/constants";
465
+
466
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
467
+ const maker = await signer.getAddress();
468
+ const nonceToCancel = 1717171717;
469
+
470
+ // Step 1: Invalidate the nonce on-chain (prevents fill)
471
+ // ⚠️ Use order.limitOrderContract — not a hardcoded address
472
+ const contract = new ethers.Contract(order.limitOrderContract, LIMIT_ORDER_ABI, signer);
473
+ const tx = await contract.invalidateNonce(nonceToCancel);
474
+ await tx.wait();
475
+
476
+ // The backend indexer observes the event and marks the order CANCELLED.
477
+ ```
478
+
479
+ To cancel **multiple orders** at once, use `invalidateNonces(uint256[])`:
480
+
481
+ ```ts
482
+ const nonces = [1717171717, 1717171718, 1717171719];
483
+ const tx = await contract.invalidateNonces(nonces);
484
+ await tx.wait();
485
+
486
+ // The backend indexer observes each NonceCancelled event.
487
+ ```
488
+
489
+ ---
490
+
491
+ ## API Reference
492
+
493
+ **Base URL:** `https://quote.switch.win`
494
+
495
+ | Method | Path | Description |
496
+ |---|---|---|
497
+ | `POST` | `/limit-orders` | Submit a signed limit order |
498
+ | `GET` | `/limit-orders` | List orders (with query filters) |
499
+ | `GET` | `/limit-orders/pairs` | Active pairs with order counts |
500
+ | `GET` | `/limit-orders/stats` | Summary statistics |
501
+ | `GET` | `/limit-orders/:maker/:nonce` | Single order by maker + nonce |
502
+
503
+ ### `POST /limit-orders`
504
+
505
+ Submit a signed limit order. The backend verifies the EIP-712 signature before storing.
506
+
507
+ > **⚠️ CRITICAL — Post immediately after signing.** For standard ERC-20 orders, the
508
+ > EIP-712 signature is purely off-chain. Nothing is recorded on-chain until an operator
509
+ > fills the order. If this POST never arrives (user closes browser, network
510
+ > error, frontend navigates away), **the order is permanently lost**.
511
+ >
512
+ > Your integration must:
513
+ > 1. Call `submitLimitOrder()` **immediately** after the user signs.
514
+ > 2. **Await** the response and confirm `success: true` before showing "order created".
515
+ > 3. **Retry** on transient failures — the backend is idempotent on `maker + nonce`.
516
+ >
517
+ > Native PLS/ETH flow orders are the exception — they are recorded on-chain first,
518
+ > so the backend discovers them via event indexing even if the POST never arrives.
519
+
520
+ **Request body:**
521
+
522
+ ```json
523
+ {
524
+ "maker": "0x...",
525
+ "tokenIn": "0x...",
526
+ "tokenOut": "0x...",
527
+ "amountIn": "1000000000000000000",
528
+ "minAmountOut": "500000000000000000000",
529
+ "deadline": 1717257600,
530
+ "nonce": 1717171717,
531
+ "feeOnOutput": false,
532
+ "recipient": "0x...",
533
+ "unwrapOutput": false,
534
+ "limitOrderContract": "0x...",
535
+ "signature": "0x..."
536
+ }
537
+ ```
538
+
539
+ **Success response (201):**
540
+
541
+ ```json
542
+ {
543
+ "success": true,
544
+ "order": {
545
+ "id": "clx...",
546
+ "maker": "0x...",
547
+ "tokenIn": "0x...",
548
+ "tokenOut": "0x...",
549
+ "amountIn": "1000000000000000000",
550
+ "minAmountOut": "500000000000000000000",
551
+ "deadline": 1717257600,
552
+ "nonce": 1717171717,
553
+ "feeOnOutput": false,
554
+ "recipient": "0x...",
555
+ "unwrapOutput": false,
556
+ "partnerAddress": "0x0000000000000000000000000000000000000000",
557
+ "limitOrderContract": "0x...",
558
+ "signature": "0x...",
559
+ "pairKey": "0x...:0x...",
560
+ "status": "ACTIVE",
561
+ "createdAt": "2025-01-01T00:00:00.000Z",
562
+ "updatedAt": "2025-01-01T00:00:00.000Z"
563
+ }
564
+ }
565
+ ```
566
+
567
+ **Error responses:**
568
+
569
+ | Error | Cause |
570
+ |---|---|
571
+ | `"Missing required field: ..."` | A required field is missing from the body |
572
+ | `"Invalid address format"` | One of the address fields is not a valid hex address |
573
+ | `"tokenIn and tokenOut must differ"` | Trying to create an order that swaps a token to itself |
574
+ | `"Invalid signature"` | EIP-712 signature verification failed |
575
+ | `"Signature does not match maker"` | The recovered signer doesn't match the `maker` field |
576
+ | `"Nonce N already used for maker 0x..."` | This nonce has already been used (filled or submitted) |
577
+
578
+ ### `GET /limit-orders`
579
+
580
+ List orders with optional query filters. See [Query Filters](#query-filters) above.
581
+
582
+ **Response:**
583
+
584
+ ```json
585
+ {
586
+ "total": 42,
587
+ "limit": 100,
588
+ "offset": 0,
589
+ "orders": [
590
+ {
591
+ "id": "clx...",
592
+ "maker": "0x...",
593
+ "recipient": "0x...",
594
+ "tokenIn": "0x...",
595
+ "tokenOut": "0x...",
596
+ "amountIn": "1000000000000000000",
597
+ "minAmountOut": "500000000000000000000",
598
+ "deadline": 1717257600,
599
+ "nonce": 1717171717,
600
+ "feeOnOutput": false,
601
+ "unwrapOutput": false,
602
+ "partnerAddress": "0xYourPartnerAddress",
603
+ "limitOrderContract": "0x...",
604
+ "signature": "0x...",
605
+ "pairKey": "0x...:0x...",
606
+ "status": "ACTIVE",
607
+ "createdAt": "2025-01-01T00:00:00.000Z",
608
+ "updatedAt": "2025-01-01T00:00:00.000Z",
609
+ "filledTxHash": null,
610
+ "filler": null,
611
+ "fillerProfit": null
612
+ }
613
+ ]
614
+ }
615
+ ```
616
+
617
+ The backend currently returns the full order record, so integrations should expect at least the fields above and tolerate additional metadata fields in future deployments.
618
+
619
+ ### `GET /limit-orders/pairs`
620
+
621
+ Returns active trading pairs with order counts.
622
+
623
+ ```json
624
+ [
625
+ { "pairKey": "0xwpls:0xplsx", "tokenIn": "0x...", "tokenOut": "0x...", "activeOrders": 12 }
626
+ ]
627
+ ```
628
+
629
+ ### `GET /limit-orders/stats`
630
+
631
+ Summary statistics.
632
+
633
+ ```json
634
+ { "active": 150, "filled": 3200, "cancelled": 45, "expired": 12, "total": 3407 }
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Types Reference
640
+
641
+ > All types are available in [`src/types.ts`](src/types.ts).
642
+
643
+ | Type | Description |
644
+ |---|---|
645
+ | `LimitOrderParams` | Order fields for EIP-712 signing |
646
+ | `SignedLimitOrder` | `LimitOrderParams` + `signature` |
647
+ | `CreateLimitOrderRequest` | Alias for `SignedLimitOrder` (POST body) |
648
+ | `LimitOrderStatus` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` |
649
+ | `LimitOrderRecord` | Full order record from the API (includes `partnerAddress`, `limitOrderContract`, status, and timestamps) |
650
+ | `CreateLimitOrderResponse` | `{ success: true, order: LimitOrderRecord }` |
651
+ | `ListLimitOrdersResponse` | `{ total, limit, offset, orders: LimitOrderRecord[] }` |
652
+ | `LimitOrderPair` | Active pair with order count |
653
+ | `LimitOrderStats` | Global order statistics |
654
+ | `LimitOrderMutationResponse` | Submission response + `ErrorResponse` |
655
+
656
+ ---
657
+
658
+ ## Helper Functions
659
+
660
+ Available from `@switch-win/sdk/limit-orders`:
661
+
662
+ | Function | Description |
663
+ |---|---|
664
+ | `buildLimitOrder(options)` | Build a `LimitOrderParams` object with sensible defaults |
665
+ | `getEIP712SigningParams()` | Get the `{ domain, types }` for `signTypedData()` |
666
+ | `getNetworkEIP712SigningParams(network, contract?)` | Build the correct PulseChain or Robinhood EIP-712 domain |
667
+ | `getLimitOrderNetworkConfig(network)` | Get static chain, router, LO, wrapped-native, and native-flow defaults |
668
+ | `getLimitOrderApprovalTarget(network, feeOnOutput, overrides?)` | Resolve the correct maker approval target |
669
+ | `getApprovalTarget()` | Get approval target for `feeOnOutput: false` orders → SwitchLimitOrder |
670
+ | `getRouterApprovalTarget()` | Get approval target for `feeOnOutput: true` orders → SwitchRouter |
671
+ | `shouldUnwrapOutput(tokenOut)` | Returns `true` if tokenOut is WPLS (should set `unwrapOutput: true`) |
672
+ | `getPLSFlowAddress()` | Get the SwitchPLSFlow contract address for native PLS limit orders |
673
+ | `getNativeFlowAddress(network)` | Get the native PLS/ETH flow address for either chain |
674
+ | `isNativePLS(tokenIn)` | Returns `true` if tokenIn is native PLS (use PLSFlow instead of EIP-712) |
675
+ | `isNativeCurrency(tokenIn)` | Network-neutral native sentinel check |
676
+ | `submitLimitOrder(signedOrder)` | POST a signed order to the Switch backend |
677
+ | `fetchLimitOrders(options?)` | GET orders with optional filters |
678
+ | `fetchLimitOrder(maker, nonce)` | GET a single order by maker + nonce |
679
+ | `fetchLimitOrderPairs()` | GET active pairs with order counts |
680
+ | `fetchLimitOrderConfig({ network })` | GET live deployments and EIP-712 domain |
681
+ | `fetchLimitOrderStats()` | GET global order statistics |
682
+
683
+ ---
684
+
685
+ ## EIP-712 Signing Details
686
+
687
+ The EIP-712 domain and types must match the on-chain contract exactly:
688
+
689
+ ```ts
690
+ // Domain
691
+ {
692
+ name: "SwitchLimitOrder",
693
+ version: "2",
694
+ chainId: 369,
695
+ verifyingContract: SWITCH_LIMIT_ORDER // must match deployed address
696
+ }
697
+
698
+ // Types
699
+ {
700
+ LimitOrder: [
701
+ { name: "maker", type: "address" },
702
+ { name: "tokenIn", type: "address" },
703
+ { name: "tokenOut", type: "address" },
704
+ { name: "amountIn", type: "uint256" },
705
+ { name: "minAmountOut", type: "uint256" },
706
+ { name: "deadline", type: "uint256" },
707
+ { name: "nonce", type: "uint256" },
708
+ { name: "feeOnOutput", type: "bool" },
709
+ { name: "recipient", type: "address" },
710
+ { name: "unwrapOutput", type: "bool" },
711
+ ]
712
+ }
713
+ ```
714
+
715
+ These are exported as `LIMIT_ORDER_EIP712_DOMAIN` and `LIMIT_ORDER_EIP712_TYPES` from `@switch-win/sdk/constants`.
716
+
717
+ ---
718
+
719
+ ## On-Chain Revert Errors
720
+
721
+ | Error | Meaning |
722
+ |---|---|
723
+ | `ExcessiveFee()` | Contract fee exceeds the maximum allowed |
724
+ | `InsufficientOutput()` | Output fell below `minAmountOut` |
725
+ | `InvalidAmount()` | Order amount is zero |
726
+ | `InvalidSignature()` | EIP-712 signature doesn't recover to the maker |
727
+ | `InvalidTokens()` | tokenIn and tokenOut are the same, or zero address |
728
+ | `NonceAlreadyUsed()` | This nonce has already been filled or invalidated |
729
+ | `OrderExpired()` | `block.timestamp > deadline` (and deadline > 0) |
730
+ | `RouteInputExceedsMax()` | Route's total input exceeds the order's `amountIn` |
731
+ | `TransferFailed()` | Token transfer failed (insufficient balance or allowance) |
732
+
733
+ ---
734
+
735
+ *See also: [README.md](README.md) for swap API docs, constants, and general integration info.*
736
+
737
+ *Last updated: July 2026*