@switch-win/sdk 1.2.9 → 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 +213 -205
  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 +184 -182
  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,68 +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
+
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:
241
247
 
242
- - **No approval needed** — users send the native currency directly to the contract
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
248
-
249
- > **Current ABI:** native-flow deployments use the seven-argument
250
- > `createOrder(tokenOut, minAmountOut, deadline, feeOnOutput, unwrapOutput,
251
- > partnerAddress, recipient)` signature. The `recipient` argument must be
252
- > present in calldata; pass the zero address to default it to `msg.sender`.
253
- > The older six-argument selector is not compatible with current deployments.
254
-
255
- ### Native-flow contract address
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.
260
+
261
+ ### Native-flow contract address
256
262
 
257
263
  ```ts
258
- import { getNativeFlowAddress, PLS_FLOW_ABI } from "@switch-win/sdk";
259
-
260
- const robinhoodNativeFlow = getNativeFlowAddress("robinhood");
261
- // "0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689"
264
+ import { getNativeFlowAddress, PLS_FLOW_ABI } from "@switch-win/sdk";
265
+
266
+ const robinhoodNativeFlow = getNativeFlowAddress("robinhood");
267
+ // "0x8170a3B0e2FD2e4333E0Ca9c9414B2D3dd6aF689"
262
268
  ```
263
269
 
264
- ### Creating a native ETH limit order on Robinhood
270
+ ### Creating a native ETH limit order on Robinhood
265
271
 
266
272
  ```ts
267
273
  import { ethers } from "ethers";
268
- import { getNativeFlowAddress, isNativeCurrency } from "@switch-win/sdk/limit-orders";
269
- import { PLS_FLOW_ABI } from "@switch-win/sdk/constants";
270
-
271
- const network = "robinhood" as const;
272
- const provider = new ethers.JsonRpcProvider("https://rpc.mainnet.chain.robinhood.com");
273
- const signer = new ethers.Wallet(PRIVATE_KEY, provider);
274
-
275
- const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
276
- if (isNativeCurrency(NATIVE)) {
277
- const nativeFlow = new ethers.Contract(
278
- getNativeFlowAddress(network),
279
- PLS_FLOW_ABI,
280
- signer,
281
- );
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
+ );
282
288
 
283
- const tokenOut = "0x5fc5360D0400a0Fd4F2aF552aDD042D716F1d168"; // USDG
284
- const amountIn = ethers.parseEther("0.01");
285
- 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);
286
292
  const deadline = Math.floor(Date.now() / 1000) + 86400; // 24h expiry
287
293
  const feeOnOutput = false;
288
294
  const unwrapOutput = false;
289
295
 
290
296
  // Single transaction — no approval, no signing
291
- const tx = await nativeFlow.createOrder(
297
+ const tx = await nativeFlow.createOrder(
292
298
  tokenOut,
293
299
  minAmountOut,
294
300
  deadline,
@@ -300,65 +306,65 @@ if (isNativeCurrency(NATIVE)) {
300
306
  );
301
307
 
302
308
  const receipt = await tx.wait();
303
- console.log("Native ETH limit order created in tx:", receipt.hash);
309
+ console.log("Native ETH limit order created in tx:", receipt.hash);
304
310
 
305
311
  // The backend will automatically detect this order via PLSOrderCreated event.
306
312
  // No POST to /limit-orders required (though it's harmless if you do).
307
313
  }
308
314
  ```
309
315
 
310
- ### Native flow vs EIP-712 orders
316
+ ### Native flow vs EIP-712 orders
311
317
 
312
- | Aspect | Native flow (PLS/ETH) | EIP-712 (ERC-20) |
318
+ | Aspect | Native flow (PLS/ETH) | EIP-712 (ERC-20) |
313
319
  |---|---|---|
314
- | **Input token** | Native currency | Any ERC-20 (including WPLS/WETH) |
320
+ | **Input token** | Native currency | Any ERC-20 (including WPLS/WETH) |
315
321
  | **User experience** | Single transaction | Approve + Sign + Submit |
316
322
  | **Gas cost** | User pays gas | Gasless signing (user pays on fill) |
317
323
  | **Order discovery** | On-chain event | Requires successful POST |
