branta 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- branta-3.2.0/.github/workflows/tests.yml +39 -0
- branta-3.2.0/.gitignore +17 -0
- branta-3.2.0/CHANGELOG.md +28 -0
- branta-3.2.0/CLAUDE.md +49 -0
- branta-3.2.0/LICENSE +21 -0
- branta-3.2.0/PKG-INFO +152 -0
- branta-3.2.0/README.md +124 -0
- branta-3.2.0/branta/__init__.py +45 -0
- branta-3.2.0/branta/enums.py +23 -0
- branta-3.2.0/branta/exceptions.py +6 -0
- branta-3.2.0/branta/extensions.py +97 -0
- branta-3.2.0/branta/models.py +58 -0
- branta-3.2.0/branta/options.py +14 -0
- branta-3.2.0/branta/v2/__init__.py +26 -0
- branta-3.2.0/branta/v2/builder.py +57 -0
- branta-3.2.0/branta/v2/client.py +163 -0
- branta-3.2.0/branta/v2/encryption.py +58 -0
- branta-3.2.0/branta/v2/parser.py +162 -0
- branta-3.2.0/branta/v2/secret_generator.py +10 -0
- branta-3.2.0/branta/v2/serialization.py +109 -0
- branta-3.2.0/branta/v2/service.py +290 -0
- branta-3.2.0/example.py +40 -0
- branta-3.2.0/pyproject.toml +50 -0
- branta-3.2.0/tests/__init__.py +0 -0
- branta-3.2.0/tests/test_builder.py +172 -0
- branta-3.2.0/tests/test_encryption.py +60 -0
- branta-3.2.0/tests/test_extensions.py +121 -0
- branta-3.2.0/tests/test_integration.py +213 -0
- branta-3.2.0/tests/test_qr_parser.py +181 -0
- branta-3.2.0/tests/test_service.py +825 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pip install -e ".[dev]"
|
|
26
|
+
|
|
27
|
+
- name: Run tests with coverage
|
|
28
|
+
run: |
|
|
29
|
+
coverage run -m pytest tests/ --ignore=tests/test_integration.py -v
|
|
30
|
+
coverage report
|
|
31
|
+
coverage xml
|
|
32
|
+
env:
|
|
33
|
+
BRANTA_SKIP_INTEGRATION: "1"
|
|
34
|
+
|
|
35
|
+
- name: Upload coverage
|
|
36
|
+
uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: coverage-${{ matrix.python-version }}
|
|
39
|
+
path: coverage.xml
|
branta-3.2.0/.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [3.2.0] - 2026-06-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `PaymentBuilder.set_child_platform(name, logo_url, logo_light_url)` — allows master-switch platforms to associate a child platform with a payment on POST
|
|
11
|
+
- `Payment.child_platform` field (optional `Platform`) to hold the child platform data before submission
|
|
12
|
+
- Removed tracked `__pycache__` files and ensured they are covered by `.gitignore`
|
|
13
|
+
|
|
14
|
+
## [3.1.0] - 2026-06-25
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Initial release of the Branta Python SDK
|
|
18
|
+
- Feature-parity port of `branta-js` 3.1.4 and `branta-dotnet` 3.1.6
|
|
19
|
+
- `BrantaService` with `get_payments`, `get_payments_by_qr_code`, `add_payment`, and `is_api_key_valid`
|
|
20
|
+
- `PaymentBuilder` fluent builder for constructing `Payment` objects
|
|
21
|
+
- `QRParser` for parsing Bitcoin and Lightning URI QR codes
|
|
22
|
+
- AES-256-GCM encryption with deterministic and random nonce modes
|
|
23
|
+
- Zero-knowledge (ZK) destination support for Bitcoin addresses, BOLT-11, Ark, and silent payments
|
|
24
|
+
- Metadata DEK-envelope encryption
|
|
25
|
+
- `PrivacyMode.Strict` (default) and `PrivacyMode.Loose` enforcement
|
|
26
|
+
- HMAC-SHA256 signing support for parent platform use
|
|
27
|
+
- Full type annotations and async/await throughout
|
|
28
|
+
- Unit and integration tests with full branch coverage
|
branta-3.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Branta Python SDK
|
|
2
|
+
|
|
3
|
+
Feature-parity Python port of the Branta SDK. Mirrors `branta-js` and `branta-dotnet` — any public API change in one must be reflected here and in the other two.
|
|
4
|
+
|
|
5
|
+
## Package layout
|
|
6
|
+
|
|
7
|
+
- `branta/` — top-level package; re-exports all public symbols
|
|
8
|
+
- `branta/enums.py` — `BrantaServerBaseUrl`, `DestinationType`, `PrivacyMode`
|
|
9
|
+
- `branta/exceptions.py` — `BrantaPaymentException`, `QRParseException`
|
|
10
|
+
- `branta/models.py` — `Payment`, `Destination`, `Platform`, `PaymentsResult`, `AddPaymentResult`
|
|
11
|
+
- `branta/options.py` — `BrantaClientOptions`
|
|
12
|
+
- `branta/extensions.py` — shared helper functions (hash, fragment, type detection)
|
|
13
|
+
- `branta/v2/service.py` — `BrantaService` (orchestrates ZK encrypt/decrypt around HTTP)
|
|
14
|
+
- `branta/v2/client.py` — `BrantaClient` (raw HTTP; do not call directly from consumer code)
|
|
15
|
+
- `branta/v2/builder.py` — `PaymentBuilder`
|
|
16
|
+
- `branta/v2/parser.py` — `QRParser`
|
|
17
|
+
- `branta/v2/encryption.py` — `AesEncryption` (static), `AesEncryptionService` (instance wrapper)
|
|
18
|
+
- `branta/v2/secret_generator.py` — `GuidSecretGenerator`
|
|
19
|
+
- `branta/v2/serialization.py` — snake_case JSON serialization helpers
|
|
20
|
+
- `tests/` — mirrors `branta/`; unit tests use mocked client + AES; integration tests hit live endpoints
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e ".[dev]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Commands
|
|
29
|
+
|
|
30
|
+
- `pytest tests/ --ignore=tests/test_integration.py` — run unit tests (no network)
|
|
31
|
+
- `pytest tests/test_integration.py` — run integration tests (requires network)
|
|
32
|
+
- `coverage run -m pytest tests/ --ignore=tests/test_integration.py && coverage report` — with coverage
|
|
33
|
+
- `pip install -e .` / `pip publish` — publish to PyPI (requires `twine`)
|
|
34
|
+
|
|
35
|
+
## Key behaviors to preserve
|
|
36
|
+
|
|
37
|
+
- **`PrivacyMode.Strict` is the default.** `get_payments` throws `BrantaPaymentException` for plain bitcoin addresses; `get_payments_by_qr_code` returns an empty `PaymentsResult`. `add_payment` throws if any destination has `is_zk=False`.
|
|
38
|
+
- **`verify_url` is always returned**, even on a miss. Format: `{base_url}/v2/verify/{lookup}` plus `#k-{zk_id}={key}` fragments.
|
|
39
|
+
- **ZK encryption:** Bitcoin addresses use a random secret (GUID); hash-ZK types (bolt11, ark, silent_payment) use a deterministic key from SHA256(normalized value). `add_payment` mutates `payment.destinations[*].value` to the encrypted form before POSTing.
|
|
40
|
+
- **Metadata DEK-envelope:** If a payment has metadata and any ZK destination, a separate DEK is generated, metadata is encrypted with it, and the DEK is encrypted per-destination.
|
|
41
|
+
- **Never surface lookup failures.** Swallow decryption errors; leave `is_encrypted=True` and value unchanged.
|
|
42
|
+
- **Domain validation.** `platform_logo_url` must match `base_url` domain.
|
|
43
|
+
|
|
44
|
+
## Conventions
|
|
45
|
+
|
|
46
|
+
- Async throughout — `BrantaService` and `BrantaClient` methods are all `async def`.
|
|
47
|
+
- Python snake_case for all public attribute and method names.
|
|
48
|
+
- Inject `client`, `aes_encryption`, `secret_generator` into `BrantaService` for testing.
|
|
49
|
+
- Keep feature-parity with `branta-js` and `branta-dotnet`: any new method, option, or enum value added here must be reflected in both.
|
branta-3.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Branta Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
branta-3.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: branta
|
|
3
|
+
Version: 3.2.0
|
|
4
|
+
Summary: Python SDK for the Branta V2 API — payment destination lookup and registration with zero-knowledge encryption support
|
|
5
|
+
Project-URL: Homepage, https://branta.pro
|
|
6
|
+
Project-URL: Repository, https://github.com/BrantaOps/branta-python
|
|
7
|
+
Project-URL: Changelog, https://github.com/BrantaOps/branta-python/blob/main/CHANGELOG.md
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: bitcoin,branta,crypto,lightning,payments
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
22
|
+
Requires-Dist: cryptography>=42.0.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: coverage>=7.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Branta Python SDK
|
|
30
|
+
|
|
31
|
+
Python SDK for the [Branta](https://branta.pro) V2 API — payment destination lookup and registration with zero-knowledge encryption support.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install branta
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Integration Guide
|
|
40
|
+
|
|
41
|
+
### Quick start
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
from branta.enums import BrantaServerBaseUrl, PrivacyMode
|
|
46
|
+
from branta.options import BrantaClientOptions
|
|
47
|
+
from branta.v2 import BrantaService
|
|
48
|
+
|
|
49
|
+
options = BrantaClientOptions(
|
|
50
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
51
|
+
privacy=PrivacyMode.Strict,
|
|
52
|
+
)
|
|
53
|
+
service = BrantaService(options)
|
|
54
|
+
|
|
55
|
+
async def main():
|
|
56
|
+
result = await service.get_payments_by_qr_code("bitcoin:bc1q...")
|
|
57
|
+
if result.payments:
|
|
58
|
+
print(result.payments[0].get_default_value())
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Privacy modes
|
|
64
|
+
|
|
65
|
+
| Mode | Behaviour |
|
|
66
|
+
|------|-----------|
|
|
67
|
+
| `PrivacyMode.Strict` (default) | Only ZK lookups. `get_payments()` raises `BrantaPaymentException` for plain addresses. `get_payments_by_qr_code()` returns empty for plain addresses. `add_payment()` raises if any destination has `is_zk=False`. |
|
|
68
|
+
| `PrivacyMode.Loose` | Both plain and ZK lookups are permitted. |
|
|
69
|
+
|
|
70
|
+
### Looking up a payment by QR code
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
result = await service.get_payments_by_qr_code(qr_text)
|
|
74
|
+
# result.payments — list of Payment objects (empty if not found)
|
|
75
|
+
# result.verify_url — always populated; share with the payer to verify
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Prefer `get_payments_by_qr_code` for QR-driven flows. It handles multi-destination payloads (`branta_id` / `branta_secret` fragments) automatically.
|
|
79
|
+
|
|
80
|
+
### Looking up a payment by destination value
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Plain bitcoin address (requires PrivacyMode.Loose or will raise):
|
|
84
|
+
result = await service.get_payments("bc1q...")
|
|
85
|
+
|
|
86
|
+
# ZK-encrypted bitcoin address with secret:
|
|
87
|
+
result = await service.get_payments(encrypted_address, destination_encryption_key="my-secret")
|
|
88
|
+
|
|
89
|
+
# BOLT-11 invoice (hash-ZK — works in strict mode):
|
|
90
|
+
result = await service.get_payments("lnbc...")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Registering a payment
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from branta.enums import DestinationType
|
|
97
|
+
|
|
98
|
+
builder = service.create_payment_builder()
|
|
99
|
+
payment = (
|
|
100
|
+
builder
|
|
101
|
+
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
|
|
102
|
+
.add_destination("lnbc...", DestinationType.Bolt11).set_zk()
|
|
103
|
+
.set_description("Donation")
|
|
104
|
+
.add_metadata("email", "donor@example.com")
|
|
105
|
+
.build()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
result = await service.add_payment(payment, BrantaClientOptions(
|
|
109
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
110
|
+
default_api_key="your-api-key",
|
|
111
|
+
privacy=PrivacyMode.Strict,
|
|
112
|
+
))
|
|
113
|
+
# result.payment — the registered payment from the server
|
|
114
|
+
# result.secret — the random encryption key for the bitcoin address
|
|
115
|
+
# result.verify_url — share this URL to verify the payment
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Validating an API key
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
is_valid = await service.is_api_key_valid(BrantaClientOptions(
|
|
122
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
123
|
+
default_api_key="your-api-key",
|
|
124
|
+
))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Per-call option overrides
|
|
128
|
+
|
|
129
|
+
Every public method accepts an optional `BrantaClientOptions` parameter that overrides the service's default options for that call only:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
service = BrantaService(default_options)
|
|
133
|
+
result = await service.get_payments("lnbc...", options=override_options)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## ZK destination types
|
|
137
|
+
|
|
138
|
+
| Type | Encryption |
|
|
139
|
+
|------|-----------|
|
|
140
|
+
| `BitcoinAddress` | Random secret (GUID) per payment |
|
|
141
|
+
| `Bolt11` | Deterministic: SHA256 of lowercase invoice |
|
|
142
|
+
| `ArkAddress` | Deterministic: SHA256 of lowercase address |
|
|
143
|
+
| `SilentPayment` | Deterministic: SHA256 of lowercase address |
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[dev]"
|
|
149
|
+
pytest tests/ --ignore=tests/test_integration.py # unit tests
|
|
150
|
+
pytest tests/test_integration.py # integration (requires network)
|
|
151
|
+
coverage run -m pytest tests/ --ignore=tests/test_integration.py && coverage report
|
|
152
|
+
```
|
branta-3.2.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Branta Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Branta](https://branta.pro) V2 API — payment destination lookup and registration with zero-knowledge encryption support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install branta
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Integration Guide
|
|
12
|
+
|
|
13
|
+
### Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
from branta.enums import BrantaServerBaseUrl, PrivacyMode
|
|
18
|
+
from branta.options import BrantaClientOptions
|
|
19
|
+
from branta.v2 import BrantaService
|
|
20
|
+
|
|
21
|
+
options = BrantaClientOptions(
|
|
22
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
23
|
+
privacy=PrivacyMode.Strict,
|
|
24
|
+
)
|
|
25
|
+
service = BrantaService(options)
|
|
26
|
+
|
|
27
|
+
async def main():
|
|
28
|
+
result = await service.get_payments_by_qr_code("bitcoin:bc1q...")
|
|
29
|
+
if result.payments:
|
|
30
|
+
print(result.payments[0].get_default_value())
|
|
31
|
+
|
|
32
|
+
asyncio.run(main())
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Privacy modes
|
|
36
|
+
|
|
37
|
+
| Mode | Behaviour |
|
|
38
|
+
|------|-----------|
|
|
39
|
+
| `PrivacyMode.Strict` (default) | Only ZK lookups. `get_payments()` raises `BrantaPaymentException` for plain addresses. `get_payments_by_qr_code()` returns empty for plain addresses. `add_payment()` raises if any destination has `is_zk=False`. |
|
|
40
|
+
| `PrivacyMode.Loose` | Both plain and ZK lookups are permitted. |
|
|
41
|
+
|
|
42
|
+
### Looking up a payment by QR code
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
result = await service.get_payments_by_qr_code(qr_text)
|
|
46
|
+
# result.payments — list of Payment objects (empty if not found)
|
|
47
|
+
# result.verify_url — always populated; share with the payer to verify
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Prefer `get_payments_by_qr_code` for QR-driven flows. It handles multi-destination payloads (`branta_id` / `branta_secret` fragments) automatically.
|
|
51
|
+
|
|
52
|
+
### Looking up a payment by destination value
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Plain bitcoin address (requires PrivacyMode.Loose or will raise):
|
|
56
|
+
result = await service.get_payments("bc1q...")
|
|
57
|
+
|
|
58
|
+
# ZK-encrypted bitcoin address with secret:
|
|
59
|
+
result = await service.get_payments(encrypted_address, destination_encryption_key="my-secret")
|
|
60
|
+
|
|
61
|
+
# BOLT-11 invoice (hash-ZK — works in strict mode):
|
|
62
|
+
result = await service.get_payments("lnbc...")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Registering a payment
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from branta.enums import DestinationType
|
|
69
|
+
|
|
70
|
+
builder = service.create_payment_builder()
|
|
71
|
+
payment = (
|
|
72
|
+
builder
|
|
73
|
+
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
|
|
74
|
+
.add_destination("lnbc...", DestinationType.Bolt11).set_zk()
|
|
75
|
+
.set_description("Donation")
|
|
76
|
+
.add_metadata("email", "donor@example.com")
|
|
77
|
+
.build()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
result = await service.add_payment(payment, BrantaClientOptions(
|
|
81
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
82
|
+
default_api_key="your-api-key",
|
|
83
|
+
privacy=PrivacyMode.Strict,
|
|
84
|
+
))
|
|
85
|
+
# result.payment — the registered payment from the server
|
|
86
|
+
# result.secret — the random encryption key for the bitcoin address
|
|
87
|
+
# result.verify_url — share this URL to verify the payment
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Validating an API key
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
is_valid = await service.is_api_key_valid(BrantaClientOptions(
|
|
94
|
+
base_url=BrantaServerBaseUrl.Production,
|
|
95
|
+
default_api_key="your-api-key",
|
|
96
|
+
))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Per-call option overrides
|
|
100
|
+
|
|
101
|
+
Every public method accepts an optional `BrantaClientOptions` parameter that overrides the service's default options for that call only:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
service = BrantaService(default_options)
|
|
105
|
+
result = await service.get_payments("lnbc...", options=override_options)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## ZK destination types
|
|
109
|
+
|
|
110
|
+
| Type | Encryption |
|
|
111
|
+
|------|-----------|
|
|
112
|
+
| `BitcoinAddress` | Random secret (GUID) per payment |
|
|
113
|
+
| `Bolt11` | Deterministic: SHA256 of lowercase invoice |
|
|
114
|
+
| `ArkAddress` | Deterministic: SHA256 of lowercase address |
|
|
115
|
+
| `SilentPayment` | Deterministic: SHA256 of lowercase address |
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install -e ".[dev]"
|
|
121
|
+
pytest tests/ --ignore=tests/test_integration.py # unit tests
|
|
122
|
+
pytest tests/test_integration.py # integration (requires network)
|
|
123
|
+
coverage run -m pytest tests/ --ignore=tests/test_integration.py && coverage report
|
|
124
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from branta.enums import BrantaServerBaseUrl, DestinationType, PrivacyMode
|
|
2
|
+
from branta.exceptions import BrantaPaymentException, QRParseException
|
|
3
|
+
from branta.extensions import (
|
|
4
|
+
get_api_key,
|
|
5
|
+
get_base_url,
|
|
6
|
+
get_hash_zk_type,
|
|
7
|
+
get_hmac_secret,
|
|
8
|
+
get_privacy,
|
|
9
|
+
get_url,
|
|
10
|
+
is_ark,
|
|
11
|
+
is_bolt11,
|
|
12
|
+
is_silent_payment,
|
|
13
|
+
to_normalized_hash,
|
|
14
|
+
to_url_fragment,
|
|
15
|
+
)
|
|
16
|
+
from branta.models import AddPaymentResult, Destination, Payment, PaymentsResult, Platform
|
|
17
|
+
from branta.options import BrantaClientOptions
|
|
18
|
+
from branta.v2.encryption import AesEncryption, AesEncryptionService
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BrantaServerBaseUrl",
|
|
22
|
+
"DestinationType",
|
|
23
|
+
"PrivacyMode",
|
|
24
|
+
"BrantaPaymentException",
|
|
25
|
+
"QRParseException",
|
|
26
|
+
"BrantaClientOptions",
|
|
27
|
+
"Payment",
|
|
28
|
+
"Destination",
|
|
29
|
+
"Platform",
|
|
30
|
+
"PaymentsResult",
|
|
31
|
+
"AddPaymentResult",
|
|
32
|
+
"AesEncryption",
|
|
33
|
+
"AesEncryptionService",
|
|
34
|
+
"get_url",
|
|
35
|
+
"get_base_url",
|
|
36
|
+
"get_privacy",
|
|
37
|
+
"get_api_key",
|
|
38
|
+
"get_hmac_secret",
|
|
39
|
+
"get_hash_zk_type",
|
|
40
|
+
"is_bolt11",
|
|
41
|
+
"is_ark",
|
|
42
|
+
"is_silent_payment",
|
|
43
|
+
"to_normalized_hash",
|
|
44
|
+
"to_url_fragment",
|
|
45
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BrantaServerBaseUrl(str, Enum):
|
|
5
|
+
Staging = "https://staging.guardrail.branta.pro"
|
|
6
|
+
Production = "https://guardrail.branta.pro"
|
|
7
|
+
Localhost = "http://localhost:3000"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrivacyMode(str, Enum):
|
|
11
|
+
Strict = "strict"
|
|
12
|
+
Loose = "loose"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DestinationType(str, Enum):
|
|
16
|
+
BitcoinAddress = "bitcoin_address"
|
|
17
|
+
Bolt11 = "bolt11"
|
|
18
|
+
Bolt12 = "bolt12"
|
|
19
|
+
LnUrl = "ln_url"
|
|
20
|
+
TetherAddress = "tether_address"
|
|
21
|
+
LnAddress = "ln_address"
|
|
22
|
+
ArkAddress = "ark_address"
|
|
23
|
+
SilentPayment = "silent_payment"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import TYPE_CHECKING, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from branta.enums import DestinationType, PrivacyMode
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from branta.options import BrantaClientOptions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_url(server: BrantaServerBaseUrl) -> str:
|
|
13
|
+
return server.value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_base_url(
|
|
17
|
+
default_options: Optional[BrantaClientOptions],
|
|
18
|
+
override_options: Optional[BrantaClientOptions],
|
|
19
|
+
) -> str:
|
|
20
|
+
base_url = None
|
|
21
|
+
if override_options is not None and override_options.base_url is not None:
|
|
22
|
+
base_url = override_options.base_url
|
|
23
|
+
elif default_options is not None and default_options.base_url is not None:
|
|
24
|
+
base_url = default_options.base_url
|
|
25
|
+
if base_url is None:
|
|
26
|
+
raise ValueError("Branta: base_url is a required option.")
|
|
27
|
+
return get_url(base_url)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_privacy(
|
|
31
|
+
default_options: Optional[BrantaClientOptions],
|
|
32
|
+
override_options: Optional[BrantaClientOptions],
|
|
33
|
+
fallback: PrivacyMode = PrivacyMode.Strict,
|
|
34
|
+
) -> PrivacyMode:
|
|
35
|
+
if override_options is not None and override_options.privacy is not None:
|
|
36
|
+
return override_options.privacy
|
|
37
|
+
if default_options is not None and default_options.privacy is not None:
|
|
38
|
+
return default_options.privacy
|
|
39
|
+
return fallback
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_api_key(
|
|
43
|
+
default_options: Optional[BrantaClientOptions],
|
|
44
|
+
override_options: Optional[BrantaClientOptions],
|
|
45
|
+
) -> Optional[str]:
|
|
46
|
+
if override_options is not None and override_options.default_api_key is not None:
|
|
47
|
+
return override_options.default_api_key
|
|
48
|
+
if default_options is not None and default_options.default_api_key is not None:
|
|
49
|
+
return default_options.default_api_key
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_hmac_secret(
|
|
54
|
+
default_options: Optional[BrantaClientOptions],
|
|
55
|
+
override_options: Optional[BrantaClientOptions],
|
|
56
|
+
) -> Optional[str]:
|
|
57
|
+
if override_options is not None and override_options.hmac_secret is not None:
|
|
58
|
+
return override_options.hmac_secret
|
|
59
|
+
if default_options is not None and default_options.hmac_secret is not None:
|
|
60
|
+
return default_options.hmac_secret
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_bolt11(value: str) -> bool:
|
|
65
|
+
lower = value.lower()
|
|
66
|
+
return lower.startswith("lnbc") or lower.startswith("lntb") or lower.startswith("lnbcrt")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_ark(value: str) -> bool:
|
|
70
|
+
return value.lower().startswith("ark1")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_silent_payment(value: str) -> bool:
|
|
74
|
+
lower = value.lower()
|
|
75
|
+
return lower.startswith("sp1") or lower.startswith("tsp1")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_hash_zk_type(value: str) -> Optional[DestinationType]:
|
|
79
|
+
if is_bolt11(value):
|
|
80
|
+
return DestinationType.Bolt11
|
|
81
|
+
if is_ark(value):
|
|
82
|
+
return DestinationType.ArkAddress
|
|
83
|
+
if is_silent_payment(value):
|
|
84
|
+
return DestinationType.SilentPayment
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def to_normalized_hash(value: str) -> str:
|
|
89
|
+
normalized = value.lower()
|
|
90
|
+
return hashlib.sha256(normalized.encode("utf-8")).hexdigest().upper()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def to_url_fragment(keys: Dict[str, str]) -> str:
|
|
94
|
+
if not keys:
|
|
95
|
+
return ""
|
|
96
|
+
fragments = [f"k-{k}={v}" for k, v in keys.items()]
|
|
97
|
+
return "#" + "&".join(fragments)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from branta.enums import DestinationType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Destination:
|
|
11
|
+
value: str
|
|
12
|
+
is_primary: bool = False
|
|
13
|
+
is_zk: bool = False
|
|
14
|
+
is_encrypted: Optional[bool] = None
|
|
15
|
+
type: Optional[DestinationType] = None
|
|
16
|
+
zk_id: Optional[str] = None
|
|
17
|
+
encrypted_dek: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Platform:
|
|
22
|
+
name: Optional[str] = None
|
|
23
|
+
logo_url: Optional[str] = None
|
|
24
|
+
logo_light_url: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Payment:
|
|
29
|
+
destinations: List[Destination] = field(default_factory=list)
|
|
30
|
+
description: Optional[str] = None
|
|
31
|
+
created_date: Optional[str] = None
|
|
32
|
+
ttl: Optional[int] = None
|
|
33
|
+
metadata: Optional[str] = None
|
|
34
|
+
platform: Optional[str] = None
|
|
35
|
+
platform_logo_url: Optional[str] = None
|
|
36
|
+
platform_logo_light_url: Optional[str] = None
|
|
37
|
+
parent_platform: Optional[Platform] = None
|
|
38
|
+
child_platform: Optional[Platform] = None
|
|
39
|
+
btc_pay_server_plugin_version: Optional[str] = None
|
|
40
|
+
is_metadata_decrypted: Optional[bool] = None
|
|
41
|
+
|
|
42
|
+
def get_default_value(self) -> str:
|
|
43
|
+
if not self.destinations:
|
|
44
|
+
raise ValueError("Payment has no destinations")
|
|
45
|
+
return self.destinations[0].value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class PaymentsResult:
|
|
50
|
+
payments: List[Payment]
|
|
51
|
+
verify_url: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class AddPaymentResult:
|
|
56
|
+
payment: Payment
|
|
57
|
+
secret: str
|
|
58
|
+
verify_url: str
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from branta.enums import BrantaServerBaseUrl, PrivacyMode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class BrantaClientOptions:
|
|
11
|
+
base_url: BrantaServerBaseUrl
|
|
12
|
+
privacy: Optional[PrivacyMode] = None
|
|
13
|
+
default_api_key: Optional[str] = None
|
|
14
|
+
hmac_secret: Optional[str] = None
|