plaxswap 0.1.0__tar.gz
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.
- plaxswap-0.1.0/LICENSE +9 -0
- plaxswap-0.1.0/PKG-INFO +106 -0
- plaxswap-0.1.0/README.md +83 -0
- plaxswap-0.1.0/pyproject.toml +32 -0
- plaxswap-0.1.0/setup.cfg +4 -0
- plaxswap-0.1.0/src/plaxswap/__init__.py +215 -0
- plaxswap-0.1.0/src/plaxswap.egg-info/PKG-INFO +106 -0
- plaxswap-0.1.0/src/plaxswap.egg-info/SOURCES.txt +9 -0
- plaxswap-0.1.0/src/plaxswap.egg-info/dependency_links.txt +1 -0
- plaxswap-0.1.0/src/plaxswap.egg-info/top_level.txt +1 -0
- plaxswap-0.1.0/tests/test_plaxswap.py +60 -0
plaxswap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Plaxswap
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
plaxswap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plaxswap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Plaxswap API client: accept crypto payments (hosted checkout, payment links, signed webhooks) and quote crypto swaps.
|
|
5
|
+
Author-email: Plaxswap <support@plaxswap.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://plaxswap.com
|
|
8
|
+
Project-URL: Documentation, https://plaxswap.com/developers/payments
|
|
9
|
+
Project-URL: API reference, https://plaxswap.com/api/v1/openapi.json
|
|
10
|
+
Project-URL: Source, https://github.com/plaxswap-com/plaxswap-python
|
|
11
|
+
Project-URL: Issues, https://github.com/plaxswap-com/plaxswap-python/issues
|
|
12
|
+
Keywords: plaxswap,crypto payments,crypto payment gateway,payment gateway,bitcoin,usdt,stablecoin,webhooks,crypto swap
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# plaxswap
|
|
25
|
+
|
|
26
|
+
The official Python client for the [Plaxswap](https://plaxswap.com) API.
|
|
27
|
+
|
|
28
|
+
- **Accept crypto payments** on your site: create a payment, send the customer to the hosted checkout, and check the signed webhook. Docs: [Crypto payment gateway API](https://plaxswap.com/developers/payments).
|
|
29
|
+
- **Payment links**: ask for an exact amount of one coin; the payer can use any supported coin. See [crypto payment links](https://plaxswap.com/pay).
|
|
30
|
+
- **Swaps**: quote routes from KyberSwap, 0x, LI.FI, Relay and NEAR Intents, best return first, with every fee shown. See the [swap API guide](https://plaxswap.com/developers).
|
|
31
|
+
|
|
32
|
+
Standard library only. Python 3.8+.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install plaxswap
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Accept a payment
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
from plaxswap import Plaxswap
|
|
45
|
+
|
|
46
|
+
px = Plaxswap(api_key=os.environ["PLAXSWAP_KEY"]) # pxs_test_... or pxs_live_...
|
|
47
|
+
|
|
48
|
+
payment = px.payments.create(
|
|
49
|
+
amount="100", currency="USD", receive="usdt-tron",
|
|
50
|
+
orderId="1042", description="2 x T-shirt", returnUrl="https://shop.example/orders/1042",
|
|
51
|
+
idempotency_key="order-1042", # retries return the same payment
|
|
52
|
+
)
|
|
53
|
+
print(payment["checkoutUrl"]) # send the customer here
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With a test key nothing real moves. Act out an outcome and get the same webhook a live payment sends:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
px.payments.simulate(payment["id"], "paid")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Check the webhook (Flask)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from flask import Flask, request
|
|
66
|
+
from plaxswap import construct_event, WebhookSignatureError
|
|
67
|
+
|
|
68
|
+
app = Flask(__name__)
|
|
69
|
+
|
|
70
|
+
@app.post("/plaxswap-webhook")
|
|
71
|
+
def webhook():
|
|
72
|
+
try:
|
|
73
|
+
event = construct_event(request.get_data(), request.headers.get("x-px-signature"), os.environ["PLAXSWAP_WEBHOOK_SECRET"])
|
|
74
|
+
except WebhookSignatureError:
|
|
75
|
+
return "", 400
|
|
76
|
+
# Skip events you have seen (event["id"]), then read the payment back before you ship:
|
|
77
|
+
payment = px.payments.retrieve(event["data"]["id"])
|
|
78
|
+
if payment["status"] == "paid":
|
|
79
|
+
pass # fulfil order payment["orderId"]
|
|
80
|
+
return "", 200
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quote a swap
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
px = Plaxswap() # no key needed for quotes
|
|
87
|
+
routes = px.swap.routes("usdt-tron", "btc", "250")["routes"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
Every failed call raises `PlaxswapError` with `status`, a stable `code` (for example `invalid_api_key`) and a plain-English `message` that is safe to show.
|
|
93
|
+
|
|
94
|
+
## Fees
|
|
95
|
+
|
|
96
|
+
Gateway: the customer pays the 1.5% platform fee at checkout; you pay 0.3% plus the network fee when you withdraw. Swaps: Plaxswap's fee is at most 0.3%, plus the route's own fee. Current figures: [plaxswap.com/fees](https://plaxswap.com/fees).
|
|
97
|
+
|
|
98
|
+
## Links
|
|
99
|
+
|
|
100
|
+
- [Plaxswap](https://plaxswap.com)
|
|
101
|
+
- [Payment gateway docs](https://plaxswap.com/developers/payments)
|
|
102
|
+
- [OpenAPI document](https://plaxswap.com/api/v1/openapi.json)
|
|
103
|
+
- [WooCommerce plugin](https://plaxswap.com/integrations/woocommerce)
|
|
104
|
+
- Support: support@plaxswap.com
|
|
105
|
+
|
|
106
|
+
MIT licence.
|
plaxswap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# plaxswap
|
|
2
|
+
|
|
3
|
+
The official Python client for the [Plaxswap](https://plaxswap.com) API.
|
|
4
|
+
|
|
5
|
+
- **Accept crypto payments** on your site: create a payment, send the customer to the hosted checkout, and check the signed webhook. Docs: [Crypto payment gateway API](https://plaxswap.com/developers/payments).
|
|
6
|
+
- **Payment links**: ask for an exact amount of one coin; the payer can use any supported coin. See [crypto payment links](https://plaxswap.com/pay).
|
|
7
|
+
- **Swaps**: quote routes from KyberSwap, 0x, LI.FI, Relay and NEAR Intents, best return first, with every fee shown. See the [swap API guide](https://plaxswap.com/developers).
|
|
8
|
+
|
|
9
|
+
Standard library only. Python 3.8+.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install plaxswap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Accept a payment
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import os
|
|
21
|
+
from plaxswap import Plaxswap
|
|
22
|
+
|
|
23
|
+
px = Plaxswap(api_key=os.environ["PLAXSWAP_KEY"]) # pxs_test_... or pxs_live_...
|
|
24
|
+
|
|
25
|
+
payment = px.payments.create(
|
|
26
|
+
amount="100", currency="USD", receive="usdt-tron",
|
|
27
|
+
orderId="1042", description="2 x T-shirt", returnUrl="https://shop.example/orders/1042",
|
|
28
|
+
idempotency_key="order-1042", # retries return the same payment
|
|
29
|
+
)
|
|
30
|
+
print(payment["checkoutUrl"]) # send the customer here
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With a test key nothing real moves. Act out an outcome and get the same webhook a live payment sends:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
px.payments.simulate(payment["id"], "paid")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Check the webhook (Flask)
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from flask import Flask, request
|
|
43
|
+
from plaxswap import construct_event, WebhookSignatureError
|
|
44
|
+
|
|
45
|
+
app = Flask(__name__)
|
|
46
|
+
|
|
47
|
+
@app.post("/plaxswap-webhook")
|
|
48
|
+
def webhook():
|
|
49
|
+
try:
|
|
50
|
+
event = construct_event(request.get_data(), request.headers.get("x-px-signature"), os.environ["PLAXSWAP_WEBHOOK_SECRET"])
|
|
51
|
+
except WebhookSignatureError:
|
|
52
|
+
return "", 400
|
|
53
|
+
# Skip events you have seen (event["id"]), then read the payment back before you ship:
|
|
54
|
+
payment = px.payments.retrieve(event["data"]["id"])
|
|
55
|
+
if payment["status"] == "paid":
|
|
56
|
+
pass # fulfil order payment["orderId"]
|
|
57
|
+
return "", 200
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quote a swap
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
px = Plaxswap() # no key needed for quotes
|
|
64
|
+
routes = px.swap.routes("usdt-tron", "btc", "250")["routes"]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Errors
|
|
68
|
+
|
|
69
|
+
Every failed call raises `PlaxswapError` with `status`, a stable `code` (for example `invalid_api_key`) and a plain-English `message` that is safe to show.
|
|
70
|
+
|
|
71
|
+
## Fees
|
|
72
|
+
|
|
73
|
+
Gateway: the customer pays the 1.5% platform fee at checkout; you pay 0.3% plus the network fee when you withdraw. Swaps: Plaxswap's fee is at most 0.3%, plus the route's own fee. Current figures: [plaxswap.com/fees](https://plaxswap.com/fees).
|
|
74
|
+
|
|
75
|
+
## Links
|
|
76
|
+
|
|
77
|
+
- [Plaxswap](https://plaxswap.com)
|
|
78
|
+
- [Payment gateway docs](https://plaxswap.com/developers/payments)
|
|
79
|
+
- [OpenAPI document](https://plaxswap.com/api/v1/openapi.json)
|
|
80
|
+
- [WooCommerce plugin](https://plaxswap.com/integrations/woocommerce)
|
|
81
|
+
- Support: support@plaxswap.com
|
|
82
|
+
|
|
83
|
+
MIT licence.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "plaxswap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Plaxswap API client: accept crypto payments (hosted checkout, payment links, signed webhooks) and quote crypto swaps."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Plaxswap", email = "support@plaxswap.com" }]
|
|
13
|
+
keywords = ["plaxswap", "crypto payments", "crypto payment gateway", "payment gateway", "bitcoin", "usdt", "stablecoin", "webhooks", "crypto swap"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Office/Business :: Financial",
|
|
20
|
+
"Topic :: Software Development :: Libraries",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://plaxswap.com"
|
|
26
|
+
Documentation = "https://plaxswap.com/developers/payments"
|
|
27
|
+
"API reference" = "https://plaxswap.com/api/v1/openapi.json"
|
|
28
|
+
Source = "https://github.com/plaxswap-com/plaxswap-python"
|
|
29
|
+
Issues = "https://github.com/plaxswap-com/plaxswap-python/issues"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
plaxswap-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Official Python client for the Plaxswap API.
|
|
2
|
+
|
|
3
|
+
Accept crypto payments (hosted checkout, payment links, signed webhooks) and quote crypto swaps.
|
|
4
|
+
Docs: https://plaxswap.com/developers/payments and https://plaxswap.com/developers
|
|
5
|
+
Standard library only (urllib, hmac, json). Python 3.8+.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import urllib.request
|
|
16
|
+
from typing import Any, Dict, Iterable, Optional, Union
|
|
17
|
+
|
|
18
|
+
__all__ = ["Plaxswap", "PlaxswapError", "WebhookSignatureError", "verify_signature", "construct_event", "sign", "SIGNATURE_HEADER"]
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://plaxswap.com"
|
|
22
|
+
SIGNATURE_HEADER = "x-px-signature"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PlaxswapError(Exception):
|
|
26
|
+
"""An API error: HTTP status, the stable code and the plain-English message from the API."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, status: int, code: str, message: str, details: Any = None):
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.status = status
|
|
31
|
+
self.code = code
|
|
32
|
+
self.message = message
|
|
33
|
+
self.details = details
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str: # pragma: no cover
|
|
36
|
+
return f"PlaxswapError(status={self.status}, code={self.code!r}, message={self.message!r})"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Plaxswap:
|
|
40
|
+
"""Client for https://plaxswap.com/api/v1.
|
|
41
|
+
|
|
42
|
+
api_key: a merchant key (pxs_test_... or pxs_live_...) for the payment gateway, or a partner key (pxk_...) for swaps.
|
|
43
|
+
Keep it on your server.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, user_ip: Optional[str] = None, timeout: float = 30.0):
|
|
47
|
+
self.api_key = api_key
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.user_ip = user_ip
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.payments = _Payments(self)
|
|
52
|
+
self.payment_links = _PaymentLinks(self)
|
|
53
|
+
self.gateway = _Gateway(self)
|
|
54
|
+
self.swap = _Swap(self)
|
|
55
|
+
|
|
56
|
+
def meta(self) -> Dict[str, Any]:
|
|
57
|
+
"""Service mode, the caller's country rules and time limits."""
|
|
58
|
+
return self.request("GET", "/api/v1/meta")
|
|
59
|
+
|
|
60
|
+
def coins(self) -> Dict[str, Any]:
|
|
61
|
+
"""Coins, networks and methods."""
|
|
62
|
+
return self.request("GET", "/api/v1/coins")
|
|
63
|
+
|
|
64
|
+
def request(self, method: str, path: str, body: Any = None, query: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Any:
|
|
65
|
+
"""Low-level call for any endpoint in https://plaxswap.com/api/v1/openapi.json"""
|
|
66
|
+
url = self.base_url + path
|
|
67
|
+
q = {k: v for k, v in (query or {}).items() if v is not None}
|
|
68
|
+
if q:
|
|
69
|
+
url += "?" + urllib.parse.urlencode(q)
|
|
70
|
+
h = {"Accept": "application/json", "User-Agent": f"plaxswap-python/{__version__}"}
|
|
71
|
+
if self.api_key:
|
|
72
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
73
|
+
if self.user_ip:
|
|
74
|
+
h["x-user-ip"] = self.user_ip
|
|
75
|
+
h.update(headers or {})
|
|
76
|
+
data = None
|
|
77
|
+
if body is not None:
|
|
78
|
+
data = json.dumps(body).encode("utf-8")
|
|
79
|
+
h["Content-Type"] = "application/json"
|
|
80
|
+
req = urllib.request.Request(url, data=data, method=method, headers=h)
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as res:
|
|
83
|
+
text = res.read().decode("utf-8")
|
|
84
|
+
return json.loads(text) if text else None
|
|
85
|
+
except urllib.error.HTTPError as e:
|
|
86
|
+
text = e.read().decode("utf-8", "replace")
|
|
87
|
+
try:
|
|
88
|
+
err = (json.loads(text) or {}).get("error") or {}
|
|
89
|
+
except ValueError:
|
|
90
|
+
err = {}
|
|
91
|
+
raise PlaxswapError(e.code, err.get("code") or f"http_{e.code}", err.get("message") or text[:200] or str(e), err.get("details")) from None
|
|
92
|
+
except urllib.error.URLError as e:
|
|
93
|
+
raise PlaxswapError(0, "network_error", str(e.reason)) from None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _Payments:
|
|
97
|
+
def __init__(self, c: Plaxswap):
|
|
98
|
+
self._c = c
|
|
99
|
+
|
|
100
|
+
def create(self, idempotency_key: Optional[str] = None, **params: Any) -> Dict[str, Any]:
|
|
101
|
+
"""Ask for a payment; send the payer to payment["checkoutUrl"].
|
|
102
|
+
|
|
103
|
+
Example: create(amount="100", currency="USD", receive="usdt-tron", orderId="1042", idempotency_key="order-1042")
|
|
104
|
+
"""
|
|
105
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
|
|
106
|
+
return self._c.request("POST", "/api/v1/payments", body=params, headers=headers)
|
|
107
|
+
|
|
108
|
+
def retrieve(self, payment_id: str) -> Dict[str, Any]:
|
|
109
|
+
return self._c.request("GET", f"/api/v1/payments/{urllib.parse.quote(payment_id, safe='')}")
|
|
110
|
+
|
|
111
|
+
def list(self, **query: Any) -> Dict[str, Any]:
|
|
112
|
+
"""Newest first. Filters: status, orderId, createdFrom, createdTo, limit, cursor."""
|
|
113
|
+
return self._c.request("GET", "/api/v1/payments", query=query)
|
|
114
|
+
|
|
115
|
+
def cancel(self, payment_id: str) -> Dict[str, Any]:
|
|
116
|
+
return self._c.request("POST", f"/api/v1/payments/{urllib.parse.quote(payment_id, safe='')}/cancel", body={})
|
|
117
|
+
|
|
118
|
+
def simulate(self, payment_id: str, outcome: str, pay_currency: Optional[str] = None) -> Dict[str, Any]:
|
|
119
|
+
"""Test keys only: act out paid, partially_paid, overpaid, late, expired or failed."""
|
|
120
|
+
body: Dict[str, Any] = {"outcome": outcome}
|
|
121
|
+
if pay_currency:
|
|
122
|
+
body["payCurrency"] = pay_currency
|
|
123
|
+
return self._c.request("POST", f"/api/v1/payments/{urllib.parse.quote(payment_id, safe='')}/simulate", body=body)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _PaymentLinks:
|
|
127
|
+
def __init__(self, c: Plaxswap):
|
|
128
|
+
self._c = c
|
|
129
|
+
|
|
130
|
+
def create(self, **params: Any) -> Dict[str, Any]:
|
|
131
|
+
"""Create a "pay me exactly this amount" link. Keep manageToken: it is shown once."""
|
|
132
|
+
return self._c.request("POST", "/api/v1/payment-links", body=params)
|
|
133
|
+
|
|
134
|
+
def retrieve(self, link_id: str) -> Dict[str, Any]:
|
|
135
|
+
return self._c.request("GET", f"/api/v1/payment-links/{urllib.parse.quote(link_id, safe='')}")
|
|
136
|
+
|
|
137
|
+
def coins(self) -> Dict[str, Any]:
|
|
138
|
+
return self._c.request("GET", "/api/v1/payment-links/coins")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class _Gateway:
|
|
142
|
+
def __init__(self, c: Plaxswap):
|
|
143
|
+
self._c = c
|
|
144
|
+
|
|
145
|
+
def coins(self) -> Dict[str, Any]:
|
|
146
|
+
"""Coins you can receive, and what payers can pay each with. No key needed."""
|
|
147
|
+
return self._c.request("GET", "/api/v1/gateway/coins")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class _Swap:
|
|
151
|
+
def __init__(self, c: Plaxswap):
|
|
152
|
+
self._c = c
|
|
153
|
+
|
|
154
|
+
def coins(self) -> Dict[str, Any]:
|
|
155
|
+
return self._c.request("GET", "/api/v1/dex/coins")
|
|
156
|
+
|
|
157
|
+
def routes(self, from_: str, to: str, amount: str, **extra: Any) -> Dict[str, Any]:
|
|
158
|
+
"""Routes for a pair and amount, best first. amount is a decimal string in coin units, for example "0.015"."""
|
|
159
|
+
body = {"from": from_, "to": to, "amount": amount, **extra}
|
|
160
|
+
return self._c.request("POST", "/api/v1/dex/routes", body=body)
|
|
161
|
+
|
|
162
|
+
def build(self, route_id: str) -> Dict[str, Any]:
|
|
163
|
+
return self._c.request("POST", "/api/v1/dex/build", body={"routeId": route_id})
|
|
164
|
+
|
|
165
|
+
def status(self, deposit_address: str) -> Dict[str, Any]:
|
|
166
|
+
return self._c.request("GET", "/api/v1/dex/status", query={"depositAddress": deposit_address})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------- webhooks
|
|
170
|
+
|
|
171
|
+
class WebhookSignatureError(Exception):
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse(header: Optional[str]):
|
|
176
|
+
t, v1 = None, []
|
|
177
|
+
for part in str(header or "").split(","):
|
|
178
|
+
if "=" not in part:
|
|
179
|
+
continue
|
|
180
|
+
k, v = part.split("=", 1)
|
|
181
|
+
k, v = k.strip(), v.strip()
|
|
182
|
+
if k == "t" and len(v) == 10 and v.isdigit():
|
|
183
|
+
t = v
|
|
184
|
+
if k == "v1" and len(v) == 64 and all(ch in "0123456789abcdef" for ch in v):
|
|
185
|
+
v1.append(v)
|
|
186
|
+
return t, v1
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _b(raw: Union[str, bytes]) -> bytes:
|
|
190
|
+
return raw.encode("utf-8") if isinstance(raw, str) else raw
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def sign(raw_body: Union[str, bytes], secret: str, timestamp: Optional[int] = None) -> str:
|
|
194
|
+
"""The header Plaxswap would send for this body (useful in your own tests)."""
|
|
195
|
+
t = int(timestamp if timestamp is not None else time.time())
|
|
196
|
+
mac = hmac.new(secret.encode("utf-8"), f"{t}.".encode("utf-8") + _b(raw_body), hashlib.sha256).hexdigest()
|
|
197
|
+
return f"t={t},v1={mac}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def verify_signature(raw_body: Union[str, bytes], header: Optional[str], secret: str, tolerance: int = 300, now: Optional[float] = None) -> bool:
|
|
201
|
+
"""True when x-px-signature is valid for this exact raw body and was signed within `tolerance` seconds."""
|
|
202
|
+
t, v1 = _parse(header)
|
|
203
|
+
if not t or not v1 or not secret:
|
|
204
|
+
return False
|
|
205
|
+
if abs((now if now is not None else time.time()) - int(t)) > tolerance:
|
|
206
|
+
return False
|
|
207
|
+
expected = hmac.new(secret.encode("utf-8"), f"{t}.".encode("utf-8") + _b(raw_body), hashlib.sha256).hexdigest()
|
|
208
|
+
return any(hmac.compare_digest(sig, expected) for sig in v1)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def construct_event(raw_body: Union[str, bytes], header: Optional[str], secret: str, tolerance: int = 300) -> Dict[str, Any]:
|
|
212
|
+
"""Checks the signature, then parses the event ({"id", "event", "livemode", "createdAt", "data"})."""
|
|
213
|
+
if not verify_signature(raw_body, header, secret, tolerance):
|
|
214
|
+
raise WebhookSignatureError("Invalid or expired Plaxswap webhook signature")
|
|
215
|
+
return json.loads(_b(raw_body).decode("utf-8"))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plaxswap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Plaxswap API client: accept crypto payments (hosted checkout, payment links, signed webhooks) and quote crypto swaps.
|
|
5
|
+
Author-email: Plaxswap <support@plaxswap.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://plaxswap.com
|
|
8
|
+
Project-URL: Documentation, https://plaxswap.com/developers/payments
|
|
9
|
+
Project-URL: API reference, https://plaxswap.com/api/v1/openapi.json
|
|
10
|
+
Project-URL: Source, https://github.com/plaxswap-com/plaxswap-python
|
|
11
|
+
Project-URL: Issues, https://github.com/plaxswap-com/plaxswap-python/issues
|
|
12
|
+
Keywords: plaxswap,crypto payments,crypto payment gateway,payment gateway,bitcoin,usdt,stablecoin,webhooks,crypto swap
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# plaxswap
|
|
25
|
+
|
|
26
|
+
The official Python client for the [Plaxswap](https://plaxswap.com) API.
|
|
27
|
+
|
|
28
|
+
- **Accept crypto payments** on your site: create a payment, send the customer to the hosted checkout, and check the signed webhook. Docs: [Crypto payment gateway API](https://plaxswap.com/developers/payments).
|
|
29
|
+
- **Payment links**: ask for an exact amount of one coin; the payer can use any supported coin. See [crypto payment links](https://plaxswap.com/pay).
|
|
30
|
+
- **Swaps**: quote routes from KyberSwap, 0x, LI.FI, Relay and NEAR Intents, best return first, with every fee shown. See the [swap API guide](https://plaxswap.com/developers).
|
|
31
|
+
|
|
32
|
+
Standard library only. Python 3.8+.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install plaxswap
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Accept a payment
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
from plaxswap import Plaxswap
|
|
45
|
+
|
|
46
|
+
px = Plaxswap(api_key=os.environ["PLAXSWAP_KEY"]) # pxs_test_... or pxs_live_...
|
|
47
|
+
|
|
48
|
+
payment = px.payments.create(
|
|
49
|
+
amount="100", currency="USD", receive="usdt-tron",
|
|
50
|
+
orderId="1042", description="2 x T-shirt", returnUrl="https://shop.example/orders/1042",
|
|
51
|
+
idempotency_key="order-1042", # retries return the same payment
|
|
52
|
+
)
|
|
53
|
+
print(payment["checkoutUrl"]) # send the customer here
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With a test key nothing real moves. Act out an outcome and get the same webhook a live payment sends:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
px.payments.simulate(payment["id"], "paid")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Check the webhook (Flask)
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from flask import Flask, request
|
|
66
|
+
from plaxswap import construct_event, WebhookSignatureError
|
|
67
|
+
|
|
68
|
+
app = Flask(__name__)
|
|
69
|
+
|
|
70
|
+
@app.post("/plaxswap-webhook")
|
|
71
|
+
def webhook():
|
|
72
|
+
try:
|
|
73
|
+
event = construct_event(request.get_data(), request.headers.get("x-px-signature"), os.environ["PLAXSWAP_WEBHOOK_SECRET"])
|
|
74
|
+
except WebhookSignatureError:
|
|
75
|
+
return "", 400
|
|
76
|
+
# Skip events you have seen (event["id"]), then read the payment back before you ship:
|
|
77
|
+
payment = px.payments.retrieve(event["data"]["id"])
|
|
78
|
+
if payment["status"] == "paid":
|
|
79
|
+
pass # fulfil order payment["orderId"]
|
|
80
|
+
return "", 200
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quote a swap
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
px = Plaxswap() # no key needed for quotes
|
|
87
|
+
routes = px.swap.routes("usdt-tron", "btc", "250")["routes"]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
Every failed call raises `PlaxswapError` with `status`, a stable `code` (for example `invalid_api_key`) and a plain-English `message` that is safe to show.
|
|
93
|
+
|
|
94
|
+
## Fees
|
|
95
|
+
|
|
96
|
+
Gateway: the customer pays the 1.5% platform fee at checkout; you pay 0.3% plus the network fee when you withdraw. Swaps: Plaxswap's fee is at most 0.3%, plus the route's own fee. Current figures: [plaxswap.com/fees](https://plaxswap.com/fees).
|
|
97
|
+
|
|
98
|
+
## Links
|
|
99
|
+
|
|
100
|
+
- [Plaxswap](https://plaxswap.com)
|
|
101
|
+
- [Payment gateway docs](https://plaxswap.com/developers/payments)
|
|
102
|
+
- [OpenAPI document](https://plaxswap.com/api/v1/openapi.json)
|
|
103
|
+
- [WooCommerce plugin](https://plaxswap.com/integrations/woocommerce)
|
|
104
|
+
- Support: support@plaxswap.com
|
|
105
|
+
|
|
106
|
+
MIT licence.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plaxswap
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import hashlib, hmac, json, unittest
|
|
2
|
+
from unittest import mock
|
|
3
|
+
import plaxswap
|
|
4
|
+
from plaxswap import Plaxswap, PlaxswapError, verify_signature, construct_event, sign
|
|
5
|
+
|
|
6
|
+
SECRET = "whsec_test_123"
|
|
7
|
+
BODY = '{"id":"evt_3k9q2m7x8c1v5b4n","event":"payment.paid","livemode":false,"data":{"id":"pay_7m2k9q4x8c1v5b3n","object":"payment","status":"paid"}}'
|
|
8
|
+
T = 1794000000
|
|
9
|
+
HEX = hmac.new(SECRET.encode(), f"{T}.{BODY}".encode(), hashlib.sha256).hexdigest()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Webhooks(unittest.TestCase):
|
|
13
|
+
def test_valid(self): self.assertTrue(verify_signature(BODY, f"t={T},v1={HEX}", SECRET, now=T + 10))
|
|
14
|
+
def test_bytes_body(self): self.assertTrue(verify_signature(BODY.encode(), f"t={T},v1={HEX}", SECRET, now=T))
|
|
15
|
+
def test_several_v1(self): self.assertTrue(verify_signature(BODY, f"t={T},v1={'0'*64},v1={HEX}", SECRET, now=T))
|
|
16
|
+
def test_changed_body(self): self.assertFalse(verify_signature(BODY + " ", f"t={T},v1={HEX}", SECRET, now=T))
|
|
17
|
+
def test_old(self): self.assertFalse(verify_signature(BODY, f"t={T},v1={HEX}", SECRET, now=T + 301))
|
|
18
|
+
def test_wrong_secret(self): self.assertFalse(verify_signature(BODY, f"t={T},v1={HEX}", "whsec_other", now=T))
|
|
19
|
+
def test_missing(self): self.assertFalse(verify_signature(BODY, None, SECRET))
|
|
20
|
+
def test_sign(self): self.assertEqual(sign(BODY, SECRET, T), f"t={T},v1={HEX}")
|
|
21
|
+
def test_construct(self):
|
|
22
|
+
ev = construct_event(BODY, sign(BODY, SECRET), SECRET)
|
|
23
|
+
self.assertEqual(ev["event"], "payment.paid"); self.assertEqual(ev["data"]["id"], "pay_7m2k9q4x8c1v5b3n")
|
|
24
|
+
def test_construct_bad(self):
|
|
25
|
+
with self.assertRaises(plaxswap.WebhookSignatureError): construct_event(BODY, "t=1,v1=x", SECRET)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FakeResp:
|
|
29
|
+
def __init__(self, body): self.body = body
|
|
30
|
+
def read(self): return json.dumps(self.body).encode()
|
|
31
|
+
def __enter__(self): return self
|
|
32
|
+
def __exit__(self, *a): return False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Client(unittest.TestCase):
|
|
36
|
+
def test_create_payment(self):
|
|
37
|
+
seen = {}
|
|
38
|
+
def fake(req, timeout=None):
|
|
39
|
+
seen["url"], seen["method"], seen["headers"], seen["body"] = req.full_url, req.get_method(), dict(req.header_items()), json.loads(req.data)
|
|
40
|
+
return FakeResp({"id": "pay_0000000000000001", "object": "payment", "status": "pending"})
|
|
41
|
+
with mock.patch("urllib.request.urlopen", fake):
|
|
42
|
+
p = Plaxswap(api_key="pxs_test_x").payments.create(amount="100", currency="USD", receive="usdt-tron", orderId="1042", idempotency_key="order-1042")
|
|
43
|
+
self.assertEqual(p["id"], "pay_0000000000000001")
|
|
44
|
+
self.assertEqual(seen["url"], "https://plaxswap.com/api/v1/payments")
|
|
45
|
+
self.assertEqual(seen["method"], "POST")
|
|
46
|
+
self.assertEqual(seen["headers"]["Authorization"], "Bearer pxs_test_x")
|
|
47
|
+
self.assertEqual(seen["headers"]["Idempotency-key"], "order-1042")
|
|
48
|
+
self.assertEqual(seen["body"], {"amount": "100", "currency": "USD", "receive": "usdt-tron", "orderId": "1042"})
|
|
49
|
+
|
|
50
|
+
def test_error(self):
|
|
51
|
+
import urllib.error, io
|
|
52
|
+
def fake(req, timeout=None):
|
|
53
|
+
raise urllib.error.HTTPError(req.full_url, 401, "Unauthorized", {}, io.BytesIO(b'{"error":{"code":"invalid_api_key","message":"That key is wrong."}}'))
|
|
54
|
+
with mock.patch("urllib.request.urlopen", fake):
|
|
55
|
+
with self.assertRaises(PlaxswapError) as cm: Plaxswap(api_key="bad").payments.retrieve("pay_x")
|
|
56
|
+
self.assertEqual((cm.exception.status, cm.exception.code), (401, "invalid_api_key"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
unittest.main()
|