318
- | **Maker address** | Native-flow contract | User's wallet |
324
+ | **Maker address** | Native-flow contract | User's wallet |
319
325
  | **Recipient** | User's wallet (or custom) | User's wallet (or custom) |
320
326
 
321
327
  ### Important: Order Discovery
322
328
 
323
- For native-flow orders, the `maker` field in the order record is the
324
- **native-flow contract address**, not the user's address. The actual user is
325
- 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.
326
332
 
327
333
  When querying orders for a user, use the `owner` parameter instead of `maker`:
328
334
 
329
335
  ```ts
330
- // ✅ Correct — finds both EIP-712 and native-flow orders
331
- const { orders } = await fetchLimitOrders({
332
- network: "robinhood",
336
+ // ✅ Correct — finds both EIP-712 and native-flow orders
337
+ const { orders } = await fetchLimitOrders({
338
+ network: "robinhood",
333
339
  owner: "0xUserAddress", // matches maker OR recipient
334
340
  status: "ACTIVE",
335
341
  });
336
342
 
337
- // ❌ Won't find native-flow orders
338
- const { orders } = await fetchLimitOrders({
339
- network: "robinhood",
343
+ // ❌ Won't find native-flow orders
344
+ const { orders } = await fetchLimitOrders({
345
+ network: "robinhood",
340
346
  maker: "0xUserAddress", // only matches maker field
341
347
  status: "ACTIVE",
342
348
  });
343
349
  ```
344
350
 
345
- ### Cancelling a native-flow order
351
+ ### Cancelling a native-flow order
346
352
 
347
- Native-flow orders can be cancelled by the original creator (`recipient`):
353
+ Native-flow orders can be cancelled by the original creator (`recipient`):
348
354
 
349
355
  ```ts
350
- const nativeFlow = new ethers.Contract(
351
- getNativeFlowAddress("robinhood"),
352
- PLS_FLOW_ABI,
353
- signer,
354
- );
356
+ const nativeFlow = new ethers.Contract(
357
+ getNativeFlowAddress("robinhood"),
358
+ PLS_FLOW_ABI,
359
+ signer,
360
+ );
355
361
 
356
362
  // Cancel on-chain (only the original creator can call this)
357
- const tx = await nativeFlow.cancelOrder(nonceToCancel);
363
+ const tx = await nativeFlow.cancelOrder(nonceToCancel);
358
364
  await tx.wait();
359
365
 
360
366
  // The backend will detect the NonceCancelled event and update the order status.
361
- // No REST mutation is required.
367
+ // No REST mutation is required.
362
368
  ```
363
369
 
364
370
  ---
@@ -389,23 +395,23 @@ When `feeOnOutput=false`, the LO contract pulls all tokens to itself first — o
389
395
 
390
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.
391
397
 
392
- > **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.
393
-
394
- ### Protected settlement
395
-
396
- The current PulseChain and Robinhood deployments charge a 30 bps (0.30%)
397
- limit-order fee. They are exempt from the Router's separate 10 bps regular-swap
398
- fee, so the two fees are not stacked.
399
-
400
- For routed fills, operator-retained surplus is capped at 5%. Input-side profit
401
- is capped at 5% of executable input; output-side profit is capped at 5% of the
402
- signed `minAmountOut`. Any additional routing improvement is returned to the
403
- maker.
404
-
405
- Direct fills are protected by the configured `directFillQuoter`. The maker must
406
- receive at least the greater of the signed `minAmountOut` and 95% of the best
407
- current direct-adapter quote. A missing or zero protected quote fails closed,
408
- 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.
409
415
 
410
416
  ### Tax token decision guide
411
417
 
@@ -414,7 +420,7 @@ and any post-fee output sent above the protected floor belongs to the maker.
414
420
  | **Neither token is taxed** | `false` (default) | Maximum operator flexibility → best chance of fill |
415
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). |
416
422
  | **Output token is taxed** | `false` (default) | Output goes directly to recipient in one transfer. |
417
- | **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. |
418
424
 
419
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.
420
426
 
@@ -474,18 +480,18 @@ const stats = await fetchLimitOrderStats();
474
480
 
475
481
  ## Cancelling a Limit Order
476
482
 
477
- Cancellation is an **on-chain operation**:
483
+ Cancellation is an **on-chain operation**:
478
484
 
479
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.
480
486
 
481
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.
482
488
 
