biatec-router 1.0.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.
- biatec_router-1.0.0/.gitignore +5 -0
- biatec_router-1.0.0/PKG-INFO +130 -0
- biatec_router-1.0.0/README.md +98 -0
- biatec_router-1.0.0/pyproject.toml +42 -0
- biatec_router-1.0.0/src/biatec_router/__init__.py +16 -0
- biatec_router-1.0.0/src/biatec_router/auth.py +62 -0
- biatec_router-1.0.0/src/biatec_router/client.py +137 -0
- biatec_router-1.0.0/tests/test_auth.py +36 -0
- biatec_router-1.0.0/tests/test_client.py +73 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: biatec-router
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for Biatec Router, a DEX aggregator API for the Algorand blockchain
|
|
5
|
+
Project-URL: Homepage, https://github.com/scholtz/BiatecRouter
|
|
6
|
+
Project-URL: Documentation, https://router.biatec.io/developers
|
|
7
|
+
Project-URL: Repository, https://github.com/scholtz/BiatecRouter
|
|
8
|
+
Project-URL: Issues, https://github.com/scholtz/BiatecRouter/issues
|
|
9
|
+
Author-email: Biatec <scholtzandcojsa@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: aggregator,algorand,amm,biatec,dex,swap
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: py-algorand-sdk>=2.6.0
|
|
25
|
+
Requires-Dist: requests>=2.31.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: responses>=0.25.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# biatec-router (Python)
|
|
34
|
+
|
|
35
|
+
Python client for [Biatec Router](https://github.com/scholtz/BiatecRouter), a DEX aggregator API for the Algorand blockchain.
|
|
36
|
+
|
|
37
|
+
At the time of writing it is the most efficient DEX aggregator on Algorand as it routes trades across Pact.Fi, Tinyman **and** Biatec DEX - the first concentrated-liquidity AMM on Algorand, which other aggregators don't support.
|
|
38
|
+
|
|
39
|
+
This mirrors the [`biatec-router` npm package](https://www.npmjs.com/package/biatec-router) for Python developers: a thin HTTP client plus an ARC-14 authentication helper. It does not sign or submit transactions for you - you decode, sign and send the transaction group with [`py-algorand-sdk`](https://pypi.org/project/py-algorand-sdk/) as shown below.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install biatec-router
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Authentication
|
|
48
|
+
|
|
49
|
+
Every endpoint requires an `Authorization` header carrying a signed [ARC-0014](https://arc.algorand.foundation/ARCs/arc-0014) transaction for the realm `BiatecRouter#ARC14`. The transaction is a zero-amount payment with the realm in the note field - it is never submitted to the network, it only proves control of an Algorand account.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from algosdk.v2client import algod
|
|
53
|
+
from biatec_router import build_auth_header, RouterClient
|
|
54
|
+
|
|
55
|
+
algod_client = algod.AlgodClient("", "https://mainnet-api.4160.nodely.dev")
|
|
56
|
+
suggested_params = algod_client.suggested_params()
|
|
57
|
+
|
|
58
|
+
# private_key from mnemonic.to_private_key(...), ARC-76, or however you manage keys
|
|
59
|
+
auth_header = build_auth_header(private_key, suggested_params)
|
|
60
|
+
|
|
61
|
+
router = RouterClient(
|
|
62
|
+
base_url="https://router.api.biatec.io", # or https://testnet.router.api.biatec.io
|
|
63
|
+
auth_header=auth_header,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Request a quote
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
output_amount = router.quote(from_asset=0, to_asset=31566704, amount=5_000_000)
|
|
71
|
+
print(output_amount) # estimated USDC (base units) for 5 ALGO
|
|
72
|
+
|
|
73
|
+
routes = router.route(from_asset=0, to_asset=31566704, amount=5_000_000, max_routes=1)
|
|
74
|
+
print(routes)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Execute a swap
|
|
78
|
+
|
|
79
|
+
**Always set `receive_minimum` when executing to protect your funds. Never use `0` outside of a preview.**
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import algosdk
|
|
83
|
+
from algosdk import transaction
|
|
84
|
+
|
|
85
|
+
response = router.route_txs(
|
|
86
|
+
sender=account_address,
|
|
87
|
+
from_asset=0,
|
|
88
|
+
to_asset=31566704,
|
|
89
|
+
swap_amount=5_000_000,
|
|
90
|
+
receive_minimum=4_900_000,
|
|
91
|
+
routes_count=1,
|
|
92
|
+
max_hops=3,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
route = response["routes"][0]
|
|
96
|
+
if not route.get("txsToSign"):
|
|
97
|
+
raise RuntimeError("No transactions to sign in the route.")
|
|
98
|
+
|
|
99
|
+
# Decode, group and sign
|
|
100
|
+
txns = [algosdk.encoding.msgpack_decode(b64) for b64 in route["txsToSign"]]
|
|
101
|
+
group_id = transaction.calculate_group_id(txns)
|
|
102
|
+
for txn in txns:
|
|
103
|
+
txn.group = group_id
|
|
104
|
+
|
|
105
|
+
signed = [txn.sign(private_key) for txn in txns]
|
|
106
|
+
txid = algod_client.send_transactions(signed)
|
|
107
|
+
algosdk.transaction.wait_for_confirmation(algod_client, txid, 4)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
- `RouterClient(base_url=..., auth_header=..., timeout=30.0)`
|
|
113
|
+
- `.quote(from_asset, to_asset, amount) -> int`
|
|
114
|
+
- `.route(from_asset, to_asset, amount, max_routes=1) -> list[dict]`
|
|
115
|
+
- `.route_txs(sender, from_asset, to_asset, swap_amount, receive_minimum, routes_count=1, max_hops=3) -> dict`
|
|
116
|
+
- `.stats() -> dict`
|
|
117
|
+
- `.snapshot() -> dict`
|
|
118
|
+
- Raises `biatec_router.RouterApiError` (with `.status_code` / `.body`) on non-2xx responses.
|
|
119
|
+
- `build_auth_header(private_key, suggested_params, realm="BiatecRouter#ARC14", sender=None) -> str`
|
|
120
|
+
- `make_arc14_transaction(sender, suggested_params, realm="BiatecRouter#ARC14")` / `make_arc14_auth_header(signed_txn)` - lower-level building blocks if you sign with something other than a raw private key (e.g. a wallet or `AccountTransactionSigner`).
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd python
|
|
126
|
+
pip install -e ".[dev]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [`docs/PYPI_CICD.md`](../docs/PYPI_CICD.md) in the repository root for how releases are published to PyPI.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# biatec-router (Python)
|
|
2
|
+
|
|
3
|
+
Python client for [Biatec Router](https://github.com/scholtz/BiatecRouter), a DEX aggregator API for the Algorand blockchain.
|
|
4
|
+
|
|
5
|
+
At the time of writing it is the most efficient DEX aggregator on Algorand as it routes trades across Pact.Fi, Tinyman **and** Biatec DEX - the first concentrated-liquidity AMM on Algorand, which other aggregators don't support.
|
|
6
|
+
|
|
7
|
+
This mirrors the [`biatec-router` npm package](https://www.npmjs.com/package/biatec-router) for Python developers: a thin HTTP client plus an ARC-14 authentication helper. It does not sign or submit transactions for you - you decode, sign and send the transaction group with [`py-algorand-sdk`](https://pypi.org/project/py-algorand-sdk/) as shown below.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install biatec-router
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
Every endpoint requires an `Authorization` header carrying a signed [ARC-0014](https://arc.algorand.foundation/ARCs/arc-0014) transaction for the realm `BiatecRouter#ARC14`. The transaction is a zero-amount payment with the realm in the note field - it is never submitted to the network, it only proves control of an Algorand account.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from algosdk.v2client import algod
|
|
21
|
+
from biatec_router import build_auth_header, RouterClient
|
|
22
|
+
|
|
23
|
+
algod_client = algod.AlgodClient("", "https://mainnet-api.4160.nodely.dev")
|
|
24
|
+
suggested_params = algod_client.suggested_params()
|
|
25
|
+
|
|
26
|
+
# private_key from mnemonic.to_private_key(...), ARC-76, or however you manage keys
|
|
27
|
+
auth_header = build_auth_header(private_key, suggested_params)
|
|
28
|
+
|
|
29
|
+
router = RouterClient(
|
|
30
|
+
base_url="https://router.api.biatec.io", # or https://testnet.router.api.biatec.io
|
|
31
|
+
auth_header=auth_header,
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Request a quote
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
output_amount = router.quote(from_asset=0, to_asset=31566704, amount=5_000_000)
|
|
39
|
+
print(output_amount) # estimated USDC (base units) for 5 ALGO
|
|
40
|
+
|
|
41
|
+
routes = router.route(from_asset=0, to_asset=31566704, amount=5_000_000, max_routes=1)
|
|
42
|
+
print(routes)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Execute a swap
|
|
46
|
+
|
|
47
|
+
**Always set `receive_minimum` when executing to protect your funds. Never use `0` outside of a preview.**
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import algosdk
|
|
51
|
+
from algosdk import transaction
|
|
52
|
+
|
|
53
|
+
response = router.route_txs(
|
|
54
|
+
sender=account_address,
|
|
55
|
+
from_asset=0,
|
|
56
|
+
to_asset=31566704,
|
|
57
|
+
swap_amount=5_000_000,
|
|
58
|
+
receive_minimum=4_900_000,
|
|
59
|
+
routes_count=1,
|
|
60
|
+
max_hops=3,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
route = response["routes"][0]
|
|
64
|
+
if not route.get("txsToSign"):
|
|
65
|
+
raise RuntimeError("No transactions to sign in the route.")
|
|
66
|
+
|
|
67
|
+
# Decode, group and sign
|
|
68
|
+
txns = [algosdk.encoding.msgpack_decode(b64) for b64 in route["txsToSign"]]
|
|
69
|
+
group_id = transaction.calculate_group_id(txns)
|
|
70
|
+
for txn in txns:
|
|
71
|
+
txn.group = group_id
|
|
72
|
+
|
|
73
|
+
signed = [txn.sign(private_key) for txn in txns]
|
|
74
|
+
txid = algod_client.send_transactions(signed)
|
|
75
|
+
algosdk.transaction.wait_for_confirmation(algod_client, txid, 4)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
- `RouterClient(base_url=..., auth_header=..., timeout=30.0)`
|
|
81
|
+
- `.quote(from_asset, to_asset, amount) -> int`
|
|
82
|
+
- `.route(from_asset, to_asset, amount, max_routes=1) -> list[dict]`
|
|
83
|
+
- `.route_txs(sender, from_asset, to_asset, swap_amount, receive_minimum, routes_count=1, max_hops=3) -> dict`
|
|
84
|
+
- `.stats() -> dict`
|
|
85
|
+
- `.snapshot() -> dict`
|
|
86
|
+
- Raises `biatec_router.RouterApiError` (with `.status_code` / `.body`) on non-2xx responses.
|
|
87
|
+
- `build_auth_header(private_key, suggested_params, realm="BiatecRouter#ARC14", sender=None) -> str`
|
|
88
|
+
- `make_arc14_transaction(sender, suggested_params, realm="BiatecRouter#ARC14")` / `make_arc14_auth_header(signed_txn)` - lower-level building blocks if you sign with something other than a raw private key (e.g. a wallet or `AccountTransactionSigner`).
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cd python
|
|
94
|
+
pip install -e ".[dev]"
|
|
95
|
+
pytest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
See [`docs/PYPI_CICD.md`](../docs/PYPI_CICD.md) in the repository root for how releases are published to PyPI.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "biatec-router"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python client for Biatec Router, a DEX aggregator API for the Algorand blockchain"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Biatec", email = "scholtzandcojsa@gmail.com" }]
|
|
13
|
+
keywords = ["algorand", "dex", "aggregator", "swap", "biatec", "amm"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Office/Business :: Financial",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["py-algorand-sdk>=2.6.0", "requests>=2.31.0"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = ["pytest>=8.0.0", "responses>=0.25.0", "build>=1.2.0", "twine>=5.0.0"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/scholtz/BiatecRouter"
|
|
34
|
+
Documentation = "https://router.biatec.io/developers"
|
|
35
|
+
Repository = "https://github.com/scholtz/BiatecRouter"
|
|
36
|
+
Issues = "https://github.com/scholtz/BiatecRouter/issues"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/biatec_router"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Biatec Router - Python client for the Algorand DEX aggregator API."""
|
|
2
|
+
|
|
3
|
+
from .auth import ARC14_REALM, build_auth_header, make_arc14_auth_header, make_arc14_transaction
|
|
4
|
+
from .client import DEFAULT_BASE_URL, RouterApiError, RouterClient
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ARC14_REALM",
|
|
8
|
+
"build_auth_header",
|
|
9
|
+
"make_arc14_auth_header",
|
|
10
|
+
"make_arc14_transaction",
|
|
11
|
+
"DEFAULT_BASE_URL",
|
|
12
|
+
"RouterApiError",
|
|
13
|
+
"RouterClient",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""ARC-14 authentication helpers.
|
|
2
|
+
|
|
3
|
+
Biatec Router requires every request to carry an ``Authorization`` header
|
|
4
|
+
built from a signed, never-submitted, zero-amount Algorand payment
|
|
5
|
+
transaction whose note field is the realm string (ARC-0014). This module
|
|
6
|
+
builds that transaction and the resulting header.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from algosdk import encoding
|
|
14
|
+
from algosdk.transaction import PaymentTxn, SuggestedParams
|
|
15
|
+
from algosdk.account import address_from_private_key
|
|
16
|
+
|
|
17
|
+
#: Realm Biatec Router accepts. ``BiatecScan#ARC14`` and ``BiatecDEX#ARC14``
|
|
18
|
+
#: are also accepted by the API but this is the canonical one for the router.
|
|
19
|
+
ARC14_REALM = "BiatecRouter#ARC14"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def make_arc14_transaction(
|
|
23
|
+
sender: str,
|
|
24
|
+
suggested_params: SuggestedParams,
|
|
25
|
+
realm: str = ARC14_REALM,
|
|
26
|
+
) -> PaymentTxn:
|
|
27
|
+
"""Build the (unsigned) zero-amount ARC-14 auth transaction for ``sender``.
|
|
28
|
+
|
|
29
|
+
The transaction is never sent to the network - it only proves control of
|
|
30
|
+
the Algorand account and carries the realm in its note field.
|
|
31
|
+
"""
|
|
32
|
+
return PaymentTxn(
|
|
33
|
+
sender=sender,
|
|
34
|
+
sp=suggested_params,
|
|
35
|
+
receiver=sender,
|
|
36
|
+
amt=0,
|
|
37
|
+
note=realm.encode("utf-8"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def make_arc14_auth_header(signed_txn) -> str:
|
|
42
|
+
"""Encode a signed ARC-14 transaction as the ``Authorization`` header value."""
|
|
43
|
+
return "SigTx " + encoding.msgpack_encode(signed_txn)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_auth_header(
|
|
47
|
+
private_key: str,
|
|
48
|
+
suggested_params: SuggestedParams,
|
|
49
|
+
realm: str = ARC14_REALM,
|
|
50
|
+
sender: Optional[str] = None,
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Sign the ARC-14 transaction with ``private_key`` and return the header value.
|
|
53
|
+
|
|
54
|
+
Convenience wrapper around :func:`make_arc14_transaction` and
|
|
55
|
+
:func:`make_arc14_auth_header` for the common case of holding the
|
|
56
|
+
account's private key directly (e.g. from ``algosdk.mnemonic.to_private_key``
|
|
57
|
+
or an ARC-76 derived key).
|
|
58
|
+
"""
|
|
59
|
+
addr = sender or address_from_private_key(private_key)
|
|
60
|
+
txn = make_arc14_transaction(addr, suggested_params, realm)
|
|
61
|
+
signed = txn.sign(private_key)
|
|
62
|
+
return make_arc14_auth_header(signed)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""HTTP client for the Biatec Router REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
DEFAULT_BASE_URL = "https://router.api.biatec.io"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RouterApiError(Exception):
|
|
13
|
+
"""Raised when the Biatec Router API returns a non-2xx response."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, status_code: int, body: Any):
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.body = body
|
|
18
|
+
super().__init__(f"Biatec Router API error {status_code}: {body}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RouterClient:
|
|
22
|
+
"""Thin wrapper around the Biatec Router HTTP API.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
base_url:
|
|
27
|
+
Router instance to call. Defaults to the mainnet production API.
|
|
28
|
+
Use ``https://testnet.router.api.biatec.io`` for testnet.
|
|
29
|
+
auth_header:
|
|
30
|
+
Value of the ``Authorization`` header (``"SigTx <...>"``), typically
|
|
31
|
+
produced by :func:`biatec_router.auth.build_auth_header`. Can also be
|
|
32
|
+
set later via :attr:`auth_header` if the signed transaction is only
|
|
33
|
+
available after construction.
|
|
34
|
+
timeout:
|
|
35
|
+
Per-request timeout in seconds.
|
|
36
|
+
session:
|
|
37
|
+
Optional pre-configured :class:`requests.Session` to reuse connection
|
|
38
|
+
pooling across multiple clients.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
44
|
+
auth_header: Optional[str] = None,
|
|
45
|
+
timeout: float = 30.0,
|
|
46
|
+
session: Optional[requests.Session] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.base_url = base_url.rstrip("/")
|
|
49
|
+
self.auth_header = auth_header
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self._session = session or requests.Session()
|
|
52
|
+
|
|
53
|
+
def _headers(self) -> Dict[str, str]:
|
|
54
|
+
headers = {"Accept": "application/json"}
|
|
55
|
+
if self.auth_header:
|
|
56
|
+
headers["Authorization"] = self.auth_header
|
|
57
|
+
return headers
|
|
58
|
+
|
|
59
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
60
|
+
response = self._session.request(
|
|
61
|
+
method,
|
|
62
|
+
f"{self.base_url}{path}",
|
|
63
|
+
headers=self._headers(),
|
|
64
|
+
timeout=self.timeout,
|
|
65
|
+
**kwargs,
|
|
66
|
+
)
|
|
67
|
+
if not response.ok:
|
|
68
|
+
try:
|
|
69
|
+
body: Any = response.json()
|
|
70
|
+
except ValueError:
|
|
71
|
+
body = response.text
|
|
72
|
+
raise RouterApiError(response.status_code, body)
|
|
73
|
+
if not response.content:
|
|
74
|
+
return None
|
|
75
|
+
return response.json()
|
|
76
|
+
|
|
77
|
+
def quote(self, from_asset: int, to_asset: int, amount: int) -> int:
|
|
78
|
+
"""Best output amount (base units) for swapping ``amount`` of ``from_asset``."""
|
|
79
|
+
return self._request(
|
|
80
|
+
"GET",
|
|
81
|
+
"/api/v1/router/quote",
|
|
82
|
+
params={"fromAsset": from_asset, "toAsset": to_asset, "amount": amount},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def route(
|
|
86
|
+
self,
|
|
87
|
+
from_asset: int,
|
|
88
|
+
to_asset: int,
|
|
89
|
+
amount: int,
|
|
90
|
+
max_routes: int = 1,
|
|
91
|
+
) -> List[Dict[str, Any]]:
|
|
92
|
+
"""Detailed route(s) including hops, pool splits and network fee."""
|
|
93
|
+
return self._request(
|
|
94
|
+
"GET",
|
|
95
|
+
"/api/v1/router/route",
|
|
96
|
+
params={
|
|
97
|
+
"fromAsset": from_asset,
|
|
98
|
+
"toAsset": to_asset,
|
|
99
|
+
"amount": amount,
|
|
100
|
+
"maxRoutes": max_routes,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def route_txs(
|
|
105
|
+
self,
|
|
106
|
+
sender: str,
|
|
107
|
+
from_asset: int,
|
|
108
|
+
to_asset: int,
|
|
109
|
+
swap_amount: int,
|
|
110
|
+
receive_minimum: int,
|
|
111
|
+
routes_count: int = 1,
|
|
112
|
+
max_hops: int = 3,
|
|
113
|
+
) -> Dict[str, Any]:
|
|
114
|
+
"""Best route plus unsigned transactions built for ``sender``, ready to sign.
|
|
115
|
+
|
|
116
|
+
``receive_minimum`` is enforced on-chain and should always reflect
|
|
117
|
+
your slippage tolerance - never call this with ``0`` outside of a
|
|
118
|
+
preview, since that disables slippage protection.
|
|
119
|
+
"""
|
|
120
|
+
body = {
|
|
121
|
+
"sender": sender,
|
|
122
|
+
"fromAsset": from_asset,
|
|
123
|
+
"toAsset": to_asset,
|
|
124
|
+
"swapAmount": swap_amount,
|
|
125
|
+
"receiveMinimum": receive_minimum,
|
|
126
|
+
"routesCount": routes_count,
|
|
127
|
+
"maxHops": max_hops,
|
|
128
|
+
}
|
|
129
|
+
return self._request("POST", "/api/v1/router/routeTxs", json=body)
|
|
130
|
+
|
|
131
|
+
def stats(self) -> Dict[str, Any]:
|
|
132
|
+
"""Live instance statistics: pools indexed, request counters, uptime."""
|
|
133
|
+
return self._request("GET", "/api/v1/router/stats")
|
|
134
|
+
|
|
135
|
+
def snapshot(self) -> Dict[str, Any]:
|
|
136
|
+
"""Export of all pools and aggregated pools currently held in memory."""
|
|
137
|
+
return self._request("GET", "/api/v1/router/snapshot")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from algosdk.account import generate_account
|
|
2
|
+
from algosdk.transaction import SuggestedParams
|
|
3
|
+
|
|
4
|
+
from biatec_router.auth import ARC14_REALM, build_auth_header, make_arc14_transaction
|
|
5
|
+
|
|
6
|
+
GENESIS_HASH = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _suggested_params() -> SuggestedParams:
|
|
10
|
+
return SuggestedParams(fee=0, first=1, last=1000, gh=GENESIS_HASH, flat_fee=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_make_arc14_transaction_is_zero_amount_with_realm_note():
|
|
14
|
+
_, addr = generate_account()
|
|
15
|
+
txn = make_arc14_transaction(addr, _suggested_params())
|
|
16
|
+
|
|
17
|
+
assert txn.sender == addr
|
|
18
|
+
assert txn.receiver == addr
|
|
19
|
+
assert txn.amt == 0
|
|
20
|
+
assert txn.note == ARC14_REALM.encode("utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_build_auth_header_has_sigtx_prefix():
|
|
24
|
+
sk, addr = generate_account()
|
|
25
|
+
header = build_auth_header(sk, _suggested_params(), sender=addr)
|
|
26
|
+
|
|
27
|
+
assert header.startswith("SigTx ")
|
|
28
|
+
assert len(header) > len("SigTx ")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_build_auth_header_derives_sender_from_private_key():
|
|
32
|
+
sk, addr = generate_account()
|
|
33
|
+
header_explicit = build_auth_header(sk, _suggested_params(), sender=addr)
|
|
34
|
+
header_derived = build_auth_header(sk, _suggested_params())
|
|
35
|
+
|
|
36
|
+
assert header_explicit == header_derived
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import responses
|
|
3
|
+
|
|
4
|
+
from biatec_router.client import RouterApiError, RouterClient
|
|
5
|
+
|
|
6
|
+
BASE_URL = "https://testnet.router.api.biatec.io"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def client() -> RouterClient:
|
|
11
|
+
return RouterClient(base_url=BASE_URL, auth_header="SigTx abc123")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@responses.activate
|
|
15
|
+
def test_quote_sends_auth_header_and_query_params(client: RouterClient):
|
|
16
|
+
responses.add(
|
|
17
|
+
responses.GET,
|
|
18
|
+
f"{BASE_URL}/api/v1/router/quote",
|
|
19
|
+
json=4987120,
|
|
20
|
+
status=200,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
result = client.quote(from_asset=0, to_asset=31566704, amount=5_000_000)
|
|
24
|
+
|
|
25
|
+
assert result == 4987120
|
|
26
|
+
sent = responses.calls[0].request
|
|
27
|
+
assert sent.headers["Authorization"] == "SigTx abc123"
|
|
28
|
+
assert "fromAsset=0" in sent.url
|
|
29
|
+
assert "toAsset=31566704" in sent.url
|
|
30
|
+
assert "amount=5000000" in sent.url
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@responses.activate
|
|
34
|
+
def test_route_txs_posts_json_body(client: RouterClient):
|
|
35
|
+
responses.add(
|
|
36
|
+
responses.POST,
|
|
37
|
+
f"{BASE_URL}/api/v1/router/routeTxs",
|
|
38
|
+
json={"routes": [{"txsToSign": ["AAA"]}]},
|
|
39
|
+
status=200,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
result = client.route_txs(
|
|
43
|
+
sender="SENDERADDRESS",
|
|
44
|
+
from_asset=0,
|
|
45
|
+
to_asset=31566704,
|
|
46
|
+
swap_amount=5_000_000,
|
|
47
|
+
receive_minimum=4_900_000,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
assert result["routes"][0]["txsToSign"] == ["AAA"]
|
|
51
|
+
sent_body = responses.calls[0].request.body
|
|
52
|
+
assert b'"receiveMinimum": 4900000' in sent_body or b'"receiveMinimum":4900000' in sent_body
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@responses.activate
|
|
56
|
+
def test_non_ok_response_raises_router_api_error(client: RouterClient):
|
|
57
|
+
responses.add(
|
|
58
|
+
responses.GET,
|
|
59
|
+
f"{BASE_URL}/api/v1/router/stats",
|
|
60
|
+
json={"error": "unauthorized"},
|
|
61
|
+
status=401,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
with pytest.raises(RouterApiError) as excinfo:
|
|
65
|
+
client.stats()
|
|
66
|
+
|
|
67
|
+
assert excinfo.value.status_code == 401
|
|
68
|
+
assert excinfo.value.body == {"error": "unauthorized"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_base_url_trailing_slash_is_stripped():
|
|
72
|
+
client = RouterClient(base_url="https://router.api.biatec.io/")
|
|
73
|
+
assert client.base_url == "https://router.api.biatec.io"
|