@switch-win/sdk 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/GASLESS.md +407 -0
  3. package/LIMIT-ORDERS.md +212 -198
  4. package/README.md +131 -105
  5. package/ROBINHOOD.md +201 -198
  6. package/abi/GaslessSwitchV2ABI.json +1095 -0
  7. package/dist/constants.d.ts +276 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +403 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/gasless-client.d.ts +81 -0
  12. package/dist/gasless-client.d.ts.map +1 -0
  13. package/dist/gasless-client.js +467 -0
  14. package/dist/gasless-client.js.map +1 -0
  15. package/dist/gasless-index.d.ts +5 -0
  16. package/dist/gasless-index.d.ts.map +1 -0
  17. package/dist/gasless-index.js +5 -0
  18. package/dist/gasless-index.js.map +1 -0
  19. package/dist/gasless-operators.d.ts +32 -0
  20. package/dist/gasless-operators.d.ts.map +1 -0
  21. package/dist/gasless-operators.js +132 -0
  22. package/dist/gasless-operators.js.map +1 -0
  23. package/dist/gasless-store.d.ts +18 -0
  24. package/dist/gasless-store.d.ts.map +1 -0
  25. package/dist/gasless-store.js +83 -0
  26. package/dist/gasless-store.js.map +1 -0
  27. package/dist/gasless-types.d.ts +218 -0
  28. package/dist/gasless-types.d.ts.map +1 -0
  29. package/dist/gasless-types.js +2 -0
  30. package/dist/gasless-types.js.map +1 -0
  31. package/dist/gasless.d.ts +135 -0
  32. package/dist/gasless.d.ts.map +1 -0
  33. package/dist/gasless.js +513 -0
  34. package/dist/gasless.js.map +1 -0
  35. package/dist/index.d.ts +29 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +30 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/limit-orders.d.ts +440 -0
  40. package/dist/limit-orders.d.ts.map +1 -0
  41. package/dist/limit-orders.js +387 -0
  42. package/dist/limit-orders.js.map +1 -0
  43. package/dist/networks/index.d.ts +3 -0
  44. package/dist/networks/index.d.ts.map +1 -0
  45. package/dist/networks/index.js +2 -0
  46. package/dist/networks/index.js.map +1 -0
  47. package/dist/networks/robinhood.d.ts +536 -0
  48. package/dist/networks/robinhood.d.ts.map +1 -0
  49. package/dist/networks/robinhood.js +489 -0
  50. package/dist/networks/robinhood.js.map +1 -0
  51. package/dist/types.d.ts +436 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +19 -0
  54. package/dist/types.js.map +1 -0
  55. package/examples/gasless-nextjs-proxy.ts +87 -0
  56. package/examples/gasless-operator.ts +76 -0
  57. package/examples/gasless-swap.ts +170 -0
  58. package/examples/limit-order-ethers.ts +195 -0
  59. package/examples/nextjs-proxy.ts +82 -0
  60. package/examples/react-hooks.tsx +396 -0
  61. package/examples/swap-ethers.ts +209 -0
  62. package/examples/swap-web3py.py +267 -0
  63. package/package.json +53 -12
  64. package/src/constants.ts +181 -178
  65. package/src/gasless-client.ts +823 -0
  66. package/src/gasless-index.ts +4 -0
  67. package/src/gasless-operators.ts +227 -0
  68. package/src/gasless-store.ts +137 -0
  69. package/src/gasless-types.ts +258 -0
  70. package/src/gasless.ts +756 -0
  71. package/src/index.ts +69 -63
  72. package/src/limit-orders.ts +206 -195
  73. package/src/types.ts +80 -55
package/LIMIT-ORDERS.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # Switch Limit Orders — Integration Guide
2
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 | `0x2afBf0aB8d958a0227742F7a8BdA00c96372E4D7` | `0x0362177FF2ad25a33a879c881a5055575C63a4cE` |
10
- | `robinhood` | 4663 | ETH | `0x1E05115387f314398bbb1A808B25308E71150396` | `0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689` |
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.
3
+ > **Gasless EIP-712 signed limit orders on PulseChain and Robinhood Chain**
4
+
5
+ This guide describes ordinary limit orders. The dedicated GaslessSwitch swap
6
+ option has a separate domain, contract, fee, payout cap and approval target;
7
+ use [GASLESS.md](GASLESS.md) and the dedicated SDK helpers for that flow.
8
+
9
+ **Limit Order API:** `https://quote.switch.win`
10
+
11
+ | API network | Chain ID | Native currency | Current limit-order contract | Native flow contract |
12
+ |---|---:|---|---|---|
13
+ | `pulsechain` | 369 | PLS | `0x2afBf0aB8d958a0227742F7a8BdA00c96372E4D7` | `0x0362177FF2ad25a33a879c881a5055575C63a4cE` |
14
+ | `robinhood` | 4663 | ETH | `0x1E05115387f314398bbb1A808B25308E71150396` | `0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689` |
15
+
16
+ Pass `network: "pulsechain"` or `network: "robinhood"` to every SDK API
17
+ helper. Call `fetchLimitOrderConfig({ network })` at startup and treat its
18
+ addresses and EIP-712 domain as the live source of truth.
15
19
 