483
- The backend indexer observes `NonceCancelled` and updates the orderbook. No
484
- separate REST mutation is required.
489
+ The backend indexer observes `NonceCancelled` and updates the orderbook. No
490
+ separate REST mutation is required.
485
491
 
486
492
  ```ts
487
493
  import { ethers } from "ethers";
488
- import { LIMIT_ORDER_ABI } from "@switch-win/sdk/constants";
494
+ import { LIMIT_ORDER_ABI } from "@switch-win/sdk/constants";
489
495
 
490
496
  const signer = new ethers.Wallet(PRIVATE_KEY, provider);
491
497
  const maker = await signer.getAddress();
@@ -497,7 +503,7 @@ const contract = new ethers.Contract(order.limitOrderContract, LIMIT_ORDER_ABI,
497
503
  const tx = await contract.invalidateNonce(nonceToCancel);
498
504
  await tx.wait();
499
505
 
500
- // The backend indexer observes the event and marks the order CANCELLED.
506
+ // The backend indexer observes the event and marks the order CANCELLED.
501
507
  ```
502
508
 
503
509
  To cancel **multiple orders** at once, use `invalidateNonces(uint256[])`:
@@ -507,7 +513,7 @@ const nonces = [1717171717, 1717171718, 1717171719];
507
513
  const tx = await contract.invalidateNonces(nonces);
508
514
  await tx.wait();
509
515
 
510
- // The backend indexer observes each NonceCancelled event.
516
+ // The backend indexer observes each NonceCancelled event.
511
517
  ```
512
518
 
513
519
  ---
@@ -536,9 +542,11 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
536
542
  > Your integration must:
537
543
  > 1. Call `submitLimitOrder()` **immediately** after the user signs.
538
544
  > 2. **Await** the response and confirm `success: true` before showing "order created".
539
- > 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.
540
548
  >
541
- > 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,
542
550
  > so the backend discovers them via event indexing even if the POST never arrives.
543
551
 
544
552
  **Request body:**
@@ -553,11 +561,11 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
553
561
  "deadline": 1717257600,
554
562
  "nonce": 1717171717,
555
563
  "feeOnOutput": false,
556
- "recipient": "0x...",
557
- "unwrapOutput": false,
558
- "limitOrderContract": "0x...",
559
- "signature": "0x..."
560
- }
564
+ "recipient": "0x...",
565
+ "unwrapOutput": false,
566
+ "limitOrderContract": "0x...",
567
+ "signature": "0x..."
568
+ }
561
569
  ```
562
570
 
563
571
  **Success response (201):**
@@ -599,7 +607,7 @@ Submit a signed limit order. The backend verifies the EIP-712 signature before s
599
607
  | `"Signature does not match maker"` | The recovered signer doesn't match the `maker` field |
600
608
  | `"Nonce N already used for maker 0x..."` | This nonce has already been used (filled or submitted) |
601
609
 
602
- ### `GET /limit-orders`
610
+ ### `GET /limit-orders`
603
611
 
