jupiter-swap-python 0.1.0__py3-none-any.whl
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.
- jupiter_swap/__init__.py +19 -0
- jupiter_swap/client.py +397 -0
- jupiter_swap/models.py +82 -0
- jupiter_swap/py.typed +0 -0
- jupiter_swap/tokens.py +232 -0
- jupiter_swap_python-0.1.0.dist-info/METADATA +187 -0
- jupiter_swap_python-0.1.0.dist-info/RECORD +9 -0
- jupiter_swap_python-0.1.0.dist-info/WHEEL +4 -0
- jupiter_swap_python-0.1.0.dist-info/licenses/LICENSE +21 -0
jupiter_swap/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jupiter-swap-python — async Jupiter DEX aggregator client for Python.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import JupiterClient
|
|
6
|
+
from .models import QuoteResponse, SwapResponse, TokenInfo, UltraOrder
|
|
7
|
+
from .tokens import TokenClient
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"JupiterClient",
|
|
13
|
+
"TokenClient",
|
|
14
|
+
"QuoteResponse",
|
|
15
|
+
"SwapResponse",
|
|
16
|
+
"UltraOrder",
|
|
17
|
+
"TokenInfo",
|
|
18
|
+
"__version__",
|
|
19
|
+
]
|
jupiter_swap/client.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jupiter DEX aggregator client — V6 quote/swap and Ultra API.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- Quote: get the best swap route and price
|
|
6
|
+
- Swap: build a transaction from a quote
|
|
7
|
+
- Ultra: combined quote + swap with MEV protection
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .models import QuoteResponse, SwapResponse, UltraOrder
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Jupiter public API base URLs
|
|
24
|
+
_DEFAULT_V6_URL = "https://quote-api.jup.ag/v6"
|
|
25
|
+
_DEFAULT_ULTRA_URL = "https://api.jup.ag/ultra/v1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JupiterError(Exception):
|
|
29
|
+
"""Raised when a Jupiter API call fails."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, status_code: int = 0, body: str = "") -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.status_code = status_code
|
|
34
|
+
self.body = body
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class JupiterClient:
|
|
38
|
+
"""
|
|
39
|
+
Async client for the Jupiter DEX aggregator.
|
|
40
|
+
|
|
41
|
+
Usage::
|
|
42
|
+
|
|
43
|
+
async with JupiterClient() as jup:
|
|
44
|
+
quote = await jup.get_quote("So11...", "EPjFW...", 1_000_000)
|
|
45
|
+
swap = await jup.get_swap_transaction(quote, "YourPublicKey...")
|
|
46
|
+
|
|
47
|
+
Handles 429 rate limiting with automatic retry and exponential backoff.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = (
|
|
51
|
+
"_v6_url",
|
|
52
|
+
"_ultra_url",
|
|
53
|
+
"_api_key",
|
|
54
|
+
"_http",
|
|
55
|
+
"_max_retries",
|
|
56
|
+
"_last_request_time",
|
|
57
|
+
"_min_interval",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
api_key: str = "",
|
|
64
|
+
v6_url: str = _DEFAULT_V6_URL,
|
|
65
|
+
ultra_url: str = _DEFAULT_ULTRA_URL,
|
|
66
|
+
max_retries: int = 3,
|
|
67
|
+
requests_per_second: float = 10.0,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Initialise the Jupiter client.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
api_key: Optional Jupiter API key for higher rate limits.
|
|
74
|
+
v6_url: Base URL for the V6 quote/swap API.
|
|
75
|
+
ultra_url: Base URL for the Ultra swap API.
|
|
76
|
+
max_retries: Max retry attempts on 429 responses.
|
|
77
|
+
requests_per_second: Simple rate limiter — minimum interval between requests.
|
|
78
|
+
"""
|
|
79
|
+
self._v6_url = v6_url.rstrip("/")
|
|
80
|
+
self._ultra_url = ultra_url.rstrip("/")
|
|
81
|
+
self._api_key = api_key
|
|
82
|
+
self._http: httpx.AsyncClient | None = None
|
|
83
|
+
self._max_retries = max_retries
|
|
84
|
+
self._last_request_time: float = 0.0
|
|
85
|
+
self._min_interval: float = 1.0 / requests_per_second if requests_per_second > 0 else 0.0
|
|
86
|
+
|
|
87
|
+
# -- Lifecycle -----------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
async def __aenter__(self) -> JupiterClient:
|
|
90
|
+
await self.connect()
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
94
|
+
await self.close()
|
|
95
|
+
|
|
96
|
+
async def connect(self) -> None:
|
|
97
|
+
"""Open the HTTP connection pool."""
|
|
98
|
+
if self._http is None:
|
|
99
|
+
self._http = httpx.AsyncClient(timeout=httpx.Timeout(15.0))
|
|
100
|
+
|
|
101
|
+
async def close(self) -> None:
|
|
102
|
+
"""Close the HTTP connection pool."""
|
|
103
|
+
if self._http:
|
|
104
|
+
await self._http.aclose()
|
|
105
|
+
self._http = None
|
|
106
|
+
|
|
107
|
+
# -- V6 Quote/Swap -------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
async def get_quote(
|
|
110
|
+
self,
|
|
111
|
+
input_mint: str,
|
|
112
|
+
output_mint: str,
|
|
113
|
+
amount: int,
|
|
114
|
+
slippage_bps: int = 50,
|
|
115
|
+
*,
|
|
116
|
+
swap_mode: str = "ExactIn",
|
|
117
|
+
restrict_intermediate_tokens: bool = True,
|
|
118
|
+
max_accounts: int = 64,
|
|
119
|
+
) -> QuoteResponse:
|
|
120
|
+
"""
|
|
121
|
+
Get the best swap route from Jupiter.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
input_mint: Source token mint address.
|
|
125
|
+
output_mint: Destination token mint address.
|
|
126
|
+
amount: Amount in smallest unit (lamports for SOL).
|
|
127
|
+
slippage_bps: Maximum slippage in basis points (1 bps = 0.01%).
|
|
128
|
+
swap_mode: "ExactIn" or "ExactOut".
|
|
129
|
+
restrict_intermediate_tokens: Avoid illiquid intermediate hops.
|
|
130
|
+
max_accounts: Max accounts in the route (higher = more complex routes).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
QuoteResponse with route details and expected output amount.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
JupiterError: If the quote request fails.
|
|
137
|
+
"""
|
|
138
|
+
params: dict[str, str | int] = {
|
|
139
|
+
"inputMint": input_mint,
|
|
140
|
+
"outputMint": output_mint,
|
|
141
|
+
"amount": str(amount),
|
|
142
|
+
"slippageBps": slippage_bps,
|
|
143
|
+
"swapMode": swap_mode,
|
|
144
|
+
}
|
|
145
|
+
if restrict_intermediate_tokens:
|
|
146
|
+
params["restrictIntermediateTokens"] = "true"
|
|
147
|
+
if max_accounts != 64:
|
|
148
|
+
params["maxAccounts"] = str(max_accounts)
|
|
149
|
+
|
|
150
|
+
data = await self._request("GET", f"{self._v6_url}/quote", params=params)
|
|
151
|
+
|
|
152
|
+
return QuoteResponse(
|
|
153
|
+
input_mint=data.get("inputMint", input_mint),
|
|
154
|
+
output_mint=data.get("outputMint", output_mint),
|
|
155
|
+
in_amount=data.get("inAmount", str(amount)),
|
|
156
|
+
out_amount=data.get("outAmount", "0"),
|
|
157
|
+
price_impact_pct=data.get("priceImpactPct", "0"),
|
|
158
|
+
route_plan=data.get("routePlan", []),
|
|
159
|
+
other_amount_threshold=data.get("otherAmountThreshold", "0"),
|
|
160
|
+
swap_mode=data.get("swapMode", swap_mode),
|
|
161
|
+
slippage_bps=data.get("slippageBps", slippage_bps),
|
|
162
|
+
raw=data,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def get_swap_transaction(
|
|
166
|
+
self,
|
|
167
|
+
quote: QuoteResponse,
|
|
168
|
+
user_public_key: str,
|
|
169
|
+
*,
|
|
170
|
+
wrap_and_unwrap_sol: bool = True,
|
|
171
|
+
priority_fee_lamports: int = 0,
|
|
172
|
+
dynamic_compute_unit_limit: bool = True,
|
|
173
|
+
priority_level: str = "veryHigh",
|
|
174
|
+
) -> SwapResponse:
|
|
175
|
+
"""
|
|
176
|
+
Build a swap transaction from a quote.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
quote: A QuoteResponse from get_quote().
|
|
180
|
+
user_public_key: The wallet public key that will sign the tx.
|
|
181
|
+
wrap_and_unwrap_sol: Auto wrap/unwrap SOL ↔ WSOL.
|
|
182
|
+
priority_fee_lamports: Max priority fee in lamports (0 = use default).
|
|
183
|
+
dynamic_compute_unit_limit: Auto-optimize compute unit limit.
|
|
184
|
+
priority_level: Priority level — "medium", "high", "veryHigh".
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
SwapResponse with the base64-encoded transaction.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
JupiterError: If the swap request fails.
|
|
191
|
+
"""
|
|
192
|
+
max_fee = priority_fee_lamports if priority_fee_lamports > 0 else 1_000_000
|
|
193
|
+
|
|
194
|
+
body: dict[str, Any] = {
|
|
195
|
+
"quoteResponse": quote.raw,
|
|
196
|
+
"userPublicKey": user_public_key,
|
|
197
|
+
"wrapAndUnwrapSol": wrap_and_unwrap_sol,
|
|
198
|
+
"dynamicComputeUnitLimit": dynamic_compute_unit_limit,
|
|
199
|
+
"dynamicSlippage": True,
|
|
200
|
+
"prioritizationFeeLamports": {
|
|
201
|
+
"priorityLevelWithMaxLamports": {
|
|
202
|
+
"maxLamports": max_fee,
|
|
203
|
+
"priorityLevel": priority_level,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
data = await self._request("POST", f"{self._v6_url}/swap", json=body)
|
|
209
|
+
|
|
210
|
+
return SwapResponse(
|
|
211
|
+
swap_transaction=data.get("swapTransaction", ""),
|
|
212
|
+
last_valid_block_height=data.get("lastValidBlockHeight", 0),
|
|
213
|
+
priority_fee_lamports=data.get("prioritizationFeeLamports", 0),
|
|
214
|
+
raw=data,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# -- Ultra API -----------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
async def ultra_order(
|
|
220
|
+
self,
|
|
221
|
+
input_mint: str,
|
|
222
|
+
output_mint: str,
|
|
223
|
+
amount: int,
|
|
224
|
+
taker: str,
|
|
225
|
+
slippage_bps: int = 50,
|
|
226
|
+
) -> UltraOrder:
|
|
227
|
+
"""
|
|
228
|
+
Request a swap via Jupiter Ultra API (combined quote + swap).
|
|
229
|
+
|
|
230
|
+
Ultra provides:
|
|
231
|
+
- Built-in MEV protection
|
|
232
|
+
- Automatic priority fee optimisation
|
|
233
|
+
- Better routing engine
|
|
234
|
+
- Gasless mode support
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
input_mint: Source token mint address.
|
|
238
|
+
output_mint: Destination token mint address.
|
|
239
|
+
amount: Amount in smallest unit.
|
|
240
|
+
taker: Wallet public key of the swap taker.
|
|
241
|
+
slippage_bps: Maximum slippage in basis points.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
UltraOrder with a transaction ready to sign and execute.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
JupiterError: If the order request fails.
|
|
248
|
+
"""
|
|
249
|
+
body: dict[str, Any] = {
|
|
250
|
+
"inputMint": input_mint,
|
|
251
|
+
"outputMint": output_mint,
|
|
252
|
+
"amount": str(amount),
|
|
253
|
+
"taker": taker,
|
|
254
|
+
"slippageBps": slippage_bps,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
data = await self._request("POST", f"{self._ultra_url}/order", json=body)
|
|
258
|
+
|
|
259
|
+
return UltraOrder(
|
|
260
|
+
request_id=data.get("requestId", ""),
|
|
261
|
+
input_mint=data.get("inputMint", input_mint),
|
|
262
|
+
output_mint=data.get("outputMint", output_mint),
|
|
263
|
+
in_amount=data.get("inAmount", str(amount)),
|
|
264
|
+
out_amount=data.get("outAmount", "0"),
|
|
265
|
+
swap_transaction=data.get("transaction", ""),
|
|
266
|
+
swap_type=data.get("type", "swap"),
|
|
267
|
+
priority_fee_lamports=data.get("prioritizationFeeLamports", 0),
|
|
268
|
+
dynamic_slippage_bps=data.get("dynamicSlippageReport", {}).get(
|
|
269
|
+
"slippageBps", slippage_bps,
|
|
270
|
+
),
|
|
271
|
+
raw=data,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def ultra_execute(
|
|
275
|
+
self,
|
|
276
|
+
signed_transaction: str,
|
|
277
|
+
request_id: str,
|
|
278
|
+
) -> str:
|
|
279
|
+
"""
|
|
280
|
+
Execute a signed Ultra swap order.
|
|
281
|
+
|
|
282
|
+
Submit the signed transaction back to Jupiter Ultra for
|
|
283
|
+
execution with MEV protection and confirmation tracking.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
signed_transaction: Base64-encoded signed transaction.
|
|
287
|
+
request_id: The request_id from the UltraOrder.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Transaction signature string.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
JupiterError: If execution fails.
|
|
294
|
+
"""
|
|
295
|
+
body = {
|
|
296
|
+
"signedTransaction": signed_transaction,
|
|
297
|
+
"requestId": request_id,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
data = await self._request("POST", f"{self._ultra_url}/execute", json=body)
|
|
301
|
+
|
|
302
|
+
status = data.get("status", "")
|
|
303
|
+
tx_sig = data.get("signature", "")
|
|
304
|
+
|
|
305
|
+
if status == "Failed":
|
|
306
|
+
raise JupiterError(
|
|
307
|
+
f"Ultra swap execution failed: {data.get('error', 'unknown')}",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return tx_sig
|
|
311
|
+
|
|
312
|
+
# -- Internal ------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
315
|
+
if self._api_key:
|
|
316
|
+
return {"x-api-key": self._api_key}
|
|
317
|
+
return {}
|
|
318
|
+
|
|
319
|
+
async def _rate_limit(self) -> None:
|
|
320
|
+
"""Simple rate limiter — ensure minimum interval between requests."""
|
|
321
|
+
if self._min_interval <= 0:
|
|
322
|
+
return
|
|
323
|
+
now = time.monotonic()
|
|
324
|
+
elapsed = now - self._last_request_time
|
|
325
|
+
if elapsed < self._min_interval:
|
|
326
|
+
await asyncio.sleep(self._min_interval - elapsed)
|
|
327
|
+
self._last_request_time = time.monotonic()
|
|
328
|
+
|
|
329
|
+
async def _request(
|
|
330
|
+
self,
|
|
331
|
+
method: str,
|
|
332
|
+
url: str,
|
|
333
|
+
*,
|
|
334
|
+
params: dict | None = None,
|
|
335
|
+
json: dict | None = None,
|
|
336
|
+
) -> dict:
|
|
337
|
+
"""
|
|
338
|
+
Make an HTTP request with rate limiting and 429 retry.
|
|
339
|
+
|
|
340
|
+
Returns parsed JSON dict on success.
|
|
341
|
+
Raises JupiterError on failure.
|
|
342
|
+
"""
|
|
343
|
+
if self._http is None:
|
|
344
|
+
raise JupiterError("Client not connected — call connect() or use async with")
|
|
345
|
+
|
|
346
|
+
await self._rate_limit()
|
|
347
|
+
|
|
348
|
+
last_status = 0
|
|
349
|
+
last_body = ""
|
|
350
|
+
|
|
351
|
+
for attempt in range(self._max_retries + 1):
|
|
352
|
+
try:
|
|
353
|
+
if method == "GET":
|
|
354
|
+
resp = await self._http.get(url, params=params, headers=self._auth_headers())
|
|
355
|
+
else:
|
|
356
|
+
resp = await self._http.post(url, json=json, headers=self._auth_headers())
|
|
357
|
+
|
|
358
|
+
last_status = resp.status_code
|
|
359
|
+
last_body = resp.text[:500] if resp.status_code != 200 else ""
|
|
360
|
+
|
|
361
|
+
if resp.status_code == 200:
|
|
362
|
+
return resp.json()
|
|
363
|
+
|
|
364
|
+
if resp.status_code == 429:
|
|
365
|
+
retry_after = self._parse_retry_after(resp)
|
|
366
|
+
wait = retry_after if retry_after else 0.5 * (attempt + 1)
|
|
367
|
+
logger.warning(
|
|
368
|
+
"Jupiter 429 rate limited (attempt %d/%d), waiting %.1fs",
|
|
369
|
+
attempt + 1, self._max_retries + 1, wait,
|
|
370
|
+
)
|
|
371
|
+
await asyncio.sleep(wait)
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
# Non-retryable error
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
except httpx.TimeoutException as exc:
|
|
378
|
+
if attempt < self._max_retries:
|
|
379
|
+
await asyncio.sleep(0.5 * (attempt + 1))
|
|
380
|
+
continue
|
|
381
|
+
raise JupiterError(f"Request timed out: {exc}") from exc
|
|
382
|
+
|
|
383
|
+
raise JupiterError(
|
|
384
|
+
f"Jupiter API error (HTTP {last_status})",
|
|
385
|
+
status_code=last_status,
|
|
386
|
+
body=last_body,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _parse_retry_after(resp: httpx.Response) -> float | None:
|
|
391
|
+
header = resp.headers.get("Retry-After")
|
|
392
|
+
if not header:
|
|
393
|
+
return None
|
|
394
|
+
try:
|
|
395
|
+
return max(0.0, float(header))
|
|
396
|
+
except (TypeError, ValueError):
|
|
397
|
+
return None
|
jupiter_swap/models.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for Jupiter API responses.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class QuoteResponse:
|
|
12
|
+
"""Parsed quote from Jupiter V6 API."""
|
|
13
|
+
|
|
14
|
+
input_mint: str
|
|
15
|
+
output_mint: str
|
|
16
|
+
in_amount: str
|
|
17
|
+
out_amount: str
|
|
18
|
+
price_impact_pct: str = "0"
|
|
19
|
+
route_plan: list[dict] = field(default_factory=list)
|
|
20
|
+
other_amount_threshold: str = "0"
|
|
21
|
+
swap_mode: str = "ExactIn"
|
|
22
|
+
slippage_bps: int = 50
|
|
23
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def in_amount_float(self) -> float:
|
|
27
|
+
"""Input amount as float (divide by decimals yourself)."""
|
|
28
|
+
try:
|
|
29
|
+
return float(self.in_amount)
|
|
30
|
+
except (TypeError, ValueError):
|
|
31
|
+
return 0.0
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def out_amount_float(self) -> float:
|
|
35
|
+
"""Output amount as float (divide by decimals yourself)."""
|
|
36
|
+
try:
|
|
37
|
+
return float(self.out_amount)
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return 0.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(slots=True)
|
|
43
|
+
class SwapResponse:
|
|
44
|
+
"""Parsed swap transaction from Jupiter V6 API."""
|
|
45
|
+
|
|
46
|
+
swap_transaction: str
|
|
47
|
+
last_valid_block_height: int = 0
|
|
48
|
+
priority_fee_lamports: int = 0
|
|
49
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class UltraOrder:
|
|
54
|
+
"""Response from Jupiter Ultra API — combined quote + swap in one call."""
|
|
55
|
+
|
|
56
|
+
request_id: str
|
|
57
|
+
input_mint: str
|
|
58
|
+
output_mint: str
|
|
59
|
+
in_amount: str
|
|
60
|
+
out_amount: str
|
|
61
|
+
swap_transaction: str
|
|
62
|
+
swap_type: str = "swap"
|
|
63
|
+
priority_fee_lamports: int = 0
|
|
64
|
+
dynamic_slippage_bps: int = 0
|
|
65
|
+
raw: dict = field(default_factory=dict, repr=False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(slots=True)
|
|
69
|
+
class TokenInfo:
|
|
70
|
+
"""Token metadata from Jupiter Token API."""
|
|
71
|
+
|
|
72
|
+
address: str
|
|
73
|
+
name: str = ""
|
|
74
|
+
symbol: str = ""
|
|
75
|
+
decimals: int = 0
|
|
76
|
+
logo_uri: str = ""
|
|
77
|
+
tags: list[str] = field(default_factory=list)
|
|
78
|
+
daily_volume: float | None = None
|
|
79
|
+
freeze_authority: str | None = None
|
|
80
|
+
mint_authority: str | None = None
|
|
81
|
+
is_verified: bool = False
|
|
82
|
+
is_banned: bool = False
|
jupiter_swap/py.typed
ADDED
|
File without changes
|
jupiter_swap/tokens.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jupiter Token API client — token verification, metadata, and banned detection.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .models import TokenInfo
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_DEFAULT_TOKEN_URL = "https://tokens.jup.ag"
|
|
18
|
+
|
|
19
|
+
# Cache TTLs
|
|
20
|
+
_TOKEN_INFO_TTL: float = 300.0
|
|
21
|
+
_BANNED_LIST_TTL: float = 600.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TokenClient:
|
|
25
|
+
"""
|
|
26
|
+
Async client for the Jupiter Token API.
|
|
27
|
+
|
|
28
|
+
Provides token metadata, verification status, and banned token detection.
|
|
29
|
+
|
|
30
|
+
Usage::
|
|
31
|
+
|
|
32
|
+
async with TokenClient() as tokens:
|
|
33
|
+
info = await tokens.get_token_info("EPjFWdd5...")
|
|
34
|
+
print(info.symbol, info.is_verified)
|
|
35
|
+
|
|
36
|
+
is_scam = await tokens.is_banned("ScamMint...")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
__slots__ = (
|
|
40
|
+
"_base_url",
|
|
41
|
+
"_api_key",
|
|
42
|
+
"_http",
|
|
43
|
+
"_banned_mints",
|
|
44
|
+
"_banned_loaded",
|
|
45
|
+
"_cache",
|
|
46
|
+
"_cache_ttl",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
base_url: str = _DEFAULT_TOKEN_URL,
|
|
53
|
+
api_key: str = "",
|
|
54
|
+
cache_ttl: float = _TOKEN_INFO_TTL,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._base_url = base_url.rstrip("/")
|
|
57
|
+
self._api_key = api_key
|
|
58
|
+
self._http: httpx.AsyncClient | None = None
|
|
59
|
+
self._banned_mints: set[str] = set()
|
|
60
|
+
self._banned_loaded: bool = False
|
|
61
|
+
self._cache: dict[str, tuple[float, Any]] = {}
|
|
62
|
+
self._cache_ttl = cache_ttl
|
|
63
|
+
|
|
64
|
+
# -- Lifecycle -----------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
async def __aenter__(self) -> TokenClient:
|
|
67
|
+
await self.connect()
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
71
|
+
await self.close()
|
|
72
|
+
|
|
73
|
+
async def connect(self) -> None:
|
|
74
|
+
"""Open HTTP client and preload the banned token list."""
|
|
75
|
+
headers: dict[str, str] = {}
|
|
76
|
+
if self._api_key:
|
|
77
|
+
headers["x-api-key"] = self._api_key
|
|
78
|
+
self._http = httpx.AsyncClient(
|
|
79
|
+
timeout=httpx.Timeout(15.0),
|
|
80
|
+
headers=headers,
|
|
81
|
+
)
|
|
82
|
+
await self._refresh_banned_list()
|
|
83
|
+
|
|
84
|
+
async def close(self) -> None:
|
|
85
|
+
"""Close the HTTP client."""
|
|
86
|
+
if self._http:
|
|
87
|
+
await self._http.aclose()
|
|
88
|
+
self._http = None
|
|
89
|
+
|
|
90
|
+
# -- Public API ----------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
async def get_token_info(self, mint: str) -> TokenInfo:
|
|
93
|
+
"""
|
|
94
|
+
Get token metadata and verification status.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
mint: Token mint address.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
TokenInfo with name, symbol, decimals, verification flags.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
httpx.HTTPError: On network failure.
|
|
104
|
+
"""
|
|
105
|
+
cached = self._get_cached(f"token:{mint}")
|
|
106
|
+
if cached is not None:
|
|
107
|
+
return cached
|
|
108
|
+
|
|
109
|
+
if self._http is None:
|
|
110
|
+
raise RuntimeError("TokenClient not connected — call connect() first")
|
|
111
|
+
|
|
112
|
+
resp = await self._http.get(f"{self._base_url}/tokens/v1/{mint}")
|
|
113
|
+
|
|
114
|
+
if resp.status_code == 404:
|
|
115
|
+
token = TokenInfo(
|
|
116
|
+
address=mint,
|
|
117
|
+
is_verified=False,
|
|
118
|
+
is_banned=mint in self._banned_mints,
|
|
119
|
+
)
|
|
120
|
+
self._set_cached(f"token:{mint}", token)
|
|
121
|
+
return token
|
|
122
|
+
|
|
123
|
+
resp.raise_for_status()
|
|
124
|
+
|
|
125
|
+
data = resp.json()
|
|
126
|
+
tags = data.get("tags", [])
|
|
127
|
+
token = TokenInfo(
|
|
128
|
+
address=data.get("address", mint),
|
|
129
|
+
name=data.get("name", ""),
|
|
130
|
+
symbol=data.get("symbol", ""),
|
|
131
|
+
decimals=data.get("decimals", 0),
|
|
132
|
+
logo_uri=data.get("logoURI", ""),
|
|
133
|
+
tags=tags,
|
|
134
|
+
daily_volume=data.get("daily_volume"),
|
|
135
|
+
freeze_authority=data.get("freeze_authority"),
|
|
136
|
+
mint_authority=data.get("mint_authority"),
|
|
137
|
+
is_verified="verified" in tags or "strict" in tags or "community" in tags,
|
|
138
|
+
is_banned=mint in self._banned_mints,
|
|
139
|
+
)
|
|
140
|
+
self._set_cached(f"token:{mint}", token)
|
|
141
|
+
return token
|
|
142
|
+
|
|
143
|
+
async def is_banned(self, mint: str) -> bool:
|
|
144
|
+
"""Check if a token is on Jupiter's banned list (known scams)."""
|
|
145
|
+
if mint in self._banned_mints:
|
|
146
|
+
return True
|
|
147
|
+
if not self._banned_loaded:
|
|
148
|
+
await self._refresh_banned_list()
|
|
149
|
+
return mint in self._banned_mints
|
|
150
|
+
|
|
151
|
+
async def is_verified(self, mint: str) -> bool:
|
|
152
|
+
"""Check if a token is on the verified list."""
|
|
153
|
+
info = await self.get_token_info(mint)
|
|
154
|
+
return info.is_verified
|
|
155
|
+
|
|
156
|
+
async def get_strict_list(self) -> list[TokenInfo]:
|
|
157
|
+
"""
|
|
158
|
+
Get the strictly verified token list (vetted tokens only).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of TokenInfo for all strictly verified tokens.
|
|
162
|
+
"""
|
|
163
|
+
cached = self._get_cached("strict_list")
|
|
164
|
+
if cached is not None:
|
|
165
|
+
return cached
|
|
166
|
+
|
|
167
|
+
if self._http is None:
|
|
168
|
+
raise RuntimeError("TokenClient not connected — call connect() first")
|
|
169
|
+
|
|
170
|
+
resp = await self._http.get(f"{self._base_url}/tokens/v1/strict")
|
|
171
|
+
resp.raise_for_status()
|
|
172
|
+
|
|
173
|
+
tokens = []
|
|
174
|
+
for item in resp.json():
|
|
175
|
+
if isinstance(item, dict):
|
|
176
|
+
tags = item.get("tags", [])
|
|
177
|
+
tokens.append(TokenInfo(
|
|
178
|
+
address=item.get("address", ""),
|
|
179
|
+
name=item.get("name", ""),
|
|
180
|
+
symbol=item.get("symbol", ""),
|
|
181
|
+
decimals=item.get("decimals", 0),
|
|
182
|
+
logo_uri=item.get("logoURI", ""),
|
|
183
|
+
tags=tags,
|
|
184
|
+
daily_volume=item.get("daily_volume"),
|
|
185
|
+
freeze_authority=item.get("freeze_authority"),
|
|
186
|
+
mint_authority=item.get("mint_authority"),
|
|
187
|
+
is_verified=True,
|
|
188
|
+
is_banned=False,
|
|
189
|
+
))
|
|
190
|
+
|
|
191
|
+
self._set_cached("strict_list", tokens, ttl=_BANNED_LIST_TTL)
|
|
192
|
+
return tokens
|
|
193
|
+
|
|
194
|
+
async def refresh_banned_list(self) -> int:
|
|
195
|
+
"""
|
|
196
|
+
Manually refresh the banned token list.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Number of banned tokens loaded.
|
|
200
|
+
"""
|
|
201
|
+
await self._refresh_banned_list()
|
|
202
|
+
return len(self._banned_mints)
|
|
203
|
+
|
|
204
|
+
# -- Internal helpers ----------------------------------------------------
|
|
205
|
+
|
|
206
|
+
async def _refresh_banned_list(self) -> None:
|
|
207
|
+
if self._http is None:
|
|
208
|
+
return
|
|
209
|
+
try:
|
|
210
|
+
resp = await self._http.get(f"{self._base_url}/tokens/v1/banned")
|
|
211
|
+
if resp.status_code == 200:
|
|
212
|
+
data = resp.json()
|
|
213
|
+
if isinstance(data, list):
|
|
214
|
+
self._banned_mints = {
|
|
215
|
+
item.get("address", item) if isinstance(item, dict) else str(item)
|
|
216
|
+
for item in data
|
|
217
|
+
}
|
|
218
|
+
self._banned_loaded = True
|
|
219
|
+
logger.debug("Loaded %d banned tokens", len(self._banned_mints))
|
|
220
|
+
except Exception as exc:
|
|
221
|
+
logger.warning("Failed to load banned token list: %s", exc)
|
|
222
|
+
|
|
223
|
+
def _get_cached(self, key: str) -> Any | None:
|
|
224
|
+
if key in self._cache:
|
|
225
|
+
expires, value = self._cache[key]
|
|
226
|
+
if time.monotonic() < expires:
|
|
227
|
+
return value
|
|
228
|
+
del self._cache[key]
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
def _set_cached(self, key: str, value: Any, ttl: float | None = None) -> None:
|
|
232
|
+
self._cache[key] = (time.monotonic() + (ttl or self._cache_ttl), value)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jupiter-swap-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python client for the Jupiter DEX aggregator on Solana — V6 quote/swap and Ultra API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JinUltimate1995/jupiter-swap-python
|
|
6
|
+
Project-URL: Repository, https://github.com/JinUltimate1995/jupiter-swap-python
|
|
7
|
+
Project-URL: Issues, https://github.com/JinUltimate1995/jupiter-swap-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JinUltimate1995/jupiter-swap-python/blob/main/CHANGELOG.md
|
|
9
|
+
Author: JinUltimate1995
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aggregator,async,crypto,defi,dex,jupiter,python,solana,swap,trading
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# jupiter-swap-python
|
|
34
|
+
|
|
35
|
+
**The missing Jupiter swap client for Python. Async. Typed. Production-tested.**
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/jupiter-swap-python/)
|
|
38
|
+
[](https://www.python.org/downloads/)
|
|
39
|
+
[](https://opensource.org/licenses/MIT)
|
|
40
|
+
[](https://github.com/JinUltimate1995/jupiter-swap-python/actions)
|
|
41
|
+
|
|
42
|
+
Jupiter's own Python SDK was abandoned (returns 404). This library fills the gap — a clean, typed, async client for [Jupiter](https://jup.ag), the #1 DEX aggregator on Solana.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ⚡ Quickstart
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install jupiter-swap-python
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Get a quote
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
from jupiter_swap import JupiterClient
|
|
57
|
+
|
|
58
|
+
SOL = "So11111111111111111111111111111111111111112"
|
|
59
|
+
USDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
|
|
60
|
+
|
|
61
|
+
async def main():
|
|
62
|
+
async with JupiterClient() as jup:
|
|
63
|
+
quote = await jup.get_quote(SOL, USDC, 1_000_000_000) # 1 SOL
|
|
64
|
+
print(f"1 SOL = {int(quote.out_amount) / 1e6:.2f} USDC")
|
|
65
|
+
print(f"Price impact: {quote.price_impact_pct}%")
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Swap tokens
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
async with JupiterClient(api_key="your-key") as jup:
|
|
74
|
+
# Step 1: Get quote
|
|
75
|
+
quote = await jup.get_quote(SOL, USDC, 1_000_000_000)
|
|
76
|
+
|
|
77
|
+
# Step 2: Build swap transaction
|
|
78
|
+
swap = await jup.get_swap_transaction(
|
|
79
|
+
quote,
|
|
80
|
+
user_public_key="YourWalletPublicKey...",
|
|
81
|
+
priority_fee_lamports=500_000, # 0.0005 SOL
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Step 3: Sign and send swap.swap_transaction with your wallet
|
|
85
|
+
print(f"Transaction ready (block height: {swap.last_valid_block_height})")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Ultra API (recommended for production)
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async with JupiterClient(api_key="your-key") as jup:
|
|
92
|
+
# Combined quote + swap in one call
|
|
93
|
+
order = await jup.ultra_order(
|
|
94
|
+
input_mint=SOL,
|
|
95
|
+
output_mint=USDC,
|
|
96
|
+
amount=1_000_000_000,
|
|
97
|
+
taker="YourWalletPublicKey...",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
print(f"Swap type: {order.swap_type}")
|
|
101
|
+
print(f"Out amount: {order.out_amount}")
|
|
102
|
+
|
|
103
|
+
# Sign order.swap_transaction with your wallet, then:
|
|
104
|
+
tx_sig = await jup.ultra_execute(signed_transaction, order.request_id)
|
|
105
|
+
print(f"Executed: {tx_sig}")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Token verification
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from jupiter_swap import TokenClient
|
|
112
|
+
|
|
113
|
+
async with TokenClient() as tokens:
|
|
114
|
+
info = await tokens.get_token_info("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
|
|
115
|
+
print(f"{info.symbol} — verified: {info.is_verified}, banned: {info.is_banned}")
|
|
116
|
+
|
|
117
|
+
# Check if a token is a known scam
|
|
118
|
+
if await tokens.is_banned("SomeScamMint..."):
|
|
119
|
+
print("🚫 Token is banned!")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 🔧 API Reference
|
|
125
|
+
|
|
126
|
+
### `JupiterClient`
|
|
127
|
+
|
|
128
|
+
| Method | Description |
|
|
129
|
+
|--------|-------------|
|
|
130
|
+
| `get_quote()` | Get the best swap route and price |
|
|
131
|
+
| `get_swap_transaction()` | Build a signable transaction from a quote |
|
|
132
|
+
| `ultra_order()` | Combined quote + swap via Ultra API (MEV protection) |
|
|
133
|
+
| `ultra_execute()` | Execute a signed Ultra swap order |
|
|
134
|
+
|
|
135
|
+
### `TokenClient`
|
|
136
|
+
|
|
137
|
+
| Method | Description |
|
|
138
|
+
|--------|-------------|
|
|
139
|
+
| `get_token_info()` | Get token metadata and verification status |
|
|
140
|
+
| `is_banned()` | Check if a token is on the banned list |
|
|
141
|
+
| `is_verified()` | Check if a token is verified |
|
|
142
|
+
| `get_strict_list()` | Get all strictly verified tokens |
|
|
143
|
+
| `refresh_banned_list()` | Manually refresh the banned list |
|
|
144
|
+
|
|
145
|
+
### Models
|
|
146
|
+
|
|
147
|
+
- `QuoteResponse` — Route details, expected output, price impact
|
|
148
|
+
- `SwapResponse` — Base64 transaction ready to sign
|
|
149
|
+
- `UltraOrder` — Combined quote + transaction from Ultra API
|
|
150
|
+
- `TokenInfo` — Name, symbol, decimals, verification flags
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 🛡️ Why this library?
|
|
155
|
+
|
|
156
|
+
| Problem | Solution |
|
|
157
|
+
|---------|----------|
|
|
158
|
+
| Jupiter's Python SDK is deleted | This exists |
|
|
159
|
+
| Raw `httpx.post()` calls everywhere | Typed client with proper models |
|
|
160
|
+
| No 429 handling | Built-in retry with exponential backoff |
|
|
161
|
+
| Token safety is an afterthought | `TokenClient` with banned/verified checks |
|
|
162
|
+
| Sync-only clients | Fully async with `httpx` |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 🔑 API Key
|
|
167
|
+
|
|
168
|
+
A Jupiter API key is optional but recommended for production use (higher rate limits).
|
|
169
|
+
|
|
170
|
+
Get one at [station.jup.ag](https://station.jup.ag/).
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
client = JupiterClient(api_key="your-api-key")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 📦 Also by JinUltimate1995
|
|
179
|
+
|
|
180
|
+
- [**solana-rpc-resilient**](https://github.com/JinUltimate1995/solana-rpc-resilient) — Solana RPC client with automatic failover, rate limiting, and circuit breaker
|
|
181
|
+
- [**dexscreener-python**](https://github.com/JinUltimate1995/dexscreener-python) — Async DexScreener API client for token/pair data across 80+ chains
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
jupiter_swap/__init__.py,sha256=0QHLBTFYjdNF3w9mv8uXvKVtHoCZLx7uY7UHlZ5HZ00,391
|
|
2
|
+
jupiter_swap/client.py,sha256=bbpxnxdAAcABWeZ7amdNjbrXO-kwytGKM_d6eI9TrYo,13062
|
|
3
|
+
jupiter_swap/models.py,sha256=PXlpq_1_f_YhYxq3MyvccNK-sjevrIpnnR4t7IHSTJo,2095
|
|
4
|
+
jupiter_swap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
jupiter_swap/tokens.py,sha256=RFgZExVcBtYWrqDhcKmwiKKllw9Xpejm2SAdfiG2hEs,7387
|
|
6
|
+
jupiter_swap_python-0.1.0.dist-info/METADATA,sha256=okCLod9i4wvpGL5-psxWMoYpfhN9oNzM-JPZYMkp5WM,6271
|
|
7
|
+
jupiter_swap_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
jupiter_swap_python-0.1.0.dist-info/licenses/LICENSE,sha256=D7y6S1R0LcQMuzvEM3af4NM2zASwS0H82DKH-3g_FDE,1072
|
|
9
|
+
jupiter_swap_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JinUltimate1995
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|