diff --git a/.claude-pr/CLAUDE.md b/.claude-pr/CLAUDE.md new file mode 100644 index 0000000..a4743c2 --- /dev/null +++ b/.claude-pr/CLAUDE.md @@ -0,0 +1,737 @@ +# CLAUDE.md -- blindpay-python SDK + +This document is the authoritative reference for an AI agent modifying this codebase. +Read it fully before making any change. + +--- + +## 1. Project structure + +``` +blindpay-python/ + pyproject.toml # Package metadata, deps, ruff/mypy config + release-please-config.json # release-please settings (Python type, v-tag) + .release-please-manifest.json # Current release-please version tracker + src/blindpay/ + __init__.py # SDK version, public re-exports (types + clients) + client.py # BlindPay (async) and BlindPaySync classes, + # ApiClientImpl / ApiClientImplSync (HTTP layer), + # _*Namespace / _*NamespaceSync classes (sub-resources) + types.py # Shared Literal types and TypedDicts used across resources + _internal/ + api_client.py # Protocol interfaces: InternalApiClient, InternalApiClientSync + exceptions.py # BlindPayError exception class + resources/ + __init__.py # Barrel re-exports of all create_*_resource factory functions + / + __init__.py # Barrel re-exports from the resource module(s) + .py # TypedDicts (input/output) + Resource classes (async+sync) + factory fns + tests/ + conftest.py # Adds src/ to sys.path + test_client.py # Tests for BlindPay / BlindPaySync (webhook verification) + resources/ + test_.py # Per-resource test files (async + sync test classes) +``` + +### Resource directory examples + +Standard resource (no sub-resources): +``` +resources/payouts/ + __init__.py # re-exports from payouts.py + payouts.py # types + PayoutsResource + PayoutsResourceSync + factory fns +``` + +Resource with sub-resources (e.g. payins has quotes): +``` +resources/payins/ + __init__.py # re-exports from BOTH payins.py and quotes.py + payins.py # types + PayinsResource + PayinsResourceSync + factory fns + quotes.py # types + PayinQuotesResource + PayinQuotesResourceSync + factory fns +``` + +Multi-module namespace (wallets -- no _base, purely a grouping): +``` +resources/wallets/ + __init__.py # re-exports from blockchain.py and offramp.py + blockchain.py # BlockchainWalletsResource + BlockchainWalletsResourceSync + factory fns + offramp.py # OfframpWalletsResource + OfframpWalletsResourceSync + factory fns +resources/custodial_wallets/ + __init__.py + custodial_wallets.py # CustodialWalletsResource + CustodialWalletsResourceSync + factory fns +``` + +--- + +## 2. Conventions + +### Naming + +| Concept | Convention | Example | +|---|---|---| +| Resource class (async) | `{Name}Resource` | `PayinsResource` | +| Resource class (sync) | `{Name}ResourceSync` | `PayinsResourceSync` | +| Factory function (async) | `create_{snake_name}_resource` | `create_payins_resource` | +| Factory function (sync) | `create_{snake_name}_resource_sync` | `create_payins_resource_sync` | +| Input TypedDict | `{ActionName}Input` | `CreatePayinQuoteInput` | +| Output TypedDict | `{ActionName}Response` | `CreatePayinQuoteResponse` | +| Entity TypedDict | `{EntityName}` (no suffix) | `Payout`, `Payin`, `BlockchainWallet` | +| List response | `List{Name}Response` (TypedDict with `data` + `pagination`) | `ListPayoutsResponse` | +| Literal type | `PascalCase` | `TransactionStatus`, `Network` | +| Client namespace (async) | `_{Name}Namespace` | `_PayinsNamespace` | +| Client namespace (sync) | `_{Name}NamespaceSync` | `_PayinsNamespaceSync` | + +### Code style + +- Line length: 120 (ruff config). +- Formatter: ruff format. Linter: ruff lint. Type checker: mypy (strict) + pyright. +- Ruff lint rules: `["I","B","E","F","ARG","T201","T203"]`, ignore `["B006"]`. +- All TypedDicts come from `typing_extensions.TypedDict` (NOT `typing.TypedDict`). Exception: some resource files use `typing.TypedDict` -- follow whichever the file already uses, but prefer `typing_extensions`. +- Use `Optional[X]` from `typing`, not `X | None`. +- Use `List[X]` from `typing`, not `list[X]`. +- Use `Literal[...]` from `typing_extensions` for resource-local literals, or from `typing` if already imported. +- No docstrings on resource methods (the verify_webhook_signature method is an exception). +- All methods return `BlindpayApiResponse[T]` where T is the response TypedDict. +- Double quotes for strings. +- Imports: standard library first, then third-party, then relative. Sorted by ruff isort. +- No trailing commas after the last parameter in function signatures that fit on one line. +- Factory functions are always at the bottom of the resource file. + +### Imports inside resource files + +```python +from ..._internal.api_client import InternalApiClient, InternalApiClientSync +from ...types import BlindpayApiResponse, +``` + +Resource-specific Literal types and TypedDicts are defined in the same file as the resource class, NOT in `types.py`. Only truly shared types (used across multiple resources) go in `types.py`. + +### Imports in client.py + +- Resource types are imported inside `TYPE_CHECKING` blocks only (lazy imports). +- Actual imports happen inside `@cached_property` methods using inline `from` statements. +- The `TYPE_CHECKING` import uses absolute paths: `from blindpay.resources.. import ...`. + +--- + +## 3. How to add a new resource + +### Step 1: Create the resource directory and files + +Create `src/blindpay/resources/{resource_name}/` with two files: + +**`__init__.py`**: +```python +from .{resource_name} import ( + {EntityName}, + {EntityName}Resource, + {EntityName}ResourceSync, + # ... all TypedDicts ... + create_{snake_name}_resource, + create_{snake_name}_resource_sync, +) + +__all__ = [ + "create_{snake_name}_resource", + "create_{snake_name}_resource_sync", + "{EntityName}Resource", + "{EntityName}ResourceSync", + # ... all TypedDicts ... +] +``` + +**`{resource_name}.py`** (example for an instance-scoped resource): +```python +from typing import List, Optional + +from typing_extensions import TypedDict + +from ..._internal.api_client import InternalApiClient, InternalApiClientSync +from ...types import BlindpayApiResponse + + +class {EntityName}(TypedDict): + id: str + instance_id: str + # ... fields ... + created_at: str + updated_at: str + + +class Create{EntityName}Input(TypedDict): + # ... fields ... + pass + + +class Create{EntityName}Response(TypedDict): + id: str + + +class List{EntityName}Response(TypedDict): + data: List[{EntityName}] + + +# --- Async resource --- + +class {EntityName}Resource: + def __init__(self, instance_id: str, client: InternalApiClient): + self._instance_id = instance_id + self._client = client + + async def list(self) -> BlindpayApiResponse[List{EntityName}Response]: + return await self._client.get(f"/instances/{self._instance_id}/{api_path}") + + async def get(self, {id_param}: str) -> BlindpayApiResponse[{EntityName}]: + return await self._client.get(f"/instances/{self._instance_id}/{api_path}/{{{id_param}}}") + + async def create(self, data: Create{EntityName}Input) -> BlindpayApiResponse[Create{EntityName}Response]: + return await self._client.post(f"/instances/{self._instance_id}/{api_path}", data) + + async def delete(self, {id_param}: str) -> BlindpayApiResponse[None]: + return await self._client.delete(f"/instances/{self._instance_id}/{api_path}/{{{id_param}}}") + + +# --- Sync resource --- + +class {EntityName}ResourceSync: + def __init__(self, instance_id: str, client: InternalApiClientSync): + self._instance_id = instance_id + self._client = client + + def list(self) -> BlindpayApiResponse[List{EntityName}Response]: + return self._client.get(f"/instances/{self._instance_id}/{api_path}") + + def get(self, {id_param}: str) -> BlindpayApiResponse[{EntityName}]: + return self._client.get(f"/instances/{self._instance_id}/{api_path}/{{{id_param}}}") + + def create(self, data: Create{EntityName}Input) -> BlindpayApiResponse[Create{EntityName}Response]: + return self._client.post(f"/instances/{self._instance_id}/{api_path}", data) + + def delete(self, {id_param}: str) -> BlindpayApiResponse[None]: + return self._client.delete(f"/instances/{self._instance_id}/{api_path}/{{{id_param}}}") + + +# --- Factory functions --- + +def create_{snake_name}_resource(instance_id: str, client: InternalApiClient) -> {EntityName}Resource: + return {EntityName}Resource(instance_id, client) + + +def create_{snake_name}_resource_sync(instance_id: str, client: InternalApiClientSync) -> {EntityName}ResourceSync: + return {EntityName}ResourceSync(instance_id, client) +``` + +CRITICAL: The sync class is identical to the async class except: +- Methods are NOT `async def`, they are plain `def`. +- Method bodies do NOT use `await`. +- Constructor takes `InternalApiClientSync` instead of `InternalApiClient`. + +For a resource that does NOT require `instance_id` (like `available`), omit `instance_id` from constructor and factory function, and do not prefix paths with `/instances/{self._instance_id}`. + +### Step 2: Register in `resources/__init__.py` + +Add the import and `__all__` entry: +```python +from .{resource_name} import create_{snake_name}_resource +``` + +Also add the sync factory if not already following the pattern (note: the barrel `__init__` only exports async factories currently; check existing pattern). + +### Step 3: Wire into `client.py` + +Add TYPE_CHECKING import at the top of client.py: +```python +if TYPE_CHECKING: + from blindpay.resources.{resource_name}.{resource_name} import {EntityName}Resource, {EntityName}ResourceSync +``` + +Add `@cached_property` to BOTH `BlindPay` (async) and `BlindPaySync` classes: + +**In `BlindPay`:** +```python +@cached_property +def {resource_name}(self) -> "{EntityName}Resource": + from blindpay.resources.{resource_name} import create_{snake_name}_resource + + return create_{snake_name}_resource(self._instance_id, self._api) +``` + +**In `BlindPaySync`:** +```python +@cached_property +def {resource_name}(self) -> "{EntityName}ResourceSync": + from blindpay.resources.{resource_name} import create_{snake_name}_resource_sync + + return create_{snake_name}_resource_sync(self._instance_id, self._api) +``` + +### Step 4: Add tests + +Create `tests/resources/test_{resource_name}.py`: +```python +from unittest.mock import patch + +import pytest + +from blindpay import BlindPay, BlindPaySync + + +class Test{EntityName}: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPay(api_key="test-key", instance_id="in_000000000000") + + @pytest.mark.asyncio + async def test_list(self): + mocked_data = [{"id": "xxx_000000000000"}] + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_data, "error": None} + + response = await self.blindpay.{resource_name}.list() + + assert response["error"] is None + assert response["data"] == mocked_data + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/{api_path}") + + +class Test{EntityName}Sync: + @pytest.fixture(autouse=True) + def setup(self): + self.blindpay = BlindPaySync(api_key="test-key", instance_id="in_000000000000") + + def test_list(self): + mocked_data = [{"id": "xxx_000000000000"}] + + with patch.object(self.blindpay._api, "_request") as mock_request: + mock_request.return_value = {"data": mocked_data, "error": None} + + response = self.blindpay.{resource_name}.list() + + assert response["error"] is None + assert response["data"] == mocked_data + mock_request.assert_called_once_with("GET", "/instances/in_000000000000/{api_path}") +``` + +Testing pattern: +- Async tests: class `Test{Name}`, use `@pytest.mark.asyncio` + `async def`, mock `self.blindpay._api._request`. +- Sync tests: class `Test{Name}Sync`, plain `def`, mock `self.blindpay._api._request`. +- Both patch `_request` on the `_api` object and assert the HTTP method + path. + +--- + +## 4. How to add a method to an existing resource + +### Step 1: Define input/output types in the resource file + +Add TypedDicts above the resource classes in the same file: +```python +class NewActionInput(TypedDict): + field_a: str + field_b: Optional[str] + + +class NewActionResponse(TypedDict): + id: str + status: str +``` + +### Step 2: Add the method to BOTH resource classes + +**Async (in `{EntityName}Resource`):** +```python +async def new_action(self, data: NewActionInput) -> BlindpayApiResponse[NewActionResponse]: + return await self._client.post(f"/instances/{self._instance_id}/{api_path}/action", data) +``` + +**Sync (in `{EntityName}ResourceSync`):** +```python +def new_action(self, data: NewActionInput) -> BlindpayApiResponse[NewActionResponse]: + return self._client.post(f"/instances/{self._instance_id}/{api_path}/action", data) +``` + +### Step 3: Export from `__init__.py` + +Add the new TypedDicts to both the import and `__all__` list in the resource's `__init__.py`. + +### Step 4: Add tests for both async and sync + +### Common method patterns + +**GET with query params:** +```python +async def list(self, params: Optional[ListInput] = None) -> BlindpayApiResponse[ListResponse]: + 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}/{path}{query_string}") +``` + +**POST with body:** +```python +async def create(self, data: CreateInput) -> BlindpayApiResponse[CreateResponse]: + return await self._client.post(f"/instances/{self._instance_id}/{path}", data) +``` + +**Method that extracts an ID from input to build the URL:** +```python +async def update(self, data: UpdateInput) -> BlindpayApiResponse[None]: + entity_id = data["entity_id"] + payload = {k: v for k, v in data.items() if k != "entity_id"} + return await self._client.put(f"/instances/{self._instance_id}/{path}/{entity_id}", payload) +``` + +**GET with path param:** +```python +async def get(self, entity_id: str) -> BlindpayApiResponse[Entity]: + return await self._client.get(f"/instances/{self._instance_id}/{path}/{entity_id}") +``` + +--- + +## 5. How to modify types + +### Add a field to a TypedDict + +Simply add the new field. Use `Optional[X]` if the field can be null in API responses. + +```python +class Payout(TypedDict): + # ... existing fields ... + new_field: str # required, always present + another_field: Optional[str] # nullable +``` + +### Remove a field from a TypedDict + +Delete the line. Search the codebase for any code that references the removed field (e.g., `data["removed_field"]`). + +### Rename a field + +1. Change the field name in the TypedDict. +2. Search for all references to the old name across the codebase. +3. If the API uses a different name than the Python field (e.g., `from` is reserved), use a renamed field and convert in the method body. See `quotes.py` for the `from_currency` -> `from` pattern. + +### Add a new shared Literal type + +Add to `src/blindpay/types.py`: +```python +NewType = Literal["value_a", "value_b", "value_c"] +``` + +Then export from `src/blindpay/__init__.py`: add to both the import block and the `__all__` list. + +### Add a resource-local Literal type + +Define it at the top of the resource file, after imports: +```python +LocalType = Literal["option_a", "option_b"] +``` + +--- + +## 6. How to remove a resource + +1. Delete the `src/blindpay/resources/{resource_name}/` directory. +2. Remove the import and `__all__` entry from `src/blindpay/resources/__init__.py`. +3. Remove the `TYPE_CHECKING` import from `client.py`. +4. Remove the `@cached_property` from BOTH `BlindPay` and `BlindPaySync` in `client.py`. +5. If the resource was part of a namespace, also remove the `@cached_property` from the corresponding `_*Namespace` and `_*NamespaceSync` classes. +6. Remove any re-exports from `src/blindpay/__init__.py` if applicable. +7. Delete `tests/resources/test_{resource_name}.py`. + +--- + +## 7. How to add a sub-resource (namespace pattern) + +Sub-resources are accessed like `client.payins.quotes.create(...)`. This uses a namespace class with `__getattr__` delegation. + +### Step 1: Create the sub-resource module + +Add the sub-resource file inside the parent resource directory: +`src/blindpay/resources/{parent}/{sub_resource}.py` + +Follow the same pattern as any resource file (types + async class + sync class + factory functions). + +### Step 2: Update the parent `__init__.py` + +Add imports from the new sub-resource module. + +### Step 3: Create namespace classes in `client.py` + +**Async namespace:** +```python +class _{ParentName}Namespace: + def __init__(self, instance_id: str, api_client: ApiClientImpl) -> None: + self._instance_id = instance_id + self._api = api_client + + @cached_property + def _base(self) -> "{ParentName}Resource": + from blindpay.resources.{parent}.{parent} import create_{parent}_resource + + return create_{parent}_resource(self._instance_id, self._api) + + @cached_property + def {sub_resource}(self) -> "{SubResourceName}Resource": + from blindpay.resources.{parent}.{sub_resource} import create_{sub_resource}_resource + + return create_{sub_resource}_resource(self._instance_id, self._api) + + def __getattr__(self, name: str) -> Any: + return getattr(self._base, name) +``` + +**Sync namespace:** +```python +class _{ParentName}NamespaceSync: + def __init__(self, instance_id: str, api_client: ApiClientImplSync) -> None: + self._instance_id = instance_id + self._api = api_client + + @cached_property + def _base(self) -> "{ParentName}ResourceSync": + from blindpay.resources.{parent}.{parent} import create_{parent}_resource_sync + + return create_{parent}_resource_sync(self._instance_id, self._api) + + @cached_property + def {sub_resource}(self) -> "{SubResourceName}ResourceSync": + from blindpay.resources.{parent}.{sub_resource} import create_{sub_resource}_resource_sync + + return create_{sub_resource}_resource_sync(self._instance_id, self._api) + + def __getattr__(self, name: str) -> Any: + return getattr(self._base, name) +``` + +The `__getattr__` method delegates unknown attribute access to `_base`, so `client.payins.list()` works -- it forwards to `PayinsResource.list()`. Meanwhile `client.payins.quotes.create()` hits the explicit `quotes` cached_property. + +### Step 4: Update the client class + +Change the `@cached_property` return type from the resource to the namespace: + +```python +@cached_property +def {parent}(self) -> _{ParentName}Namespace: + return _{ParentName}Namespace(self._instance_id, self._api) +``` + +Do the same in the sync client with `_{ParentName}NamespaceSync`. + +### Step 5: Add TYPE_CHECKING imports + +Add the sub-resource types to the `TYPE_CHECKING` block in `client.py`. + +### Namespace without a base resource (wallets pattern) + +The `_WalletsNamespace` has no `_base` and no `__getattr__`. It is purely a grouping: + +```python +class _WalletsNamespace: + def __init__(self, instance_id: str, api_client: ApiClientImpl) -> None: + self._instance_id = instance_id + self._api = api_client + + @cached_property + def blockchain(self) -> "BlockchainWalletsResource": + from blindpay.resources.wallets.blockchain import create_blockchain_wallets_resource + + return create_blockchain_wallets_resource(self._instance_id, self._api) + + @cached_property + def offramp(self) -> "OfframpWalletsResource": + from blindpay.resources.wallets.offramp import create_offramp_wallets_resource + + 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) +``` + +Note: `custodial` imports from `custodial_wallets` package (not from `wallets/`). Use this pattern when the parent is not itself an API resource, only a namespace. + +--- + +## 8. Testing + +### Run tests + +```bash +# All tests +pytest tests/ -v + +# Specific resource +pytest tests/resources/test_payins.py -v + +# Only async tests +pytest tests/ -v -k "not Sync" + +# Only sync tests +pytest tests/ -v -k "Sync" +``` + +### Linting and type checking + +```bash +# Format +ruff format src/ + +# Lint +ruff check src/ + +# Lint with auto-fix +ruff check src/ --fix + +# Type check +mypy src/blindpay/ +pyright src/blindpay/ +``` + +### Test file pattern + +- One test file per resource: `tests/resources/test_{resource_name}.py`. +- Two classes per file: `Test{Name}` (async) and `Test{Name}Sync` (sync). +- Each class has `@pytest.fixture(autouse=True) def setup(self)` that creates the client. +- Async class uses `BlindPay`, sync class uses `BlindPaySync`. +- Mock target: `patch.object(self.blindpay._api, "_request")`. +- Mock return: `{"data": , "error": None}` for success. +- Assert: `response["error"] is None`, `response["data"] == expected`, `mock_request.assert_called_once_with(, )`. +- For POST/PUT/PATCH methods, assert the body as well: `mock_request.assert_called_once_with("POST", "/path", {body_dict})`. + +--- + +## 9. Versioning + +### release-please with conventional commits + +The project uses [release-please](https://github.com/googleapis/release-please) for automated versioning. + +**Config:** `release-please-config.json` +- Release type: `python` +- Tags include `v` prefix (e.g., `v1.3.0`) +- Bumps version in `src/blindpay/__init__.py` (via `extra-files`) + +**Manifest:** `.release-please-manifest.json` +- Tracks the current version at the root path `.` + +**Version is stored in TWO places** (keep them in sync for local dev, release-please handles it in CI): +- `pyproject.toml` -> `[project].version` +- `src/blindpay/__init__.py` -> `__version__` +- `src/blindpay/client.py` -> `__version__` (duplicated, used in User-Agent header) + +### Commit message format + +Use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: add invoices resource +feat(payouts): add submit_documents method +fix(types): correct Currency literal values +chore: update dependencies +docs: update README examples +refactor(client): simplify namespace initialization +``` + +- `feat:` triggers a MINOR version bump. +- `fix:` triggers a PATCH version bump. +- `feat!:` or `BREAKING CHANGE:` in the footer triggers a MAJOR bump. + +--- + +## 10. OpenAPI to SDK mapping rules + +### Path to resource mapping + +| API path pattern | SDK access pattern | Resource location | +|---|---|---| +| `/available/*` | `client.available.method()` | `resources/available/available.py` | +| `/instances/{id}/{resource}` | `client.{resource}.method()` | `resources/{resource}/{resource}.py` | +| `/instances/{id}/{resource}/{rid}/sub` | `client.{resource}.method(data)` where data contains `{rid}` | Same resource file, ID extracted from input | +| `/instances/{id}/payin-quotes` | `client.payins.quotes.method()` | `resources/payins/quotes.py` (sub-resource) | +| `/e/{resource}/{id}` | `client.{resource}.get_track(id)` | Same resource file (external tracking endpoint) | + +### HTTP method to class method mapping + +| HTTP method | SDK method name | Notes | +|---|---|---| +| `GET /resources` | `list()` | Optional params TypedDict, builds query string | +| `GET /resources/{id}` | `get(id)` | Takes ID as string param | +| `POST /resources` | `create(data)` | Takes input TypedDict | +| `PUT /resources/{id}` | `update(data)` | ID extracted from TypedDict, excluded from payload | +| `PATCH /resources/{id}` | `update(data)` | Same as PUT | +| `DELETE /resources/{id}` | `delete(id)` | Takes ID as string param | +| `POST /resources/{id}/action` | `action_name(data)` | Descriptive method name | +| `GET /export/resources` | `export(params)` | Query string params | + +### Schema to TypedDict mapping + +| OpenAPI schema concept | Python representation | +|---|---| +| Object schema | `class MyType(TypedDict)` | +| String property | `field: str` | +| Number property | `field: float` | +| Integer property | `field: int` | +| Boolean property | `field: bool` | +| Nullable property | `field: Optional[str]` | +| String enum | `MyEnum = Literal["a", "b", "c"]` | +| Array of objects | `List[MyType]` | +| Nested object | Separate TypedDict, referenced by name | +| Optional input field | Use `total=False` on the TypedDict or `Optional[X]` per field | +| Paginated list response | TypedDict with `data: List[Entity]` + `pagination: PaginationMetadata` | +| Request body | `{Action}Input(TypedDict)` | +| Response body | `{Action}Response(TypedDict)` | + +### API path parameter handling in methods + +When a method needs to extract an ID from the input to build a URL path: +```python +async def action(self, data: ActionInput) -> BlindpayApiResponse[ActionResponse]: + entity_id = data["entity_id"] + payload = {k: v for k, v in data.items() if k != "entity_id"} + return await self._client.post(f"/instances/{self._instance_id}/path/{entity_id}/action", payload) +``` + +When multiple IDs need extraction: +```python +async def action(self, data: ActionInput) -> BlindpayApiResponse[ActionResponse]: + parent_id = data["parent_id"] + child_id = data["child_id"] + payload = {k: v for k, v in data.items() if k not in ["parent_id", "child_id"]} + return await self._client.post(f"/instances/{self._instance_id}/parents/{parent_id}/children/{child_id}", payload) +``` + +### Python reserved word handling + +If an API field name clashes with a Python keyword, rename the field and convert in the method body: +```python +# In TypedDict: use suffixed name +class MyInput(TypedDict): + from_currency: Currency # API field is "from" + +# In method: convert back +async def method(self, data: MyInput) -> BlindpayApiResponse[MyResponse]: + payload = { + "from": data["from_currency"], + "to": data["to"], + } + return await self._client.post(f"/path", payload) +``` + +--- + +## Checklist for any change + +- [ ] Types defined in the resource file (not `types.py`) unless shared across resources +- [ ] BOTH async and sync classes updated with identical logic +- [ ] Factory functions (async + sync) present at bottom of resource file +- [ ] Resource `__init__.py` re-exports all public names +- [ ] `resources/__init__.py` imports factory functions +- [ ] `client.py` has TYPE_CHECKING import + `@cached_property` in BOTH client classes +- [ ] Tests cover both async and sync variants +- [ ] `ruff format src/` and `ruff check src/` pass diff --git a/pyproject.toml b/pyproject.toml index cdaf9e8..bfec037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blindpay" -version = "1.3.0" +version = "1.4.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 f6c4c1c..5b7295b 100644 --- a/src/blindpay/__init__.py +++ b/src/blindpay/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.3.0" +__version__ = "1.4.0" from ._internal.exceptions import BlindPayError from .client import BlindPay, BlindPaySync diff --git a/src/blindpay/client.py b/src/blindpay/client.py index e2d0c00..bbb99a4 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__ = "1.3.0" +__version__ = "1.4.0" T = TypeVar("T") diff --git a/src/blindpay/resources/available/available.py b/src/blindpay/resources/available/available.py index 29452ec..e74f0e9 100644 --- a/src/blindpay/resources/available/available.py +++ b/src/blindpay/resources/available/available.py @@ -8,7 +8,11 @@ BankDetailKey = Literal[ "type", "name", + "status", + "recipient_relationship", + "swift_payment_code", "pix_key", + "force_cpf_cnpj", "beneficiary_name", "routing_number", "account_number", @@ -22,6 +26,10 @@ "postal_code", "checkbook_account_id", "checkbook_user_key", + "onemoney_external_account_id", + "pix_safe_bank_code", + "pix_safe_branch_code", + "pix_safe_cpf_cnpj", "spei_protocol", "spei_institution_code", "spei_clabe", @@ -54,6 +62,10 @@ "swift_intermediary_bank_account_number_iban", "swift_intermediary_bank_name", "swift_intermediary_bank_country", + "business_industry", + "phone_number", + "tax_id", + "date_of_birth", ] @@ -68,7 +80,7 @@ class BankDetail(TypedDict): regex: str key: BankDetailKey items: Optional[List[BankDetailItem]] - required: bool + required: Optional[bool] class RailInfo(TypedDict): @@ -96,8 +108,8 @@ class SwiftCodeBankDetail(TypedDict): class NaicsCode(TypedDict): - code: str - title: str + label: str + value: str GetNaicsCodesResponse = List[NaicsCode] diff --git a/src/blindpay/resources/bank_accounts/bank_accounts.py b/src/blindpay/resources/bank_accounts/bank_accounts.py index 11586f5..dfde1c5 100644 --- a/src/blindpay/resources/bank_accounts/bank_accounts.py +++ b/src/blindpay/resources/bank_accounts/bank_accounts.py @@ -15,6 +15,7 @@ ArgentinaTransfers = Literal["CVU", "CBU", "ALIAS"] AchCopDocument = Literal["CC", "CE", "NIT", "PASS", "PEP"] +BankAccountStatus = Literal["verifying", "approved", "rejected", "deprecated"] OfframpNetwork = Literal["tron"] PixType = Literal["pix"] TransfersBitsoType = Literal["transfers_bitso"] @@ -84,26 +85,24 @@ class BankAccount(TypedDict): swift_intermediary_bank_country: Optional[Country] tron_wallet_hash: Optional[str] offramp_wallets: Optional[List[OfframpWallet]] - created_at: str + status: Optional[BankAccountStatus] + recipient_relationship: Optional[RecipientRelationship] + swift_payment_code: Optional[str] + business_industry: Optional[str] + phone_number: Optional[str] + tax_id: Optional[str] + date_of_birth: Optional[str] + pix_safe_bank_code: Optional[str] + pix_safe_branch_code: Optional[str] + pix_safe_cpf_cnpj: Optional[str] + created_at: Optional[str] class ListBankAccountsResponse(TypedDict): data: List[BankAccount] -class GetBankAccountResponse(TypedDict): - id: str - receiver_id: str - account_holder_name: str - account_number: str - routing_number: str - account_type: BankAccountType - bank_name: str - swift_code: Optional[str] - iban: Optional[str] - is_primary: bool - created_at: str - updated_at: str +GetBankAccountResponse = BankAccount class CreatePixInput(TypedDict): diff --git a/src/blindpay/resources/fees/fees.py b/src/blindpay/resources/fees/fees.py index 49967fd..8887cfb 100644 --- a/src/blindpay/resources/fees/fees.py +++ b/src/blindpay/resources/fees/fees.py @@ -7,17 +7,33 @@ class FeeOptions(TypedDict): - payin_flat: float - payin_percentage: float - payout_flat: float - payout_percentage: float + payin_flat: int + payin_percentage: int + payout_flat: int + payout_percentage: int class FeesResponse(TypedDict): - ach: Optional[FeeOptions] - domestic_wire: Optional[FeeOptions] - pix: Optional[FeeOptions] - solana: Optional[FeeOptions] + id: str + instance_id: str + ach: FeeOptions + domestic_wire: FeeOptions + rtp: FeeOptions + international_swift: FeeOptions + pix: FeeOptions + pix_safe: FeeOptions + ach_colombia: FeeOptions + transfers_3: FeeOptions + spei: FeeOptions + tron: FeeOptions + ethereum: FeeOptions + polygon: FeeOptions + base: FeeOptions + arbitrum: FeeOptions + stellar: FeeOptions + solana: FeeOptions + created_at: Optional[str] + updated_at: Optional[str] class FeesResource: diff --git a/src/blindpay/resources/payins/payins.py b/src/blindpay/resources/payins/payins.py index 3f4e949..a1c2999 100644 --- a/src/blindpay/resources/payins/payins.py +++ b/src/blindpay/resources/payins/payins.py @@ -1,4 +1,4 @@ -from typing import List, Optional, TypedDict +from typing import Any, Dict, List, Optional, TypedDict from urllib.parse import urlencode from ..._internal.api_client import InternalApiClient, InternalApiClientSync @@ -64,31 +64,39 @@ class Payin(TypedDict): status: TransactionStatus payin_quote_id: str instance_id: str - tracking_transaction: Optional[TrackingTransaction] - tracking_payment: Optional[TrackingPayment] - tracking_complete: Optional[TrackingComplete] + partner_fee: Optional[int] + tracking_transaction: TrackingTransaction + tracking_payment: TrackingPayment + tracking_complete: TrackingComplete tracking_partner_fee: Optional[TrackingPartnerFee] - created_at: str - updated_at: str + created_at: Optional[str] + updated_at: Optional[str] image_url: Optional[str] first_name: Optional[str] last_name: Optional[str] legal_name: Optional[str] type: str payment_method: str - sender_amount: float - receiver_amount: float + sender_amount: Optional[float] + receiver_amount: Optional[float] token: StablecoinToken - partner_fee_amount: float - total_fee_amount: float - commercial_quotation: float - blindpay_quotation: float + partner_fee_amount: Optional[float] + total_fee_amount: Optional[float] + commercial_quotation: Optional[float] + blindpay_quotation: Optional[float] currency: str - billing_fee: float + billing_fee_amount: Optional[float] + transaction_fee_amount: Optional[float] + is_otc: Optional[bool] + payer_rules: Optional[Dict[str, Any]] name: str - address: str + address: Optional[str] network: Network - blindpay_bank_details: BankDetails + blindpay_bank_details: Optional[BankDetails] + pse_payment_link: Optional[str] + pse_full_name: Optional[str] + pse_tax_id: Optional[str] + pse_document_type: Optional[str] class ListPayinsInput(PaginationParams): @@ -125,40 +133,7 @@ class GetPayinTrackingTransaction(TypedDict): description: str -class GetPayinTrackResponse(TypedDict): - receiver_id: str - id: str - pix_code: str - memo_code: str - clabe: str - status: str - payin_quote_id: str - instance_id: str - tracking_transaction: TrackingTransaction - tracking_payment: TrackingPayment - tracking_complete: TrackingComplete - tracking_partner_fee: TrackingPartnerFee - created_at: str - updated_at: str - image_url: str - first_name: str - last_name: str - legal_name: str - type: str - payment_method: str - sender_amount: float - receiver_amount: float - token: StablecoinToken - partner_fee_amount: float - total_fee_amount: float - commercial_quotation: float - blindpay_quotation: float - currency: str - billing_fee: float - name: str - address: str - network: Network - blindpay_bank_details: BankDetails +GetPayinTrackResponse = Payin class ExportPayinsInput(TypedDict): @@ -176,13 +151,18 @@ class CreateEvmPayinResponse(TypedDict): pix_code: Optional[str] memo_code: Optional[str] clabe: Optional[str] - tracking_complete: Optional[TrackingComplete] - tracking_payment: Optional[TrackingPayment] - tracking_transaction: Optional[TrackingTransaction] + partner_fee: Optional[int] + tracking_complete: TrackingComplete + tracking_payment: TrackingPayment + tracking_transaction: TrackingTransaction tracking_partner_fee: Optional[TrackingPartnerFee] + billing_fee_amount: Optional[float] + transaction_fee_amount: Optional[float] blindpay_bank_details: BankDetails - receiver_id: str - receiver_amount: float + receiver_id: Optional[str] + receiver_amount: Optional[float] + payment_method: Optional[str] + sender_amount: Optional[float] class PayinsResource: @@ -206,6 +186,9 @@ async def export(self, params: ExportPayinsInput) -> BlindpayApiResponse[ExportP query_string = f"?{urlencode(filtered_params)}" if filtered_params else "" return await self._client.get(f"/instances/{self._instance_id}/export/payins{query_string}") + async def get_track(self, payin_id: str) -> BlindpayApiResponse[GetPayinTrackResponse]: + return await self._client.get(f"/e/payins/{payin_id}") + async def create_evm(self, payin_quote_id: str) -> BlindpayApiResponse[CreateEvmPayinResponse]: return await self._client.post(f"/instances/{self._instance_id}/payins/evm", {"payin_quote_id": payin_quote_id}) diff --git a/src/blindpay/resources/payins/quotes.py b/src/blindpay/resources/payins/quotes.py index 3b93565..3cdc074 100644 --- a/src/blindpay/resources/payins/quotes.py +++ b/src/blindpay/resources/payins/quotes.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Optional +from typing import List, Optional, Union from typing_extensions import TypedDict @@ -7,43 +7,49 @@ BlindpayApiResponse, Currency, CurrencyType, + PaymentMethod, StablecoinToken, ) -PaymentMethod = Literal["ach", "wire", "pix", "spei"] - class PayerRules(TypedDict, total=False): pix_allowed_tax_ids: List[str] -class CreatePayinQuoteInput(TypedDict): - blockchain_wallet_id: str +class _CreatePayinQuoteInputRequired(TypedDict): currency_type: CurrencyType payment_method: PaymentMethod - request_amount: float + request_amount: int token: StablecoinToken - cover_fees: bool + + +class CreatePayinQuoteInput(_CreatePayinQuoteInputRequired, total=False): + blockchain_wallet_id: Optional[str] + wallet_id: Optional[str] + cover_fees: Optional[bool] partner_fee_id: Optional[str] - payer_rules: PayerRules + payer_rules: Optional[PayerRules] + is_otc: Optional[bool] class CreatePayinQuoteResponse(TypedDict): id: str - expires_at: int - commercial_quotation: float - blindpay_quotation: float - receiver_amount: float - sender_amount: float + expires_at: Optional[float] + commercial_quotation: Optional[float] + blindpay_quotation: Optional[float] + receiver_amount: Optional[float] + sender_amount: Optional[float] partner_fee_amount: Optional[float] flat_fee: Optional[float] + billing_fee_amount: Optional[float] + is_otc: Optional[bool] class GetPayinFxRateInput(TypedDict): currency_type: CurrencyType - from_currency: Currency # 'from' is a reserved keyword in Python - to: Currency - request_amount: float + from_currency: Union[StablecoinToken, Currency] # 'from' is a reserved keyword in Python + to: Union[StablecoinToken, Currency] + request_amount: int class GetPayinFxRateResponse(TypedDict): diff --git a/src/blindpay/resources/payouts/__init__.py b/src/blindpay/resources/payouts/__init__.py index 85905a2..6034518 100644 --- a/src/blindpay/resources/payouts/__init__.py +++ b/src/blindpay/resources/payouts/__init__.py @@ -3,6 +3,9 @@ AuthorizeStellarTokenResponse, CreateEvmPayoutInput, CreateEvmPayoutResponse, + CreatePayoutResponse, + CreateSolanaPayoutInput, + CreateSolanaPayoutResponse, CreateStellarPayoutInput, CreateStellarPayoutResponse, ExportPayoutsInput, @@ -12,6 +15,8 @@ Payout, PayoutsResource, PayoutsResourceSync, + SubmitPayoutDocumentsInput, + SubmitPayoutDocumentsResponse, create_payouts_resource, create_payouts_resource_sync, ) @@ -28,8 +33,13 @@ "ExportPayoutsResponse", "AuthorizeStellarTokenInput", "AuthorizeStellarTokenResponse", + "CreatePayoutResponse", "CreateStellarPayoutInput", "CreateStellarPayoutResponse", + "CreateSolanaPayoutInput", + "CreateSolanaPayoutResponse", "CreateEvmPayoutInput", "CreateEvmPayoutResponse", + "SubmitPayoutDocumentsInput", + "SubmitPayoutDocumentsResponse", ] diff --git a/src/blindpay/resources/payouts/payouts.py b/src/blindpay/resources/payouts/payouts.py index 2b9eaaf..c090db9 100644 --- a/src/blindpay/resources/payouts/payouts.py +++ b/src/blindpay/resources/payouts/payouts.py @@ -120,22 +120,40 @@ class AuthorizeStellarTokenResponse(TypedDict): transaction_hash: str +class CreatePayoutResponse(TypedDict): + id: str + status: TransactionStatus + sender_wallet_address: str + billing_fee_amount: Optional[float] + transaction_fee_amount: Optional[float] + partner_fee: Optional[int] + tracking_complete: TrackingComplete + tracking_payment: TrackingPayment + tracking_transaction: TrackingTransaction + tracking_partner_fee: Optional[TrackingPartnerFee] + tracking_liquidity: Optional[TrackingLiquidity] + tracking_documents: Optional[TrackingPartnerFee] + receiver_id: Optional[str] + bank_account_id: Optional[str] + offramp_wallet_id: Optional[str] + + class CreateStellarPayoutInput(TypedDict): quote_id: str sender_wallet_address: str signed_transaction: Optional[str] -class CreateStellarPayoutResponse(TypedDict): - id: str - status: TransactionStatus +CreateStellarPayoutResponse = CreatePayoutResponse + + +class CreateSolanaPayoutInput(TypedDict): + quote_id: str sender_wallet_address: str - tracking_complete: Optional[TrackingComplete] - tracking_payment: Optional[TrackingPayment] - tracking_transaction: Optional[TrackingTransaction] - tracking_partner_fee: Optional[TrackingPartnerFee] - tracking_liquidity: Optional[TrackingLiquidity] - receiver_id: str + signed_transaction: Optional[str] + + +CreateSolanaPayoutResponse = CreatePayoutResponse class CreateEvmPayoutInput(TypedDict): @@ -143,16 +161,7 @@ class CreateEvmPayoutInput(TypedDict): sender_wallet_address: str -class CreateEvmPayoutResponse(TypedDict): - id: str - status: TransactionStatus - sender_wallet_address: str - tracking_complete: Optional[TrackingComplete] - tracking_payment: Optional[TrackingPayment] - tracking_transaction: Optional[TrackingTransaction] - tracking_partner_fee: Optional[TrackingPartnerFee] - tracking_liquidity: Optional[TrackingLiquidity] - receiver_id: str +CreateEvmPayoutResponse = CreatePayoutResponse class SubmitPayoutDocumentsInput(TypedDict): @@ -164,7 +173,7 @@ class SubmitPayoutDocumentsInput(TypedDict): class SubmitPayoutDocumentsResponse(TypedDict): - id: str + success: bool class PayoutsResource: @@ -202,6 +211,9 @@ async def authorize_stellar_token( async def create_stellar(self, data: CreateStellarPayoutInput) -> BlindpayApiResponse[CreateStellarPayoutResponse]: return await self._client.post(f"/instances/{self._instance_id}/payouts/stellar", data) + async def create_solana(self, data: CreateSolanaPayoutInput) -> BlindpayApiResponse[CreateSolanaPayoutResponse]: + return await self._client.post(f"/instances/{self._instance_id}/payouts/solana", data) + async def create_evm(self, data: CreateEvmPayoutInput) -> BlindpayApiResponse[CreateEvmPayoutResponse]: return await self._client.post(f"/instances/{self._instance_id}/payouts/evm", data) @@ -248,6 +260,9 @@ def authorize_stellar_token( def create_stellar(self, data: CreateStellarPayoutInput) -> BlindpayApiResponse[CreateStellarPayoutResponse]: return self._client.post(f"/instances/{self._instance_id}/payouts/stellar", data) + def create_solana(self, data: CreateSolanaPayoutInput) -> BlindpayApiResponse[CreateSolanaPayoutResponse]: + return self._client.post(f"/instances/{self._instance_id}/payouts/solana", data) + def create_evm(self, data: CreateEvmPayoutInput) -> BlindpayApiResponse[CreateEvmPayoutResponse]: return self._client.post(f"/instances/{self._instance_id}/payouts/evm", data) diff --git a/src/blindpay/resources/quotes/quotes.py b/src/blindpay/resources/quotes/quotes.py index b1f362e..0d28b98 100644 --- a/src/blindpay/resources/quotes/quotes.py +++ b/src/blindpay/resources/quotes/quotes.py @@ -30,36 +30,37 @@ class Contract(TypedDict): class CreateQuoteInput(TypedDict): bank_account_id: str currency_type: CurrencyType - network: Optional[Network] - request_amount: float - token: Optional[StablecoinToken] + request_amount: int + network: Network + token: StablecoinToken cover_fees: Optional[bool] description: Optional[str] partner_fee_id: Optional[str] transaction_document_file: Optional[str] transaction_document_id: Optional[str] - transaction_document_type: TransactionDocumentType + transaction_document_type: Optional[TransactionDocumentType] class CreateQuoteResponse(TypedDict): id: str - expires_at: int - commercial_quotation: float - blindpay_quotation: float - receiver_amount: float - sender_amount: float - partner_fee_amount: float - flat_fee: float - contract: Contract - receiver_local_amount: float - description: str + expires_at: Optional[float] + commercial_quotation: Optional[float] + blindpay_quotation: Optional[float] + receiver_amount: Optional[float] + sender_amount: Optional[float] + partner_fee_amount: Optional[float] + flat_fee: Optional[float] + billing_fee_amount: Optional[float] + contract: Optional[Contract] + receiver_local_amount: Optional[float] + description: Optional[str] class GetFxRateInput(TypedDict): currency_type: CurrencyType from_currency: Currency # 'from' is reserved in Python to: Currency - request_amount: float + request_amount: int class GetFxRateResponse(TypedDict): diff --git a/src/blindpay/resources/transfers/__init__.py b/src/blindpay/resources/transfers/__init__.py index af56673..46f74e3 100644 --- a/src/blindpay/resources/transfers/__init__.py +++ b/src/blindpay/resources/transfers/__init__.py @@ -5,11 +5,10 @@ CreateTransferResponse, GetTransferResponse, ListTransfersResponse, + TrackingStep, Transfer, - TransferQuote, TransfersResource, TransfersResourceSync, - TransferTrackingStep, create_transfers_resource, create_transfers_resource_sync, ) @@ -20,8 +19,7 @@ "TransfersResource", "TransfersResourceSync", "Transfer", - "TransferQuote", - "TransferTrackingStep", + "TrackingStep", "CreateTransferQuoteInput", "CreateTransferQuoteResponse", "CreateTransferInput", diff --git a/src/blindpay/resources/transfers/transfers.py b/src/blindpay/resources/transfers/transfers.py index 671ebe3..c6452b2 100644 --- a/src/blindpay/resources/transfers/transfers.py +++ b/src/blindpay/resources/transfers/transfers.py @@ -4,23 +4,20 @@ from typing_extensions import TypedDict from ..._internal.api_client import InternalApiClient, InternalApiClientSync -from ...types import BlindpayApiResponse, Currency, PaginationMetadata, PaginationParams, TransactionStatus +from ...types import ( + BlindpayApiResponse, + Currency, + PaginationMetadata, + PaginationParams, + TransactionStatus, +) -class TransferTrackingStep(TypedDict): +class TrackingStep(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 @@ -30,9 +27,9 @@ class Transfer(TypedDict): destination_wallet_id: str amount: float currency: Currency - tracking_transaction: TransferTrackingStep - tracking_transaction_monitoring: TransferTrackingStep - tracking_complete: TransferTrackingStep + tracking_transaction: TrackingStep + tracking_transaction_monitoring: TrackingStep + tracking_complete: TrackingStep created_at: str updated_at: str @@ -43,7 +40,13 @@ class CreateTransferQuoteInput(TypedDict): amount: float -CreateTransferQuoteResponse = TransferQuote +class CreateTransferQuoteResponse(TypedDict): + id: str + amount: float + currency: Currency + fee_amount: float + source_wallet_id: str + destination_wallet_id: str class CreateTransferInput(TypedDict): @@ -51,6 +54,7 @@ class CreateTransferInput(TypedDict): CreateTransferResponse = Transfer + GetTransferResponse = Transfer diff --git a/src/blindpay/types.py b/src/blindpay/types.py index 73f503e..e2684ac 100644 --- a/src/blindpay/types.py +++ b/src/blindpay/types.py @@ -369,12 +369,12 @@ class TrackingPartnerFee(TypedDict): "first_party", "employee", "independent_contractor", - "vendor_supplier", - "client_customer", - "subsidiary_affiliate", - "joint_venture_partner", - "landlord_property_manager", - "government_agency", + "vendor_or_supplier", + "subsidiary_or_affiliate", + "merchant_or_partner", + "customer", + "landlord", + "family", "other", ] diff --git a/tests/resources/test_payouts.py b/tests/resources/test_payouts.py index a39bcd2..cf2eaaa 100644 --- a/tests/resources/test_payouts.py +++ b/tests/resources/test_payouts.py @@ -517,7 +517,7 @@ async def test_create_stellar_payout(self): ) assert response["error"] is None - assert response["data"] == mocked_stellar_payout + assert response["data"] == mocked_stellar_payout # type: ignore[comparison-overlap] mock_request.assert_called_once_with( "POST", "/instances/in_000000000000/payouts/stellar", @@ -580,7 +580,7 @@ async def test_create_evm_payout(self): ) assert response["error"] is None - assert response["data"] == mocked_evm_payout + assert response["data"] == mocked_evm_payout # type: ignore[comparison-overlap] mock_request.assert_called_once_with( "POST", "/instances/in_000000000000/payouts/evm", @@ -1097,7 +1097,7 @@ def test_create_stellar_payout(self): ) assert response["error"] is None - assert response["data"] == mocked_stellar_payout + assert response["data"] == mocked_stellar_payout # type: ignore[comparison-overlap] mock_request.assert_called_once_with( "POST", "/instances/in_000000000000/payouts/stellar", @@ -1159,7 +1159,7 @@ def test_create_evm_payout(self): ) assert response["error"] is None - assert response["data"] == mocked_evm_payout + assert response["data"] == mocked_evm_payout # type: ignore[comparison-overlap] mock_request.assert_called_once_with( "POST", "/instances/in_000000000000/payouts/evm", diff --git a/tests/resources/test_quotes.py b/tests/resources/test_quotes.py index 9e8ed63..29e4e67 100644 --- a/tests/resources/test_quotes.py +++ b/tests/resources/test_quotes.py @@ -22,6 +22,7 @@ async def test_create_quote(self): "sender_amount": 1010, "partner_fee_amount": 150, "flat_fee": 50, + "billing_fee_amount": None, "contract": { "abi": [{}], "address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", @@ -127,6 +128,7 @@ def test_create_quote(self): "sender_amount": 1010, "partner_fee_amount": 150, "flat_fee": 50, + "billing_fee_amount": None, "contract": { "abi": [{}], "address": "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",