16
20
  ---
17
21
 
@@ -21,7 +25,7 @@ addresses and EIP-712 domain as the live source of truth.
21
25
  1. [Overview](#overview)
22
26
  2. [How It Works](#how-it-works)
23
27
  3. [Creating a Limit Order](#creating-a-limit-order)
24
- 4. [Native Currency Limit Orders (SwitchPLSFlow)](#native-currency-limit-orders-switchplsflow)
28
+ 4. [Native Currency Limit Orders (SwitchPLSFlow)](#native-currency-limit-orders-switchplsflow)
25
29
  5. [Choosing `feeOnOutput`](#choosing-feeonoutput)
26
30
  6. [Querying Limit Orders](#querying-limit-orders)
27
31
  7. [Cancelling a Limit Order](#cancelling-a-limit-order)
@@ -60,37 +64,37 @@ Switch Limit Orders let users place **gasless, signed orders** that are filled a
60
64
  - **Nonce-based replay protection** — each order has a unique nonce per maker
61
65
  - **Optional expiry** — set a deadline or make the order valid forever
62
66
  - **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
- ```
67
+ - **Native unwrap** — WPLS can be unwrapped to PLS and Robinhood WETH can be
68
+ unwrapped to ETH by setting `unwrapOutput`
69
+
70
+ > **⚠️ 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.
71
+
72
+ ### Network-aware setup
73
+
74
+ ```ts
75
+ import {
76
+ fetchLimitOrderConfig,
77
+ getLimitOrderApprovalTarget,
78
+ getNetworkEIP712SigningParams,
79
+ submitLimitOrder,
80
+ } from "@switch-win/sdk/limit-orders";
81
+
82
+ // `signer` and a completed `order` are assumed below.
83
+ const network = "robinhood" as const;
84
+ const live = await fetchLimitOrderConfig({ network });
85
+ const { domain, types } = getNetworkEIP712SigningParams(
86
+ network,
87
+ live.limitOrderContract,
88
+ );
89
+ const approvalTarget = getLimitOrderApprovalTarget(network, order.feeOnOutput, {
90
+ limitOrderContract: live.limitOrderContract,
91
+ });
92
+ const signature = await signer.signTypedData(domain, types, order);
93
+ await submitLimitOrder(
94
+ { ...order, signature, limitOrderContract: live.limitOrderContract },
95
+ { network },
96
+ );
97
+ ```
94
98
 
95
99
  ---
96
100
 
@@ -113,9 +117,9 @@ await submitLimitOrder(
113
117
  │ Tokens are pulled from the maker's wallet and swapped. │
114
118
  │ Output is sent to the maker (or custom recipient). │
115
119
  │ │
116
- │ 5. CANCEL (optional) │
117
- │ invalidateNonce(nonce) on-chain — prevents execution │
118
- │ The backend indexer observes the cancellation event. │
120
+ │ 5. CANCEL (optional) │
121
+ │ invalidateNonce(nonce) on-chain — prevents execution │
122
+ │ The backend indexer observes the cancellation event. │
119
123
  └─────────────────────────────────────────────────────────────────────────┘
120
124
  ```
121
125
 
@@ -131,10 +135,12 @@ await submitLimitOrder(
131
135
  > Your integration must:
132
136
  > 1. Call `submitLimitOrder()` **immediately** after the user signs.
133
137
  > 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`.
138
+ > 3. On a transient failure, look up that exact maker + nonce and verify its
139
+ > contract and signed fields. Duplicates return an error, not idempotent
140
+ > success. Retry only the original signed payload; never generate a new nonce.
135
141
  > 4. Do **not** navigate away or close the signing flow until the backend confirms.
136
142
  >
137
- > *Native PLS/ETH flow orders are the exception — they are recorded on-chain*
143
+ > *Native PLS/ETH flow orders are the exception — they are recorded on-chain*
138
144
  > *first, so the backend discovers them via event indexing even if the POST*
139
145
  > *never arrives.*
140
146
 
@@ -146,28 +152,28 @@ The full lifecycle in code (ethers.js v6):
146
152
 
147
153
  ```ts
148
154
  import { ethers } from "ethers";
149
- import {
150
- buildLimitOrder,
151
- fetchLimitOrderConfig,
152
- getLimitOrderApprovalTarget,
153
- getNetworkEIP712SigningParams,
154
- submitLimitOrder,
155
- } from "@switch-win/sdk/limit-orders";
155
+ import {
156
+ buildLimitOrder,
157
+ fetchLimitOrderConfig,
158
+ getLimitOrderApprovalTarget,
159
+ getNetworkEIP712SigningParams,
160
+ submitLimitOrder,
161
+ } from "@switch-win/sdk/limit-orders";
156
162
  import { ERC20_ABI } from "@switch-win/sdk/constants";
157
163
 
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 });
164
+ const network = "robinhood" as const;
165
+ const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
166
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
167
+ const maker = await signer.getAddress();
168
+ const live = await fetchLimitOrderConfig({ network });
163
169
 
164
170
  // ── Step 1: Build the order ──
165
171
  const order = buildLimitOrder({
166
172
  maker,
167
- tokenIn: "0x0Bd7D308f8E1639FAb988Df18A8011f41EAcAD73", // WETH
168
- tokenOut: "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168", // USDG
169
- amountIn: ethers.parseUnits("0.01", 18).toString(),
170
- minAmountOut: ethers.parseUnits("30", 18).toString(),
173
+ tokenIn: "0x0Bd7D308f8E1639FAb988Df18A8011f41EAcAD73", // WETH
174
+ tokenOut: "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168", // USDG
175
+ amountIn: ethers.parseUnits("0.01", 18).toString(),
176
+ minAmountOut: ethers.parseUnits("30", 18).toString(),
171
177
  deadline: Math.floor(Date.now() / 1000) + 86400, // 24h expiry
172
178
  // nonce: auto-generated from Date.now()
173
179
  // feeOnOutput: false (default — fee taken from input)
@@ -176,11 +182,11 @@ const order = buildLimitOrder({
176
182
  });
177
183
 
178
184
  // ── 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
- });
185
+ // feeOnOutput=false (default) → approve this network's LO contract
186
+ // feeOnOutput=true → approve this network's SwitchRouter
187
+ const approvalTarget = getLimitOrderApprovalTarget(network, order.feeOnOutput, {
188
+ limitOrderContract: live.limitOrderContract,
189
+ });
184
190
  const token = new ethers.Contract(order.tokenIn, ERC20_ABI, signer);
185
191
  const allowance: bigint = await token.allowance(maker, approvalTarget);
186
192
 
@@ -190,23 +196,23 @@ if (allowance < BigInt(order.amountIn)) {
190
196
  }
191
197
 
192
198
  // ── 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);
199
+ const { domain, types } = getNetworkEIP712SigningParams(
200
+ network,
201
+ live.limitOrderContract,
202
+ );
203
+ const signature = await signer.signTypedData(domain, types, order);
198
204
 
199
205
  // ── Step 4: Submit to the Switch backend ──
200
206
  // ⚠️ CRITICAL: The signature is off-chain only — if this POST fails,
201
207
  // the order is lost. Submit IMMEDIATELY after signing, await the
202
208
  // response, and retry on network failure.
203
- const result = await submitLimitOrder(
204
- { ...order, signature, limitOrderContract: live.limitOrderContract },
205
- { network },
206
- );
209
+ const result = await submitLimitOrder(
210
+ { ...order, signature, limitOrderContract: live.limitOrderContract },
211
+ { network },
212
+ );
207
213
 
208
214
  if ("error" in result) {
209
- // Retry logic recommended here — the backend is idempotent on maker+nonce
215
+ // Look up the exact order before retrying the identical signed payload.
210
216
  console.error("Failed:", result.error);
211
217
  } else {
212
218
  console.log("Order created:", result.order.id);
@@ -227,62 +233,68 @@ if ("error" in result) {
227
233
  | `nonce` | `number` | No | `Date.now()` | Unique per maker. Auto-generated if omitted |
228
234
  | `feeOnOutput` | `boolean` | No | `false` | Fee mode — see [Choosing `feeOnOutput`](#choosing-feeonoutput) below |
229
235
  | `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 |
236
+ | `unwrapOutput` | `boolean` | No | `false` | Unwrap wrapped native output to PLS/ETH on the selected network |
231
237
 
232
238
  ---
233
239
 
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:
240
+ ## Native Currency Limit Orders (SwitchPLSFlow)
241
241
 
242
- - **No approval needed** — users send the native currency directly to the contract
242
+ When selling native **PLS** on PulseChain or native **ETH** on Robinhood
243
+ (rather than WPLS/WETH), use the selected network's native-flow contract
244
+ instead of the standard EIP-712 signing flow. The deployed contract retains
245
+ the legacy `SwitchPLSFlow` name on both networks, but its behavior is
246
+ native-currency neutral:
247
+
248
+ - **No approval needed** — users send the native currency directly to the contract
243
249
  - **No EIP-712 signature** — the contract creates the order on-chain immediately
244
250
  - **Single transaction** — wrap + approve + place order all in one tx
245
- - **Fully indexed** — the backend discovers orders via `PLSOrderCreated` events
246
- - **Drain-mode capable** — maintainers can disable new native deposits without
247
- blocking fills or cancellations of existing orders
251
+ - **Fully indexed** — the backend discovers orders via `PLSOrderCreated` events
252
+ - **Drain-mode capable** — maintainers can disable new native deposits without
253
+ blocking fills or cancellations of existing orders
254
+
255
+ > **Current ABI:** native-flow deployments use the seven-argument
256
+ > `createOrder(tokenOut, minAmountOut, deadline, feeOnOutput, unwrapOutput,
257
+ > partnerAddress, recipient)` signature. The `recipient` argument must be
258
+ > present in calldata; pass the zero address to default it to `msg.sender`.
259
+ > The older six-argument selector is not compatible with current deployments.
248
260
 
249
- ### Native-flow contract address
261
+ ### Native-flow contract address
250
262
 
251
263
  ```ts
252
- import { getNativeFlowAddress, PLS_FLOW_ABI } from "@switch-win/sdk";
253
-
254
- const robinhoodNativeFlow = getNativeFlowAddress("robinhood");
255
- // "0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689"
264
+ import { getNativeFlowAddress, PLS_FLOW_ABI } from "@switch-win/sdk";
265
+
266
+ const robinhoodNativeFlow = getNativeFlowAddress("robinhood");
267
+ // "0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689"
256
268
  ```
257
269
 
258
- ### Creating a native ETH limit order on Robinhood
270
+ ### Creating a native ETH limit order on Robinhood
259
271
 
260
272
  ```ts
261
273
  import { ethers } from "ethers";
262
- import { getNativeFlowAddress, isNativeCurrency } from "@switch-win/sdk/limit-orders";
263
- import { PLS_FLOW_ABI } from "@switch-win/sdk/constants";
264
-
265
- const network = "robinhood" as const;
266
- const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
267
- const signer = new ethers.Wallet(PRIVATE_KEY, provider);
268
-
269
- const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
270
- if (isNativeCurrency(NATIVE)) {
271
- const nativeFlow = new ethers.Contract(
272
- getNativeFlowAddress(network),
273
- PLS_FLOW_ABI,
274
- signer,
275
- );
274
+ import { getNativeFlowAddress, isNativeCurrency } from "@switch-win/sdk/limit-orders";
275
+ import { PLS_FLOW_ABI } from "@switch-win/sdk/constants";
276
+
277
+ const network = "robinhood" as const;
278
+ const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
279
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
280
+
281
+ const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
282
+ if (isNativeCurrency(NATIVE)) {
283
+ const nativeFlow = new ethers.Contract(
284
+ getNativeFlowAddress(network),
285
+ PLS_FLOW_ABI,
286
+ signer,
287
+ );
276
288
 
277
- const tokenOut = "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168"; // USDG
278
- const amountIn = ethers.parseEther("0.01");
279
- const minAmountOut = ethers.parseUnits("30", 18);
289
+ const tokenOut = "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168"; // USDG
290
+ const amountIn = ethers.parseEther("0.01");
291
+ const minAmountOut = ethers.parseUnits("30", 18);
280
292
  const deadline = Math.floor(Date.now() / 1000) + 86400; // 24h expiry
281
293
  const feeOnOutput = false;
282
294
  const unwrapOutput = false;
283
295
 
284
296
  // Single transaction — no approval, no signing
285
- const tx = await nativeFlow.createOrder(
297
+ const tx = await nativeFlow.createOrder(
286
298
  tokenOut,
287
299
  minAmountOut,
288
300
  deadline,
@@ -294,65 +306,65 @@ if (isNativeCurrency(NATIVE)) {
294
306
  );
295
307
 
296
308
  const receipt = await tx.wait();
297
- console.log("Native ETH limit order created in tx:", receipt.hash);
309
+ console.log("Native ETH limit order created in tx:", receipt.hash);
298
310
 
299
311
  // The backend will automatically detect this order via PLSOrderCreated event.
300
312
  // No POST to /limit-orders required (though it's harmless if you do).
301
313
  }
302
314
  ```
303
315
 
304
- ### Native flow vs EIP-712 orders
316
+ ### Native flow vs EIP-712 orders
305
317
 
306
- | Aspect | Native flow (PLS/ETH) | EIP-712 (ERC-20) |
318
+ | Aspect | Native flow (PLS/ETH) | EIP-712 (ERC-20) |
307
319
  |---|---|---|
308
- | **Input token** | Native currency | Any ERC-20 (including WPLS/WETH) |
320
+ | **Input token** | Native currency | Any ERC-20 (including WPLS/WETH) |
309
321
  | **User experience** | Single transaction | Approve + Sign + Submit |
310
322
  | **Gas cost** | User pays gas | Gasless signing (user pays on fill) |
311
323
  | **Order discovery** | On-chain event | Requires successful POST |
312
- | **Maker address** | Native-flow contract | User's wallet |
324
+ | **Maker address** | Native-flow contract | User's wallet |
313
325
  | **Recipient** | User's wallet (or custom) | User's wallet (or custom) |
314
326
 
315
327
  ### Important: Order Discovery
316
328
 
317
- For native-flow orders, the `maker` field in the order record is the
318
- **native-flow contract address**, not the user's address. The actual user is
319
- stored in the `recipient` field.
329
+ For native-flow orders, the `maker` field in the order record is the
330
+ **native-flow contract address**, not the user's address. The actual user is
331
+ stored in the `recipient` field.
320
332
 
321
333
  When querying orders for a user, use the `owner` parameter instead of `maker`:
322
334
 
323
335
  ```ts
324
- // ✅ Correct — finds both EIP-712 and native-flow orders
325
- const { orders } = await fetchLimitOrders({
326
- network: "robinhood",
336
+ // ✅ Correct — finds both EIP-712 and native-flow orders
337
+ const { orders } = await fetchLimitOrders({
338
+ network: "robinhood",
327
339
  owner: "0xUserAddress", // matches maker OR recipient
328
340
  status: "ACTIVE",
329
341
  });
330
342
 
331
- // ❌ Won't find native-flow orders
332
- const { orders } = await fetchLimitOrders({
333
- network: "robinhood",
343
+ // ❌ Won't find native-flow orders
344
+ const { orders } = await fetchLimitOrders({
345
+ network: "robinhood",
334
346
  maker: "0xUserAddress", // only matches maker field
335
347
  status: "ACTIVE",
336
348
  });
337
349
  ```
338
350
 
339
- ### Cancelling a native-flow order
351
+ ### Cancelling a native-flow order
340
352
 
341
- Native-flow orders can be cancelled by the original creator (`recipient`):
353
+ Native-flow orders can be cancelled by the original creator (`recipient`):
342
354
 
343
355
  ```ts
344
- const nativeFlow = new ethers.Contract(
345
- getNativeFlowAddress("robinhood"),
346
- PLS_FLOW_ABI,
347
- signer,
348
- );
356
+ const nativeFlow = new ethers.Contract(
357
+ getNativeFlowAddress("robinhood"),
358
+ PLS_FLOW_ABI,
359
+ signer,
360
+ );
349
361
 
350
362
  // Cancel on-chain (only the original creator can call this)
351
- const tx = await nativeFlow.cancelOrder(nonceToCancel);
363
+ const tx = await nativeFlow.cancelOrder(nonceToCancel);
352
364
  await tx.wait();
353
365
 
354
366
  // The backend will detect the NonceCancelled event and update the order status.
355
- // No REST mutation is required.
367
+ // No REST mutation is required.
356
368
  ```
357
369
 
358
370
  ---
@@ -383,23 +395,23 @@ When `feeOnOutput=false`, the LO contract pulls all tokens to itself first — o
383
395
 
384
396
  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.
385
397
 
386
- > **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.
387
-
388
- ### Protected settlement
389
-
390
- The current PulseChain and Robinhood deployments charge a 30 bps (0.30%)
391
- limit-order fee. They are exempt from the Router's separate 10 bps regular-swap
392
- fee, so the two fees are not stacked.
393
-
394
- For routed fills, operator-retained surplus is capped at 5%. Input-side profit
395
- is capped at 5% of executable input; output-side profit is capped at 5% of the
396
- signed `minAmountOut`. Any additional routing improvement is returned to the
397
- maker.
398
-
399
- Direct fills are protected by the configured `directFillQuoter`. The maker must
400
- receive at least the greater of the signed `minAmountOut` and 95% of the best
401
- current direct-adapter quote. A missing or zero protected quote fails closed,
402
- and any post-fee output sent above the protected floor belongs to the maker.
398
+ > **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.
399
+
400
+ ### Protected settlement
401
+
402
+ The current PulseChain and Robinhood deployments charge a 30 bps (0.30%)
403
+ limit-order fee. They are exempt from the Router's separate 10 bps regular-swap
404
+ fee, so the two fees are not stacked.
405
+
406
+ For routed fills, operator-retained surplus is capped at 5%. Input-side profit
407
+ is capped at 5% of executable input; output-side profit is capped at 5% of the
408
+ signed `minAmountOut`. Any additional routing improvement is returned to the
409
+ maker.
410
+
411
+ Direct fills are protected by the configured `directFillQuoter`. The maker must
412
+ receive at least the greater of the signed `minAmountOut` and 95% of the best
413
+ current direct-adapter quote. A missing or zero protected quote fails closed,
414
+ and any post-fee output sent above the protected floor belongs to the maker.
403
415
 
404
416
  ### Tax token decision guide
405
417
 
@@ -408,7 +420,7 @@ and any post-fee output sent above the protected floor belongs to the maker.
408
420
  | **Neither token is taxed** | `false` (default) | Maximum operator flexibility → best chance of fill |
409
421
  | **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). |
410
422
  | **Output token is taxed** | `false` (default) | Output goes directly to recipient in one transfer. |
411
- | **Both tokens taxed** | `false` | Supported through tax-safe routes; fee on input avoids another output-token transfer. |
423
+ | **Both tokens taxed** | `false` | Supported through tax-safe routes; fee on input avoids another output-token transfer. |
412
424
 
413
425
  > **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.
414
426
 
@@ -468,18 +480,18 @@ const stats = await fetchLimitOrderStats();
468
480
 
469
481
  ## Cancelling a Limit Order
470
482
 
471
- Cancellation is an **on-chain operation**:
483
+ Cancellation is an **on-chain operation**:
472
484
 
473
485
  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.
474
486
 
475
487
  > **⚠️** 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.
476
488
 
477
- The backend indexer observes `NonceCancelled` and updates the orderbook. No
478
- separate REST mutation is required.
489
+ The backend indexer observes `NonceCancelled` and updates the orderbook. No
490
+ separate REST mutation is required.
479
491
 
480
492
  ```ts
481
493
  import { ethers } from "ethers";
482
- import { LIMIT_ORDER_ABI } from "@switch-win/sdk/constants";
494
+ import { LIMIT_ORDER_ABI } from "@switch-win/sdk/constants";
483
495
 
484
496
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
485
497
  const maker = await signer.getAddress();
@@ -491,7 +503,7 @@ const contract = new ethers.Contract(order.limitOrderContract, LIMIT_ORDER_ABI,
491
503
  const tx = await contract.invalidateNonce(nonceToCancel);
492
504
  await tx.wait();
493
505
 
494
- // The backend indexer observes the event and marks the order CANCELLED.
506
+ // The backend indexer observes the event and marks the order CANCELLED.
495
507
  ```
496
508
 
497
509
  To cancel **multiple orders** at once, use `invalidateNonces(uint256[])`:
@@ -501,7 +513,7 @@ const nonces = [1717171717, 1717171718, 1717171719];
501
513
  const tx = await contract.invalidateNonces(nonces);
502
514
  await tx.wait();
503
515
 
504
- // The backend indexer observes each NonceCancelled event.
516
+ // The backend indexer observes each NonceCancelled event.
505
517
  ```
506
518
 
507
519
  ---
@@ -530,9 +542,11 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
530
542
  > Your integration must:
531
543
  > 1. Call `submitLimitOrder()` **immediately** after the user signs.
532
544
  > 2. **Await** the response and confirm `success: true` before showing "order created".
533
- > 3. **Retry** on transient failures — the backend is idempotent on `maker + nonce`.
545
+ > 3. On a transient failure, look up that exact maker + nonce and verify its
546
+ > contract and signed fields. Duplicates return an error, not idempotent
547
+ > success. Retry only the original signed payload; never generate a new nonce.
534
548
  >
535
- > Native PLS/ETH flow orders are the exception — they are recorded on-chain first,
549
+ > Native PLS/ETH flow orders are the exception — they are recorded on-chain first,
536
550
  > so the backend discovers them via event indexing even if the POST never arrives.
537
551
 
538
552
  **Request body:**
@@ -547,11 +561,11 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
547
561
  "deadline": 1717257600,
548
562
  "nonce": 1717171717,
549
563
  "feeOnOutput": false,
550
- "recipient": "0x...",
551
- "unwrapOutput": false,
552
- "limitOrderContract": "0x...",
553
- "signature": "0x..."
554
- }
564
+ "recipient": "0x...",
565
+ "unwrapOutput": false,
566
+ "limitOrderContract": "0x...",
567
+ "signature": "0x..."
568
+ }
555
569
  ```
556
570
 
557
571
  **Success response (201):**
@@ -593,7 +607,7 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
593
607
  | `"Signature does not match maker"` | The recovered signer doesn't match the `maker` field |
594
608
  | `"Nonce N already used for maker 0x..."` | This nonce has already been used (filled or submitted) |
595
609
 
596
- ### `GET /limit-orders`
610
+ ### `GET /limit-orders`
597
611
 
598
612
  List orders with optional query filters. See [Query Filters](#query-filters) above.
599
613
 
@@ -663,13 +677,13 @@ Summary statistics.
663
677
  | `LimitOrderParams` | Order fields for EIP-712 signing |
664
678
  | `SignedLimitOrder` | `LimitOrderParams` + `signature` |
665
679
  | `CreateLimitOrderRequest` | Alias for `SignedLimitOrder` (POST body) |
666
- | `LimitOrderStatus` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` |
680
+ | `LimitOrderStatus` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` |
667
681
  | `LimitOrderRecord` | Full order record from the API (includes `partnerAddress`, `limitOrderContract`, status, and timestamps) |
668
682
  | `CreateLimitOrderResponse` | `{ success: true, order: LimitOrderRecord }` |
669
683
  | `ListLimitOrdersResponse` | `{ total, limit, offset, orders: LimitOrderRecord[] }` |
670
684
  | `LimitOrderPair` | Active pair with order count |
671
685
  | `LimitOrderStats` | Global order statistics |
672
- | `LimitOrderMutationResponse` | Submission response + `ErrorResponse` |
686
+ | `LimitOrderMutationResponse` | Submission response + `ErrorResponse` |
673
687
 
674
688
  ---
675
689
 
@@ -680,23 +694,23 @@ Available from `@switch-win/sdk/limit-orders`:
680
694
  | Function | Description |
681
695
  |---|---|
682
696
  | `buildLimitOrder(options)` | Build a `LimitOrderParams` object with sensible defaults |
683
- | `getEIP712SigningParams()` | Get the `{ domain, types }` for `signTypedData()` |
684
- | `getNetworkEIP712SigningParams(network, contract?)` | Build the correct PulseChain or Robinhood EIP-712 domain |
685
- | `getLimitOrderNetworkConfig(network)` | Get static chain, router, LO, wrapped-native, and native-flow defaults |
686
- | `getLimitOrderApprovalTarget(network, feeOnOutput, overrides?)` | Resolve the correct maker approval target |
697
+ | `getEIP712SigningParams()` | Get the `{ domain, types }` for `signTypedData()` |
698
+ | `getNetworkEIP712SigningParams(network, contract?)` | Build the correct PulseChain or Robinhood EIP-712 domain |
699
+ | `getLimitOrderNetworkConfig(network)` | Get static chain, router, LO, wrapped-native, and native-flow defaults |
700
+ | `getLimitOrderApprovalTarget(network, feeOnOutput, overrides?)` | Resolve the correct maker approval target |
687
701
  | `getApprovalTarget()` | Get approval target for `feeOnOutput: false` orders → SwitchLimitOrder |
688
702
  | `getRouterApprovalTarget()` | Get approval target for `feeOnOutput: true` orders → SwitchRouter |
689
703
  | `shouldUnwrapOutput(tokenOut)` | Returns `true` if tokenOut is WPLS (should set `unwrapOutput: true`) |
690
- | `getPLSFlowAddress()` | Get the SwitchPLSFlow contract address for native PLS limit orders |
691
- | `getNativeFlowAddress(network)` | Get the native PLS/ETH flow address for either chain |
692
- | `isNativePLS(tokenIn)` | Returns `true` if tokenIn is native PLS (use PLSFlow instead of EIP-712) |
693
- | `isNativeCurrency(tokenIn)` | Network-neutral native sentinel check |
704
+ | `getPLSFlowAddress()` | Get the SwitchPLSFlow contract address for native PLS limit orders |
705
+ | `getNativeFlowAddress(network)` | Get the native PLS/ETH flow address for either chain |
706
+ | `isNativePLS(tokenIn)` | Returns `true` if tokenIn is native PLS (use PLSFlow instead of EIP-712) |
707
+ | `isNativeCurrency(tokenIn)` | Network-neutral native sentinel check |
694
708
  | `submitLimitOrder(signedOrder)` | POST a signed order to the Switch backend |
695
709
  | `fetchLimitOrders(options?)` | GET orders with optional filters |
696
710
  | `fetchLimitOrder(maker, nonce)` | GET a single order by maker + nonce |
697
- | `fetchLimitOrderPairs()` | GET active pairs with order counts |
698
- | `fetchLimitOrderConfig({ network })` | GET live deployments and EIP-712 domain |
699
- | `fetchLimitOrderStats()` | GET global order statistics |
711
+ | `fetchLimitOrderPairs()` | GET active pairs with order counts |
712
+ | `fetchLimitOrderConfig({ network })` | GET live deployments and EIP-712 domain |
713
+ | `fetchLimitOrderStats()` | GET global order statistics |
700
714
 
701
715
  ---
702
716
 
@@ -723,12 +737,12 @@ The EIP-712 domain and types must match the on-chain contract exactly:
723
737
  { name: "minAmountOut", type: "uint256" },
724
738
  { name: "deadline", type: "uint256" },
725
739
  { name: "nonce", type: "uint256" },
726
- { name: "feeOnOutput", type: "bool" },
727
- { name: "recipient", type: "address" },
728
- { name: "unwrapOutput", type: "bool" },
729
- { name: "partnerAddress", type: "address" },
730
- ]
731
- }
740
+ { name: "feeOnOutput", type: "bool" },
741
+ { name: "recipient", type: "address" },
742
+ { name: "unwrapOutput", type: "bool" },
743
+ { name: "partnerAddress", type: "address" },
744
+ ]
745
+ }
732
746
  ```
733
747
 
734
748
  These are exported as `LIMIT_ORDER_EIP712_DOMAIN` and `LIMIT_ORDER_EIP712_TYPES` from `@switch-win/sdk/constants`.
@@ -746,16 +760,16 @@ These are exported as `LIMIT_ORDER_EIP712_DOMAIN` and `LIMIT_ORDER_EIP712_TYPES`
746
760
  | `InvalidTokens()` | tokenIn and tokenOut are the same, or zero address |
747
761
  | `NonceAlreadyUsed()` | This nonce has already been filled or invalidated |
748
762
  | `OrderExpired()` | `block.timestamp > deadline` (and deadline > 0) |
749
- | `RouteInputExceedsMax()` | Route's total input exceeds the order's `amountIn` |
750
- | `NativeFlowInputNotFullyConsumed()` | Native-flow output-surplus route did not consume all executable input |
751
- | `RouteTokenInMismatch()` | A route does not start with the signed input token |
752
- | `RouteTokenOutMismatch()` | A route does not end with the signed output token |
753
- | `DirectFillQuoteUnavailable()` | The protected direct-fill quote is missing or zero |
754
- | `DirectFillPriceTooLow()` | The maker would receive less than the protected direct-fill floor |
755
- | `TransferFailed()` | Token transfer failed (insufficient balance or allowance) |
763
+ | `RouteInputExceedsMax()` | Route's total input exceeds the order's `amountIn` |
764
+ | `NativeFlowInputNotFullyConsumed()` | Native-flow output-surplus route did not consume all executable input |
765
+ | `RouteTokenInMismatch()` | A route does not start with the signed input token |
766
+ | `RouteTokenOutMismatch()` | A route does not end with the signed output token |
767
+ | `DirectFillQuoteUnavailable()` | The protected direct-fill quote is missing or zero |
768
+ | `DirectFillPriceTooLow()` | The maker would receive less than the protected direct-fill floor |
769
+ | `TransferFailed()` | Token transfer failed (insufficient balance or allowance) |
756
770
 
757
771
  ---
758
772
 
759
773
  *See also: [README.md](README.md) for swap API docs, constants, and general integration info.*
760
774
 
761
- *Last updated: August 2026*
775
+ *Last updated: August 2026*