diff --git a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py index 789ad5b..e4ca99f 100644 --- a/bfxapi/rest/endpoints/rest_authenticated_endpoints.py +++ b/bfxapi/rest/endpoints/rest_authenticated_endpoints.py @@ -302,13 +302,13 @@ class RestAuthenticatedEndpoints(Middleware): return serializers._Notification[DepositAddress](serializers.DepositAddress).parse(*self._POST("auth/w/deposit/address", body=body)) - def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> Invoice: + def generate_deposit_invoice(self, wallet: str, currency: str, amount: Union[Decimal, float, str]) -> LightningNetworkInvoice: body = { "wallet": wallet, "currency": currency, "amount": amount } - return serializers.Invoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) + return serializers.LightningNetworkInvoice.parse(*self._POST("auth/w/deposit/invoice", body=body)) def get_movements(self, currency: Optional[str] = None, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[int] = None) -> List[Movement]: if currency == None: @@ -320,4 +320,22 @@ class RestAuthenticatedEndpoints(Middleware): "limit": limit } - return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] \ No newline at end of file + return [ serializers.Movement.parse(*sub_data) for sub_data in self._POST(endpoint, body=body) ] + + def submit_invoice(self, amount: Union[Decimal, float, str], currency: str, order_id: str, + customer_info: CustomerInfo, pay_currencies: List[str], duration: Optional[int] = None, + webhook: Optional[str] = None, redirect_url: Optional[str] = None) -> InvoiceSubmission: + data = self._POST("auth/w/ext/pay/invoice/create", body={ + "amount": amount, "currency": currency, "order_id": order_id, + "customer_info": customer_info, "pay_currencies": pay_currencies, "duration": duration, + "webhook": webhook, "redirect_url": redirect_url + }) + + if "customer_info" in data and data["customer_info"] != None: + data["customer_info"] = CustomerInfo(**data["customer_info"]) + + if "invoices" in data and data["invoices"] != None: + for index, invoice in enumerate(data["invoices"]): + data["invoices"][index] = Invoice(**invoice) + + return InvoiceSubmission(**data) \ No newline at end of file diff --git a/bfxapi/rest/serializers.py b/bfxapi/rest/serializers.py index 43943ff..6e80ed7 100644 --- a/bfxapi/rest/serializers.py +++ b/bfxapi/rest/serializers.py @@ -18,7 +18,7 @@ __serializers__ = [ "FundingOffer", "FundingCredit", "FundingLoan", "FundingAutoRenew", "FundingInfo", "Wallet", "Transfer", "Withdrawal", "DepositAddress", - "Invoice", "Movement", "SymbolMarginInfo", + "LightningNetworkInvoice", "Movement", "SymbolMarginInfo", "BaseMarginInfo", "PositionClaim", "PositionIncreaseInfo", "PositionIncrease", "PositionHistory", "PositionSnapshot", "PositionAudit", "DerivativePositionCollateral", "DerivativePositionCollateralLimits", @@ -581,7 +581,7 @@ DepositAddress = generate_labeler_serializer("DepositAddress", klass=types.Depos "pool_address" ]) -Invoice = generate_labeler_serializer("Invoice", klass=types.Invoice, labels=[ +LightningNetworkInvoice = generate_labeler_serializer("LightningNetworkInvoice", klass=types.LightningNetworkInvoice, labels=[ "invoice_hash", "invoice", "_PLACEHOLDER", diff --git a/bfxapi/rest/types.py b/bfxapi/rest/types.py index eee3c30..964b24b 100644 --- a/bfxapi/rest/types.py +++ b/bfxapi/rest/types.py @@ -1,10 +1,12 @@ -from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Any +from typing import Type, Tuple, List, Dict, TypedDict, Union, Optional, Literal, Any from dataclasses import dataclass +from types import SimpleNamespace + from .. labeler import _Type from .. notification import Notification -from ..utils.JSONEncoder import JSON +from .. utils.JSONEncoder import JSON #region Type hinting for Rest Public Endpoints @@ -440,7 +442,7 @@ class DepositAddress(_Type): pool_address: str @dataclass -class Invoice(_Type): +class LightningNetworkInvoice(_Type): invoice_hash: str invoice: str amount: str @@ -561,4 +563,44 @@ class DerivativePositionCollateralLimits(_Type): min_collateral: float max_collateral: float +#endregion + +#region Type hinting for models which are not serializable + +@dataclass +class InvoiceSubmission(_Type): + id: str + t: int + type: Literal["ECOMMERCE", "POS"] + duration: int + amount: float + currency: str + order_id: str + pay_currencies: List[str] + webhook: str + redirect_url: str + status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] + customer_info: Optional["CustomerInfo"] + invoices: List["Invoice"] + +class CustomerInfo(SimpleNamespace): + nationality: str + resid_country: str + resid_state: str + resid_city: str + resid_zip_code: str + resid_street: str + resid_building_no: str + full_name: str + email: str + tos_accepted: bool + +class Invoice(SimpleNamespace): + amount: float + currency: str + pay_currency: str + pool_currency: str + address: str + ext: JSON + #endregion \ No newline at end of file diff --git a/bfxapi/utils/JSONEncoder.py b/bfxapi/utils/JSONEncoder.py index 506bad1..b322795 100644 --- a/bfxapi/utils/JSONEncoder.py +++ b/bfxapi/utils/JSONEncoder.py @@ -2,33 +2,31 @@ import json from decimal import Decimal from datetime import datetime +from types import SimpleNamespace + from typing import Type, List, Dict, Union, Any JSON = Union[Dict[str, "JSON"], List["JSON"], bool, int, float, str, Type[None]] +def _strip(dictionary: Dict) -> Dict: + return { key: value for key, value in dictionary.items() if value != None} + +def _convert_float_to_str(data: JSON) -> JSON: + if isinstance(data, float): + return format(Decimal(repr(data)), "f") + elif isinstance(data, list): + return [ _convert_float_to_str(sub_data) for sub_data in data ] + elif isinstance(data, dict): + return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) + else: return data + class JSONEncoder(json.JSONEncoder): def encode(self, obj: JSON) -> str: - def _strip(dictionary: Dict) -> Dict: - return { key: value for key, value in dictionary.items() if value != None} - - def _convert_float_to_str(data: JSON) -> JSON: - if isinstance(data, float): - return format(Decimal(repr(data)), "f") - elif isinstance(data, list): - return [ _convert_float_to_str(sub_data) for sub_data in data ] - elif isinstance(data, dict): - return _strip({ key: _convert_float_to_str(value) for key, value in data.items() }) - else: return data - - data = _convert_float_to_str(obj) - - return json.JSONEncoder.encode(self, data) + return json.JSONEncoder.encode(self, _convert_float_to_str(obj)) def default(self, obj: Any) -> Any: - if isinstance(obj, Decimal): - return format(obj, "f") - - if isinstance(obj, datetime): - return str(obj) + if isinstance(obj, SimpleNamespace): return _convert_float_to_str(vars(obj)) + elif isinstance(obj, Decimal): return format(obj, "f") + elif isinstance(obj, datetime): return str(obj) return json.JSONEncoder.default(self, obj) \ No newline at end of file diff --git a/examples/rest/merchant.py b/examples/rest/merchant.py new file mode 100644 index 0000000..dcacbbf --- /dev/null +++ b/examples/rest/merchant.py @@ -0,0 +1,33 @@ +# python -c "import examples.rest.merchant" + +import os + +from bfxapi.client import Client, Constants +from bfxapi.rest.types import CustomerInfo + +bfx = Client( + REST_HOST=Constants.REST_HOST, + API_KEY=os.getenv("BFX_API_KEY"), + API_SECRET=os.getenv("BFX_API_SECRET") +) + +customer_info: CustomerInfo = CustomerInfo( + nationality="GB", + resid_country="DE", + resid_city="Berlin", + resid_zip_code=1, + resid_street="Timechain", + full_name="Satoshi", + email="satoshi3@bitfinex.com", + tos_accepted=None, + resid_building_no=None +) + +print(bfx.rest.auth.submit_invoice( + amount=1, + currency="USD", + duration=864000, + order_id="order123", + customer_info=customer_info, + pay_currencies=["ETH"], +)) \ No newline at end of file