From 80ec01a5b98d0dd025341a5594a2460c2730c520 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 16:55:56 -0300 Subject: [PATCH 1/3] feat: update SDK to v2.0.0 with new resources and breaking changes Add transfers, custodial wallets, and fees resources (async + sync). Add pix_safe rail, solana/solana_devnet networks, pending_review status. Add recipient_relationship, account_class, address fields to bank account inputs. Add payout document submission, NAICS codes endpoint. Add 10 new webhook events (22 total). Fix receiver update to use PUT instead of PATCH. Add 22 new tests (171 total). --- pyproject.toml | 2 +- src/blindpay/__init__.py | 6 +- src/blindpay/client.py | 44 ++- src/blindpay/resources/__init__.py | 6 + src/blindpay/resources/available/available.py | 14 + .../resources/bank_accounts/bank_accounts.py | 83 +++++- .../resources/custodial_wallets/__init__.py | 29 ++ .../custodial_wallets/custodial_wallets.py | 112 +++++++ src/blindpay/resources/fees/__init__.py | 17 ++ src/blindpay/resources/fees/fees.py | 46 +++ src/blindpay/resources/payouts/payouts.py | 24 ++ src/blindpay/resources/receivers/receivers.py | 4 +- src/blindpay/resources/transfers/__init__.py | 31 ++ src/blindpay/resources/transfers/transfers.py | 119 ++++++++ src/blindpay/resources/webhooks/webhooks.py | 10 + src/blindpay/types.py | 28 +- tests/resources/test_custodial_wallets.py | 251 ++++++++++++++++ tests/resources/test_fees.py | 87 ++++++ tests/resources/test_transfers.py | 275 ++++++++++++++++++ uv.lock | 2 +- 20 files changed, 1175 insertions(+), 15 deletions(-) create mode 100644 src/blindpay/resources/custodial_wallets/__init__.py create mode 100644 src/blindpay/resources/custodial_wallets/custodial_wallets.py create mode 100644 src/blindpay/resources/fees/__init__.py create mode 100644 src/blindpay/resources/fees/fees.py create mode 100644 src/blindpay/resources/transfers/__init__.py create mode 100644 src/blindpay/resources/transfers/transfers.py create mode 100644 tests/resources/test_custodial_wallets.py create mode 100644 tests/resources/test_fees.py create mode 100644 tests/resources/test_transfers.py diff --git a/pyproject.toml b/pyproject.toml index fe76075..eb23a88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blindpay" -version = "1.2.0" +version = "2.0.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..132e04d 100644 --- a/src/blindpay/__init__.py +++ b/src/blindpay/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.2.0" +__version__ = "2.0.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..85133d4 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__ = "2.0.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..539a54d 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,7 +186,7 @@ class CreateColombiaAchResponse(TypedDict): created_at: str -class CreateAchInput(TypedDict): +class CreateAchInput(TypedDict, total=False): receiver_id: str name: str account_class: AccountClass @@ -185,6 +194,17 @@ class CreateAchInput(TypedDict): 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 +232,24 @@ class CreateAchResponse(TypedDict): created_at: str -class CreateWireInput(TypedDict): +class CreateWireInput(TypedDict, total=False): receiver_id: str 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 +268,11 @@ class CreateWireResponse(TypedDict): created_at: str -class CreateInternationalSwiftInput(TypedDict): +class CreateInternationalSwiftInput(TypedDict, total=False): receiver_id: str 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 +293,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 +334,24 @@ class CreateInternationalSwiftResponse(TypedDict): created_at: str -class CreateRtpInput(TypedDict): +class CreateRtpInput(TypedDict, total=False): receiver_id: str 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 +370,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 +458,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 +531,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..ed7a062 --- /dev/null +++ b/tests/resources/test_custodial_wallets.py @@ -0,0 +1,251 @@ +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"]["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 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"]["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"]["usdc"]["amount"] == 150.50 + 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"]["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 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"]["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"]["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..3e51cb3 --- /dev/null +++ b/tests/resources/test_fees.py @@ -0,0 +1,87 @@ +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"]["ach"]["payout_flat"] == 5.0 + assert response["data"]["domestic_wire"]["payout_flat"] == 25.0 + 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"]["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..a8d8fb4 --- /dev/null +++ b/tests/resources/test_transfers.py @@ -0,0 +1,275 @@ +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"]["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"]["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"]["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 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"]["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"]["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"]["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 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" }, From 004bc3e4d50811aa8823d51b55706b5f56f82e26 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 17:09:27 -0300 Subject: [PATCH 2/3] fix: bump version to 1.3.0 (minor, not major) --- pyproject.toml | 2 +- src/blindpay/__init__.py | 2 +- src/blindpay/client.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eb23a88..cdaf9e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blindpay" -version = "2.0.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 132e04d..f6c4c1c 100644 --- a/src/blindpay/__init__.py +++ b/src/blindpay/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.0.0" +__version__ = "1.3.0" from ._internal.exceptions import BlindPayError from .client import BlindPay, BlindPaySync diff --git a/src/blindpay/client.py b/src/blindpay/client.py index 85133d4..e2d0c00 100644 --- a/src/blindpay/client.py +++ b/src/blindpay/client.py @@ -38,7 +38,7 @@ from blindpay.resources.wallets.offramp import OfframpWalletsResource, OfframpWalletsResourceSync from blindpay.resources.webhooks.webhooks import WebhookEndpointsResource, WebhookEndpointsResourceSync -__version__ = "2.0.0" +__version__ = "1.3.0" T = TypeVar("T") From 3a1e6e86d5d7b97a66105bc172df4f872b118790 Mon Sep 17 00:00:00 2001 From: alvseven Date: Thu, 9 Apr 2026 18:22:08 -0300 Subject: [PATCH 3/3] fix: resolve type checking errors --- .../resources/bank_accounts/bank_accounts.py | 20 +++++++++++++++---- tests/resources/test_custodial_wallets.py | 11 ++++++++++ tests/resources/test_fees.py | 6 ++++++ tests/resources/test_transfers.py | 8 ++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/blindpay/resources/bank_accounts/bank_accounts.py b/src/blindpay/resources/bank_accounts/bank_accounts.py index 539a54d..11586f5 100644 --- a/src/blindpay/resources/bank_accounts/bank_accounts.py +++ b/src/blindpay/resources/bank_accounts/bank_accounts.py @@ -186,8 +186,11 @@ class CreateColombiaAchResponse(TypedDict): created_at: str -class CreateAchInput(TypedDict, total=False): +class _CreateAchInputRequired(TypedDict): receiver_id: str + + +class CreateAchInput(_CreateAchInputRequired, total=False): name: str account_class: AccountClass account_number: str @@ -232,8 +235,11 @@ class CreateAchResponse(TypedDict): created_at: str -class CreateWireInput(TypedDict, total=False): +class _CreateWireInputRequired(TypedDict): receiver_id: str + + +class CreateWireInput(_CreateWireInputRequired, total=False): name: str account_number: str account_class: AccountClass @@ -268,8 +274,11 @@ class CreateWireResponse(TypedDict): created_at: str -class CreateInternationalSwiftInput(TypedDict, total=False): +class _CreateInternationalSwiftInputRequired(TypedDict): receiver_id: str + + +class CreateInternationalSwiftInput(_CreateInternationalSwiftInputRequired, total=False): name: str account_class: AccountClass recipient_relationship: RecipientRelationship @@ -334,8 +343,11 @@ class CreateInternationalSwiftResponse(TypedDict): created_at: str -class CreateRtpInput(TypedDict, total=False): +class _CreateRtpInputRequired(TypedDict): receiver_id: str + + +class CreateRtpInput(_CreateRtpInputRequired, total=False): name: str account_class: AccountClass beneficiary_name: str diff --git a/tests/resources/test_custodial_wallets.py b/tests/resources/test_custodial_wallets.py index ed7a062..1909ab0 100644 --- a/tests/resources/test_custodial_wallets.py +++ b/tests/resources/test_custodial_wallets.py @@ -32,6 +32,7 @@ async def test_create_custodial_wallet(self): ) 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" @@ -60,6 +61,7 @@ async def test_list_custodial_wallets(self): 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") @@ -83,6 +85,7 @@ async def test_get_custodial_wallet(self): ) 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" @@ -112,7 +115,10 @@ async def test_get_custodial_wallet_balance(self): ) 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( @@ -160,6 +166,7 @@ def test_create_custodial_wallet(self): ) assert response["error"] is None + assert response["data"] is not None assert response["data"]["id"] == "cw_000000000000" mock_request.assert_called_once_with( "POST", @@ -185,6 +192,7 @@ def test_list_custodial_wallets(self): 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") @@ -204,6 +212,7 @@ def test_get_custodial_wallet(self): 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" @@ -232,6 +241,8 @@ def test_get_custodial_wallet_balance(self): ) 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" diff --git a/tests/resources/test_fees.py b/tests/resources/test_fees.py index 3e51cb3..016f82e 100644 --- a/tests/resources/test_fees.py +++ b/tests/resources/test_fees.py @@ -40,8 +40,12 @@ async def test_get_fees(self): 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 @@ -82,6 +86,8 @@ def test_get_fees(self): 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 index a8d8fb4..2178901 100644 --- a/tests/resources/test_transfers.py +++ b/tests/resources/test_transfers.py @@ -33,6 +33,7 @@ async def test_create_transfer_quote(self): ) 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 @@ -70,6 +71,7 @@ async def test_create_transfer(self): 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 @@ -103,6 +105,7 @@ async def test_get_transfer(self): 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") @@ -140,6 +143,7 @@ async def test_list_transfers(self): 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") @@ -172,6 +176,7 @@ def test_create_transfer_quote(self): ) assert response["error"] is None + assert response["data"] is not None assert response["data"]["id"] == "tq_000000000000" mock_request.assert_called_once_with( "POST", @@ -206,6 +211,7 @@ def test_create_transfer(self): 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", @@ -236,6 +242,7 @@ def test_get_transfer(self): 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") @@ -271,5 +278,6 @@ def test_list_transfers(self): 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")