bitcoinvn 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.
- bitcoinvn/__init__.py +57 -0
- bitcoinvn/client.py +483 -0
- bitcoinvn/errors.py +52 -0
- bitcoinvn/types.py +238 -0
- bitcoinvn-0.1.0.dist-info/METADATA +173 -0
- bitcoinvn-0.1.0.dist-info/RECORD +8 -0
- bitcoinvn-0.1.0.dist-info/WHEEL +4 -0
- bitcoinvn-0.1.0.dist-info/licenses/LICENSE +15 -0
bitcoinvn/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Python client for the BitcoinVN exchange API.
|
|
2
|
+
|
|
3
|
+
The production base URL is https://bitcoinvn.io. Official OpenAPI:
|
|
4
|
+
https://bitcoinvn.io/api/doc.json
|
|
5
|
+
|
|
6
|
+
Public reads do not need a key. Account calls send ``X-API-KEY`` when
|
|
7
|
+
``api_key`` is set. Order creation always includes the JSON field ``referrer``.
|
|
8
|
+
|
|
9
|
+
Examples:
|
|
10
|
+
from bitcoinvn import BitcoinVn
|
|
11
|
+
|
|
12
|
+
client = BitcoinVn()
|
|
13
|
+
ticker = client.get_ticker("BTC", "USDT")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from bitcoinvn.client import REFERRER, BitcoinVn
|
|
17
|
+
from bitcoinvn.errors import BitcoinVnError
|
|
18
|
+
from bitcoinvn.types import (
|
|
19
|
+
AddressValidation,
|
|
20
|
+
Asset,
|
|
21
|
+
Balances,
|
|
22
|
+
BooleanConfiguration,
|
|
23
|
+
EnsResolution,
|
|
24
|
+
HealthStatus,
|
|
25
|
+
Info,
|
|
26
|
+
LedgerItem,
|
|
27
|
+
Order,
|
|
28
|
+
OrderStatus,
|
|
29
|
+
Pair,
|
|
30
|
+
Quote,
|
|
31
|
+
Ticker,
|
|
32
|
+
TransferMethod,
|
|
33
|
+
Vault,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"REFERRER",
|
|
38
|
+
"AddressValidation",
|
|
39
|
+
"Asset",
|
|
40
|
+
"Balances",
|
|
41
|
+
"BitcoinVn",
|
|
42
|
+
"BitcoinVnError",
|
|
43
|
+
"BooleanConfiguration",
|
|
44
|
+
"EnsResolution",
|
|
45
|
+
"HealthStatus",
|
|
46
|
+
"Info",
|
|
47
|
+
"LedgerItem",
|
|
48
|
+
"Order",
|
|
49
|
+
"OrderStatus",
|
|
50
|
+
"Pair",
|
|
51
|
+
"Quote",
|
|
52
|
+
"Ticker",
|
|
53
|
+
"TransferMethod",
|
|
54
|
+
"Vault",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
__version__ = "0.1.0"
|
bitcoinvn/client.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Synchronous client for the BitcoinVN exchange API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from bitcoinvn.errors import BitcoinVnError
|
|
12
|
+
from bitcoinvn.types import (
|
|
13
|
+
AddressValidation,
|
|
14
|
+
Balances,
|
|
15
|
+
BooleanConfiguration,
|
|
16
|
+
EnsResolution,
|
|
17
|
+
HealthStatus,
|
|
18
|
+
Info,
|
|
19
|
+
LedgerItem,
|
|
20
|
+
Order,
|
|
21
|
+
Pair,
|
|
22
|
+
Quote,
|
|
23
|
+
Ticker,
|
|
24
|
+
Vault,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
DEFAULT_BASE_URL = "https://bitcoinvn.io"
|
|
28
|
+
USER_AGENT = "bitcoinvn/0.1.0"
|
|
29
|
+
REFERRER = "35b2eec68a1741ac"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BitcoinVn:
|
|
33
|
+
"""Client for the BitcoinVN exchange API.
|
|
34
|
+
|
|
35
|
+
Public reads, quotes, and order creation do not need a key. Pass ``api_key``
|
|
36
|
+
to send ``X-API-KEY``. The key is created on the API keys page at
|
|
37
|
+
bitcoinvn.io. This client does not manage keys.
|
|
38
|
+
|
|
39
|
+
``create_order`` always sends the JSON property ``referrer``. Callers cannot
|
|
40
|
+
pass or disable it. Other methods do not send that field.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
client = BitcoinVn()
|
|
44
|
+
ticker = client.get_ticker("BTC", "USDT")
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
api_key: str | None = None,
|
|
50
|
+
*,
|
|
51
|
+
base_url: str | None = None,
|
|
52
|
+
timeout_ms: float | None = None,
|
|
53
|
+
client: httpx.Client | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create a client.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
api_key: Key sent as ``X-API-KEY``. A blank value sends no header.
|
|
59
|
+
base_url: Defaults to ``https://bitcoinvn.io``.
|
|
60
|
+
timeout_ms: Request timeout in milliseconds. Defaults to 30000.
|
|
61
|
+
client: Optional ``httpx.Client``. When set, this object does not
|
|
62
|
+
close it and ignores ``base_url`` and ``timeout_ms``.
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
client = BitcoinVn(api_key)
|
|
66
|
+
"""
|
|
67
|
+
key = api_key.strip() if api_key else ""
|
|
68
|
+
self._api_key = key or None
|
|
69
|
+
self._owns_client = client is None
|
|
70
|
+
if client is not None:
|
|
71
|
+
self._http = client
|
|
72
|
+
return
|
|
73
|
+
root = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
74
|
+
timeout = 30.0 if timeout_ms is None else timeout_ms / 1000
|
|
75
|
+
self._http = httpx.Client(
|
|
76
|
+
base_url=root,
|
|
77
|
+
timeout=timeout,
|
|
78
|
+
headers={
|
|
79
|
+
"Accept": "application/json, text/plain",
|
|
80
|
+
"User-Agent": USER_AGENT,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""Close the HTTP client when this object created it.
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
client.close()
|
|
89
|
+
"""
|
|
90
|
+
if self._owns_client:
|
|
91
|
+
self._http.close()
|
|
92
|
+
|
|
93
|
+
def __enter__(self) -> BitcoinVn:
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, *exc: object) -> None:
|
|
97
|
+
self.close()
|
|
98
|
+
|
|
99
|
+
def get_health(self) -> HealthStatus:
|
|
100
|
+
"""``GET /api/health``.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
health = client.get_health()
|
|
104
|
+
"""
|
|
105
|
+
return self._request("GET", "/api/health")
|
|
106
|
+
|
|
107
|
+
def get_info(self) -> Info:
|
|
108
|
+
"""``GET /api/info``.
|
|
109
|
+
|
|
110
|
+
Use method ids exactly as returned. Check ``depositEnabled`` or
|
|
111
|
+
``settleEnabled`` for the direction you need.
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
info = client.get_info()
|
|
115
|
+
"""
|
|
116
|
+
return self._request("GET", "/api/info")
|
|
117
|
+
|
|
118
|
+
def get_pair(self, deposit_method_id: str, settle_method_id: str) -> Pair:
|
|
119
|
+
"""``GET /api/pairs/{depositMethodId}/{settleMethodId}``.
|
|
120
|
+
|
|
121
|
+
A successful response does not reserve liquidity. An API key applies
|
|
122
|
+
the account minimum to ``min``.
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
pair = client.get_pair("btc", "xmr")
|
|
126
|
+
"""
|
|
127
|
+
path = f"/api/pairs/{_segment(deposit_method_id)}/{_segment(settle_method_id)}"
|
|
128
|
+
return self._request("GET", path)
|
|
129
|
+
|
|
130
|
+
def get_ticker(self, base_asset_id: str, quote_asset_id: str) -> Ticker:
|
|
131
|
+
"""``GET /api/ticker/{baseAssetId}/{quoteAssetId}``.
|
|
132
|
+
|
|
133
|
+
Asset ids are case-sensitive. ``rate`` is quote units per one base unit.
|
|
134
|
+
|
|
135
|
+
Examples:
|
|
136
|
+
ticker = client.get_ticker("BTC", "USDT")
|
|
137
|
+
"""
|
|
138
|
+
path = f"/api/ticker/{_segment(base_asset_id)}/{_segment(quote_asset_id)}"
|
|
139
|
+
return self._request("GET", path)
|
|
140
|
+
|
|
141
|
+
def validate_address(
|
|
142
|
+
self,
|
|
143
|
+
transfer_method: str,
|
|
144
|
+
address: str,
|
|
145
|
+
*,
|
|
146
|
+
invoice_amount: float | None = None,
|
|
147
|
+
) -> AddressValidation:
|
|
148
|
+
"""``GET /api/addresses/validate``.
|
|
149
|
+
|
|
150
|
+
For Lightning, pass the BOLT11 invoice as ``address``.
|
|
151
|
+
|
|
152
|
+
Examples:
|
|
153
|
+
result = client.validate_address("btc", "bc1qexample")
|
|
154
|
+
"""
|
|
155
|
+
return self._request(
|
|
156
|
+
"GET",
|
|
157
|
+
"/api/addresses/validate",
|
|
158
|
+
query={
|
|
159
|
+
"transferMethod": transfer_method,
|
|
160
|
+
"address": address,
|
|
161
|
+
"invoiceAmount": invoice_amount,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def create_quote(
|
|
166
|
+
self,
|
|
167
|
+
deposit_method: str,
|
|
168
|
+
settle_method: str,
|
|
169
|
+
*,
|
|
170
|
+
deposit_amount: float | None = None,
|
|
171
|
+
settle_amount: float | None = None,
|
|
172
|
+
markup_rate: float | None = None,
|
|
173
|
+
) -> Quote:
|
|
174
|
+
"""``POST /api/quotes``.
|
|
175
|
+
|
|
176
|
+
Send ``deposit_amount``, ``settle_amount``, or neither, but not both.
|
|
177
|
+
This creates a production quote and does not move funds.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
quote = client.create_quote("btc", "xmr", deposit_amount=0.01)
|
|
181
|
+
"""
|
|
182
|
+
return self._request(
|
|
183
|
+
"POST",
|
|
184
|
+
"/api/quotes",
|
|
185
|
+
body=_omit_none(
|
|
186
|
+
{
|
|
187
|
+
"depositMethod": deposit_method,
|
|
188
|
+
"settleMethod": settle_method,
|
|
189
|
+
"depositAmount": deposit_amount,
|
|
190
|
+
"settleAmount": settle_amount,
|
|
191
|
+
"markupRate": markup_rate,
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def list_orders(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
page: int | None = None,
|
|
200
|
+
custom_order_id: str | None = None,
|
|
201
|
+
) -> list[Order]:
|
|
202
|
+
"""``GET /api/orders``. Requires ``X-API-KEY``. Pages contain 10 orders.
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
orders = client.list_orders(page=1)
|
|
206
|
+
"""
|
|
207
|
+
return self._request(
|
|
208
|
+
"GET",
|
|
209
|
+
"/api/orders",
|
|
210
|
+
query={"page": page, "customOrderId": custom_order_id},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def create_order(
|
|
214
|
+
self,
|
|
215
|
+
*,
|
|
216
|
+
quote: str | None = None,
|
|
217
|
+
deposit_method: str | None = None,
|
|
218
|
+
settle_method: str | None = None,
|
|
219
|
+
deposit_amount: float | None = None,
|
|
220
|
+
settle_amount: float | None = None,
|
|
221
|
+
custom_order_id: str | None = None,
|
|
222
|
+
webhook_url: str | None = None,
|
|
223
|
+
settle_data: Mapping[str, Any] | None = None,
|
|
224
|
+
) -> Order:
|
|
225
|
+
"""``POST /api/orders``. Creates a production order.
|
|
226
|
+
|
|
227
|
+
Pass ``quote``, or ``deposit_method`` and ``settle_method`` for a
|
|
228
|
+
variable-amount order. An amountless internal-balance quote can also
|
|
229
|
+
take ``deposit_amount`` or ``settle_amount``. The JSON body always
|
|
230
|
+
includes ``referrer``. Funding the order moves real funds.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
order = client.create_order(
|
|
234
|
+
quote="8120a8a8-8dd1-4168-80fc-c4d932ce5b93",
|
|
235
|
+
settle_data={"address": "4example"},
|
|
236
|
+
)
|
|
237
|
+
"""
|
|
238
|
+
body = _omit_none(
|
|
239
|
+
{
|
|
240
|
+
"quote": quote,
|
|
241
|
+
"depositMethod": deposit_method,
|
|
242
|
+
"settleMethod": settle_method,
|
|
243
|
+
"depositAmount": deposit_amount,
|
|
244
|
+
"settleAmount": settle_amount,
|
|
245
|
+
"customOrderId": custom_order_id,
|
|
246
|
+
"webhookUrl": webhook_url,
|
|
247
|
+
"settleData": dict(settle_data) if settle_data is not None else None,
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
body["referrer"] = REFERRER
|
|
251
|
+
return self._request("POST", "/api/orders", body=body)
|
|
252
|
+
|
|
253
|
+
def get_order(self, order_id: str) -> Order:
|
|
254
|
+
"""``GET /api/orders/{id}``. ``order_id`` is the 8-character short id.
|
|
255
|
+
|
|
256
|
+
Examples:
|
|
257
|
+
order = client.get_order("BV2MTZAF")
|
|
258
|
+
"""
|
|
259
|
+
return self._request("GET", f"/api/orders/{_segment(order_id)}")
|
|
260
|
+
|
|
261
|
+
def submit_deposit(self, order_id: str, txid: str) -> Order:
|
|
262
|
+
"""``POST /api/orders/{id}/deposit``.
|
|
263
|
+
|
|
264
|
+
Optional fast path after a supported on-chain deposit is broadcast and
|
|
265
|
+
the order is ``new``. Fiat, Lightning, internal-balance, and manual
|
|
266
|
+
deposits do not use this call.
|
|
267
|
+
|
|
268
|
+
Examples:
|
|
269
|
+
order = client.submit_deposit("BV2MTZAF", "0x0123456789abcdef")
|
|
270
|
+
"""
|
|
271
|
+
return self._request(
|
|
272
|
+
"POST",
|
|
273
|
+
f"/api/orders/{_segment(order_id)}/deposit",
|
|
274
|
+
body={"txid": txid},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def update_settle_data(self, order_id: str, settle_data: Mapping[str, Any]) -> Order:
|
|
278
|
+
"""``PUT /api/orders/{id}/settle-data``.
|
|
279
|
+
|
|
280
|
+
Requires a key with write access. The order must be ``settle_data_error``.
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
order = client.update_settle_data("BV2MTZAF", {"address": "bc1qexample"})
|
|
284
|
+
"""
|
|
285
|
+
return self._request(
|
|
286
|
+
"PUT",
|
|
287
|
+
f"/api/orders/{_segment(order_id)}/settle-data",
|
|
288
|
+
body=dict(settle_data),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def get_balances(self) -> Balances:
|
|
292
|
+
"""``GET /api/balances``. Requires ``X-API-KEY``.
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
balances = client.get_balances()
|
|
296
|
+
"""
|
|
297
|
+
return self._request("GET", "/api/balances")
|
|
298
|
+
|
|
299
|
+
def list_balance_history(
|
|
300
|
+
self,
|
|
301
|
+
*,
|
|
302
|
+
asset_id: str | None = None,
|
|
303
|
+
page: int | None = None,
|
|
304
|
+
) -> list[LedgerItem]:
|
|
305
|
+
"""``GET /api/balances/history``. Requires ``X-API-KEY``.
|
|
306
|
+
|
|
307
|
+
Pages contain 25 rows.
|
|
308
|
+
|
|
309
|
+
Examples:
|
|
310
|
+
history = client.list_balance_history(asset_id="BTC", page=1)
|
|
311
|
+
"""
|
|
312
|
+
return self._request(
|
|
313
|
+
"GET",
|
|
314
|
+
"/api/balances/history",
|
|
315
|
+
query={"assetId": asset_id, "page": page},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def list_staking_offers(self) -> list[Vault]:
|
|
319
|
+
"""``GET /api/staking``.
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
offers = client.list_staking_offers()
|
|
323
|
+
"""
|
|
324
|
+
return self._request("GET", "/api/staking")
|
|
325
|
+
|
|
326
|
+
def set_auto_stake(self, offer_id: str, value: bool) -> BooleanConfiguration:
|
|
327
|
+
"""``PUT /api/staking/{id}/auto-stake``. Requires write access.
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
setting = client.set_auto_stake(
|
|
331
|
+
"8120a8a8-8dd1-4168-80fc-c4d932ce5b93",
|
|
332
|
+
True,
|
|
333
|
+
)
|
|
334
|
+
"""
|
|
335
|
+
return self._request(
|
|
336
|
+
"PUT",
|
|
337
|
+
f"/api/staking/{_segment(offer_id)}/auto-stake",
|
|
338
|
+
body={"value": value},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def set_staking_notifications(self, offer_id: str, value: bool) -> BooleanConfiguration:
|
|
342
|
+
"""``PUT /api/staking/{id}/notifications``. Requires write access.
|
|
343
|
+
|
|
344
|
+
Examples:
|
|
345
|
+
setting = client.set_staking_notifications(
|
|
346
|
+
"8120a8a8-8dd1-4168-80fc-c4d932ce5b93",
|
|
347
|
+
False,
|
|
348
|
+
)
|
|
349
|
+
"""
|
|
350
|
+
return self._request(
|
|
351
|
+
"PUT",
|
|
352
|
+
f"/api/staking/{_segment(offer_id)}/notifications",
|
|
353
|
+
body={"value": value},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def resolve_address(self, transfer_method: str, name: str) -> EnsResolution:
|
|
357
|
+
"""``GET /api/addresses/resolve``.
|
|
358
|
+
|
|
359
|
+
Resolves an ASCII ENS name. The token expires after 15 minutes.
|
|
360
|
+
|
|
361
|
+
Examples:
|
|
362
|
+
resolved = client.resolve_address("eth", "alice.eth")
|
|
363
|
+
"""
|
|
364
|
+
return self._request(
|
|
365
|
+
"GET",
|
|
366
|
+
"/api/addresses/resolve",
|
|
367
|
+
query={"transferMethod": transfer_method, "name": name},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def get_guarantee_public_key(self) -> str:
|
|
371
|
+
"""``GET /api/guarantee/public-key``.
|
|
372
|
+
|
|
373
|
+
Returns armored OpenPGP certificates.
|
|
374
|
+
|
|
375
|
+
Examples:
|
|
376
|
+
certificates = client.get_guarantee_public_key()
|
|
377
|
+
"""
|
|
378
|
+
return self._request("GET", "/api/guarantee/public-key", text=True)
|
|
379
|
+
|
|
380
|
+
def get_order_guarantee(self, order_id: str) -> str:
|
|
381
|
+
"""``GET /api/orders/{id}/guarantee``.
|
|
382
|
+
|
|
383
|
+
Returns the stored clear-signed letter. Access matches ``get_order``.
|
|
384
|
+
|
|
385
|
+
Examples:
|
|
386
|
+
letter = client.get_order_guarantee("BV2MTZAF")
|
|
387
|
+
"""
|
|
388
|
+
return self._request("GET", f"/api/orders/{_segment(order_id)}/guarantee", text=True)
|
|
389
|
+
|
|
390
|
+
def _request(
|
|
391
|
+
self,
|
|
392
|
+
method: str,
|
|
393
|
+
path: str,
|
|
394
|
+
*,
|
|
395
|
+
query: Mapping[str, Any] | None = None,
|
|
396
|
+
body: Mapping[str, Any] | None = None,
|
|
397
|
+
text: bool = False,
|
|
398
|
+
) -> Any:
|
|
399
|
+
headers: dict[str, str] = {}
|
|
400
|
+
if self._api_key:
|
|
401
|
+
headers["X-API-KEY"] = self._api_key
|
|
402
|
+
params = _omit_none(dict(query)) if query is not None else None
|
|
403
|
+
try:
|
|
404
|
+
response = self._http.request(
|
|
405
|
+
method,
|
|
406
|
+
path,
|
|
407
|
+
params=params,
|
|
408
|
+
json=dict(body) if body is not None else None,
|
|
409
|
+
headers=headers or None,
|
|
410
|
+
)
|
|
411
|
+
except httpx.HTTPError as exc:
|
|
412
|
+
raise BitcoinVnError(str(exc), status=0, code=0, raw=str(exc)) from exc
|
|
413
|
+
return _decode(response, text=text)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _segment(value: str) -> str:
|
|
417
|
+
return quote(value, safe="")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _omit_none(values: Mapping[str, Any]) -> dict[str, Any]:
|
|
421
|
+
return {key: value for key, value in values.items() if value is not None}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _retry_after(response: httpx.Response) -> int | None:
|
|
425
|
+
raw = response.headers.get("x-ratelimit-retry-after")
|
|
426
|
+
if raw is None or not str(raw).strip():
|
|
427
|
+
return None
|
|
428
|
+
try:
|
|
429
|
+
return int(raw)
|
|
430
|
+
except ValueError:
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _decode(response: httpx.Response, *, text: bool) -> Any:
|
|
435
|
+
retry_after = _retry_after(response)
|
|
436
|
+
if response.status_code >= 400:
|
|
437
|
+
raise _error_response(response, retry_after)
|
|
438
|
+
if text:
|
|
439
|
+
return response.text
|
|
440
|
+
try:
|
|
441
|
+
return response.json()
|
|
442
|
+
except ValueError as exc:
|
|
443
|
+
raise BitcoinVnError(
|
|
444
|
+
"Response was not JSON",
|
|
445
|
+
status=response.status_code,
|
|
446
|
+
code=response.status_code,
|
|
447
|
+
raw=response.text,
|
|
448
|
+
) from exc
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _error_response(response: httpx.Response, retry_after: int | None) -> BitcoinVnError:
|
|
452
|
+
raw: Any
|
|
453
|
+
try:
|
|
454
|
+
raw = response.json()
|
|
455
|
+
except ValueError:
|
|
456
|
+
raw = response.text
|
|
457
|
+
error = raw.get("error") if isinstance(raw, dict) else None
|
|
458
|
+
if isinstance(error, dict):
|
|
459
|
+
message = error.get("message")
|
|
460
|
+
code = error.get("code")
|
|
461
|
+
text = message.strip() if isinstance(message, str) and message.strip() else ""
|
|
462
|
+
if not text:
|
|
463
|
+
text = f"HTTP {response.status_code}"
|
|
464
|
+
status_code = code if isinstance(code, int) else response.status_code
|
|
465
|
+
return BitcoinVnError(
|
|
466
|
+
text,
|
|
467
|
+
status=response.status_code,
|
|
468
|
+
code=status_code,
|
|
469
|
+
raw=raw,
|
|
470
|
+
details=error.get("details"),
|
|
471
|
+
retry_after=retry_after,
|
|
472
|
+
)
|
|
473
|
+
if isinstance(raw, str) and raw.strip():
|
|
474
|
+
message = raw.strip()
|
|
475
|
+
else:
|
|
476
|
+
message = f"HTTP {response.status_code}"
|
|
477
|
+
return BitcoinVnError(
|
|
478
|
+
message,
|
|
479
|
+
status=response.status_code,
|
|
480
|
+
code=response.status_code,
|
|
481
|
+
raw=raw,
|
|
482
|
+
retry_after=retry_after,
|
|
483
|
+
)
|
bitcoinvn/errors.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Errors returned by the BitcoinVN API and transport failures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BitcoinVnError(Exception):
|
|
9
|
+
"""HTTP error envelope from BitcoinVN, or a transport failure.
|
|
10
|
+
|
|
11
|
+
Responses with status 4xx or 5xx use ``error.message`` and ``error.code``.
|
|
12
|
+
``code`` equals the HTTP status. ``retry_after`` is set from the
|
|
13
|
+
``X-RateLimit-Retry-After`` header when it is present. A transport failure
|
|
14
|
+
uses ``status`` 0 and ``code`` 0.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
try:
|
|
18
|
+
client.get_ticker("BTC", "USDT")
|
|
19
|
+
except BitcoinVnError as error:
|
|
20
|
+
if error.code == 404:
|
|
21
|
+
pass
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
*,
|
|
28
|
+
status: int,
|
|
29
|
+
code: int,
|
|
30
|
+
raw: Any = None,
|
|
31
|
+
details: Any = None,
|
|
32
|
+
retry_after: int | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Create an error.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
message: Text from ``error.message``, or a transport message.
|
|
38
|
+
status: HTTP status, or 0 when the request was not sent.
|
|
39
|
+
code: API ``error.code``, or ``status`` when the body has no envelope.
|
|
40
|
+
raw: Original response body.
|
|
41
|
+
details: ``error.details`` when the API sent validation details.
|
|
42
|
+
retry_after: Seconds from ``X-RateLimit-Retry-After``.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
raise BitcoinVnError("Pair does not exist.", status=404, code=404)
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
self.status = status
|
|
49
|
+
self.code = code
|
|
50
|
+
self.raw = message if raw is None else raw
|
|
51
|
+
self.details = details
|
|
52
|
+
self.retry_after = retry_after
|
bitcoinvn/types.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Types for the BitcoinVN exchange API.
|
|
2
|
+
|
|
3
|
+
Response dictionaries keep the JSON names from the OpenAPI spec.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Literal, TypedDict
|
|
9
|
+
|
|
10
|
+
OrderStatus = Literal[
|
|
11
|
+
"new",
|
|
12
|
+
"pending",
|
|
13
|
+
"settle_data_error",
|
|
14
|
+
"on_hold",
|
|
15
|
+
"processing",
|
|
16
|
+
"completed",
|
|
17
|
+
"canceled",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HealthStatus(TypedDict):
|
|
22
|
+
"""Body of ``GET /api/health``."""
|
|
23
|
+
|
|
24
|
+
status: Literal["ok"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AssetRequired(TypedDict):
|
|
28
|
+
"""Required asset fields from ``GET /api/info``."""
|
|
29
|
+
|
|
30
|
+
id: str
|
|
31
|
+
name: str
|
|
32
|
+
exchangeFee: float
|
|
33
|
+
bridgeFee: float
|
|
34
|
+
tags: list[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Asset(AssetRequired, total=False):
|
|
38
|
+
"""Asset record. ``sameAs`` is set when the asset aliases another asset."""
|
|
39
|
+
|
|
40
|
+
sameAs: str | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SettleDataField(TypedDict):
|
|
44
|
+
"""One external payout field on a transfer method."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
required: bool
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TransferMethodRequired(TypedDict):
|
|
51
|
+
"""Required transfer-method fields."""
|
|
52
|
+
|
|
53
|
+
enabled: bool
|
|
54
|
+
id: str
|
|
55
|
+
asset: str
|
|
56
|
+
decimals: int
|
|
57
|
+
depositEnabled: bool
|
|
58
|
+
settleEnabled: bool
|
|
59
|
+
internalBalance: bool
|
|
60
|
+
tags: list[str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TransferMethod(TransferMethodRequired, total=False):
|
|
64
|
+
"""Transfer method. Use ``id`` exactly as returned by ``GET /api/info``."""
|
|
65
|
+
|
|
66
|
+
settleDataFields: list[SettleDataField]
|
|
67
|
+
batched: bool
|
|
68
|
+
predictedSettleWaitTime: int
|
|
69
|
+
network: str | None
|
|
70
|
+
extraLabel: str | None
|
|
71
|
+
networkFee: float
|
|
72
|
+
depositFee: float
|
|
73
|
+
settleFee: float
|
|
74
|
+
exchangeFee: float
|
|
75
|
+
exchangeBalanceIndex: float
|
|
76
|
+
infoUrl: str | None
|
|
77
|
+
minOrderSize: float | None
|
|
78
|
+
maxOrderSize: float | None
|
|
79
|
+
invoice: bool
|
|
80
|
+
warning: str
|
|
81
|
+
tokenId: str | None
|
|
82
|
+
details: dict[str, Any] | None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Info(TypedDict):
|
|
86
|
+
"""Public catalogs from ``GET /api/info``."""
|
|
87
|
+
|
|
88
|
+
assets: dict[str, Asset]
|
|
89
|
+
transferMethods: dict[str, TransferMethod]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PairRequired(TypedDict):
|
|
93
|
+
"""Required pair fields."""
|
|
94
|
+
|
|
95
|
+
rate: float
|
|
96
|
+
min: float
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Pair(PairRequired, total=False):
|
|
100
|
+
"""Rate and deposit range. ``max`` below ``min`` means no valid range."""
|
|
101
|
+
|
|
102
|
+
max: float | None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Ticker(TypedDict):
|
|
106
|
+
"""Public rate and 24-hour volume."""
|
|
107
|
+
|
|
108
|
+
rate: float
|
|
109
|
+
volume24h: float
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AddressValidationRequired(TypedDict):
|
|
113
|
+
"""Required address-validation field."""
|
|
114
|
+
|
|
115
|
+
valid: bool
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class AddressValidation(AddressValidationRequired, total=False):
|
|
119
|
+
"""Result of ``GET /api/addresses/validate``."""
|
|
120
|
+
|
|
121
|
+
error: str | None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class QuoteRequired(TypedDict):
|
|
125
|
+
"""Required quote fields."""
|
|
126
|
+
|
|
127
|
+
depositMethod: str
|
|
128
|
+
settleMethod: str
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Quote(QuoteRequired, total=False):
|
|
132
|
+
"""Quote from ``POST /api/quotes`` or nested on an order."""
|
|
133
|
+
|
|
134
|
+
id: str
|
|
135
|
+
depositAmount: float | None
|
|
136
|
+
depositFee: float
|
|
137
|
+
settleAmount: float | None
|
|
138
|
+
settleFee: float
|
|
139
|
+
rate: float
|
|
140
|
+
rawRate: float
|
|
141
|
+
createdAt: str
|
|
142
|
+
expiresAt: str
|
|
143
|
+
accepted: bool
|
|
144
|
+
markupRate: float | None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class OrderRequired(TypedDict):
|
|
148
|
+
"""Required order fields."""
|
|
149
|
+
|
|
150
|
+
id: str
|
|
151
|
+
shortId: str
|
|
152
|
+
status: OrderStatus
|
|
153
|
+
depositMethod: str
|
|
154
|
+
depositAsset: str
|
|
155
|
+
depositFee: float
|
|
156
|
+
depositData: dict[str, Any]
|
|
157
|
+
settleMethod: str
|
|
158
|
+
settleAsset: str
|
|
159
|
+
settleFee: float
|
|
160
|
+
settleData: dict[str, Any]
|
|
161
|
+
depositMin: float
|
|
162
|
+
createdAt: str
|
|
163
|
+
expiresAt: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Order(OrderRequired, total=False):
|
|
167
|
+
"""Order object returned by the order endpoints and by order webhooks."""
|
|
168
|
+
|
|
169
|
+
customOrderId: str
|
|
170
|
+
depositAmount: float | None
|
|
171
|
+
depositConfsRequired: int | None
|
|
172
|
+
depositTxns: list[str]
|
|
173
|
+
settleAmount: float | None
|
|
174
|
+
settleTxns: list[str]
|
|
175
|
+
settleReceiptUrl: str
|
|
176
|
+
settleProof: dict[str, Any] | None
|
|
177
|
+
rate: float
|
|
178
|
+
depositMax: float
|
|
179
|
+
quote: Quote
|
|
180
|
+
adminMessage: str | None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class LedgerItemRequired(TypedDict):
|
|
184
|
+
"""Required ledger fields."""
|
|
185
|
+
|
|
186
|
+
id: str
|
|
187
|
+
asset: str
|
|
188
|
+
value: float
|
|
189
|
+
createdAt: str
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class LedgerItem(LedgerItemRequired, total=False):
|
|
193
|
+
"""One balance-history row."""
|
|
194
|
+
|
|
195
|
+
note: str
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class VaultRequired(TypedDict):
|
|
199
|
+
"""Required staking-offer fields."""
|
|
200
|
+
|
|
201
|
+
id: str
|
|
202
|
+
asset: str
|
|
203
|
+
distribution: float
|
|
204
|
+
supplyTarget: float
|
|
205
|
+
staked: float
|
|
206
|
+
latestApr: float
|
|
207
|
+
latestFullyDilutedApr: float
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Vault(VaultRequired, total=False):
|
|
211
|
+
"""Staking offer from ``GET /api/staking``."""
|
|
212
|
+
|
|
213
|
+
tvl: float
|
|
214
|
+
unstakingFee: float
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class BooleanConfiguration(TypedDict):
|
|
218
|
+
"""Staking preference body."""
|
|
219
|
+
|
|
220
|
+
value: bool
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class EnsResolution(TypedDict):
|
|
224
|
+
"""Signed ENS resolution. Times are Unix seconds."""
|
|
225
|
+
|
|
226
|
+
name: str
|
|
227
|
+
address: str
|
|
228
|
+
transferMethod: str
|
|
229
|
+
networkId: str
|
|
230
|
+
networkName: str
|
|
231
|
+
chainId: int
|
|
232
|
+
resolvedAt: int
|
|
233
|
+
expiresAt: int
|
|
234
|
+
ensResolutionToken: str
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# Available balances keyed by asset id.
|
|
238
|
+
Balances = dict[str, float]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bitcoinvn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the BitcoinVN exchange API
|
|
5
|
+
Project-URL: Repository, https://github.com/TysikGG/api-kit
|
|
6
|
+
Project-URL: Documentation, https://bitcoinvn.io/api/doc.json
|
|
7
|
+
License-Expression: ISC
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# bitcoinvn
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/bitcoinvn/)
|
|
16
|
+
[](https://pypi.org/project/bitcoinvn/)
|
|
17
|
+
[](https://opensource.org/licenses/ISC)
|
|
18
|
+
|
|
19
|
+
Modern, typed Python client for the [BitcoinVN Exchange API](https://bitcoinvn.io/api/doc.json) — Vietnam's premier cryptocurrency exchange and instant swap platform. Built on [HTTPX](https://www.python-httpx.org).
|
|
20
|
+
|
|
21
|
+
Official API documentation:
|
|
22
|
+
- OpenAPI specification: <https://bitcoinvn.io/api/doc.json>
|
|
23
|
+
- Exchange website: <https://bitcoinvn.io>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- 📊 **Public Market Data** — Tickers, real-time exchange rates, pair metadata, and system status (no API key required).
|
|
30
|
+
- 🔄 **Instant Swaps & Quotes** — Calculate competitive quotes for crypto-to-crypto and crypto-to-fiat trades.
|
|
31
|
+
- 📦 **Order Management** — Create orders with support for crypto wallets, Lightning Network invoices, destination tags, bank wires, and cash pickups.
|
|
32
|
+
- 💰 **Balances & History** — Inspect internal wallet balances, transaction ledgers, and attach deposit transaction IDs.
|
|
33
|
+
- 📈 **Staking Vaults** — Discover staking opportunities, toggle auto-staking, and adjust notification alerts.
|
|
34
|
+
- 🛡️ **Cryptographic Proofs** — Retrieve and verify BitcoinVN cryptographic guarantee letters and public keys.
|
|
35
|
+
- ⚡ **Resilient** — Context manager support (`with BitcoinVn(...) as client:`), type annotations, and rate-limit handling (`retry_after` header).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install bitcoinvn
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10 or higher.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Start (Safe Check)
|
|
50
|
+
|
|
51
|
+
The quick check queries the public market ticker for `BTC/USDT`. It does not require an API key and does not place orders or move funds.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import os
|
|
55
|
+
from bitcoinvn import BitcoinVn
|
|
56
|
+
|
|
57
|
+
# Anonymous client for public market queries (or pass api_key)
|
|
58
|
+
api_key = os.environ.get("BITCOINVN_API_KEY")
|
|
59
|
+
|
|
60
|
+
with BitcoinVn(api_key) as client:
|
|
61
|
+
ticker = client.get_ticker("BTC", "USDT")
|
|
62
|
+
print(f"Rate: {ticker['rate']}, 24h Volume: {ticker['volume24h']}")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Usage Guide
|
|
68
|
+
|
|
69
|
+
### 1. Authentication
|
|
70
|
+
|
|
71
|
+
Public read operations (health, info, pairs, ticker, address validation) work without a key.
|
|
72
|
+
|
|
73
|
+
For account-specific operations (viewing balances, order history, internal balance orders, staking), pass your API key:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from bitcoinvn import BitcoinVn
|
|
77
|
+
|
|
78
|
+
with BitcoinVn(api_key="your_api_key_here") as client:
|
|
79
|
+
balances = client.get_balances()
|
|
80
|
+
print(balances)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
API keys can be generated in your account dashboard on [bitcoinvn.io](https://bitcoinvn.io).
|
|
84
|
+
|
|
85
|
+
### 2. Market Pairs and Quotes
|
|
86
|
+
|
|
87
|
+
Inspect supported trading pairs and request live price quotes:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from bitcoinvn import BitcoinVn
|
|
91
|
+
|
|
92
|
+
with BitcoinVn() as client:
|
|
93
|
+
# Fetch trading pair specifications and limits
|
|
94
|
+
pair = client.get_pair("btc", "usdt")
|
|
95
|
+
print(f"Min: {pair['minDeposit']}, Max: {pair['maxDeposit']}")
|
|
96
|
+
|
|
97
|
+
# Request an instant quote
|
|
98
|
+
quote = client.create_quote(
|
|
99
|
+
deposit_method="btc",
|
|
100
|
+
settle_method="usdt",
|
|
101
|
+
deposit_amount=0.05,
|
|
102
|
+
)
|
|
103
|
+
print(f"Expected settle amount: {quote['settleAmount']}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Creating and Managing Orders
|
|
107
|
+
|
|
108
|
+
Create an exchange order with specific settlement details:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from bitcoinvn import BitcoinVn
|
|
112
|
+
|
|
113
|
+
with BitcoinVn(api_key=os.environ.get("BITCOINVN_API_KEY")) as client:
|
|
114
|
+
# Create an order swapping BTC for USDT
|
|
115
|
+
order = client.create_order(
|
|
116
|
+
deposit_method="btc",
|
|
117
|
+
settle_method="usdt",
|
|
118
|
+
settle_amount=1000,
|
|
119
|
+
settle_data={"address": "0xYourUsdtAddressHere..."},
|
|
120
|
+
)
|
|
121
|
+
print(f"Order created: {order['shortId']}")
|
|
122
|
+
print(f"Send BTC deposit to: {order['depositAddress']}")
|
|
123
|
+
|
|
124
|
+
# Check order status
|
|
125
|
+
status = client.get_order(order["shortId"])
|
|
126
|
+
print(f"Order status: {status['status']}")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Account Balances and History
|
|
130
|
+
|
|
131
|
+
Inspect your account's internal wallet balances and ledger transactions:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
with BitcoinVn(api_key=os.environ["BITCOINVN_API_KEY"]) as client:
|
|
135
|
+
# List balances
|
|
136
|
+
balances = client.get_balances()
|
|
137
|
+
print(balances)
|
|
138
|
+
|
|
139
|
+
# List ledger history
|
|
140
|
+
history = client.list_balance_history(limit=10)
|
|
141
|
+
for entry in history:
|
|
142
|
+
print(f"{entry['createdAt']}: {entry['amount']} {entry['asset']}")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Error Handling
|
|
148
|
+
|
|
149
|
+
When the API returns a `4xx` or `5xx` error, the client raises a `BitcoinVnError` containing HTTP status code, API error message, and rate-limit backoff details:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from bitcoinvn import BitcoinVn, BitcoinVnError
|
|
153
|
+
|
|
154
|
+
with BitcoinVn() as client:
|
|
155
|
+
try:
|
|
156
|
+
client.get_ticker("INVALID", "PAIR")
|
|
157
|
+
except BitcoinVnError as err:
|
|
158
|
+
print(f"BitcoinVN error [{err.status_code}]: {err.message}")
|
|
159
|
+
if err.status_code == 429 and err.retry_after:
|
|
160
|
+
print(f"Rate limited. Retry after {err.retry_after} seconds.")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Partner Referrer ID
|
|
166
|
+
|
|
167
|
+
In accordance with BitcoinVN's partner program, outgoing `POST /api/orders` requests automatically include the required referrer identifier in the payload.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
[ISC](LICENSE) © api-kit contributors
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
bitcoinvn/__init__.py,sha256=HfBLwfyZSHo-EjMJt-J5As10Vzy0DeXGz3f6ATWs8Hc,1132
|
|
2
|
+
bitcoinvn/client.py,sha256=1i2kPLNsOrAeo-3eBgHGnfBkVxGX784oOGG3k084Fs4,15039
|
|
3
|
+
bitcoinvn/errors.py,sha256=Ye10Jtxi0R7kh0xWR0ChEKT7MdNhYqJsaYBac1bvV3c,1659
|
|
4
|
+
bitcoinvn/types.py,sha256=U_v22vXfCyRGoIclJwNUrXkdPutZ_meojfvS-iR3JBQ,4785
|
|
5
|
+
bitcoinvn-0.1.0.dist-info/METADATA,sha256=STq3mkWEfqvJI_PGeOlKVEMVwfl3n5M2xuvSdd4whwY,5558
|
|
6
|
+
bitcoinvn-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
bitcoinvn-0.1.0.dist-info/licenses/LICENSE,sha256=8S0uYmn_aeljwFudfBoV91HcIo79yab9s0yhMVsOHJ0,751
|
|
8
|
+
bitcoinvn-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 api-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|