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 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
+ [![PyPI version](https://img.shields.io/pypi/v/bitcoinvn.svg)](https://pypi.org/project/bitcoinvn/)
16
+ [![Python versions](https://img.shields.io/pypi/pyversions/bitcoinvn.svg)](https://pypi.org/project/bitcoinvn/)
17
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.