calimero-client-py 0.1.1__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.
- calimero_client_py-0.1.1/LICENSE +21 -0
- calimero_client_py-0.1.1/PKG-INFO +124 -0
- calimero_client_py-0.1.1/README.md +84 -0
- calimero_client_py-0.1.1/calimero/__init__.py +19 -0
- calimero_client_py-0.1.1/calimero/config.py +104 -0
- calimero_client_py-0.1.1/calimero/json_rpc_client.py +153 -0
- calimero_client_py-0.1.1/calimero/keypair.py +111 -0
- calimero_client_py-0.1.1/calimero/ws_subscriptions_client.py +85 -0
- calimero_client_py-0.1.1/calimero_client_py.egg-info/PKG-INFO +124 -0
- calimero_client_py-0.1.1/calimero_client_py.egg-info/SOURCES.txt +20 -0
- calimero_client_py-0.1.1/calimero_client_py.egg-info/dependency_links.txt +1 -0
- calimero_client_py-0.1.1/calimero_client_py.egg-info/requires.txt +13 -0
- calimero_client_py-0.1.1/calimero_client_py.egg-info/top_level.txt +2 -0
- calimero_client_py-0.1.1/pyproject.toml +47 -0
- calimero_client_py-0.1.1/setup.cfg +4 -0
- calimero_client_py-0.1.1/setup.py +42 -0
- calimero_client_py-0.1.1/tests/__init__.py +3 -0
- calimero_client_py-0.1.1/tests/conftest.py +64 -0
- calimero_client_py-0.1.1/tests/test_config.py +70 -0
- calimero_client_py-0.1.1/tests/test_json_rpc_client.py +93 -0
- calimero_client_py-0.1.1/tests/test_keypair.py +87 -0
- calimero_client_py-0.1.1/tests/test_ws_subscriptions_client.py +93 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Calimero Network
|
|
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.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: calimero-client-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Python client SDK for Calimero Network
|
|
5
|
+
Home-page: https://github.com/calimero-network/calimero-client-py
|
|
6
|
+
Author: Calimero Network
|
|
7
|
+
Author-email: Calimero Network <support@calimero.network>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/calimero-network/calimero-client-py
|
|
10
|
+
Project-URL: Documentation, https://docs.calimero.network/python-sdk
|
|
11
|
+
Project-URL: Repository, https://github.com/calimero-network/calimero-client-py
|
|
12
|
+
Project-URL: Issues, https://github.com/calimero-network/calimero-client-py/issues
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: requests>=2.31.0
|
|
25
|
+
Requires-Dist: websockets>=12.0
|
|
26
|
+
Requires-Dist: base58>=2.1.1
|
|
27
|
+
Requires-Dist: pydantic>=2.5.0
|
|
28
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
29
|
+
Requires-Dist: toml>=0.10.2
|
|
30
|
+
Requires-Dist: pynacl>=1.5.0
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: home-page
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
Dynamic: requires-python
|
|
40
|
+
|
|
41
|
+
# Calimero Network Python Client SDK
|
|
42
|
+
|
|
43
|
+
The **Calimero Python Client SDK** helps developers interact with decentralized apps by handling server communication. It simplifies the process, letting you focus on building your app while the SDK manages the technical details.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- JSON-RPC client for sending queries and updates to Calimero nodes
|
|
48
|
+
- WebSocket client for real-time subscriptions
|
|
49
|
+
- Authentication handling with Ed25519 keypairs
|
|
50
|
+
- Configuration management
|
|
51
|
+
- Type hints and comprehensive documentation
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install calimero-client-py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### Using JsonRpcClient
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from calimero import JsonRpcClient
|
|
65
|
+
|
|
66
|
+
client = JsonRpcClient(
|
|
67
|
+
base_url="http://localhost:2428",
|
|
68
|
+
endpoint="/jsonrpc"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
params = {
|
|
72
|
+
"applicationId": "your_application_id",
|
|
73
|
+
"method": "create_post",
|
|
74
|
+
"argsJson": {"title": "My First Post", "text": "This is my first post"}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
response = await client.mutate(params)
|
|
78
|
+
print(response)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Using WsSubscriptionsClient
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from calimero import WsSubscriptionsClient
|
|
85
|
+
|
|
86
|
+
client = WsSubscriptionsClient(
|
|
87
|
+
base_url="http://localhost:2428",
|
|
88
|
+
endpoint="/ws"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
await client.connect()
|
|
92
|
+
client.subscribe(["your_application_id"])
|
|
93
|
+
|
|
94
|
+
def callback(data):
|
|
95
|
+
print(data)
|
|
96
|
+
|
|
97
|
+
client.add_callback(callback)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
For detailed documentation, please visit [our documentation site](https://docs.calimero.network).
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
107
|
+
|
|
108
|
+
## Test Dependencies and Commands
|
|
109
|
+
|
|
110
|
+
To run tests, you need to install the test dependencies and then run the tests.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Install test dependencies
|
|
114
|
+
pip install -e ".[test]"
|
|
115
|
+
|
|
116
|
+
# Run tests
|
|
117
|
+
pytest
|
|
118
|
+
|
|
119
|
+
# Run tests with coverage
|
|
120
|
+
pytest --cov=calimero
|
|
121
|
+
|
|
122
|
+
# Run specific test file
|
|
123
|
+
pytest tests/test_keypair.py
|
|
124
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Calimero Network Python Client SDK
|
|
2
|
+
|
|
3
|
+
The **Calimero Python Client SDK** helps developers interact with decentralized apps by handling server communication. It simplifies the process, letting you focus on building your app while the SDK manages the technical details.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- JSON-RPC client for sending queries and updates to Calimero nodes
|
|
8
|
+
- WebSocket client for real-time subscriptions
|
|
9
|
+
- Authentication handling with Ed25519 keypairs
|
|
10
|
+
- Configuration management
|
|
11
|
+
- Type hints and comprehensive documentation
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install calimero-client-py
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
### Using JsonRpcClient
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from calimero import JsonRpcClient
|
|
25
|
+
|
|
26
|
+
client = JsonRpcClient(
|
|
27
|
+
base_url="http://localhost:2428",
|
|
28
|
+
endpoint="/jsonrpc"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
params = {
|
|
32
|
+
"applicationId": "your_application_id",
|
|
33
|
+
"method": "create_post",
|
|
34
|
+
"argsJson": {"title": "My First Post", "text": "This is my first post"}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
response = await client.mutate(params)
|
|
38
|
+
print(response)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Using WsSubscriptionsClient
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from calimero import WsSubscriptionsClient
|
|
45
|
+
|
|
46
|
+
client = WsSubscriptionsClient(
|
|
47
|
+
base_url="http://localhost:2428",
|
|
48
|
+
endpoint="/ws"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await client.connect()
|
|
52
|
+
client.subscribe(["your_application_id"])
|
|
53
|
+
|
|
54
|
+
def callback(data):
|
|
55
|
+
print(data)
|
|
56
|
+
|
|
57
|
+
client.add_callback(callback)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
For detailed documentation, please visit [our documentation site](https://docs.calimero.network).
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
67
|
+
|
|
68
|
+
## Test Dependencies and Commands
|
|
69
|
+
|
|
70
|
+
To run tests, you need to install the test dependencies and then run the tests.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Install test dependencies
|
|
74
|
+
pip install -e ".[test]"
|
|
75
|
+
|
|
76
|
+
# Run tests
|
|
77
|
+
pytest
|
|
78
|
+
|
|
79
|
+
# Run tests with coverage
|
|
80
|
+
pytest --cov=calimero
|
|
81
|
+
|
|
82
|
+
# Run specific test file
|
|
83
|
+
pytest tests/test_keypair.py
|
|
84
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calimero Network Python Client SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
from .config import Config, ConfigError
|
|
8
|
+
from .keypair import Ed25519Keypair, KeypairError
|
|
9
|
+
from .json_rpc_client import JsonRpcClient
|
|
10
|
+
from .ws_subscriptions_client import WsSubscriptionsClient
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'Config',
|
|
14
|
+
'ConfigError',
|
|
15
|
+
'Ed25519Keypair',
|
|
16
|
+
'KeypairError',
|
|
17
|
+
'JsonRpcClient',
|
|
18
|
+
'WsSubscriptionsClient'
|
|
19
|
+
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
import toml
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
from .keypair import Ed25519Keypair
|
|
7
|
+
|
|
8
|
+
class IdentityConfig(BaseModel):
|
|
9
|
+
peer_id: str
|
|
10
|
+
keypair: str
|
|
11
|
+
|
|
12
|
+
class SwarmConfig(BaseModel):
|
|
13
|
+
listen: list[str]
|
|
14
|
+
|
|
15
|
+
class ServerConfig(BaseModel):
|
|
16
|
+
listen: list[str]
|
|
17
|
+
admin: Dict[str, bool]
|
|
18
|
+
jsonrpc: Dict[str, bool]
|
|
19
|
+
websocket: Dict[str, bool]
|
|
20
|
+
|
|
21
|
+
class ConfigError(Exception):
|
|
22
|
+
"""Exception raised for configuration-related errors."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
"""Configuration for Calimero client."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, file_path: str):
|
|
29
|
+
"""Initialize with a TOML file path."""
|
|
30
|
+
self._config_data = self._load_toml(file_path)
|
|
31
|
+
|
|
32
|
+
# Load keypair
|
|
33
|
+
keypair_value = self._config_data.get('identity', {}).get('keypair')
|
|
34
|
+
if not keypair_value:
|
|
35
|
+
raise ConfigError("'keypair' not found in [identity] section")
|
|
36
|
+
self.keypair = Ed25519Keypair.from_base58(keypair_value)
|
|
37
|
+
|
|
38
|
+
# Load network configuration
|
|
39
|
+
network = self._config_data.get('network', {})
|
|
40
|
+
self.node_url = network.get('rpc_url')
|
|
41
|
+
self.context_id = network.get('context_id')
|
|
42
|
+
self.executor_public_key = network.get('executor_public_key')
|
|
43
|
+
self.node_name = network.get('node_name')
|
|
44
|
+
|
|
45
|
+
# Validate required fields
|
|
46
|
+
if not self.node_url:
|
|
47
|
+
raise ConfigError("'rpc_url' not found in [network] section")
|
|
48
|
+
if not self.context_id:
|
|
49
|
+
raise ConfigError("'context_id' not found in [network] section")
|
|
50
|
+
if not self.executor_public_key:
|
|
51
|
+
raise ConfigError("'executor_public_key' not found in [network] section")
|
|
52
|
+
if not self.node_name:
|
|
53
|
+
raise ConfigError("'node_name' not found in [network] section")
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: str) -> Any:
|
|
56
|
+
"""Allow dynamic access to raw config data."""
|
|
57
|
+
return self._config_data[key]
|
|
58
|
+
|
|
59
|
+
def __getattr__(self, name: str) -> Any:
|
|
60
|
+
"""Allow dynamic access to config sections."""
|
|
61
|
+
if name in self._config_data:
|
|
62
|
+
return self._config_data[name]
|
|
63
|
+
raise AttributeError(f"'Config' object has no attribute '{name}'")
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def load_from_file(cls, file_path: str) -> 'Config':
|
|
67
|
+
"""Load configuration from a TOML file."""
|
|
68
|
+
return cls(file_path)
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_default_config_path(cls) -> Path:
|
|
72
|
+
"""Get the default configuration path."""
|
|
73
|
+
node_name = os.getenv('CALIMERO_NODE_NAME', 'node1')
|
|
74
|
+
return Path.home() / '.calimero' / node_name / 'config.toml'
|
|
75
|
+
|
|
76
|
+
def _load_toml(self, file_path: str) -> dict:
|
|
77
|
+
"""Load and parse TOML file."""
|
|
78
|
+
try:
|
|
79
|
+
with open(file_path, 'r') as f:
|
|
80
|
+
return toml.load(f)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise ConfigError(f"Failed to load config file: {str(e)}")
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def load_from_env(cls) -> 'Config':
|
|
86
|
+
"""Load configuration from environment variables."""
|
|
87
|
+
import tempfile
|
|
88
|
+
|
|
89
|
+
# Create temporary TOML file
|
|
90
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f:
|
|
91
|
+
config_data = {
|
|
92
|
+
'identity': {
|
|
93
|
+
'keypair': os.getenv('CALIMERO_KEYPAIR')
|
|
94
|
+
},
|
|
95
|
+
'network': {
|
|
96
|
+
'rpc_url': os.getenv('RPC_URL', 'http://localhost:2428'),
|
|
97
|
+
'context_id': os.getenv('CONTEXT_ID'),
|
|
98
|
+
'executor_public_key': os.getenv('EXECUTOR_PUBLIC_KEY'),
|
|
99
|
+
'node_name': os.getenv('CALIMERO_NODE_NAME')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
toml.dump(config_data, f)
|
|
103
|
+
f.flush()
|
|
104
|
+
return cls(f.name)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
import base58
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, Dict, Any, TypedDict
|
|
7
|
+
import aiohttp
|
|
8
|
+
from .config import Config
|
|
9
|
+
from .keypair import Ed25519Keypair
|
|
10
|
+
|
|
11
|
+
class JsonRpcError(Exception):
|
|
12
|
+
"""Base exception for JSON-RPC errors."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class JsonRpcResponse(TypedDict):
|
|
16
|
+
"""Type definition for JSON-RPC response."""
|
|
17
|
+
jsonrpc: str
|
|
18
|
+
id: int
|
|
19
|
+
result: Optional[Dict[str, Any]]
|
|
20
|
+
error: Optional[Dict[str, Any]]
|
|
21
|
+
|
|
22
|
+
class JsonRpcClient:
|
|
23
|
+
"""JSON-RPC client for Calimero.
|
|
24
|
+
|
|
25
|
+
This client handles communication with the Calimero JSON-RPC server,
|
|
26
|
+
including request formatting, signing, and response handling.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Constants
|
|
30
|
+
JSONRPC_VERSION = '2.0'
|
|
31
|
+
DEFAULT_TIMEOUT = 1000
|
|
32
|
+
JSONRPC_PATH = '/jsonrpc/dev'
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: Config):
|
|
35
|
+
"""Initialize the JSON-RPC client with a config.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: Configuration object containing node URL and keypair.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If required environment variables are not set.
|
|
42
|
+
"""
|
|
43
|
+
self.config = config
|
|
44
|
+
self.node_url = config.node_url
|
|
45
|
+
self.keypair = config.keypair
|
|
46
|
+
self.context_id = os.getenv('CONTEXT_ID')
|
|
47
|
+
self.executor_public_key = os.getenv('EXECUTOR_PUBLIC_KEY')
|
|
48
|
+
|
|
49
|
+
self._validate_environment()
|
|
50
|
+
|
|
51
|
+
def _validate_environment(self) -> None:
|
|
52
|
+
"""Validate that required environment variables are set.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If any required environment variable is missing.
|
|
56
|
+
"""
|
|
57
|
+
if not self.context_id:
|
|
58
|
+
raise ValueError("CONTEXT_ID environment variable is not set")
|
|
59
|
+
if not self.executor_public_key:
|
|
60
|
+
raise ValueError("EXECUTOR_PUBLIC_KEY environment variable is not set")
|
|
61
|
+
|
|
62
|
+
def _prepare_headers(self) -> Dict[str, str]:
|
|
63
|
+
"""Prepare request headers with signature and timestamp.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dictionary containing request headers.
|
|
67
|
+
"""
|
|
68
|
+
timestamp = str(int(time.time()))
|
|
69
|
+
signature = self.keypair.sign(timestamp.encode())
|
|
70
|
+
signature_b58 = base58.b58encode(signature).decode()
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
'Content-Type': 'application/json',
|
|
74
|
+
'X-Signature': signature_b58,
|
|
75
|
+
'X-Timestamp': timestamp
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def _prepare_request(self, method: str, args: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
79
|
+
"""Prepare the JSON-RPC request payload.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
method: The method to call.
|
|
83
|
+
args: Optional arguments for the method.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Dictionary containing the JSON-RPC request payload.
|
|
87
|
+
"""
|
|
88
|
+
return {
|
|
89
|
+
'jsonrpc': self.JSONRPC_VERSION,
|
|
90
|
+
'id': 1,
|
|
91
|
+
'method': 'execute',
|
|
92
|
+
'params': {
|
|
93
|
+
'contextId': self.context_id,
|
|
94
|
+
'method': method,
|
|
95
|
+
'argsJson': args or {},
|
|
96
|
+
'executorPublicKey': self.executor_public_key,
|
|
97
|
+
'timeout': self.DEFAULT_TIMEOUT
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async def _handle_response(self, response: aiohttp.ClientResponse) -> JsonRpcResponse:
|
|
102
|
+
"""Handle the JSON-RPC response.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
response: The HTTP response from the server.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Parsed JSON-RPC response.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
JsonRpcError: If the response indicates an error.
|
|
112
|
+
"""
|
|
113
|
+
if response.status != 200:
|
|
114
|
+
error_text = await response.text()
|
|
115
|
+
raise JsonRpcError(f"HTTP error {response.status}: {error_text}")
|
|
116
|
+
|
|
117
|
+
result = await response.json()
|
|
118
|
+
if os.getenv('VERBOSE') == '1':
|
|
119
|
+
print(f"Result: {json.dumps(result, indent=2)}")
|
|
120
|
+
|
|
121
|
+
if 'error' in result:
|
|
122
|
+
raise JsonRpcError(f"JSON-RPC error: {result['error']}")
|
|
123
|
+
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
async def execute(self, method: str, args: Optional[Dict[str, Any]] = None) -> JsonRpcResponse:
|
|
127
|
+
"""Execute a JSON-RPC request.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
method: The method to call.
|
|
131
|
+
args: Optional arguments for the method.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Parsed JSON-RPC response.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
JsonRpcError: If the request fails or returns an error.
|
|
138
|
+
"""
|
|
139
|
+
headers = self._prepare_headers()
|
|
140
|
+
payload = self._prepare_request(method, args)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
async with aiohttp.ClientSession() as session:
|
|
144
|
+
async with session.post(
|
|
145
|
+
f"{self.node_url}{self.JSONRPC_PATH}",
|
|
146
|
+
headers=headers,
|
|
147
|
+
json=payload
|
|
148
|
+
) as response:
|
|
149
|
+
return await self._handle_response(response)
|
|
150
|
+
except aiohttp.ClientError as e:
|
|
151
|
+
raise JsonRpcError(f"Network error: {str(e)}")
|
|
152
|
+
except json.JSONDecodeError as e:
|
|
153
|
+
raise JsonRpcError(f"Invalid JSON response: {str(e)}")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import base58
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
import nacl.signing
|
|
4
|
+
import nacl.encoding
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
class KeypairError(Exception):
|
|
8
|
+
"""Exception raised for keypair-related errors."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
class Ed25519Keypair:
|
|
12
|
+
"""Ed25519 keypair implementation."""
|
|
13
|
+
|
|
14
|
+
PROTOBUF_PREFIX = b'\x08\x01\x12\x40' # 4-byte protobuf prefix for Ed25519 keypair
|
|
15
|
+
|
|
16
|
+
def __init__(self, signing_key: nacl.signing.SigningKey):
|
|
17
|
+
"""Initialize with a PyNaCl signing key."""
|
|
18
|
+
self._signing_key = signing_key
|
|
19
|
+
self._verify_key = signing_key.verify_key
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_base58(cls, base58_keypair: str) -> 'Ed25519Keypair':
|
|
23
|
+
"""Create a keypair from a base58-encoded keypair string."""
|
|
24
|
+
if not base58_keypair:
|
|
25
|
+
raise KeypairError("Base58 keypair cannot be None")
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
# Decode base58
|
|
29
|
+
key_bytes = base58.b58decode(base58_keypair)
|
|
30
|
+
|
|
31
|
+
# Validate keypair length (should be 68 bytes: 4-byte prefix + 64-byte keypair)
|
|
32
|
+
if len(key_bytes) != 68:
|
|
33
|
+
raise KeypairError(f"Unexpected keypair length: {len(key_bytes)} bytes (expected 68)")
|
|
34
|
+
|
|
35
|
+
# Validate protobuf prefix
|
|
36
|
+
prefix = key_bytes[:4]
|
|
37
|
+
if prefix != cls.PROTOBUF_PREFIX:
|
|
38
|
+
raise KeypairError(f"Invalid protobuf prefix: {prefix.hex()} (expected {cls.PROTOBUF_PREFIX.hex()})")
|
|
39
|
+
|
|
40
|
+
# Extract private key (first 32 bytes after prefix)
|
|
41
|
+
private_key = key_bytes[4:36]
|
|
42
|
+
if len(private_key) != 32:
|
|
43
|
+
raise KeypairError(f"Invalid private key length: {len(private_key)} (expected 32)")
|
|
44
|
+
|
|
45
|
+
# Create signing key from private key
|
|
46
|
+
signing_key = nacl.signing.SigningKey(private_key)
|
|
47
|
+
return cls(signing_key)
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise KeypairError(f"Failed to create keypair: {str(e)}")
|
|
51
|
+
|
|
52
|
+
def to_base58(self) -> str:
|
|
53
|
+
"""Convert keypair to base58-encoded string with protobuf prefix."""
|
|
54
|
+
# Get the private key bytes
|
|
55
|
+
private_key = self._signing_key.encode()
|
|
56
|
+
|
|
57
|
+
# Create the full keypair bytes with protobuf prefix
|
|
58
|
+
keypair_bytes = self.PROTOBUF_PREFIX + private_key
|
|
59
|
+
|
|
60
|
+
# Encode to base58
|
|
61
|
+
return base58.b58encode(keypair_bytes).decode()
|
|
62
|
+
|
|
63
|
+
def sign(self, message: bytes) -> bytes:
|
|
64
|
+
"""Sign a message."""
|
|
65
|
+
return self._signing_key.sign(message).signature
|
|
66
|
+
|
|
67
|
+
def verify(self, message: bytes, signature: bytes) -> bool:
|
|
68
|
+
"""Verify a signature."""
|
|
69
|
+
try:
|
|
70
|
+
self._verify_key.verify(message, signature)
|
|
71
|
+
return True
|
|
72
|
+
except nacl.exceptions.BadSignatureError:
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
def get_public_key(self) -> bytes:
|
|
76
|
+
"""Get the public key."""
|
|
77
|
+
return self._verify_key.encode()
|
|
78
|
+
|
|
79
|
+
def get_public_key_base58(self) -> str:
|
|
80
|
+
"""Get the public key as base58-encoded string."""
|
|
81
|
+
return base58.b58encode(self.get_public_key()).decode()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def public_key(self) -> bytes:
|
|
85
|
+
"""Get the public key in raw bytes."""
|
|
86
|
+
return self._verify_key.encode()
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def public_key_b58(self) -> str:
|
|
90
|
+
"""Get the public key in base58 format."""
|
|
91
|
+
return base58.b58encode(self.public_key).decode()
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def private_key(self) -> bytes:
|
|
95
|
+
"""Get the private key."""
|
|
96
|
+
return self._signing_key.encode()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def private_key_base58(self) -> str:
|
|
100
|
+
"""Get the base58-encoded private key."""
|
|
101
|
+
return base58.b58encode(self.private_key).decode()
|
|
102
|
+
|
|
103
|
+
def sign_base58(self, message: str) -> str:
|
|
104
|
+
"""Sign a message and return base58-encoded signature."""
|
|
105
|
+
signature = self.sign(message.encode())
|
|
106
|
+
return base58.b58encode(signature).decode()
|
|
107
|
+
|
|
108
|
+
def verify_base58(self, message: str, base58_signature: str) -> bool:
|
|
109
|
+
"""Verify a base58-encoded signature."""
|
|
110
|
+
signature = base58.b58decode(base58_signature)
|
|
111
|
+
return self.verify(message.encode(), signature)
|