diff --git a/pyproject.toml b/pyproject.toml index fe76075..cdaf9e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blindpay" -version = "1.2.0" +version = "1.3.0" description = "Official Python SDK for the Blindpay API — Global payments infrastructure" readme = "README.md" authors = [{ name = "Blindpay", email = "alves@blindpay.com" }] diff --git a/src/blindpay/__init__.py b/src/blindpay/__init__.py index 1374c19..f6c4c1c 100644 --- a/src/blindpay/__init__.py +++ b/src/blindpay/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.2.0" +__version__ = "1.3.0" from ._internal.exceptions import BlindPayError from .client import BlindPay, BlindPaySync @@ -15,7 +15,9 @@ Network, PaginationMetadata, PaginationParams, + PaymentMethod, Rail, + RecipientRelationship, StablecoinToken, TrackingComplete, TrackingLiquidity, @@ -36,7 +38,9 @@ "Currency", "CurrencyType", "Network", + "PaymentMethod", "Rail", + "RecipientRelationship", "StablecoinToken", "TransactionDocumentType", "TransactionStatus", diff --git a/src/blindpay/client.py b/src/blindpay/client.py index 5d36381..e2d0c00 100644 --- a/src/blindpay/client.py +++ b/src/blindpay/client.py @@ -13,6 +13,11 @@ from blindpay.resources.api_keys.api_keys import ApiKeysResource, ApiKeysResourceSync from blindpay.resources.available.available import AvailableResource, AvailableResourceSync from blindpay.resources.bank_accounts.bank_accounts import BankAccountsResource, BankAccountsResourceSync + from blindpay.resources.custodial_wallets.custodial_wallets import ( + CustodialWalletsResource, + CustodialWalletsResourceSync, + ) + from blindpay.resources.fees.fees import FeesResource, FeesResourceSync from blindpay.resources.instances.instances import InstancesResource, InstancesResourceSync from blindpay.resources.partner_fees.partner_fees import PartnerFeesResource, PartnerFeesResourceSync from blindpay.resources.payins.payins import PayinsResource, PayinsResourceSync @@ -24,6 +29,7 @@ TermsOfServiceResource, TermsOfServiceResourceSync, ) + from blindpay.resources.transfers.transfers import TransfersResource, TransfersResourceSync from blindpay.resources.virtual_accounts.virtual_accounts import ( VirtualAccountsResource, VirtualAccountsResourceSync, @@ -32,7 +38,7 @@ from blindpay.resources.wallets.offramp import OfframpWalletsResource, OfframpWalletsResourceSync from blindpay.resources.webhooks.webhooks import WebhookEndpointsResource, WebhookEndpointsResourceSync -__version__ = "1.1.0" +__version__ = "1.3.0" T = TypeVar("T") @@ -234,6 +240,12 @@ def offramp(self) -> "OfframpWalletsResource": return create_offramp_wallets_resource(self._instance_id, self._api) + @cached_property + def custodial(self) -> "CustodialWalletsResource": + from blindpay.resources.custodial_wallets.custodial_wallets import create_custodial_wallets_resource + + return create_custodial_wallets_resource(self._instance_id, self._api) + class BlindPay: _api_key: str @@ -306,6 +318,18 @@ def virtual_accounts(self) -> "VirtualAccountsResource": def wallets(self) -> _WalletsNamespace: return _WalletsNamespace(self._instance_id, self._api) + @cached_property + def transfers(self) -> "TransfersResource": + from blindpay.resources.transfers import create_transfers_resource + + return create_transfers_resource(self._instance_id, self._api) + + @cached_property + def fees(self) -> "FeesResource": + from blindpay.resources.fees import create_fees_resource + + return create_fees_resource(self._instance_id, self._api) + def verify_webhook_signature( self, *, secret: str, id: str, timestamp: str, payload: str, svix_signature: str ) -> bool: @@ -434,6 +458,12 @@ def offramp(self) -> "OfframpWalletsResourceSync": return create_offramp_wallets_resource_sync(self._instance_id, self._api) + @cached_property + def custodial(self) -> "CustodialWalletsResourceSync": + from blindpay.resources.custodial_wallets.custodial_wallets import create_custodial_wallets_resource_sync + + return create_custodial_wallets_resource_sync(self._instance_id, self._api) + class BlindPaySync: _api_key: str @@ -506,6 +536,18 @@ def virtual_accounts(self) -> "VirtualAccountsResourceSync": def wallets(self) -> _WalletsNamespaceSync: return _WalletsNamespaceSync(self._instance_id, self._api) + @cached_property + def transfers(self) -> "TransfersResourceSync": + from blindpay.resources.transfers import create_transfers_resource_sync + + return create_transfers_resource_sync(self._instance_id, self._api) + + @cached_property + def fees(self) -> "FeesResourceSync": + from blindpay.resources.fees import create_fees_resource_sync + + return create_fees_resource_sync(self._instance_id, self._api) + def verify_webhook_signature( self, *, secret: str, id: str, timestamp: str, payload: str, svix_signature: str ) -> bool: diff --git a/src/blindpay/resources/__init__.py b/src/blindpay/resources/__init__.py index e9f1e90..e2214b9 100644 --- a/src/blindpay/resources/__init__.py +++ b/src/blindpay/resources/__init__.py @@ -1,6 +1,8 @@ from .api_keys import create_api_keys_resource from .available import create_available_resource from .bank_accounts import create_bank_accounts_resource +from .custodial_wallets import create_custodial_wallets_resource +from .fees import create_fees_resource from .instances import create_instances_resource from .partner_fees import create_partner_fees_resource from .payins import create_payin_quotes_resource, create_payins_resource @@ -8,6 +10,7 @@ from .quotes import create_quotes_resource from .receivers import create_receivers_resource from .terms_of_service import create_terms_of_service_resource +from .transfers import create_transfers_resource from .virtual_accounts import create_virtual_accounts_resource from .wallets import create_blockchain_wallets_resource, create_offramp_wallets_resource from .webhooks import create_webhook_endpoints_resource @@ -16,6 +19,8 @@ "create_api_keys_resource", "create_available_resource", "create_bank_accounts_resource", + "create_custodial_wallets_resource", + "create_fees_resource", "create_instances_resource", "create_partner_fees_resource", "create_payins_resource", @@ -24,6 +29,7 @@ "create_quotes_resource", "create_receivers_resource", "create_terms_of_service_resource", + "create_transfers_resource", "create_virtual_accounts_resource", "create_blockchain_wallets_resource", "create_offramp_wallets_resource", diff --git a/src/blindpay/resources/available/available.py b/src/blindpay/resources/available/available.py index 270884f..29452ec 100644 --- a/src/blindpay/resources/available/available.py +++ b/src/blindpay/resources/available/available.py @@ -95,6 +95,14 @@ class SwiftCodeBankDetail(TypedDict): GetSwiftCodeBankDetailsResponse = List[SwiftCodeBankDetail] +class NaicsCode(TypedDict): + code: str + title: str + + +GetNaicsCodesResponse = List[NaicsCode] + + class AvailableResource: def __init__(self, client: InternalApiClient): self._client = client @@ -108,6 +116,9 @@ async def get_rails(self) -> BlindpayApiResponse[GetRailsResponse]: async def get_swift_code_bank_details(self, swift: str) -> BlindpayApiResponse[GetSwiftCodeBankDetailsResponse]: return await self._client.get(f"/available/swift/{swift}") + async def get_naics_codes(self) -> BlindpayApiResponse[GetNaicsCodesResponse]: + return await self._client.get("/available/naics") + class AvailableResourceSync: """Synchronous version of AvailableResource""" @@ -124,6 +135,9 @@ def get_rails(self) -> BlindpayApiResponse[GetRailsResponse]: def get_swift_code_bank_details(self, swift: str) -> BlindpayApiResponse[GetSwiftCodeBankDetailsResponse]: return self._client.get(f"/available/swift/{swift}") + def get_naics_codes(self) -> BlindpayApiResponse[GetNaicsCodesResponse]: + return self._client.get("/available/naics") + def create_available_resource(client: InternalApiClient) -> AvailableResource: return AvailableResource(client) diff --git a/src/blindpay/resources/bank_accounts/bank_accounts.py b/src/blindpay/resources/bank_accounts/bank_accounts.py index d9d6507..11586f5 100644 --- a/src/blindpay/resources/bank_accounts/bank_accounts.py +++ b/src/blindpay/resources/bank_accounts/bank_accounts.py @@ -3,7 +3,15 @@ from typing_extensions import TypedDict from ..._internal.api_client import InternalApiClient, InternalApiClientSync -from ...types import AccountClass, BankAccountType, BlindpayApiResponse, Country, Rail, SpeiProtocol +from ...types import ( + AccountClass, + BankAccountType, + BlindpayApiResponse, + Country, + Rail, + RecipientRelationship, + SpeiProtocol, +) ArgentinaTransfers = Literal["CVU", "CBU", "ALIAS"] AchCopDocument = Literal["CC", "CE", "NIT", "PASS", "PEP"] @@ -16,6 +24,7 @@ WireType = Literal["wire"] InternationalSwiftType = Literal["international_swift"] RtpType = Literal["rtp"] +PixSafeType = Literal["pix_safe"] class OfframpWallet(TypedDict): @@ -177,14 +186,28 @@ class CreateColombiaAchResponse(TypedDict): created_at: str -class CreateAchInput(TypedDict): +class _CreateAchInputRequired(TypedDict): receiver_id: str + + +class CreateAchInput(_CreateAchInputRequired, total=False): name: str account_class: AccountClass account_number: str account_type: BankAccountType beneficiary_name: str routing_number: str + recipient_relationship: RecipientRelationship + address_line_1: str + city: str + state_province_region: str + country: Country + postal_code: str + address_line_2: Optional[str] + business_industry: Optional[str] + phone_number: Optional[str] + tax_id: Optional[str] + date_of_birth: Optional[str] class CreateAchResponse(TypedDict): @@ -212,18 +235,27 @@ class CreateAchResponse(TypedDict): created_at: str -class CreateWireInput(TypedDict): +class _CreateWireInputRequired(TypedDict): receiver_id: str + + +class CreateWireInput(_CreateWireInputRequired, total=False): name: str account_number: str + account_class: AccountClass beneficiary_name: str routing_number: str + recipient_relationship: RecipientRelationship address_line_1: str address_line_2: Optional[str] city: str state_province_region: str country: Country postal_code: str + business_industry: Optional[str] + phone_number: Optional[str] + tax_id: Optional[str] + date_of_birth: Optional[str] class CreateWireResponse(TypedDict): @@ -242,9 +274,14 @@ class CreateWireResponse(TypedDict): created_at: str -class CreateInternationalSwiftInput(TypedDict): +class _CreateInternationalSwiftInputRequired(TypedDict): receiver_id: str + + +class CreateInternationalSwiftInput(_CreateInternationalSwiftInputRequired, total=False): name: str + account_class: AccountClass + recipient_relationship: RecipientRelationship swift_account_holder_name: str swift_account_number_iban: str swift_bank_address_line_1: str @@ -265,6 +302,11 @@ class CreateInternationalSwiftInput(TypedDict): swift_intermediary_bank_country: Optional[Country] swift_intermediary_bank_name: Optional[str] swift_intermediary_bank_swift_code_bic: Optional[str] + swift_payment_code: Optional[str] + business_industry: Optional[str] + phone_number: Optional[str] + tax_id: Optional[str] + date_of_birth: Optional[str] class CreateInternationalSwiftResponse(TypedDict): @@ -301,18 +343,27 @@ class CreateInternationalSwiftResponse(TypedDict): created_at: str -class CreateRtpInput(TypedDict): +class _CreateRtpInputRequired(TypedDict): receiver_id: str + + +class CreateRtpInput(_CreateRtpInputRequired, total=False): name: str + account_class: AccountClass beneficiary_name: str routing_number: str account_number: str + recipient_relationship: RecipientRelationship address_line_1: str address_line_2: Optional[str] city: str state_province_region: str country: Country postal_code: str + business_industry: Optional[str] + phone_number: Optional[str] + tax_id: Optional[str] + date_of_birth: Optional[str] class CreateRtpResponse(TypedDict): @@ -331,6 +382,28 @@ class CreateRtpResponse(TypedDict): created_at: str +class CreatePixSafeInput(TypedDict): + receiver_id: str + name: str + account_number: str + account_type: BankAccountType + pix_safe_bank_code: str + pix_safe_branch_code: str + pix_safe_cpf_cnpj: str + + +class CreatePixSafeResponse(TypedDict): + id: str + type: PixSafeType + name: str + account_number: str + account_type: BankAccountType + pix_safe_bank_code: str + pix_safe_branch_code: str + pix_safe_cpf_cnpj: str + created_at: str + + class BankAccountsResource: def __init__(self, instance_id: str, client: InternalApiClient): self._instance_id = instance_id @@ -397,6 +470,12 @@ async def create_rtp(self, data: CreateRtpInput) -> BlindpayApiResponse[CreateRt payload["type"] = "rtp" return await self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/bank-accounts", payload) + async def create_pix_safe(self, data: CreatePixSafeInput) -> BlindpayApiResponse[CreatePixSafeResponse]: + receiver_id = data["receiver_id"] + payload = {k: v for k, v in data.items() if k != "receiver_id"} + payload["type"] = "pix_safe" + return await self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/bank-accounts", payload) + class BankAccountsResourceSync: def __init__(self, instance_id: str, client: InternalApiClientSync): @@ -464,6 +543,12 @@ def create_rtp(self, data: CreateRtpInput) -> BlindpayApiResponse[CreateRtpRespo payload["type"] = "rtp" return self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/bank-accounts", payload) + def create_pix_safe(self, data: CreatePixSafeInput) -> BlindpayApiResponse[CreatePixSafeResponse]: + receiver_id = data["receiver_id"] + payload = {k: v for k, v in data.items() if k != "receiver_id"} + payload["type"] = "pix_safe" + return self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/bank-accounts", payload) + def create_bank_accounts_resource(instance_id: str, client: InternalApiClient) -> BankAccountsResource: return BankAccountsResource(instance_id, client) diff --git a/src/blindpay/resources/custodial_wallets/__init__.py b/src/blindpay/resources/custodial_wallets/__init__.py new file mode 100644 index 0000000..38ace72 --- /dev/null +++ b/src/blindpay/resources/custodial_wallets/__init__.py @@ -0,0 +1,29 @@ +from .custodial_wallets import ( + CreateCustodialWalletInput, + CreateCustodialWalletResponse, + CustodialWallet, + CustodialWalletBalance, + CustodialWalletBalanceToken, + CustodialWalletsResource, + CustodialWalletsResourceSync, + DeleteCustodialWalletInput, + GetCustodialWalletInput, + ListCustodialWalletsResponse, + create_custodial_wallets_resource, + create_custodial_wallets_resource_sync, +) + +__all__ = [ + "create_custodial_wallets_resource", + "create_custodial_wallets_resource_sync", + "CustodialWalletsResource", + "CustodialWalletsResourceSync", + "CustodialWallet", + "CustodialWalletBalance", + "CustodialWalletBalanceToken", + "CreateCustodialWalletInput", + "CreateCustodialWalletResponse", + "GetCustodialWalletInput", + "DeleteCustodialWalletInput", + "ListCustodialWalletsResponse", +] diff --git a/src/blindpay/resources/custodial_wallets/custodial_wallets.py b/src/blindpay/resources/custodial_wallets/custodial_wallets.py new file mode 100644 index 0000000..923a35e --- /dev/null +++ b/src/blindpay/resources/custodial_wallets/custodial_wallets.py @@ -0,0 +1,112 @@ +from typing import List, Optional + +from typing_extensions import TypedDict + +from ..._internal.api_client import InternalApiClient, InternalApiClientSync +from ...types import BlindpayApiResponse, Network + + +class CustodialWallet(TypedDict): + id: str + receiver_id: str + instance_id: str + network: Network + address: str + created_at: str + + +class CustodialWalletBalanceToken(TypedDict): + amount: float + token: str + address: str + + +class CustodialWalletBalance(TypedDict): + usdc: Optional[CustodialWalletBalanceToken] + usdt: Optional[CustodialWalletBalanceToken] + usdb: Optional[CustodialWalletBalanceToken] + + +class CreateCustodialWalletInput(TypedDict): + receiver_id: str + network: Network + + +CreateCustodialWalletResponse = CustodialWallet +ListCustodialWalletsResponse = List[CustodialWallet] + + +class GetCustodialWalletInput(TypedDict): + receiver_id: str + id: str + + +class DeleteCustodialWalletInput(TypedDict): + receiver_id: str + id: str + + +class CustodialWalletsResource: + def __init__(self, instance_id: str, client: InternalApiClient): + self._instance_id = instance_id + self._client = client + + async def list(self, receiver_id: str) -> BlindpayApiResponse[ListCustodialWalletsResponse]: + return await self._client.get(f"/instances/{self._instance_id}/receivers/{receiver_id}/wallets") + + async def get(self, data: GetCustodialWalletInput) -> BlindpayApiResponse[CustodialWallet]: + return await self._client.get( + f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}" + ) + + async def create(self, data: CreateCustodialWalletInput) -> BlindpayApiResponse[CreateCustodialWalletResponse]: + receiver_id = data["receiver_id"] + payload = {k: v for k, v in data.items() if k != "receiver_id"} + return await self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/wallets", payload) + + async def get_balance(self, data: GetCustodialWalletInput) -> BlindpayApiResponse[CustodialWalletBalance]: + return await self._client.get( + f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}/balance" + ) + + async def delete(self, data: DeleteCustodialWalletInput) -> BlindpayApiResponse[None]: + return await self._client.delete( + f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}" + ) + + +class CustodialWalletsResourceSync: + def __init__(self, instance_id: str, client: InternalApiClientSync): + self._instance_id = instance_id + self._client = client + + def list(self, receiver_id: str) -> BlindpayApiResponse[ListCustodialWalletsResponse]: + return self._client.get(f"/instances/{self._instance_id}/receivers/{receiver_id}/wallets") + + def get(self, data: GetCustodialWalletInput) -> BlindpayApiResponse[CustodialWallet]: + return self._client.get(f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}") + + def create(self, data: CreateCustodialWalletInput) -> BlindpayApiResponse[CreateCustodialWalletResponse]: + receiver_id = data["receiver_id"] + payload = {k: v for k, v in data.items() if k != "receiver_id"} + return self._client.post(f"/instances/{self._instance_id}/receivers/{receiver_id}/wallets", payload) + + def get_balance(self, data: GetCustodialWalletInput) -> BlindpayApiResponse[CustodialWalletBalance]: + return self._client.get( + f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}/balance" + ) + + def delete(self, data: DeleteCustodialWalletInput) -> BlindpayApiResponse[None]: + return self._client.delete( + f"/instances/{self._instance_id}/receivers/{data['receiver_id']}/wallets/{data['id']}" + ) + + +def create_custodial_wallets_resource(instance_id: str, client: InternalApiClient) -> CustodialWalletsResource: + return CustodialWalletsResource(instance_id, client) + + +def create_custodial_wallets_resource_sync( + instance_id: str, client: InternalApiClientSync +) -> CustodialWalletsResourceSync: + return CustodialWalletsResourceSync(instance_id, client) diff --git a/src/blindpay/resources/fees/__init__.py b/src/blindpay/resources/fees/__init__.py new file mode 100644 index 0000000..d380ff1 --- /dev/null +++ b/src/blindpay/resources/fees/__init__.py @@ -0,0 +1,17 @@ +from .fees import ( + FeeOptions, + FeesResource, + FeesResourceSync, + FeesResponse, + create_fees_resource, + create_fees_resource_sync, +) + +__all__ = [ + "create_fees_resource", + "create_fees_resource_sync", + "FeesResource", + "FeesResourceSync", + "FeeOptions", + "FeesResponse", +] diff --git a/src/blindpay/resources/fees/fees.py b/src/blindpay/resources/fees/fees.py new file mode 100644 index 0000000..49967fd --- /dev/null +++ b/src/blindpay/resources/fees/fees.py @@ -0,0 +1,46 @@ +from typing import Optional + +from typing_extensions import TypedDict + +from ..._internal.api_client import InternalApiClient, InternalApiClientSync +from ...types import BlindpayApiResponse + + +class FeeOptions(TypedDict): + payin_flat: float + payin_percentage: float + payout_flat: float + payout_percentage: float + + +class FeesResponse(TypedDict): + ach: Optional[FeeOptions] + domestic_wire: Optional[FeeOptions] + pix: Optional[FeeOptions] + solana: Optional[FeeOptions] + + +class FeesResource: + def __init__(self, instance_id: str, client: InternalApiClient): + self._instance_id = instance_id + self._client = client + + async def get(self) -> BlindpayApiResponse[FeesResponse]: + return await self._client.get(f"/instances/{self._instance_id}/billing/fees") + + +class FeesResourceSync: + def __init__(self, instance_id: str, client: InternalApiClientSync): + self._instance_id = instance_id + self._client = client + + def get(self) -> BlindpayApiResponse[FeesResponse]: + return self._client.get(f"/instances/{self._instance_id}/billing/fees") + + +def create_fees_resource(instance_id: str, client: InternalApiClient) -> FeesResource: + return FeesResource(instance_id, client) + + +def create_fees_resource_sync(instance_id: str, client: InternalApiClientSync) -> FeesResourceSync: + return FeesResourceSync(instance_id, client) diff --git a/src/blindpay/resources/payouts/payouts.py b/src/blindpay/resources/payouts/payouts.py index a3e165b..2b9eaaf 100644 --- a/src/blindpay/resources/payouts/payouts.py +++ b/src/blindpay/resources/payouts/payouts.py @@ -155,6 +155,18 @@ class CreateEvmPayoutResponse(TypedDict): receiver_id: str +class SubmitPayoutDocumentsInput(TypedDict): + payout_id: str + transaction_document_type: TransactionDocumentType + transaction_document_id: str + transaction_document_file: str + description: Optional[str] + + +class SubmitPayoutDocumentsResponse(TypedDict): + id: str + + class PayoutsResource: def __init__(self, instance_id: str, client: InternalApiClient): self._instance_id = instance_id @@ -193,6 +205,13 @@ async def create_stellar(self, data: CreateStellarPayoutInput) -> BlindpayApiRes async def create_evm(self, data: CreateEvmPayoutInput) -> BlindpayApiResponse[CreateEvmPayoutResponse]: return await self._client.post(f"/instances/{self._instance_id}/payouts/evm", data) + async def submit_documents( + self, data: SubmitPayoutDocumentsInput + ) -> BlindpayApiResponse[SubmitPayoutDocumentsResponse]: + payout_id = data["payout_id"] + payload = {k: v for k, v in data.items() if k != "payout_id"} + return await self._client.post(f"/instances/{self._instance_id}/payouts/{payout_id}/documents", payload) + class PayoutsResourceSync: def __init__(self, instance_id: str, client: InternalApiClientSync): @@ -232,6 +251,11 @@ def create_stellar(self, data: CreateStellarPayoutInput) -> BlindpayApiResponse[ def create_evm(self, data: CreateEvmPayoutInput) -> BlindpayApiResponse[CreateEvmPayoutResponse]: return self._client.post(f"/instances/{self._instance_id}/payouts/evm", data) + def submit_documents(self, data: SubmitPayoutDocumentsInput) -> BlindpayApiResponse[SubmitPayoutDocumentsResponse]: + payout_id = data["payout_id"] + payload = {k: v for k, v in data.items() if k != "payout_id"} + return self._client.post(f"/instances/{self._instance_id}/payouts/{payout_id}/documents", payload) + def create_payouts_resource(instance_id: str, client: InternalApiClient) -> PayoutsResource: return PayoutsResource(instance_id, client) diff --git a/src/blindpay/resources/receivers/receivers.py b/src/blindpay/resources/receivers/receivers.py index 50c17fb..80d905f 100644 --- a/src/blindpay/resources/receivers/receivers.py +++ b/src/blindpay/resources/receivers/receivers.py @@ -434,7 +434,7 @@ async def get(self, receiver_id: str) -> BlindpayApiResponse[GetReceiverResponse async def update(self, data: UpdateReceiverInput) -> BlindpayApiResponse[None]: receiver_id = data["receiver_id"] payload = {k: v for k, v in data.items() if k != "receiver_id"} - return await self._client.patch(f"/instances/{self._instance_id}/receivers/{receiver_id}", payload) + return await self._client.put(f"/instances/{self._instance_id}/receivers/{receiver_id}", payload) async def delete(self, receiver_id: str) -> BlindpayApiResponse[None]: return await self._client.delete(f"/instances/{self._instance_id}/receivers/{receiver_id}") @@ -489,7 +489,7 @@ def get(self, receiver_id: str) -> BlindpayApiResponse[GetReceiverResponse]: def update(self, data: UpdateReceiverInput) -> BlindpayApiResponse[None]: receiver_id = data["receiver_id"] payload = {k: v for k, v in data.items() if k != "receiver_id"} - return self._client.patch(f"/instances/{self._instance_id}/receivers/{receiver_id}", payload) + return self._client.put(f"/instances/{self._instance_id}/receivers/{receiver_id}", payload) def delete(self, receiver_id: str) -> BlindpayApiResponse[None]: return self._client.delete(f"/instances/{self._instance_id}/receivers/{receiver_id}") diff --git a/src/blindpay/resources/transfers/__init__.py b/src/blindpay/resources/transfers/__init__.py new file mode 100644 index 0000000..af56673 --- /dev/null +++ b/src/blindpay/resources/transfers/__init__.py @@ -0,0 +1,31 @@ +from .transfers import ( + CreateTransferInput, + CreateTransferQuoteInput, + CreateTransferQuoteResponse, + CreateTransferResponse, + GetTransferResponse, + ListTransfersResponse, + Transfer, + TransferQuote, + TransfersResource, + TransfersResourceSync, + TransferTrackingStep, + create_transfers_resource, + create_transfers_resource_sync, +) + +__all__ = [ + "create_transfers_resource", + "create_transfers_resource_sync", + "TransfersResource", + "TransfersResourceSync", + "Transfer", + "TransferQuote", + "TransferTrackingStep", + "CreateTransferQuoteInput", + "CreateTransferQuoteResponse", + "CreateTransferInput", + "CreateTransferResponse", + "GetTransferResponse", + "ListTransfersResponse", +] diff --git a/src/blindpay/resources/transfers/transfers.py b/src/blindpay/resources/transfers/transfers.py new file mode 100644 index 0000000..671ebe3 --- /dev/null +++ b/src/blindpay/resources/transfers/transfers.py @@ -0,0 +1,119 @@ +from typing import List, Optional +from urllib.parse import urlencode + +from typing_extensions import TypedDict + +from ..._internal.api_client import InternalApiClient, InternalApiClientSync +from ...types import BlindpayApiResponse, Currency, PaginationMetadata, PaginationParams, TransactionStatus + + +class TransferTrackingStep(TypedDict): + status: str + date: Optional[str] + + +class TransferQuote(TypedDict): + id: str + amount: float + currency: Currency + fee_amount: float + source_wallet_id: str + destination_wallet_id: str + + +class Transfer(TypedDict): + id: str + instance_id: str + status: TransactionStatus + quote_id: str + source_wallet_id: str + destination_wallet_id: str + amount: float + currency: Currency + tracking_transaction: TransferTrackingStep + tracking_transaction_monitoring: TransferTrackingStep + tracking_complete: TransferTrackingStep + created_at: str + updated_at: str + + +class CreateTransferQuoteInput(TypedDict): + source_wallet_id: str + destination_wallet_id: str + amount: float + + +CreateTransferQuoteResponse = TransferQuote + + +class CreateTransferInput(TypedDict): + quote_id: str + + +CreateTransferResponse = Transfer +GetTransferResponse = Transfer + + +class ListTransfersResponse(TypedDict): + data: List[Transfer] + pagination: PaginationMetadata + + +class TransfersResource: + def __init__(self, instance_id: str, client: InternalApiClient): + self._instance_id = instance_id + self._client = client + + async def create_quote(self, data: CreateTransferQuoteInput) -> BlindpayApiResponse[CreateTransferQuoteResponse]: + return await self._client.post(f"/instances/{self._instance_id}/transfer-quotes", data) + + async def create(self, data: CreateTransferInput) -> BlindpayApiResponse[CreateTransferResponse]: + return await self._client.post(f"/instances/{self._instance_id}/transfers", data) + + async def list(self, params: Optional[PaginationParams] = None) -> BlindpayApiResponse[ListTransfersResponse]: + query_string = "" + if params: + filtered_params = {k: v for k, v in params.items() if v is not None} + if filtered_params: + query_string = f"?{urlencode(filtered_params)}" + return await self._client.get(f"/instances/{self._instance_id}/transfers{query_string}") + + async def get(self, transfer_id: str) -> BlindpayApiResponse[GetTransferResponse]: + return await self._client.get(f"/instances/{self._instance_id}/transfers/{transfer_id}") + + async def get_track(self, transfer_id: str) -> BlindpayApiResponse[GetTransferResponse]: + return await self._client.get(f"/e/transfers/{transfer_id}") + + +class TransfersResourceSync: + def __init__(self, instance_id: str, client: InternalApiClientSync): + self._instance_id = instance_id + self._client = client + + def create_quote(self, data: CreateTransferQuoteInput) -> BlindpayApiResponse[CreateTransferQuoteResponse]: + return self._client.post(f"/instances/{self._instance_id}/transfer-quotes", data) + + def create(self, data: CreateTransferInput) -> BlindpayApiResponse[CreateTransferResponse]: + return self._client.post(f"/instances/{self._instance_id}/transfers", data) + + def list(self, params: Optional[PaginationParams] = None) -> BlindpayApiResponse[ListTransfersResponse]: + query_string = "" + if params: + filtered_params = {k: v for k, v in params.items() if v is not None} + if filtered_params: + query_string = f"?{urlencode(filtered_params)}" + return self._client.get(f"/instances/{self._instance_id}/transfers{query_string}") + + def get(self, transfer_id: str) -> BlindpayApiResponse[GetTransferResponse]: + return self._client.get(f"/instances/{self._instance_id}/transfers/{transfer_id}") + + def get_track(self, transfer_id: str) -> BlindpayApiResponse[GetTransferResponse]: + return self._client.get(f"/e/transfers/{transfer_id}") + + +def create_transfers_resource(instance_id: str, client: InternalApiClient) -> TransfersResource: + return TransfersResource(instance_id, client) + + +def create_transfers_resource_sync(instance_id: str, client: InternalApiClientSync) -> TransfersResourceSync: + return TransfersResourceSync(instance_id, client) diff --git a/src/blindpay/resources/webhooks/webhooks.py b/src/blindpay/resources/webhooks/webhooks.py index 269dea5..a75dcba 100644 --- a/src/blindpay/resources/webhooks/webhooks.py +++ b/src/blindpay/resources/webhooks/webhooks.py @@ -18,6 +18,16 @@ "payin.update", "payin.complete", "payin.partnerFee", + "tos.accept", + "limitIncrease.new", + "limitIncrease.update", + "virtualAccount.new", + "virtualAccount.complete", + "transfer.new", + "transfer.update", + "transfer.complete", + "wallet.new", + "wallet.inbound", ] diff --git a/src/blindpay/types.py b/src/blindpay/types.py index 6cf0034..73f503e 100644 --- a/src/blindpay/types.py +++ b/src/blindpay/types.py @@ -35,6 +35,8 @@ class BlindpaySuccessResponse(TypedDict, Generic[T]): "stellar", "stellar_testnet", "tron", + "solana", + "solana_devnet", ] StablecoinToken = Literal["USDC", "USDT", "USDB"] @@ -47,11 +49,13 @@ class BlindpaySuccessResponse(TypedDict, Generic[T]): Currency = Literal["USDC", "USDT", "USDB", "BRL", "USD", "MXN", "COP", "ARS"] -Rail = Literal["wire", "ach", "pix", "spei_bitso", "transfers_bitso", "ach_cop_bitso", "international_swift", "rtp"] +Rail = Literal[ + "wire", "ach", "pix", "pix_safe", "spei_bitso", "transfers_bitso", "ach_cop_bitso", "international_swift", "rtp" +] AccountClass = Literal["individual", "business"] -TransactionStatus = Literal["refunded", "processing", "completed", "failed", "on_hold"] +TransactionStatus = Literal["refunded", "processing", "completed", "failed", "on_hold", "pending_review"] SpeiProtocol = Literal["clabe", "debitcard", "phonenum"] @@ -307,8 +311,8 @@ class BlindpaySuccessResponse(TypedDict, Generic[T]): "AX", ] -PaginationLimit = Literal["10", "50", "100", "200", "1000"] -PaginationOffset = Literal["0", "10", "50", "100", "200", "1000"] +PaginationLimit = Literal["10", "50", "100", "200", "500", "1000"] +PaginationOffset = Literal["0", "10", "50", "100", "200", "500", "1000"] class PaginationParams(TypedDict, total=False): @@ -359,3 +363,19 @@ class TrackingPartnerFee(TypedDict): step: str transaction_hash: str completed_at: str + + +RecipientRelationship = Literal[ + "first_party", + "employee", + "independent_contractor", + "vendor_supplier", + "client_customer", + "subsidiary_affiliate", + "joint_venture_partner", + "landlord_property_manager", + "government_agency", + "other", +] + +PaymentMethod = Literal["ach", "wire", "pix", "spei", "transfers", "pse", "international_swift"] diff --git a/tests/resources/test_custodial_wallets.py b/tests/resources/test_custodial_wallets.py new file mode 100644 index 0000000..1909ab0 --- /dev/null +++ b/tests/resources/test_custodial_wallets.py @@ -0,0 +1,262 @@ +from unittest.mock import patch + +import pytest + +from blindpay import BlindPay, BlindPaySync + + +class TestCustodialWallets: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000") + + @pytest.mark.asyncio + async def test_create_custodial_wallet(self): + mocked_wallet = { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallet, "error": None} + + response = await self.blindpay.wallets.custodial.create( + { + "receiver_id": "re_000000000000", + "network": "solana", + } + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "cw_000000000000" + assert response["data"]["network"] == "solana" + assert response["data"]["address"] == "So1ana1234567890" + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/receivers/re_000000000000/wallets", + {"network": "solana"}, + ) + + @pytest.mark.asyncio + async def test_list_custodial_wallets(self): + mocked_wallets = [ + { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + }, + ] + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallets, "error": None} + + response = await self.blindpay.wallets.custodial.list("re_000000000000") + + assert response["error"] is None + assert response["data"] is not None + assert len(response["data"]) == 1 + assert response["data"][0]["id"] == "cw_000000000000" + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/receivers/re_000000000000/wallets") + + @pytest.mark.asyncio + async def test_get_custodial_wallet(self): + mocked_wallet = { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallet, "error": None} + + response = await self.blindpay.wallets.custodial.get( + {"receiver_id": "re_000000000000", "id": "cw_000000000000"} + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "cw_000000000000" + mock_request.assert_called_once_with( + "GET", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000" + ) + + @pytest.mark.asyncio + async def test_get_custodial_wallet_balance(self): + mocked_balance = { + "usdc": { + "amount": 150.50, + "token": "USDC", + "address": "So1ana1234567890", + }, + "usdt": { + "amount": 0.0, + "token": "USDT", + "address": "So1ana1234567890", + }, + "usdb": None, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_balance, "error": None} + + response = await self.blindpay.wallets.custodial.get_balance( + {"receiver_id": "re_000000000000", "id": "cw_000000000000"} + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["usdc"] is not None + assert response["data"]["usdc"]["amount"] == 150.50 + assert response["data"]["usdt"] is not None + assert response["data"]["usdt"]["amount"] == 0.0 + assert response["data"]["usdb"] is None + mock_request.assert_called_once_with( + "GET", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000/balance" + ) + + @pytest.mark.asyncio + async def test_delete_custodial_wallet(self): + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": None, "error": None} + + response = await self.blindpay.wallets.custodial.delete( + {"receiver_id": "re_000000000000", "id": "cw_000000000000"} + ) + + assert response["error"] is None + mock_request.assert_called_once_with( + "DELETE", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000", None + ) + + +class TestCustodialWalletsSync: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000") + + def test_create_custodial_wallet(self): + mocked_wallet = { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallet, "error": None} + + response = self.blindpay.wallets.custodial.create( + { + "receiver_id": "re_000000000000", + "network": "solana", + } + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "cw_000000000000" + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/receivers/re_000000000000/wallets", + {"network": "solana"}, + ) + + def test_list_custodial_wallets(self): + mocked_wallets = [ + { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + }, + ] + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallets, "error": None} + + response = self.blindpay.wallets.custodial.list("re_000000000000") + + assert response["error"] is None + assert response["data"] is not None + assert len(response["data"]) == 1 + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/receivers/re_000000000000/wallets") + + def test_get_custodial_wallet(self): + mocked_wallet = { + "id": "cw_000000000000", + "receiver_id": "re_000000000000", + "instance_id": "in_000000000000", + "network": "solana", + "address": "So1ana1234567890", + "created_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_wallet, "error": None} + + response = self.blindpay.wallets.custodial.get({"receiver_id": "re_000000000000", "id": "cw_000000000000"}) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "cw_000000000000" + mock_request.assert_called_once_with( + "GET", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000" + ) + + def test_get_custodial_wallet_balance(self): + mocked_balance = { + "usdc": { + "amount": 150.50, + "token": "USDC", + "address": "So1ana1234567890", + }, + "usdt": { + "amount": 0.0, + "token": "USDT", + "address": "So1ana1234567890", + }, + "usdb": None, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_balance, "error": None} + + response = self.blindpay.wallets.custodial.get_balance( + {"receiver_id": "re_000000000000", "id": "cw_000000000000"} + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["usdc"] is not None + assert response["data"]["usdc"]["amount"] == 150.50 + mock_request.assert_called_once_with( + "GET", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000/balance" + ) + + def test_delete_custodial_wallet(self): + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": None, "error": None} + + response = self.blindpay.wallets.custodial.delete( + {"receiver_id": "re_000000000000", "id": "cw_000000000000"} + ) + + assert response["error"] is None + mock_request.assert_called_once_with( + "DELETE", "/instances/in_000000000000/receivers/re_000000000000/wallets/cw_000000000000", None + ) diff --git a/tests/resources/test_fees.py b/tests/resources/test_fees.py new file mode 100644 index 0000000..016f82e --- /dev/null +++ b/tests/resources/test_fees.py @@ -0,0 +1,93 @@ +from unittest.mock import patch + +import pytest + +from blindpay import BlindPay, BlindPaySync + + +class TestFees: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000") + + @pytest.mark.asyncio + async def test_get_fees(self): + mocked_fees = { + "ach": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 5.0, + "payout_percentage": 0.0, + }, + "domestic_wire": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 25.0, + "payout_percentage": 0.0, + }, + "pix": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 3.0, + "payout_percentage": 0.5, + }, + "solana": None, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_fees, "error": None} + + response = await self.blindpay.fees.get() + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["ach"] is not None + assert response["data"]["ach"]["payout_flat"] == 5.0 + assert response["data"]["domestic_wire"] is not None + assert response["data"]["domestic_wire"]["payout_flat"] == 25.0 + assert response["data"]["pix"] is not None + assert response["data"]["pix"]["payout_flat"] == 3.0 + assert response["data"]["pix"]["payout_percentage"] == 0.5 + assert response["data"]["solana"] is None + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/billing/fees") + + +class TestFeesSync: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000") + + def test_get_fees(self): + mocked_fees = { + "ach": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 5.0, + "payout_percentage": 0.0, + }, + "domestic_wire": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 25.0, + "payout_percentage": 0.0, + }, + "pix": { + "payin_flat": 0.0, + "payin_percentage": 0.0, + "payout_flat": 3.0, + "payout_percentage": 0.5, + }, + "solana": None, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_fees, "error": None} + + response = self.blindpay.fees.get() + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["ach"] is not None + assert response["data"]["ach"]["payout_flat"] == 5.0 + assert response["data"]["solana"] is None + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/billing/fees") diff --git a/tests/resources/test_transfers.py b/tests/resources/test_transfers.py new file mode 100644 index 0000000..2178901 --- /dev/null +++ b/tests/resources/test_transfers.py @@ -0,0 +1,283 @@ +from unittest.mock import patch + +import pytest + +from blindpay import BlindPay, BlindPaySync + + +class TestTransfers: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000") + + @pytest.mark.asyncio + async def test_create_transfer_quote(self): + mocked_quote = { + "id": "tq_000000000000", + "amount": 100.00, + "currency": "USD", + "fee_amount": 1.50, + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_quote, "error": None} + + response = await self.blindpay.transfers.create_quote( + { + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + } + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tq_000000000000" + assert response["data"]["amount"] == 100.00 + assert response["data"]["fee_amount"] == 1.50 + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/transfer-quotes", + { + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + }, + ) + + @pytest.mark.asyncio + async def test_create_transfer(self): + mocked_transfer = { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "processing", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "processing", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "processing", "date": None}, + "tracking_complete": {"status": "processing", "date": None}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfer, "error": None} + + response = await self.blindpay.transfers.create({"quote_id": "tq_000000000000"}) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tr_000000000000" + assert response["data"]["status"] == "processing" + assert response["data"]["amount"] == 100.00 + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/transfers", + {"quote_id": "tq_000000000000"}, + ) + + @pytest.mark.asyncio + async def test_get_transfer(self): + mocked_transfer = { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "completed", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_complete": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfer, "error": None} + + response = await self.blindpay.transfers.get("tr_000000000000") + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tr_000000000000" + assert response["data"]["status"] == "completed" + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/transfers/tr_000000000000") + + @pytest.mark.asyncio + async def test_list_transfers(self): + mocked_transfers = { + "data": [ + { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "completed", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_complete": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + }, + ], + "pagination": { + "has_more": False, + "next_page": None, + "prev_page": None, + }, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfers, "error": None} + + response = await self.blindpay.transfers.list() + + assert response["error"] is None + assert response["data"] is not None + assert len(response["data"]["data"]) == 1 + assert response["data"]["data"][0]["id"] == "tr_000000000000" + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/transfers") + + +class TestTransfersSync: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000") + + def test_create_transfer_quote(self): + mocked_quote = { + "id": "tq_000000000000", + "amount": 100.00, + "currency": "USD", + "fee_amount": 1.50, + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_quote, "error": None} + + response = self.blindpay.transfers.create_quote( + { + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + } + ) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tq_000000000000" + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/transfer-quotes", + { + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + }, + ) + + def test_create_transfer(self): + mocked_transfer = { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "processing", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "processing", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "processing", "date": None}, + "tracking_complete": {"status": "processing", "date": None}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfer, "error": None} + + response = self.blindpay.transfers.create({"quote_id": "tq_000000000000"}) + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tr_000000000000" + mock_request.assert_called_once_with( + "POST", + "/instances/in_000000000000/transfers", + {"quote_id": "tq_000000000000"}, + ) + + def test_get_transfer(self): + mocked_transfer = { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "completed", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_complete": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfer, "error": None} + + response = self.blindpay.transfers.get("tr_000000000000") + + assert response["error"] is None + assert response["data"] is not None + assert response["data"]["id"] == "tr_000000000000" + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/transfers/tr_000000000000") + + def test_list_transfers(self): + mocked_transfers = { + "data": [ + { + "id": "tr_000000000000", + "instance_id": "in_000000000000", + "status": "completed", + "quote_id": "tq_000000000000", + "source_wallet_id": "cw_source", + "destination_wallet_id": "cw_dest", + "amount": 100.00, + "currency": "USD", + "tracking_transaction": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_transaction_monitoring": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "tracking_complete": {"status": "completed", "date": "2025-01-01T00:00:00Z"}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + }, + ], + "pagination": { + "has_more": False, + "next_page": None, + "prev_page": None, + }, + } + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_transfers, "error": None} + + response = self.blindpay.transfers.list() + + assert response["error"] is None + assert response["data"] is not None + assert len(response["data"]["data"]) == 1 + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/transfers") diff --git a/uv.lock b/uv.lock index 565a494..96f592d 100644 --- a/uv.lock +++ b/uv.lock @@ -27,7 +27,7 @@ wheels = [ [[package]] name = "blindpay" -version = "1.2.0" +version = "2.0.0" source = { editable = "." } dependencies = [ { name = "httpx" },