@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
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Switch DEX Aggregator — React Hooks Example
3
+ *
4
+ * Ready-to-use hooks for React / Next.js frontends:
5
+ * • useAdapters() — fetches available DEX adapters (cached)
6
+ * • useCheckTax() — checks buy/sell tax on a token
7
+ * • useSwapQuote() — fetches a swap quote with feeOnOutput logic
8
+ *
9
+ * These hooks call YOUR proxy (see nextjs-proxy.ts), not the Switch API
10
+ * directly — so your API key stays server-side.
11
+ *
12
+ * Usage: copy the hooks into your project and import them where needed.
13
+ */
14
+
15
+ import { useCallback, useEffect, useRef, useState } from "react";
16
+
17
+ // ── Types --
18
+
19
+ interface AdapterInfo {
20
+ index: number;
21
+ name: string;
22
+ address: string;
23
+ }
24
+
25
+ interface AdaptersResponse {
26
+ adapters: AdapterInfo[];
27
+ }
28
+
29
+ interface CheckTaxResult {
30
+ token: string;
31
+ isTaxToken: boolean;
32
+ buyTaxBps: number;
33
+ sellTaxBps: number;
34
+ }
35
+
36
+ interface SwapPath {
37
+ adapter: string;
38
+ fromToken: string;
39
+ toToken: string;
40
+ amountIn: string;
41
+ amountOut: string;
42
+ }
43
+
44
+ interface SwapQuote {
45
+ totalAmountOut: string;
46
+ expectedOutputAmount: string;
47
+ minAmountOut: string;
48
+ effectiveSlippagePercent: string;
49
+ paths: SwapPath[];
50
+ tx?: { to: string; data: string; value: string };
51
+ txFeeOnOutput?: { to: string; data: string; value: string };
52
+ }
53
+
54
+ // ── Constants --
55
+
56
+ const NATIVE_PLS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
57
+ const WPLS = "0xA1077a294dDE1B09bB078844df40758a5D0f9a27";
58
+
59
+ // Your proxy base path (Next.js catch-all route)
60
+ const API_BASE = "/api/swap";
61
+
62
+ // ── Helper --
63
+
64
+ async function apiFetch<T>(path: string, params?: Record<string, string>): Promise<T> {
65
+ const url = new URL(`${API_BASE}${path}`, window.location.origin);
66
+ if (params) {
67
+ Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
68
+ }
69
+ const res = await fetch(url.toString());
70
+ if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
71
+ return res.json();
72
+ }
73
+
74
+ /**
75
+ * Determine feeOnOutput based on token taxes & PLS involvement.
76
+ *
77
+ * - Selling a tax token → fee on output
78
+ * - Buying a tax token → fee on input
79
+ * - Buying PLS/WPLS → fee on output (collect PLS)
80
+ * - Selling PLS/WPLS → fee on input (collect PLS)
81
+ * - Default → false
82
+ */
83
+ function determineFeeOnOutput(
84
+ fromToken: string,
85
+ toToken: string,
86
+ fromTax: CheckTaxResult | null,
87
+ toTax: CheckTaxResult | null,
88
+ ): boolean {
89
+ const plsAddresses = [NATIVE_PLS.toLowerCase(), WPLS.toLowerCase()];
90
+ const from = fromToken.toLowerCase();
91
+ const to = toToken.toLowerCase();
92
+
93
+ if (fromTax?.isTaxToken && fromTax.sellTaxBps > 0) return true;
94
+ if (toTax?.isTaxToken && toTax.buyTaxBps > 0) return false;
95
+ if (plsAddresses.includes(to)) return true;
96
+ if (plsAddresses.includes(from)) return false;
97
+ return false;
98
+ }
99
+
100
+ // ── useAdapters --
101
+
102
+ /** Fetch the list of supported DEX adapters (cached for session). */
103
+ export function useAdapters() {
104
+ const [adapters, setAdapters] = useState<AdapterInfo[]>([]);
105
+ const [allIndices, setAllIndices] = useState<Set<number>>(new Set());
106
+ const [loading, setLoading] = useState(true);
107
+ const [error, setError] = useState<string | null>(null);
108
+
109
+ useEffect(() => {
110
+ let cancelled = false;
111
+ apiFetch<AdaptersResponse>("/adapters?network=pulsechain")
112
+ .then((data) => {
113
+ if (!cancelled) {
114
+ setAdapters(data.adapters);
115
+ setAllIndices(new Set(data.adapters.map((a) => a.index)));
116
+ }
117
+ })
118
+ .catch((err) => {
119
+ if (!cancelled) setError(err.message);
120
+ })
121
+ .finally(() => {
122
+ if (!cancelled) setLoading(false);
123
+ });
124
+ return () => { cancelled = true; };
125
+ }, []);
126
+
127
+ return { adapters, allIndices, loading, error };
128
+ }
129
+
130
+ // ── useCheckTax --
131
+
132
+ /** Check buy/sell tax for a single token address. */
133
+ export function useCheckTax(token: string | undefined) {
134
+ const [tax, setTax] = useState<CheckTaxResult | null>(null);
135
+ const [loading, setLoading] = useState(false);
136
+ const [error, setError] = useState<string | null>(null);
137
+
138
+ useEffect(() => {
139
+ if (!token) { setTax(null); return; }
140
+ let cancelled = false;
141
+
142
+ setLoading(true);
143
+ setError(null);
144
+
145
+ apiFetch<CheckTaxResult>("/checkTax", { token, network: "pulsechain" })
146
+ .then((data) => {
147
+ if (!cancelled) setTax(data);
148
+ })
149
+ .catch((err) => {
150
+ if (!cancelled) setError(err.message);
151
+ })
152
+ .finally(() => {
153
+ if (!cancelled) setLoading(false);
154
+ });
155
+
156
+ return () => { cancelled = true; };
157
+ }, [token]);
158
+
159
+ return { tax, loading, error };
160
+ }
161
+
162
+ // ── useSwapQuote --
163
+
164
+ interface UseSwapQuoteParams {
165
+ fromToken: string;
166
+ toToken: string;
167
+ amount: string; // wei string
168
+ sender: string;
169
+ slippageBps?: number;
170
+ feeBps?: number;
171
+ partnerAddress?: string;
172
+ receiver?: string;
173
+ /**
174
+ * Optional set of enabled adapter indices. When provided and smaller than
175
+ * allIndices, only these DEXes are used for routing. Omit or pass the full
176
+ * set to route through all DEXes.
177
+ */
178
+ enabledDexes?: Set<number>;
179
+ /** Full set of adapter indices (from useAdapters). Needed to detect subset. */
180
+ allDexIndices?: Set<number>;
181
+ /** Set to false to pause fetching (e.g. while user is typing). */
182
+ enabled?: boolean;
183
+ }
184
+
185
+ /**
186
+ * Fetches a swap quote with automatic feeOnOutput determination.
187
+ *
188
+ * 1. Checks taxes on both tokens
189
+ * 2. Determines feeOnOutput based on tax info & PLS involvement
190
+ * 3. Requests quote with the correct feeOnOutput flag
191
+ *
192
+ * Returns the full quote plus the chosen tx object and feeOnOutput flag.
193
+ */
194
+ export function useSwapQuote({
195
+ fromToken,
196
+ toToken,
197
+ amount,
198
+ sender,
199
+ slippageBps = 50,
200
+ feeBps,
201
+ partnerAddress,
202
+ receiver,
203
+ enabledDexes,
204
+ allDexIndices,
205
+ enabled = true,
206
+ }: UseSwapQuoteParams) {
207
+ const [quote, setQuote] = useState<SwapQuote | null>(null);
208
+ const [feeOnOutput, setFeeOnOutput] = useState(false);
209
+ const [loading, setLoading] = useState(false);
210
+ const [error, setError] = useState<string | null>(null);
211
+
212
+ // Debounce: cancel in-flight requests when inputs change
213
+ const abortRef = useRef<AbortController | null>(null);
214
+
215
+ const fetchQuote = useCallback(async () => {
216
+ if (!enabled || !fromToken || !toToken || !amount || amount === "0" || !sender) {
217
+ setQuote(null);
218
+ return;
219
+ }
220
+
221
+ // Cancel previous request
222
+ abortRef.current?.abort();
223
+ const controller = new AbortController();
224
+ abortRef.current = controller;
225
+
226
+ setLoading(true);
227
+ setError(null);
228
+
229
+ try {
230
+ // Step 1 — check taxes on both tokens (parallel)
231
+ const [fromTax, toTax] = await Promise.all([
232
+ apiFetch<CheckTaxResult>("/checkTax", { token: fromToken, network: "pulsechain" }),
233
+ apiFetch<CheckTaxResult>("/checkTax", { token: toToken, network: "pulsechain" }),
234
+ ]);
235
+
236
+ if (controller.signal.aborted) return;
237
+
238
+ // Step 2 — determine fee mode
239
+ const feeMode = determineFeeOnOutput(fromToken, toToken, fromTax, toTax);
240
+ setFeeOnOutput(feeMode);
241
+
242
+ // Step 3 — fetch quote
243
+ const params: Record<string, string> = {
244
+ network: "pulsechain",
245
+ from: fromToken,
246
+ to: toToken,
247
+ amount,
248
+ sender,
249
+ slippage: String(slippageBps),
250
+ feeOnOutput: String(feeMode),
251
+ };
252
+ if (feeBps) params.fee = String(feeBps);
253
+ if (partnerAddress) params.partnerAddress = partnerAddress;
254
+ if (receiver) params.receiver = receiver;
255
+ // Only send adapter filter when a subset of DEXes is enabled
256
+ if (enabledDexes && allDexIndices && enabledDexes.size > 0 && enabledDexes.size < allDexIndices.size) {
257
+ params.adapters = Array.from(enabledDexes).sort((a, b) => a - b).join(",");
258
+ }
259
+
260
+ const data = await apiFetch<SwapQuote>("/quote", params);
261
+ if (controller.signal.aborted) return;
262
+
263
+ setQuote(data);
264
+ } catch (err: unknown) {
265
+ if (controller.signal.aborted) return;
266
+ setError(err instanceof Error ? err.message : "Quote fetch failed");
267
+ } finally {
268
+ if (!controller.signal.aborted) setLoading(false);
269
+ }
270
+ }, [fromToken, toToken, amount, sender, slippageBps, feeBps, partnerAddress, receiver, enabledDexes, allDexIndices, enabled]);
271
+
272
+ useEffect(() => {
273
+ fetchQuote();
274
+ return () => abortRef.current?.abort();
275
+ }, [fetchQuote]);
276
+
277
+ // The tx object the caller should submit — already matched to fee mode
278
+ const chosenTx = feeOnOutput ? (quote?.txFeeOnOutput ?? quote?.tx) : quote?.tx;
279
+
280
+ return {
281
+ quote,
282
+ chosenTx,
283
+ feeOnOutput,
284
+ loading,
285
+ error,
286
+ /** Manually re-fetch the quote (e.g. on a countdown timer). */
287
+ refetch: fetchQuote,
288
+ };
289
+ }
290
+
291
+ // ── Example component --
292
+
293
+ /**
294
+ * Minimal swap card showing how to compose the hooks above.
295
+ * Includes DEX toggles so users can enable/disable individual liquidity sources.
296
+ * Replace with your own UI — this is just a starting point.
297
+ */
298
+ export function SwapCard() {
299
+ const { adapters, allIndices } = useAdapters();
300
+ const [enabledDexes, setEnabledDexes] = useState<Set<number>>(new Set());
301
+
302
+ // Initialize enabledDexes once adapters load
303
+ useEffect(() => {
304
+ if (allIndices.size > 0 && enabledDexes.size === 0) {
305
+ setEnabledDexes(new Set(allIndices));
306
+ }
307
+ }, [allIndices]); // eslint-disable-line react-hooks/exhaustive-deps
308
+
309
+ const toggleDex = (index: number) => {
310
+ const next = new Set(enabledDexes);
311
+ if (next.has(index)) {
312
+ if (next.size > 1) next.delete(index); // keep at least one
313
+ } else {
314
+ next.add(index);
315
+ }
316
+ setEnabledDexes(next);
317
+ };
318
+
319
+ const FROM = "0xA1077a294dDE1B09bB078844df40758a5D0f9a27"; // WPLS
320
+ const TO = "0x95B303987A60C71504D99Aa1b13B4DA07b0790ab"; // PLSX
321
+
322
+ const { tax: fromTax } = useCheckTax(FROM);
323
+ const { tax: toTax } = useCheckTax(TO);
324
+
325
+ const {
326
+ quote,
327
+ chosenTx,
328
+ feeOnOutput,
329
+ loading,
330
+ error,
331
+ } = useSwapQuote({
332
+ fromToken: FROM,
333
+ toToken: TO,
334
+ amount: String(1000n * 10n ** 18n),
335
+ sender: "0x0000000000000000000000000000000000000000", // replace with connected wallet
336
+ slippageBps: 100,
337
+ enabledDexes,
338
+ allDexIndices: allIndices,
339
+ });
340
+
341
+ return (
342
+ <div style={{ fontFamily: "monospace", padding: 24 }}>
343
+ <h2>Switch Swap</h2>
344
+
345
+ {/* DEX toggles */}
346
+ <div style={{ marginBottom: 16 }}>
347
+ <strong>DEX Sources:</strong>
348
+ {adapters.map((a) => (
349
+ <label key={a.index} style={{ display: "block", cursor: "pointer" }}>
350
+ <input
351
+ type="checkbox"
352
+ checked={enabledDexes.has(a.index)}
353
+ onChange={() => toggleDex(a.index)}
354
+ />{" "}
355
+ {a.name}
356
+ </label>
357
+ ))}
358
+ </div>
359
+
360
+ {fromTax?.isTaxToken && (
361
+ <p style={{ color: "orange" }}>
362
+ ⚠ From token has {fromTax.sellTaxBps / 100}% sell tax
363
+ </p>
364
+ )}
365
+ {toTax?.isTaxToken && (
366
+ <p style={{ color: "orange" }}>
367
+ ⚠ To token has {toTax.buyTaxBps / 100}% buy tax
368
+ </p>
369
+ )}
370
+
371
+ {loading && <p>Fetching quote...</p>}
372
+ {error && <p style={{ color: "red" }}>Error: {error}</p>}
373
+
374
+ {quote && (
375
+ <div>
376
+ <p>DEX output: {quote.totalAmountOut}</p>
377
+ <p>You receive: {quote.expectedOutputAmount}</p>
378
+ <p>Min output: {quote.minAmountOut}</p>
379
+ <p>Slippage: {quote.effectiveSlippagePercent}%</p>
380
+ <p>Fee mode: {feeOnOutput ? "output" : "input"}</p>
381
+ <p>Paths: {quote.paths.length}</p>
382
+
383
+ <button
384
+ disabled={!chosenTx}
385
+ onClick={() => {
386
+ // Send chosenTx via your wallet (ethers / wagmi / viem)
387
+ console.log("Submit tx:", chosenTx);
388
+ }}
389
+ >
390
+ Swap
391
+ </button>
392
+ </div>
393
+ )}
394
+ </div>
395
+ );
396
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Switch DEX Aggregator — Complete Swap Example (ethers.js v6)
3
+ *
4
+ * Demonstrates the full swap lifecycle:
5
+ * 1. Fetch adapters (optional — for DEX filtering)
6
+ * 2. Check token taxes (determines feeOnOutput mode)
7
+ * 3. Get quote with exact expectedOutputAmount
8
+ * 4. Approve ERC-20 (if needed)
9
+ * 5. Send swap transaction
10
+ *
11
+ * Usage:
12
+ * npx tsx examples/swap-ethers.ts
13
+ *
14
+ * Prerequisites:
15
+ * npm install ethers
16
+ * Set SWITCH_API_KEY and PRIVATE_KEY environment variables.
17
+ */
18
+
19
+ import { ethers } from "ethers";
20
+ import type {
21
+ BestPathResponse,
22
+ CheckTaxResponse,
23
+ AdaptersResponse,
24
+ } from "../src/types";
25
+ import {
26
+ API_BASE,
27
+ QUOTE_ENDPOINT,
28
+ ADAPTERS_ENDPOINT,
29
+ CHECK_TAX_ENDPOINT,
30
+ SWITCH_ROUTER,
31
+ NATIVE_PLS,
32
+ WPLS,
33
+ ERC20_ABI,
34
+ } from "../src/constants";
35
+
36
+ // ── Configuration --
37
+
38
+ const API_KEY = process.env.SWITCH_API_KEY!;
39
+ const PRIVATE_KEY = process.env.PRIVATE_KEY!;
40
+ const RPC_URL = process.env.RPC_URL ?? "https://rpc.pulsechain.com";
41
+
42
+ if (!API_KEY) throw new Error("Set SWITCH_API_KEY env var");
43
+ if (!PRIVATE_KEY) throw new Error("Set PRIVATE_KEY env var");
44
+
45
+ // ── Swap parameters --
46
+
47
+ const FROM_TOKEN = "0xA1077a294dDE1B09bB078844df40758a5D0f9a27"; // WPLS
48
+ const TO_TOKEN = "0x95B303987A60C71504D99Aa1b13B4DA07b0790ab"; // PLSX
49
+ const AMOUNT = ethers.parseUnits("1000", 18).toString(); // 1000 WPLS
50
+ const SLIPPAGE_BPS = 100; // 1%
51
+ const FEE_BPS = 10; // 0.10%
52
+ const PARTNER_ADDRESS = "0x0000000000000000000000000000000000000000"; // your partner wallet
53
+ const RECEIVER_ADDRESS = ""; // custom recipient address, or "" to send to sender
54
+
55
+ // ── Main --
56
+
57
+ /** Helper to make authenticated API calls */
58
+ async function apiFetch<T>(url: string): Promise<T> {
59
+ const res = await fetch(url, { headers: { "x-api-key": API_KEY } });
60
+ if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`);
61
+ return res.json() as Promise<T>;
62
+ }
63
+
64
+ /** Determine feeOnOutput based on tax info and PLS involvement */
65
+ function determineFeeOnOutput(
66
+ fromToken: string,
67
+ toToken: string,
68
+ fromTax: CheckTaxResponse,
69
+ toTax: CheckTaxResponse,
70
+ ): boolean {
71
+ const plsAddresses = [NATIVE_PLS.toLowerCase(), WPLS.toLowerCase()];
72
+ const fromAddr = fromToken.toLowerCase();
73
+ const toAddr = toToken.toLowerCase();
74
+
75
+ // Both tax tokens → fee on input to avoid extra output-token transfers.
76
+ if (fromTax.isTaxToken && toTax.isTaxToken) return false;
77
+
78
+ // If selling a tax token → fee on output (avoids fee being reduced by sell tax)
79
+ if (fromTax.isTaxToken && fromTax.sellTaxBps > 0) return true;
80
+
81
+ // If buying a tax token → fee on input (avoids collecting tax tokens as fee)
82
+ if (toTax.isTaxToken && toTax.buyTaxBps > 0) return false;
83
+
84
+ // Buying PLS → fee on output (collect PLS)
85
+ if (plsAddresses.includes(toAddr)) return true;
86
+
87
+ // Selling PLS → fee on input (collect PLS)
88
+ if (plsAddresses.includes(fromAddr)) return false;
89
+
90
+ // Default
91
+ return true;
92
+ }
93
+
94
+ async function main() {
95
+ const provider = new ethers.JsonRpcProvider(RPC_URL);
96
+ const signer = new ethers.Wallet(PRIVATE_KEY, provider);
97
+ const sender = await signer.getAddress();
98
+
99
+ console.log(`Sender: ${sender}`);
100
+ console.log(`Swapping ${AMOUNT} wei of ${FROM_TOKEN} → ${TO_TOKEN}`);
101
+
102
+ // ── Step 1: Fetch available adapters (optional — cache this) ──
103
+ console.log("\n── Step 1: Fetching adapters ──");
104
+ const { adapters } = await apiFetch<AdaptersResponse>(
105
+ `${ADAPTERS_ENDPOINT}?network=pulsechain`,
106
+ );
107
+ console.log(`Available DEXes: ${adapters.map((a) => a.name).join(", ")}`);
108
+
109
+ // ── Step 2: Check token taxes --
110
+ console.log("\n── Step 2: Checking token taxes ──");
111
+ const [fromTax, toTax] = await Promise.all([
112
+ apiFetch<CheckTaxResponse>(
113
+ `${CHECK_TAX_ENDPOINT}?token=${FROM_TOKEN}&network=pulsechain`,
114
+ ),
115
+ apiFetch<CheckTaxResponse>(
116
+ `${CHECK_TAX_ENDPOINT}?token=${TO_TOKEN}&network=pulsechain`,
117
+ ),
118
+ ]);
119
+
120
+ console.log(
121
+ `From token: ${fromTax.isTaxToken ? `TAX (sell=${fromTax.sellTaxBps}bps, buy=${fromTax.buyTaxBps}bps)` : "not a tax token"}`,
122
+ );
123
+ console.log(
124
+ `To token: ${toTax.isTaxToken ? `TAX (sell=${toTax.sellTaxBps}bps, buy=${toTax.buyTaxBps}bps)` : "not a tax token"}`,
125
+ );
126
+
127
+ // ── Determine fee mode --
128
+ const feeOnOutput = determineFeeOnOutput(FROM_TOKEN, TO_TOKEN, fromTax, toTax);
129
+ console.log(`Fee mode: fee on ${feeOnOutput ? "output" : "input"}`);
130
+
131
+ // ── Step 3: Get quote with feeOnOutput for exact estimate --
132
+ console.log("\n── Step 3: Fetching quote ──");
133
+ const params = new URLSearchParams({
134
+ network: "pulsechain",
135
+ from: FROM_TOKEN,
136
+ to: TO_TOKEN,
137
+ amount: AMOUNT,
138
+ sender,
139
+ slippage: String(SLIPPAGE_BPS),
140
+ feeOnOutput: String(feeOnOutput),
141
+ });
142
+ if (FEE_BPS > 0) params.set("fee", String(FEE_BPS));
143
+ if (PARTNER_ADDRESS !== "0x0000000000000000000000000000000000000000") {
144
+ params.set("partnerAddress", PARTNER_ADDRESS);
145
+ }
146
+ if (RECEIVER_ADDRESS) params.set("receiver", RECEIVER_ADDRESS);
147
+
148
+ // (Optional) restrict routing to specific DEXes by index.
149
+ // Get indices from Step 1 (adapters). Omit to use all DEXes.
150
+ // Example: route only through PulseXV2 (3) and UniswapV3 (7):
151
+ // params.set("adapters", "3,7");
152
+
153
+ const url = `${QUOTE_ENDPOINT}?${params}`;
154
+ console.log(`URL: ${url}\n`);
155
+
156
+ const quote = await apiFetch<BestPathResponse | { error: string }>(url);
157
+
158
+ if ("error" in quote) {
159
+ throw new Error(`Quote error: ${quote.error}`);
160
+ }
161
+
162
+ if (!quote.tx) {
163
+ throw new Error("No tx object in response — did you provide sender?");
164
+ }
165
+
166
+ // Choose the matching tx variant for the fee mode we determined
167
+ const chosenTx = feeOnOutput ? quote.txFeeOnOutput! : quote.tx;
168
+
169
+ console.log(`Raw DEX output: ${quote.totalAmountOut}`);
170
+ console.log(`Expected received: ${quote.expectedOutputAmount}`);
171
+ console.log(`Min output: ${quote.minAmountOut}`);
172
+ console.log(`Eff. slippage: ${quote.effectiveSlippagePercent}%`);
173
+ console.log(`Paths: ${quote.paths.length}`);
174
+
175
+ // ── Step 4: Approve ERC-20 (skip for native PLS) --
176
+ if (FROM_TOKEN.toLowerCase() !== NATIVE_PLS.toLowerCase()) {
177
+ const token = new ethers.Contract(FROM_TOKEN, ERC20_ABI, signer);
178
+ const currentAllowance: bigint = await token.allowance(
179
+ sender,
180
+ SWITCH_ROUTER,
181
+ );
182
+
183
+ if (currentAllowance < BigInt(AMOUNT)) {
184
+ console.log("\nApproving SwitchRouter to spend tokens...");
185
+ const approveTx = await token.approve(SWITCH_ROUTER, AMOUNT);
186
+ const approveReceipt = await approveTx.wait();
187
+ console.log(`Approved in tx: ${approveReceipt.hash}`);
188
+ } else {
189
+ console.log("\nSufficient allowance already granted");
190
+ }
191
+ }
192
+
193
+ // ── Step 5: Send swap transaction --
194
+ console.log("\nSending swap transaction...");
195
+ const txResponse = await signer.sendTransaction({
196
+ to: chosenTx.to,
197
+ data: chosenTx.data,
198
+ value: chosenTx.value,
199
+ });
200
+
201
+ console.log(`Tx hash: ${txResponse.hash}`);
202
+ const receipt = await txResponse.wait();
203
+ console.log(`\nSwap confirmed in block ${receipt!.blockNumber}`);
204
+ }
205
+
206
+ main().catch((err) => {
207
+ console.error(err);
208
+ process.exit(1);
209
+ });