-
Notifications
You must be signed in to change notification settings - Fork 11
/
dispenser_api.py
178 lines (137 loc) · 5.45 KB
/
dispenser_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import contextlib
import enum
import logging
import os
from dataclasses import dataclass
import httpx
logger = logging.getLogger(__name__)
class DispenserApiConfig:
BASE_URL = "https://api.dispenser.algorandfoundation.tools"
class DispenserAssetName(enum.IntEnum):
ALGO = 0
@dataclass
class DispenserAsset:
asset_id: int
decimals: int
description: str
@dataclass
class DispenserFundResponse:
tx_id: str
amount: int
@dataclass
class DispenserLimitResponse:
amount: int
DISPENSER_ASSETS = {
DispenserAssetName.ALGO: DispenserAsset(
asset_id=0,
decimals=6,
description="Algo",
),
}
DISPENSER_REQUEST_TIMEOUT = 15
DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN"
class TestNetDispenserApiClient:
"""
Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md).
To get started create a new access token via `algokit dispenser login --ci`
and pass it to the client constructor as `auth_token`.
Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`,
and it will be auto loaded. If both are set, the constructor argument takes precedence.
Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor.
"""
auth_token: str
request_timeout = DISPENSER_REQUEST_TIMEOUT
def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT):
auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY)
if auth_token:
self.auth_token = auth_token
elif auth_token_from_env:
self.auth_token = auth_token_from_env
else:
raise Exception(
f"Can't init AlgoKit TestNet Dispenser API client "
f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or "
"the auth_token were provided."
)
self.request_timeout = request_timeout
def _process_dispenser_request(
self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST"
) -> httpx.Response:
"""
Generalized method to process http requests to dispenser API
"""
headers = {"Authorization": f"Bearer {(auth_token)}"}
# Set request arguments
request_args = {
"url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}",
"headers": headers,
"timeout": self.request_timeout,
}
if method.upper() != "GET" and data is not None:
request_args["json"] = data
try:
response: httpx.Response = getattr(httpx, method.lower())(**request_args)
response.raise_for_status()
return response
except httpx.HTTPStatusError as err:
error_message = f"Error processing dispenser API request: {err.response.status_code}"
error_response = None
with contextlib.suppress(Exception):
error_response = err.response.json()
if error_response and error_response.get("code"):
error_message = error_response.get("code")
elif err.response.status_code == httpx.codes.BAD_REQUEST:
error_message = err.response.json()["message"]
raise Exception(error_message) from err
except Exception as err:
error_message = "Error processing dispenser API request"
logger.debug(f"{error_message}: {err}", exc_info=True)
raise err
def fund(self, address: str, amount: int, asset_id: int) -> DispenserFundResponse:
"""
Fund an account with Algos from the dispenser API
"""
try:
response = self._process_dispenser_request(
auth_token=self.auth_token,
url_suffix=f"fund/{asset_id}",
data={"receiver": address, "amount": amount, "assetID": asset_id},
method="POST",
)
content = response.json()
return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"])
except Exception as err:
logger.exception(f"Error funding account {address}: {err}")
raise err
def refund(self, refund_txn_id: str) -> None:
"""
Register a refund for a transaction with the dispenser API
"""
try:
self._process_dispenser_request(
auth_token=self.auth_token,
url_suffix="refund",
data={"refundTransactionID": refund_txn_id},
method="POST",
)
except Exception as err:
logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}")
raise err
def get_limit(
self,
address: str,
) -> DispenserLimitResponse:
"""
Get current limit for an account with Algos from the dispenser API
"""
try:
response = self._process_dispenser_request(
auth_token=self.auth_token,
url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit",
method="GET",
)
content = response.json()
return DispenserLimitResponse(amount=content["amount"])
except Exception as err:
logger.exception(f"Error setting limit for account {address}: {err}")
raise err