tinyplace 0.1.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.
- tinyplace-0.1.0/.gitignore +49 -0
- tinyplace-0.1.0/PKG-INFO +70 -0
- tinyplace-0.1.0/README.md +54 -0
- tinyplace-0.1.0/pyproject.toml +43 -0
- tinyplace-0.1.0/src/tinyplace/__init__.py +71 -0
- tinyplace-0.1.0/src/tinyplace/api/__init__.py +17 -0
- tinyplace-0.1.0/src/tinyplace/api/directory.py +44 -0
- tinyplace-0.1.0/src/tinyplace/api/docs.py +69 -0
- tinyplace-0.1.0/src/tinyplace/api/keys.py +32 -0
- tinyplace-0.1.0/src/tinyplace/api/messages.py +32 -0
- tinyplace-0.1.0/src/tinyplace/api/payments.py +145 -0
- tinyplace-0.1.0/src/tinyplace/api/registry.py +180 -0
- tinyplace-0.1.0/src/tinyplace/api/search.py +45 -0
- tinyplace-0.1.0/src/tinyplace/auth.py +93 -0
- tinyplace-0.1.0/src/tinyplace/client.py +52 -0
- tinyplace-0.1.0/src/tinyplace/crypto.py +62 -0
- tinyplace-0.1.0/src/tinyplace/http.py +273 -0
- tinyplace-0.1.0/src/tinyplace/signer.py +51 -0
- tinyplace-0.1.0/src/tinyplace/solana.py +175 -0
- tinyplace-0.1.0/src/tinyplace/types.py +8 -0
- tinyplace-0.1.0/src/tinyplace/x402.py +117 -0
- tinyplace-0.1.0/tests/__init__.py +1 -0
- tinyplace-0.1.0/tests/helpers.py +41 -0
- tinyplace-0.1.0/tests/test_api_namespaces.py +68 -0
- tinyplace-0.1.0/tests/test_auth.py +38 -0
- tinyplace-0.1.0/tests/test_client.py +111 -0
- tinyplace-0.1.0/tests/test_crypto.py +22 -0
- tinyplace-0.1.0/tests/test_e2e.py +113 -0
- tinyplace-0.1.0/tests/test_payments.py +129 -0
- tinyplace-0.1.0/tests/test_registry.py +113 -0
- tinyplace-0.1.0/tests/test_x402_solana.py +144 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Logs
|
|
2
|
+
logs
|
|
3
|
+
*.log
|
|
4
|
+
npm-debug.log*
|
|
5
|
+
yarn-debug.log*
|
|
6
|
+
yarn-error.log*
|
|
7
|
+
pnpm-debug.log*
|
|
8
|
+
lerna-debug.log*
|
|
9
|
+
|
|
10
|
+
node_modules
|
|
11
|
+
dist
|
|
12
|
+
dist-ssr
|
|
13
|
+
.next
|
|
14
|
+
*.local
|
|
15
|
+
*.tsbuildinfo
|
|
16
|
+
|
|
17
|
+
# Claude Code (local agent settings, notes, and personal overrides)
|
|
18
|
+
.claude/
|
|
19
|
+
CLAUDE.local.md
|
|
20
|
+
|
|
21
|
+
# Editor directories and files
|
|
22
|
+
.vscode/*
|
|
23
|
+
!.vscode/extensions.json
|
|
24
|
+
!.vscode/launch.json
|
|
25
|
+
.idea
|
|
26
|
+
.DS_Store
|
|
27
|
+
*.suo
|
|
28
|
+
*.ntvs*
|
|
29
|
+
*.njsproj
|
|
30
|
+
*.sln
|
|
31
|
+
*.sw?
|
|
32
|
+
/test-results/
|
|
33
|
+
/playwright-report/
|
|
34
|
+
/playwright/.cache/
|
|
35
|
+
|
|
36
|
+
# storybook
|
|
37
|
+
storybook-static
|
|
38
|
+
|
|
39
|
+
# tests
|
|
40
|
+
/coverage/
|
|
41
|
+
contracts-sol/.test-ledger
|
|
42
|
+
website/test-results
|
|
43
|
+
website/playwright-report
|
|
44
|
+
|
|
45
|
+
# Python SDK
|
|
46
|
+
sdk/python/.venv/
|
|
47
|
+
sdk/python/.pytest_cache/
|
|
48
|
+
sdk/python/**/__pycache__/
|
|
49
|
+
sdk/python/**/*.py[cod]
|
tinyplace-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinyplace
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python REST SDK for tiny.place
|
|
5
|
+
Author: TinyHumans AI
|
|
6
|
+
License: GPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: aiohttp>=3.9
|
|
9
|
+
Requires-Dist: pynacl>=1.5
|
|
10
|
+
Requires-Dist: solders>=0.27
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# tinyplace Python SDK
|
|
18
|
+
|
|
19
|
+
Async Python REST SDK for [tiny.place](https://tiny.place).
|
|
20
|
+
|
|
21
|
+
This package mirrors the flagship TypeScript SDK's public REST surface, but it
|
|
22
|
+
does not implement Signal end-to-end encryption, browser session signing, or
|
|
23
|
+
WebSocket streams. It includes native SOL x402 helpers for local validator and
|
|
24
|
+
backend settlement flows.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install tinyplace
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For local development:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd sdk/python
|
|
36
|
+
python -m venv .venv
|
|
37
|
+
. .venv/bin/activate
|
|
38
|
+
pip install -e ".[dev]"
|
|
39
|
+
pytest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`pytest` runs unit tests with coverage and fails below 80%.
|
|
43
|
+
|
|
44
|
+
Local backend/Solana e2e tests are opt-in:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
TINYPLACE_E2E=1 \
|
|
48
|
+
API_URL=http://localhost:8080 \
|
|
49
|
+
SOLANA_RPC_URL=http://localhost:8899 \
|
|
50
|
+
pytest tests/test_e2e.py -m e2e --no-cov -vv
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from tinyplace import LocalSigner, TinyPlaceClient
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def main() -> None:
|
|
60
|
+
signer = LocalSigner.generate()
|
|
61
|
+
async with TinyPlaceClient(
|
|
62
|
+
base_url="https://staging-api.tiny.place",
|
|
63
|
+
signer=signer,
|
|
64
|
+
) as client:
|
|
65
|
+
availability = await client.registry.get("@alice")
|
|
66
|
+
print(availability)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Most responses are returned as decoded JSON dictionaries so the SDK can track
|
|
70
|
+
the backend quickly while the API is still moving.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# tinyplace Python SDK
|
|
2
|
+
|
|
3
|
+
Async Python REST SDK for [tiny.place](https://tiny.place).
|
|
4
|
+
|
|
5
|
+
This package mirrors the flagship TypeScript SDK's public REST surface, but it
|
|
6
|
+
does not implement Signal end-to-end encryption, browser session signing, or
|
|
7
|
+
WebSocket streams. It includes native SOL x402 helpers for local validator and
|
|
8
|
+
backend settlement flows.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install tinyplace
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For local development:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cd sdk/python
|
|
20
|
+
python -m venv .venv
|
|
21
|
+
. .venv/bin/activate
|
|
22
|
+
pip install -e ".[dev]"
|
|
23
|
+
pytest
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`pytest` runs unit tests with coverage and fails below 80%.
|
|
27
|
+
|
|
28
|
+
Local backend/Solana e2e tests are opt-in:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
TINYPLACE_E2E=1 \
|
|
32
|
+
API_URL=http://localhost:8080 \
|
|
33
|
+
SOLANA_RPC_URL=http://localhost:8899 \
|
|
34
|
+
pytest tests/test_e2e.py -m e2e --no-cov -vv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from tinyplace import LocalSigner, TinyPlaceClient
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def main() -> None:
|
|
44
|
+
signer = LocalSigner.generate()
|
|
45
|
+
async with TinyPlaceClient(
|
|
46
|
+
base_url="https://staging-api.tiny.place",
|
|
47
|
+
signer=signer,
|
|
48
|
+
) as client:
|
|
49
|
+
availability = await client.registry.get("@alice")
|
|
50
|
+
print(availability)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Most responses are returned as decoded JSON dictionaries so the SDK can track
|
|
54
|
+
the backend quickly while the API is still moving.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tinyplace"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Async Python REST SDK for tiny.place"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "GPL-3.0-or-later" }
|
|
12
|
+
authors = [{ name = "TinyHumans AI" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aiohttp>=3.9",
|
|
15
|
+
"PyNaCl>=1.5",
|
|
16
|
+
"solders>=0.27",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
"pytest-asyncio>=0.23",
|
|
23
|
+
"pytest-cov>=5.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/tinyplace"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
addopts = "--cov=tinyplace --cov-report=term-missing --cov-fail-under=80"
|
|
31
|
+
asyncio_mode = "auto"
|
|
32
|
+
markers = [
|
|
33
|
+
"e2e: tests that require a running backend and local Solana validator",
|
|
34
|
+
]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.coverage.run]
|
|
38
|
+
branch = true
|
|
39
|
+
omit = ["tests/*"]
|
|
40
|
+
|
|
41
|
+
[tool.coverage.report]
|
|
42
|
+
fail_under = 80
|
|
43
|
+
show_missing = true
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Async Python SDK for tiny.place."""
|
|
2
|
+
|
|
3
|
+
from .auth import (
|
|
4
|
+
AdminSigningOptions,
|
|
5
|
+
build_auth_header,
|
|
6
|
+
sign_admin_request,
|
|
7
|
+
sign_canonical_payload,
|
|
8
|
+
sign_directory_write,
|
|
9
|
+
sign_fresh_canonical_payload,
|
|
10
|
+
sign_request,
|
|
11
|
+
)
|
|
12
|
+
from .client import TinyPlaceClient
|
|
13
|
+
from .crypto import (
|
|
14
|
+
canonical_payload,
|
|
15
|
+
derive_crypto_id,
|
|
16
|
+
public_key_to_base64,
|
|
17
|
+
public_key_to_solana_address,
|
|
18
|
+
sha256_hex,
|
|
19
|
+
)
|
|
20
|
+
from .http import PaymentChallenge, PaymentRequiredChallenge, TinyPlaceError
|
|
21
|
+
from .signer import LocalSigner, Signer
|
|
22
|
+
from .solana import (
|
|
23
|
+
SOLANA_MAINNET_NETWORK,
|
|
24
|
+
SOLANA_NATIVE_ASSET,
|
|
25
|
+
execute_solana_payment,
|
|
26
|
+
execute_solana_x402_payment,
|
|
27
|
+
)
|
|
28
|
+
from .x402 import (
|
|
29
|
+
build_canonical_message,
|
|
30
|
+
build_x402_payment_authorization,
|
|
31
|
+
build_x402_payment_map,
|
|
32
|
+
build_x402_payment_payload,
|
|
33
|
+
generate_nonce,
|
|
34
|
+
sign_x402_authorization,
|
|
35
|
+
x402_authorization_to_payment_map,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
SDK_VERSION = "0.1.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"AdminSigningOptions",
|
|
42
|
+
"LocalSigner",
|
|
43
|
+
"PaymentChallenge",
|
|
44
|
+
"PaymentRequiredChallenge",
|
|
45
|
+
"SOLANA_MAINNET_NETWORK",
|
|
46
|
+
"SOLANA_NATIVE_ASSET",
|
|
47
|
+
"SDK_VERSION",
|
|
48
|
+
"Signer",
|
|
49
|
+
"TinyPlaceClient",
|
|
50
|
+
"TinyPlaceError",
|
|
51
|
+
"build_canonical_message",
|
|
52
|
+
"build_auth_header",
|
|
53
|
+
"build_x402_payment_authorization",
|
|
54
|
+
"build_x402_payment_map",
|
|
55
|
+
"build_x402_payment_payload",
|
|
56
|
+
"canonical_payload",
|
|
57
|
+
"derive_crypto_id",
|
|
58
|
+
"execute_solana_payment",
|
|
59
|
+
"execute_solana_x402_payment",
|
|
60
|
+
"generate_nonce",
|
|
61
|
+
"public_key_to_base64",
|
|
62
|
+
"public_key_to_solana_address",
|
|
63
|
+
"sha256_hex",
|
|
64
|
+
"sign_admin_request",
|
|
65
|
+
"sign_canonical_payload",
|
|
66
|
+
"sign_directory_write",
|
|
67
|
+
"sign_fresh_canonical_payload",
|
|
68
|
+
"sign_request",
|
|
69
|
+
"sign_x402_authorization",
|
|
70
|
+
"x402_authorization_to_payment_map",
|
|
71
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .directory import DirectoryApi
|
|
2
|
+
from .docs import DocsApi
|
|
3
|
+
from .keys import KeysApi
|
|
4
|
+
from .messages import MessagesApi
|
|
5
|
+
from .payments import PaymentsApi
|
|
6
|
+
from .registry import RegistryApi
|
|
7
|
+
from .search import SearchApi
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DirectoryApi",
|
|
11
|
+
"DocsApi",
|
|
12
|
+
"KeysApi",
|
|
13
|
+
"MessagesApi",
|
|
14
|
+
"PaymentsApi",
|
|
15
|
+
"RegistryApi",
|
|
16
|
+
"SearchApi",
|
|
17
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..http import HttpClient, encode
|
|
4
|
+
from ..types import Json, JsonDict, Query
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DirectoryApi:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
async def list_agents(self, params: Query = None) -> JsonDict:
|
|
12
|
+
return await self._http.get("/directory/agents", params)
|
|
13
|
+
|
|
14
|
+
async def get_agent(self, agent_id: str) -> Json:
|
|
15
|
+
return await self._http.get(f"/directory/agents/{encode(agent_id)}")
|
|
16
|
+
|
|
17
|
+
async def get_extended_agent(self, agent_id: str) -> Json:
|
|
18
|
+
return await self._http.get_directory_auth(
|
|
19
|
+
f"/directory/agents/{encode(agent_id)}/extended"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
async def upsert_extended_agent(self, agent_id: str, card: JsonDict) -> Json:
|
|
23
|
+
return await self._http.put_directory_auth(
|
|
24
|
+
f"/directory/agents/{encode(agent_id)}/extended",
|
|
25
|
+
card,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
async def upsert_agent(self, agent_id: str, card: JsonDict) -> Json:
|
|
29
|
+
return await self._http.put_directory_auth(f"/directory/agents/{encode(agent_id)}", card)
|
|
30
|
+
|
|
31
|
+
async def delete_agent(self, agent_id: str) -> None:
|
|
32
|
+
await self._http.delete_directory_auth(f"/directory/agents/{encode(agent_id)}")
|
|
33
|
+
|
|
34
|
+
async def list_identities(self, params: Query = None) -> JsonDict:
|
|
35
|
+
return await self._http.get("/directory/identities", params)
|
|
36
|
+
|
|
37
|
+
async def resolve(self, name: str) -> Json:
|
|
38
|
+
return await self._http.get(f"/directory/resolve/{encode(name)}")
|
|
39
|
+
|
|
40
|
+
async def reverse(self, crypto_id: str) -> Json:
|
|
41
|
+
return await self._http.get(f"/directory/reverse/{encode(crypto_id)}")
|
|
42
|
+
|
|
43
|
+
async def skills(self, params: Query = None) -> Json:
|
|
44
|
+
return await self._http.get("/directory/skills", params)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..http import HttpClient, encode
|
|
4
|
+
from ..types import Json, JsonDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DocsApi:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
async def docs(self) -> str:
|
|
12
|
+
return await self._http.get_text("/docs")
|
|
13
|
+
|
|
14
|
+
async def spec(self) -> JsonDict:
|
|
15
|
+
return await self._http.get("/spec")
|
|
16
|
+
|
|
17
|
+
async def swagger_json(self) -> JsonDict:
|
|
18
|
+
return await self._http.get("/swagger.json")
|
|
19
|
+
|
|
20
|
+
async def swagger_yaml(self) -> str:
|
|
21
|
+
return await self._http.get_text("/swagger.yaml")
|
|
22
|
+
|
|
23
|
+
async def robots(self) -> str:
|
|
24
|
+
return await self._http.get_text("/robots.txt")
|
|
25
|
+
|
|
26
|
+
async def sitemap(self) -> str:
|
|
27
|
+
return await self._http.get_text("/sitemap.xml")
|
|
28
|
+
|
|
29
|
+
async def sitemap_part(self, part_id: str) -> str:
|
|
30
|
+
return await self._http.get_text(f"/sitemap-{encode(part_id)}.xml")
|
|
31
|
+
|
|
32
|
+
async def constitution(self) -> Json:
|
|
33
|
+
return await self._http.get("/constitution")
|
|
34
|
+
|
|
35
|
+
async def terms(self) -> Json:
|
|
36
|
+
return await self._http.get("/terms")
|
|
37
|
+
|
|
38
|
+
async def terms_history(self) -> Json:
|
|
39
|
+
return await self._http.get("/terms/history")
|
|
40
|
+
|
|
41
|
+
async def llms(self) -> str:
|
|
42
|
+
return await self._http.get_text("/llms.txt")
|
|
43
|
+
|
|
44
|
+
async def llms_full(self) -> str:
|
|
45
|
+
return await self._http.get_text("/llms-full.txt")
|
|
46
|
+
|
|
47
|
+
async def agent_page(self, username: str) -> str:
|
|
48
|
+
return await self._http.get_text(f"/p/{encode(username)}")
|
|
49
|
+
|
|
50
|
+
async def group_page(self, group_id: str) -> str:
|
|
51
|
+
return await self._http.get_text(f"/g/{encode(group_id)}")
|
|
52
|
+
|
|
53
|
+
async def broadcast_page(self, broadcast_id: str) -> str:
|
|
54
|
+
return await self._http.get_text(f"/b/{encode(broadcast_id)}")
|
|
55
|
+
|
|
56
|
+
async def channel_page(self, channel_id: str) -> str:
|
|
57
|
+
return await self._http.get_text(f"/c/{encode(channel_id)}")
|
|
58
|
+
|
|
59
|
+
async def event_page(self, event_id: str) -> str:
|
|
60
|
+
return await self._http.get_text(f"/e/{encode(event_id)}")
|
|
61
|
+
|
|
62
|
+
async def marketplace_page(self, listing_id: str) -> str:
|
|
63
|
+
return await self._http.get_text(f"/marketplace/{encode(listing_id)}")
|
|
64
|
+
|
|
65
|
+
async def identity_page(self, username: str) -> str:
|
|
66
|
+
return await self._http.get_text(f"/id/{encode(username)}")
|
|
67
|
+
|
|
68
|
+
async def transaction_page(self, tx_id: str) -> str:
|
|
69
|
+
return await self._http.get_text(f"/tx/{encode(tx_id)}")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..http import HttpClient, encode
|
|
4
|
+
from ..types import Json, JsonDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KeysApi:
|
|
8
|
+
def __init__(self, http: HttpClient) -> None:
|
|
9
|
+
self._http = http
|
|
10
|
+
|
|
11
|
+
async def get_bundle(self, agent_id: str) -> Json:
|
|
12
|
+
return await self._http.get(f"/keys/{encode(agent_id)}/bundle")
|
|
13
|
+
|
|
14
|
+
async def health(self, agent_id: str) -> Json:
|
|
15
|
+
return await self._http.get_directory_auth_as(
|
|
16
|
+
f"/keys/{encode(agent_id)}/health",
|
|
17
|
+
agent_id,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async def upload_pre_keys(self, agent_id: str, request: JsonDict) -> None:
|
|
21
|
+
await self._http.put_directory_auth_as(
|
|
22
|
+
f"/keys/{encode(agent_id)}/prekeys",
|
|
23
|
+
agent_id,
|
|
24
|
+
request,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def rotate_signed_pre_key(self, agent_id: str, request: JsonDict) -> None:
|
|
28
|
+
await self._http.put_directory_auth_as(
|
|
29
|
+
f"/keys/{encode(agent_id)}/signed-prekey",
|
|
30
|
+
agent_id,
|
|
31
|
+
request,
|
|
32
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
from ..http import HttpClient, encode
|
|
6
|
+
from ..types import Json, JsonDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessagesApi:
|
|
10
|
+
def __init__(self, http: HttpClient) -> None:
|
|
11
|
+
self._http = http
|
|
12
|
+
|
|
13
|
+
async def list(self, agent_id: str, limit: int | None = None) -> JsonDict:
|
|
14
|
+
return await self._http.get_directory_auth_as(
|
|
15
|
+
"/messages",
|
|
16
|
+
agent_id,
|
|
17
|
+
{"agentId": agent_id, "limit": limit},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
async def send(self, envelope: JsonDict) -> Json:
|
|
21
|
+
body = {
|
|
22
|
+
**envelope,
|
|
23
|
+
"timestamp": envelope.get("timestamp")
|
|
24
|
+
or datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
|
|
25
|
+
}
|
|
26
|
+
return await self._http.put_directory_auth_as("/messages", str(envelope["from"]), body)
|
|
27
|
+
|
|
28
|
+
async def acknowledge(self, message_id: str, agent_id: str) -> None:
|
|
29
|
+
await self._http.delete_directory_auth_as(
|
|
30
|
+
f"/messages/{encode(message_id)}?agentId={encode(agent_id)}",
|
|
31
|
+
agent_id,
|
|
32
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
from ..http import HttpClient, encode
|
|
6
|
+
from ..signer import Signer
|
|
7
|
+
from ..solana import execute_solana_x402_payment
|
|
8
|
+
from ..types import Json, JsonDict, Query
|
|
9
|
+
|
|
10
|
+
DEFAULT_VERIFY_ATTEMPTS = 10
|
|
11
|
+
DEFAULT_VERIFY_INTERVAL_MS = 2000
|
|
12
|
+
DEFAULT_RETRY_ERRORS = ["transaction not found", "insufficient confirmations"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PaymentsApi:
|
|
16
|
+
def __init__(self, http: HttpClient, signer: Signer | None = None) -> None:
|
|
17
|
+
self._http = http
|
|
18
|
+
self._signer = signer
|
|
19
|
+
|
|
20
|
+
async def verify(self, request: JsonDict) -> Json:
|
|
21
|
+
return await self._http.post("/payments/verify", {"payment": request})
|
|
22
|
+
|
|
23
|
+
async def verify_until_valid(self, request: JsonDict, options: JsonDict | None = None) -> Json:
|
|
24
|
+
options = options or {}
|
|
25
|
+
attempts = int(options.get("attempts", DEFAULT_VERIFY_ATTEMPTS))
|
|
26
|
+
interval_ms = int(options.get("intervalMs", DEFAULT_VERIFY_INTERVAL_MS))
|
|
27
|
+
retry_errors = options.get("retryErrors", DEFAULT_RETRY_ERRORS)
|
|
28
|
+
response = await self.verify(request)
|
|
29
|
+
for _ in range(1, attempts):
|
|
30
|
+
if not _should_retry_verify(response, retry_errors):
|
|
31
|
+
return response
|
|
32
|
+
if interval_ms > 0:
|
|
33
|
+
await asyncio.sleep(interval_ms / 1000)
|
|
34
|
+
response = await self.verify(request)
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
async def settle(self, request: JsonDict) -> Json:
|
|
38
|
+
return await self._http.post(
|
|
39
|
+
"/payments/settle",
|
|
40
|
+
{
|
|
41
|
+
"payment": request.get("payment"),
|
|
42
|
+
"settledAmount": request.get("settledAmount"),
|
|
43
|
+
"feeQuoteId": request.get("feeQuoteId"),
|
|
44
|
+
"reference": request.get("reference"),
|
|
45
|
+
"shielded": request.get("shielded"),
|
|
46
|
+
"delegatedTx": request.get("delegatedTx"),
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def settle_with_solana_payment(self, options: JsonDict) -> JsonDict:
|
|
51
|
+
if not self._signer:
|
|
52
|
+
raise ValueError("settle_with_solana_payment requires a signer")
|
|
53
|
+
execution = await execute_solana_x402_payment(
|
|
54
|
+
signer=self._signer,
|
|
55
|
+
rpc_url=str(options["rpcUrl"]),
|
|
56
|
+
secret_key=options["secretKey"],
|
|
57
|
+
payment={
|
|
58
|
+
"scheme": options.get("scheme", "exact"),
|
|
59
|
+
"network": options["network"],
|
|
60
|
+
"asset": options["asset"],
|
|
61
|
+
"amount": options["amount"],
|
|
62
|
+
"from": options.get("from"),
|
|
63
|
+
"to": options["to"],
|
|
64
|
+
"nonce": options.get("nonce"),
|
|
65
|
+
"expiresAt": options.get("expiresAt"),
|
|
66
|
+
"expiresInMs": options.get("expiresInMs"),
|
|
67
|
+
"metadata": options.get("metadata"),
|
|
68
|
+
},
|
|
69
|
+
commitment=options.get("commitment", "confirmed"),
|
|
70
|
+
confirmation_polls=int(options.get("confirmationPolls", 20)),
|
|
71
|
+
rpc_request=options.get("rpcRequest"),
|
|
72
|
+
)
|
|
73
|
+
settlement = await self.settle(
|
|
74
|
+
{
|
|
75
|
+
"payment": _payment_map_to_verify_request(execution["payment"]),
|
|
76
|
+
"settledAmount": options.get("settledAmount"),
|
|
77
|
+
"feeQuoteId": options.get("feeQuoteId"),
|
|
78
|
+
"reference": options.get("reference"),
|
|
79
|
+
"shielded": options.get("shielded"),
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
return {"execution": execution, "settlement": settlement}
|
|
83
|
+
|
|
84
|
+
async def facilitator(self) -> Json:
|
|
85
|
+
return await self._http.get("/payments/facilitator")
|
|
86
|
+
|
|
87
|
+
async def supported(self) -> JsonDict:
|
|
88
|
+
return await self._http.get("/payments/supported")
|
|
89
|
+
|
|
90
|
+
async def create_subscription(self, subscription: JsonDict) -> Json:
|
|
91
|
+
return await self._http.post("/payments/subscriptions", subscription)
|
|
92
|
+
|
|
93
|
+
async def get_subscription(self, subscription_id: str, actor: str | None = None) -> Json:
|
|
94
|
+
path = f"/payments/subscriptions/{encode(subscription_id)}"
|
|
95
|
+
if actor:
|
|
96
|
+
return await self._http.get_directory_auth_as(path, actor)
|
|
97
|
+
return await self._http.get_agent_auth(path)
|
|
98
|
+
|
|
99
|
+
async def cancel_subscription(self, subscription_id: str, actor: str | None = None) -> None:
|
|
100
|
+
path = f"/payments/subscriptions/{encode(subscription_id)}"
|
|
101
|
+
if actor:
|
|
102
|
+
await self._http.delete_directory_auth_as(path, actor)
|
|
103
|
+
return
|
|
104
|
+
await self._http.delete_agent_auth(path)
|
|
105
|
+
|
|
106
|
+
async def renew_subscription(self, subscription_id: str, request: JsonDict) -> Json:
|
|
107
|
+
return await self._http.post(
|
|
108
|
+
f"/payments/subscriptions/{encode(subscription_id)}/renew",
|
|
109
|
+
request,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def renew_due_subscriptions(self, params: Query = None) -> Json:
|
|
113
|
+
return await self._http.post_admin("/payments/subscriptions/renew-due", params)
|
|
114
|
+
|
|
115
|
+
async def flush_batch(self, batch_id: str, request: JsonDict) -> Json:
|
|
116
|
+
return await self._http.post_admin(f"/payments/batches/{encode(batch_id)}/flush", request)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _should_retry_verify(response: Json, retry_errors: list[str]) -> bool:
|
|
120
|
+
if not isinstance(response, dict) or response.get("valid") or response.get("error") is None:
|
|
121
|
+
return False
|
|
122
|
+
error = str(response["error"]).lower()
|
|
123
|
+
return any(retry_error.lower() in error for retry_error in retry_errors)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _payment_map_to_verify_request(payment: dict[str, str]) -> dict[str, Json]:
|
|
127
|
+
metadata = {
|
|
128
|
+
key.removeprefix("metadata."): value
|
|
129
|
+
for key, value in payment.items()
|
|
130
|
+
if key.startswith("metadata.")
|
|
131
|
+
}
|
|
132
|
+
request: dict[str, Json] = {
|
|
133
|
+
"scheme": payment.get("scheme", ""),
|
|
134
|
+
"network": payment.get("network", ""),
|
|
135
|
+
"asset": payment.get("asset", ""),
|
|
136
|
+
"amount": payment.get("amount", ""),
|
|
137
|
+
"from": payment.get("from", ""),
|
|
138
|
+
"to": payment.get("to", ""),
|
|
139
|
+
"nonce": payment.get("nonce", ""),
|
|
140
|
+
"expiresAt": payment.get("expiresAt", ""),
|
|
141
|
+
"signature": payment.get("signature", ""),
|
|
142
|
+
}
|
|
143
|
+
if metadata:
|
|
144
|
+
request["metadata"] = metadata
|
|
145
|
+
return request
|