@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.
- package/CHANGELOG.md +26 -0
- package/GASLESS.md +407 -0
- package/LIMIT-ORDERS.md +212 -198
- package/README.md +131 -105
- package/ROBINHOOD.md +201 -198
- package/abi/GaslessSwitchV2ABI.json +1095 -0
- package/dist/constants.d.ts +276 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +403 -0
- package/dist/constants.js.map +1 -0
- package/dist/gasless-client.d.ts +81 -0
- package/dist/gasless-client.d.ts.map +1 -0
- package/dist/gasless-client.js +467 -0
- package/dist/gasless-client.js.map +1 -0
- package/dist/gasless-index.d.ts +5 -0
- package/dist/gasless-index.d.ts.map +1 -0
- package/dist/gasless-index.js +5 -0
- package/dist/gasless-index.js.map +1 -0
- package/dist/gasless-operators.d.ts +32 -0
- package/dist/gasless-operators.d.ts.map +1 -0
- package/dist/gasless-operators.js +132 -0
- package/dist/gasless-operators.js.map +1 -0
- package/dist/gasless-store.d.ts +18 -0
- package/dist/gasless-store.d.ts.map +1 -0
- package/dist/gasless-store.js +83 -0
- package/dist/gasless-store.js.map +1 -0
- package/dist/gasless-types.d.ts +218 -0
- package/dist/gasless-types.d.ts.map +1 -0
- package/dist/gasless-types.js +2 -0
- package/dist/gasless-types.js.map +1 -0
- package/dist/gasless.d.ts +135 -0
- package/dist/gasless.d.ts.map +1 -0
- package/dist/gasless.js +513 -0
- package/dist/gasless.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/limit-orders.d.ts +440 -0
- package/dist/limit-orders.d.ts.map +1 -0
- package/dist/limit-orders.js +387 -0
- package/dist/limit-orders.js.map +1 -0
- package/dist/networks/index.d.ts +3 -0
- package/dist/networks/index.d.ts.map +1 -0
- package/dist/networks/index.js +2 -0
- package/dist/networks/index.js.map +1 -0
- package/dist/networks/robinhood.d.ts +536 -0
- package/dist/networks/robinhood.d.ts.map +1 -0
- package/dist/networks/robinhood.js +489 -0
- package/dist/networks/robinhood.js.map +1 -0
- package/dist/types.d.ts +436 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +19 -0
- package/dist/types.js.map +1 -0
- package/examples/gasless-nextjs-proxy.ts +87 -0
- package/examples/gasless-operator.ts +76 -0
- package/examples/gasless-swap.ts +170 -0
- package/examples/limit-order-ethers.ts +195 -0
- package/examples/nextjs-proxy.ts +82 -0
- package/examples/react-hooks.tsx +396 -0
- package/examples/swap-ethers.ts +209 -0
- package/examples/swap-web3py.py +267 -0
- package/package.json +53 -12
- package/src/constants.ts +181 -178
- package/src/gasless-client.ts +823 -0
- package/src/gasless-index.ts +4 -0
- package/src/gasless-operators.ts +227 -0
- package/src/gasless-store.ts +137 -0
- package/src/gasless-types.ts +258 -0
- package/src/gasless.ts +756 -0
- package/src/index.ts +69 -63
- package/src/limit-orders.ts +206 -195
- 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
|
+
});
|