604
612
  List orders with optional query filters. See [Query Filters](#query-filters) above.
605
613
 
@@ -669,13 +677,13 @@ Summary statistics.
669
677
  | `LimitOrderParams` | Order fields for EIP-712 signing |
670
678
  | `SignedLimitOrder` | `LimitOrderParams` + `signature` |
671
679
  | `CreateLimitOrderRequest` | Alias for `SignedLimitOrder` (POST body) |
672
- | `LimitOrderStatus` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` |
680
+ | `LimitOrderStatus` | `"ACTIVE" \| "FILLED" \| "CANCELLED" \| "EXPIRED"` |
673
681
  | `LimitOrderRecord` | Full order record from the API (includes `partnerAddress`, `limitOrderContract`, status, and timestamps) |
674
682
  | `CreateLimitOrderResponse` | `{ success: true, order: LimitOrderRecord }` |
675
683
  | `ListLimitOrdersResponse` | `{ total, limit, offset, orders: LimitOrderRecord[] }` |
676
684
  | `LimitOrderPair` | Active pair with order count |
677
685
  | `LimitOrderStats` | Global order statistics |
678
- | `LimitOrderMutationResponse` | Submission response + `ErrorResponse` |
686
+ | `LimitOrderMutationResponse` | Submission response + `ErrorResponse` |
679
687
 
680
688
  ---
681
689
 
@@ -686,23 +694,23 @@ Available from `@switch-win/sdk/limit-orders`:
686
694
  | Function | Description |
687
695
  |---|---|
688
696
  | `buildLimitOrder(options)` | Build a `LimitOrderParams` object with sensible defaults |
689
- | `getEIP712SigningParams()` | Get the `{ domain, types }` for `signTypedData()` |
690
- | `getNetworkEIP712SigningParams(network, contract?)` | Build the correct PulseChain or Robinhood EIP-712 domain |
691
- | `getLimitOrderNetworkConfig(network)` | Get static chain, router, LO, wrapped-native, and native-flow defaults |
692
- | `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 |
693
701
  | `getApprovalTarget()` | Get approval target for `feeOnOutput: false` orders → SwitchLimitOrder |
694
702
  | `getRouterApprovalTarget()` | Get approval target for `feeOnOutput: true` orders → SwitchRouter |
695
703
  | `shouldUnwrapOutput(tokenOut)` | Returns `true` if tokenOut is WPLS (should set `unwrapOutput: true`) |
696
- | `getPLSFlowAddress()` | Get the SwitchPLSFlow contract address for native PLS limit orders |
697
- | `getNativeFlowAddress(network)` | Get the native PLS/ETH flow address for either chain |
698
- | `isNativePLS(tokenIn)` | Returns `true` if tokenIn is native PLS (use PLSFlow instead of EIP-712) |
699
- | `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 |
700
708
  | `submitLimitOrder(signedOrder)` | POST a signed order to the Switch backend |
701
709
  | `fetchLimitOrders(options?)` | GET orders with optional filters |
702
710
  | `fetchLimitOrder(maker, nonce)` | GET a single order by maker + nonce |
703
- | `fetchLimitOrderPairs()` | GET active pairs with order counts |
704
- | `fetchLimitOrderConfig({ network })` | GET live deployments and EIP-712 domain |
705
- | `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 |
706
714
 
707
715
  ---
708
716
 
@@ -729,12 +737,12 @@ The EIP-712 domain and types must match the on-chain contract exactly:
729
737
  { name: "minAmountOut", type: "uint256" },
730
738
  { name: "deadline", type: "uint256" },
731
739
  { name: "nonce", type: "uint256" },
732
- { name: "feeOnOutput", type: "bool" },
733
- { name: "recipient", type: "address" },
734
- { name: "unwrapOutput", type: "bool" },
735
- { name: "partnerAddress", type: "address" },
736
- ]
737
- }
740
+ { name: "feeOnOutput", type: "bool" },
741
+ { name: "recipient", type: "address" },
742
+ { name: "unwrapOutput", type: "bool" },
743
+ { name: "partnerAddress", type: "address" },
744
+ ]
745
+ }
738
746
  ```
739
747
 
740
748
  These are exported as `LIMIT_ORDER_EIP712_DOMAIN` and `LIMIT_ORDER_EIP712_TYPES` from `@switch-win/sdk/constants`.
@@ -752,16 +760,16 @@ These are exported as `LIMIT_ORDER_EIP712_DOMAIN` and `LIMIT_ORDER_EIP712_TYPES`
752
760
  | `InvalidTokens()` | tokenIn and tokenOut are the same, or zero address |
753
761
  | `NonceAlreadyUsed()` | This nonce has already been filled or invalidated |
754
762
  | `OrderExpired()` | `block.timestamp > deadline` (and deadline > 0) |
755
- | `RouteInputExceedsMax()` | Route's total input exceeds the order's `amountIn` |
756
- | `NativeFlowInputNotFullyConsumed()` | Native-flow output-surplus route did not consume all executable input |
757
- | `RouteTokenInMismatch()` | A route does not start with the signed input token |
758
- | `RouteTokenOutMismatch()` | A route does not end with the signed output token |
759
- | `DirectFillQuoteUnavailable()` | The protected direct-fill quote is missing or zero |
760
- | `DirectFillPriceTooLow()` | The maker would receive less than the protected direct-fill floor |
761
- | `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) |
762
770
 
763
771
  ---
764
772
 
765
773
  *See also: [README.md](README.md) for swap API docs, constants, and general integration info.*
766
774
 
767
- *Last updated: August 2026*
775
+ *Last updated: August 2026*