Initial commit: Python SDK for Dragonchain (Prime)
Synchronous Python SDK modeled on prime-sdk-go. Provides DC1-HMAC-SHA256 auth, dataclass models, and resource clients for system, transaction, transaction-type, smart-contract, and block endpoints, plus a YAML credentials loader.
This commit is contained in:
145
prime_sdk/__init__.py
Normal file
145
prime_sdk/__init__.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Python SDK for interacting with Dragonchain (Prime) nodes.
|
||||
|
||||
Example — fire and forget (most use cases)::
|
||||
|
||||
from prime_sdk import DragonchainSDK, TransactionCreateRequest
|
||||
|
||||
client = DragonchainSDK(
|
||||
"your-public-id",
|
||||
"your-auth-key-id",
|
||||
"your-auth-key",
|
||||
"https://your-dragonchain-endpoint.com",
|
||||
)
|
||||
|
||||
resp = client.transaction.create(
|
||||
TransactionCreateRequest(
|
||||
txn_type="my-transaction-type",
|
||||
payload='{"message": "Hello Dragonchain"}',
|
||||
)
|
||||
)
|
||||
print("Transaction ID:", resp.transaction_id)
|
||||
# Done — no need to wait for a block.
|
||||
|
||||
Example — wait for block inclusion (interchain / Daria)::
|
||||
|
||||
import time
|
||||
|
||||
resp = client.transaction.create(
|
||||
TransactionCreateRequest(
|
||||
txn_type="my-transaction-type",
|
||||
payload='{"message": "Hello Dragonchain"}',
|
||||
)
|
||||
)
|
||||
|
||||
while True:
|
||||
txn = client.transaction.get(resp.transaction_id)
|
||||
if txn.header.block_id:
|
||||
print("Block ID:", txn.header.block_id)
|
||||
break
|
||||
time.sleep(2)
|
||||
"""
|
||||
|
||||
from .block import BlockClient
|
||||
from .client import CONTENT_TYPE_JSON, Client
|
||||
from .contract import ContractClient
|
||||
from .errors import DragonchainAPIError, DragonchainError
|
||||
from .models import (
|
||||
Block,
|
||||
BlockProof,
|
||||
GrpcConnectionInfo,
|
||||
ListResponse,
|
||||
ListTransactionsResponse,
|
||||
SmartContract,
|
||||
SmartContractCreateRequest,
|
||||
SmartContractExecutionInfo,
|
||||
SmartContractUpdateRequest,
|
||||
SuccessResponse,
|
||||
SystemStatus,
|
||||
Transaction,
|
||||
TransactionBulkRequest,
|
||||
TransactionBulkResponse,
|
||||
TransactionCreateRequest,
|
||||
TransactionCreateResponse,
|
||||
TransactionHeader,
|
||||
TransactionListResponse,
|
||||
TransactionProof,
|
||||
TransactionType,
|
||||
TransactionTypeCreateRequest,
|
||||
TransactionTypeCreateResponse,
|
||||
)
|
||||
from .system import SystemClient
|
||||
from .transaction import TransactionClient
|
||||
from .transaction_type import TransactionTypeClient
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
class DragonchainSDK:
|
||||
"""Main SDK client for interacting with Dragonchain nodes.
|
||||
|
||||
Provides access to all API endpoints through specialized client instances:
|
||||
``system``, ``transaction``, ``transaction_type``, ``contract``, ``block``.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
public_id: str,
|
||||
auth_key_id: str,
|
||||
auth_key: str,
|
||||
base_url: str,
|
||||
):
|
||||
"""Create a new Dragonchain SDK client.
|
||||
|
||||
Args:
|
||||
public_id: Your Dragonchain public ID.
|
||||
auth_key_id: Your authentication key ID.
|
||||
auth_key: Your authentication key.
|
||||
base_url: Base URL of your node (e.g. "https://chains.dragonchain.com").
|
||||
"""
|
||||
self._client = Client(public_id, auth_key_id, auth_key, base_url)
|
||||
self.transaction = TransactionClient(self._client)
|
||||
self.transaction_type = TransactionTypeClient(self._client)
|
||||
self.contract = ContractClient(self._client)
|
||||
self.block = BlockClient(self._client)
|
||||
self.system = SystemClient(self._client)
|
||||
|
||||
def get_client(self) -> Client:
|
||||
"""Return the underlying HTTP client (advanced use)."""
|
||||
return self._client
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DragonchainSDK",
|
||||
"Client",
|
||||
"CONTENT_TYPE_JSON",
|
||||
"DragonchainError",
|
||||
"DragonchainAPIError",
|
||||
"BlockClient",
|
||||
"ContractClient",
|
||||
"SystemClient",
|
||||
"TransactionClient",
|
||||
"TransactionTypeClient",
|
||||
# models
|
||||
"Block",
|
||||
"BlockProof",
|
||||
"GrpcConnectionInfo",
|
||||
"ListResponse",
|
||||
"ListTransactionsResponse",
|
||||
"SmartContract",
|
||||
"SmartContractCreateRequest",
|
||||
"SmartContractExecutionInfo",
|
||||
"SmartContractUpdateRequest",
|
||||
"SuccessResponse",
|
||||
"SystemStatus",
|
||||
"Transaction",
|
||||
"TransactionBulkRequest",
|
||||
"TransactionBulkResponse",
|
||||
"TransactionCreateRequest",
|
||||
"TransactionCreateResponse",
|
||||
"TransactionHeader",
|
||||
"TransactionListResponse",
|
||||
"TransactionProof",
|
||||
"TransactionType",
|
||||
"TransactionTypeCreateRequest",
|
||||
"TransactionTypeCreateResponse",
|
||||
]
|
||||
12
prime_sdk/block.py
Normal file
12
prime_sdk/block.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Block endpoints."""
|
||||
|
||||
from .client import Client
|
||||
from .models import Block
|
||||
|
||||
|
||||
class BlockClient:
|
||||
def __init__(self, client: Client):
|
||||
self._client = client
|
||||
|
||||
def get(self, block_id: str) -> Block:
|
||||
return self._client.get(f"/api/v1/block/{block_id}", Block)
|
||||
167
prime_sdk/client.py
Normal file
167
prime_sdk/client.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""Low-level HTTP client implementing Dragonchain's DC1-HMAC-SHA256 auth.
|
||||
|
||||
This is a direct port of the Go SDK's ``client/client.go``. The signing scheme
|
||||
must match the prime-node authorizer byte-for-byte: the signed message is six
|
||||
fields joined by ``\\n`` (no trailing newline) in this exact order::
|
||||
|
||||
UPPER(method)
|
||||
path # full path incl. /api/v1/..., no query string
|
||||
publicId
|
||||
timestamp
|
||||
contentType
|
||||
base64(sha256(body))
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Optional, Type, TypeVar
|
||||
|
||||
import requests
|
||||
|
||||
from .errors import DragonchainAPIError
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
CONTENT_TYPE_JSON = "application/json"
|
||||
DEFAULT_TIMEOUT = 30 # seconds
|
||||
|
||||
|
||||
class Client:
|
||||
"""Authenticated HTTP client for a single Dragonchain node."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
public_id: str,
|
||||
auth_key_id: str,
|
||||
auth_key: str,
|
||||
base_url: str,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
):
|
||||
self.public_id = public_id
|
||||
self.auth_key_id = auth_key_id
|
||||
self.auth_key = auth_key
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout
|
||||
self._session = requests.Session()
|
||||
|
||||
# -- introspection helpers (mirror the Go accessors) --------------------
|
||||
|
||||
def internal_id(self) -> str:
|
||||
return self.public_id
|
||||
|
||||
def get_public_id(self) -> str:
|
||||
return self.public_id
|
||||
|
||||
def get_auth_key_id(self) -> str:
|
||||
return self.auth_key_id
|
||||
|
||||
def endpoint(self) -> str:
|
||||
return self.base_url
|
||||
|
||||
# -- auth ---------------------------------------------------------------
|
||||
|
||||
def _hmac_message(
|
||||
self, method: str, path: str, timestamp: str, content_type: str, body: bytes
|
||||
) -> str:
|
||||
b64_content = base64.b64encode(hashlib.sha256(body).digest()).decode("ascii")
|
||||
return "\n".join(
|
||||
[
|
||||
method.upper(),
|
||||
path,
|
||||
self.public_id,
|
||||
timestamp,
|
||||
content_type,
|
||||
b64_content,
|
||||
]
|
||||
)
|
||||
|
||||
def _generate_auth_header(
|
||||
self, method: str, path: str, timestamp: str, content_type: str, body: bytes
|
||||
) -> str:
|
||||
msg = self._hmac_message(method, path, timestamp, content_type, body)
|
||||
digest = hmac.new(
|
||||
self.auth_key.encode("utf-8"), msg.encode("utf-8"), hashlib.sha256
|
||||
).digest()
|
||||
b64_hmac = base64.b64encode(digest).decode("ascii")
|
||||
return f"DC1-HMAC-SHA256 {self.auth_key_id}:{b64_hmac}"
|
||||
|
||||
# -- request plumbing ---------------------------------------------------
|
||||
|
||||
def _do_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
content_type: str = "",
|
||||
body: Any = None,
|
||||
response_cls: Optional[Type[T]] = None,
|
||||
) -> Optional[T]:
|
||||
if isinstance(body, (bytes, bytearray)):
|
||||
body_bytes = bytes(body)
|
||||
elif body is not None:
|
||||
body_bytes = json.dumps(_to_jsonable(body)).encode("utf-8")
|
||||
content_type = CONTENT_TYPE_JSON
|
||||
else:
|
||||
body_bytes = b""
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
auth_header = self._generate_auth_header(
|
||||
method, path, timestamp, content_type, body_bytes
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": auth_header,
|
||||
"Dragonchain": self.public_id,
|
||||
"Timestamp": timestamp,
|
||||
}
|
||||
if content_type:
|
||||
headers["Content-Type"] = content_type
|
||||
|
||||
url = self.base_url + path
|
||||
resp = self._session.request(
|
||||
method, url, data=body_bytes, headers=headers, timeout=self.timeout
|
||||
)
|
||||
|
||||
resp_text = resp.text
|
||||
if resp.status_code >= 400:
|
||||
raise DragonchainAPIError(resp.status_code, resp_text.strip())
|
||||
|
||||
if response_cls is not None and resp_text:
|
||||
parsed = json.loads(resp_text)
|
||||
return response_cls.from_dict(parsed) # type: ignore[attr-defined]
|
||||
return None
|
||||
|
||||
# -- verb helpers -------------------------------------------------------
|
||||
|
||||
def get(self, path: str, response_cls: Optional[Type[T]] = None) -> Optional[T]:
|
||||
return self._do_request("GET", path, response_cls=response_cls)
|
||||
|
||||
def post(
|
||||
self,
|
||||
path: str,
|
||||
content_type: str,
|
||||
body: Any,
|
||||
response_cls: Optional[Type[T]] = None,
|
||||
) -> Optional[T]:
|
||||
return self._do_request("POST", path, content_type, body, response_cls)
|
||||
|
||||
def put(
|
||||
self,
|
||||
path: str,
|
||||
content_type: str,
|
||||
body: Any,
|
||||
response_cls: Optional[Type[T]] = None,
|
||||
) -> Optional[T]:
|
||||
return self._do_request("PUT", path, content_type, body, response_cls)
|
||||
|
||||
def delete(self, path: str, response_cls: Optional[Type[T]] = None) -> Optional[T]:
|
||||
return self._do_request("DELETE", path, response_cls=response_cls)
|
||||
|
||||
|
||||
def _to_jsonable(body: Any) -> Any:
|
||||
"""Convert request models (which expose ``to_dict``) to JSON-ready data."""
|
||||
if hasattr(body, "to_dict"):
|
||||
return body.to_dict()
|
||||
return body
|
||||
49
prime_sdk/contract.py
Normal file
49
prime_sdk/contract.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Smart-contract endpoints."""
|
||||
|
||||
from .client import CONTENT_TYPE_JSON, Client
|
||||
from .models import (
|
||||
ListResponse,
|
||||
SmartContract,
|
||||
SmartContractCreateRequest,
|
||||
SmartContractUpdateRequest,
|
||||
SuccessResponse,
|
||||
)
|
||||
|
||||
|
||||
class ContractClient:
|
||||
def __init__(self, client: Client):
|
||||
self._client = client
|
||||
|
||||
def create(self, req: SmartContractCreateRequest) -> SmartContract:
|
||||
return self._client.post(
|
||||
"/api/v1/contract", CONTENT_TYPE_JSON, req, SmartContract
|
||||
)
|
||||
|
||||
def get(self, contract_id: str) -> SmartContract:
|
||||
return self._client.get(f"/api/v1/contract/{contract_id}", SmartContract)
|
||||
|
||||
def list(self) -> ListResponse:
|
||||
return self._client.get("/api/v1/contract", ListResponse)
|
||||
|
||||
def update(
|
||||
self, contract_id: str, req: SmartContractUpdateRequest
|
||||
) -> SuccessResponse:
|
||||
return self._client.put(
|
||||
f"/api/v1/contract/{contract_id}", CONTENT_TYPE_JSON, req, SuccessResponse
|
||||
)
|
||||
|
||||
def upload(self, contract_id: str, filepath: str) -> SuccessResponse:
|
||||
"""Upload smart-contract code from a local file (raw octet-stream body)."""
|
||||
with open(filepath, "rb") as f:
|
||||
file_content = f.read()
|
||||
return self._client.put(
|
||||
f"/api/v1/contract/{contract_id}/upload",
|
||||
"application/octet-stream",
|
||||
file_content,
|
||||
SuccessResponse,
|
||||
)
|
||||
|
||||
def delete(self, contract_id: str) -> SuccessResponse:
|
||||
return self._client.delete(
|
||||
f"/api/v1/contract/{contract_id}", SuccessResponse
|
||||
)
|
||||
127
prime_sdk/credentials.py
Normal file
127
prime_sdk/credentials.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""YAML-based chain credentials, ported from ``credentials/credentials.go``.
|
||||
|
||||
The YAML format mirrors the Go SDK::
|
||||
|
||||
default: my-public-id
|
||||
chains:
|
||||
- name: my-chain
|
||||
publicId: my-public-id
|
||||
authKeyId: my-auth-key-id
|
||||
authKey: my-auth-key
|
||||
endpoint: https://chains.dragonchain.com
|
||||
|
||||
Note: unlike the Go version (which uses yaml.Node to preserve comments and
|
||||
blank-line formatting), this loader uses PyYAML and does not round-trip
|
||||
comments on ``save_config``.
|
||||
"""
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChainConfig:
|
||||
name: str = ""
|
||||
public_id: str = ""
|
||||
auth_key_id: str = ""
|
||||
auth_key: str = ""
|
||||
endpoint: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "ChainConfig":
|
||||
return cls(
|
||||
name=d.get("name", ""),
|
||||
public_id=d.get("publicId", ""),
|
||||
auth_key_id=d.get("authKeyId", ""),
|
||||
auth_key=d.get("authKey", ""),
|
||||
endpoint=d.get("endpoint", ""),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"name": self.name,
|
||||
"publicId": self.public_id,
|
||||
"authKeyId": self.auth_key_id,
|
||||
"authKey": self.auth_key,
|
||||
"endpoint": self.endpoint,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
default: str = ""
|
||||
chains: List[ChainConfig] = field(default_factory=list)
|
||||
|
||||
# -- factory helpers ----------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "Config":
|
||||
return cls(
|
||||
default=d.get("default", ""),
|
||||
chains=[ChainConfig.from_dict(c) for c in (d.get("chains") or [])],
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"default": self.default,
|
||||
"chains": [c.to_dict() for c in self.chains],
|
||||
}
|
||||
|
||||
# -- lookups ------------------------------------------------------------
|
||||
|
||||
def get_default_chain(self) -> Optional[ChainConfig]:
|
||||
"""Return the chain configuration for the default publicId, or None."""
|
||||
return self.get_chain_by_public_id(self.default)
|
||||
|
||||
def get_chain_by_public_id(self, public_id: str) -> Optional[ChainConfig]:
|
||||
for chain in self.chains:
|
||||
if chain.public_id == public_id:
|
||||
return chain
|
||||
return None
|
||||
|
||||
def list_chains(self) -> List[str]:
|
||||
"""Return all chain names."""
|
||||
return [c.name for c in self.chains]
|
||||
|
||||
# -- mutations ----------------------------------------------------------
|
||||
|
||||
def add_chain(self, chain: ChainConfig) -> None:
|
||||
self.chains.append(chain)
|
||||
|
||||
def set_default(self, public_id: str) -> None:
|
||||
self.default = public_id
|
||||
|
||||
def delete_chain(self, public_id: str) -> None:
|
||||
"""Delete a chain by publicId. Raises ValueError if it's the default."""
|
||||
if public_id == self.default:
|
||||
raise ValueError("cannot delete default chain")
|
||||
self.chains = [c for c in self.chains if c.public_id != public_id]
|
||||
|
||||
# -- persistence --------------------------------------------------------
|
||||
|
||||
def save_config(self, file_path: str) -> None:
|
||||
expanded = expand_path(file_path)
|
||||
with open(expanded, "w", encoding="utf-8") as f:
|
||||
yaml.safe_dump(self.to_dict(), f, sort_keys=False, default_flow_style=False)
|
||||
|
||||
|
||||
def load_config(file_path: str) -> Config:
|
||||
"""Read and parse a YAML configuration file."""
|
||||
expanded = expand_path(file_path)
|
||||
with open(expanded, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f) or {}
|
||||
return Config.from_dict(data)
|
||||
|
||||
|
||||
def load_config_from_string(yaml_content: str) -> Config:
|
||||
"""Parse YAML configuration from a string."""
|
||||
data = yaml.safe_load(yaml_content) or {}
|
||||
return Config.from_dict(data)
|
||||
|
||||
|
||||
def expand_path(path: str) -> str:
|
||||
"""Expand environment variables and a leading ``~`` in a path."""
|
||||
return os.path.expanduser(os.path.expandvars(path))
|
||||
19
prime_sdk/errors.py
Normal file
19
prime_sdk/errors.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Exceptions raised by the Dragonchain SDK."""
|
||||
|
||||
|
||||
class DragonchainError(Exception):
|
||||
"""Base class for all errors raised by the SDK."""
|
||||
|
||||
|
||||
class DragonchainAPIError(DragonchainError):
|
||||
"""Raised when the Dragonchain node returns an HTTP error (status >= 400).
|
||||
|
||||
Attributes:
|
||||
status_code: The HTTP status code returned by the node.
|
||||
body: The (trimmed) response body returned by the node.
|
||||
"""
|
||||
|
||||
def __init__(self, status_code: int, body: str):
|
||||
self.status_code = status_code
|
||||
self.body = body
|
||||
super().__init__(f"API error (status {status_code}): {body}")
|
||||
395
prime_sdk/models.py
Normal file
395
prime_sdk/models.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""Request and response models, ported from the Go SDK's ``models/models.go``.
|
||||
|
||||
Request models expose ``to_dict()`` which emits the exact JSON keys the node
|
||||
expects and drops empty/optional values (the Go ``omitempty`` equivalent).
|
||||
Response models expose a ``from_dict()`` classmethod that reads the exact JSON
|
||||
keys returned by the node (snake_case for transaction/block models, camelCase
|
||||
for contract/status models — preserved verbatim from the Go struct tags).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
CONTENT_TYPE_JSON = "application/json"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Transactions
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionCreateRequest:
|
||||
txn_type: str
|
||||
payload: str
|
||||
tag: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {"txn_type": self.txn_type, "payload": self.payload}
|
||||
if self.version:
|
||||
d["version"] = self.version
|
||||
if self.tag:
|
||||
d["tag"] = self.tag
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionCreateResponse:
|
||||
transaction_id: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionCreateResponse":
|
||||
return cls(transaction_id=d.get("transaction_id", ""))
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionBulkRequest:
|
||||
transactions: List[TransactionCreateRequest] = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {"transactions": [t.to_dict() for t in self.transactions]}
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionBulkResponse:
|
||||
transaction_ids: List[str] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionBulkResponse":
|
||||
return cls(transaction_ids=d.get("transaction_ids") or [])
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionHeader:
|
||||
tag: str = ""
|
||||
dc_id: str = ""
|
||||
txn_id: str = ""
|
||||
invoker: str = ""
|
||||
block_id: str = ""
|
||||
txn_type: str = ""
|
||||
timestamp: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionHeader":
|
||||
return cls(
|
||||
tag=d.get("tag", ""),
|
||||
dc_id=d.get("dc_id", ""),
|
||||
txn_id=d.get("txn_id", ""),
|
||||
invoker=d.get("invoker", ""),
|
||||
block_id=d.get("block_id", ""),
|
||||
txn_type=d.get("txn_type", ""),
|
||||
timestamp=d.get("timestamp", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionProof:
|
||||
full: str = ""
|
||||
stripped: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionProof":
|
||||
return cls(full=d.get("full", ""), stripped=d.get("stripped", ""))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
version: str = ""
|
||||
header: TransactionHeader = field(default_factory=TransactionHeader)
|
||||
proof: TransactionProof = field(default_factory=TransactionProof)
|
||||
payload: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "Transaction":
|
||||
return cls(
|
||||
version=d.get("version", ""),
|
||||
header=TransactionHeader.from_dict(d.get("header") or {}),
|
||||
proof=TransactionProof.from_dict(d.get("proof") or {}),
|
||||
payload=d.get("payload", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListTransactionsResponse:
|
||||
transactions: List[Transaction] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "ListTransactionsResponse":
|
||||
return cls(
|
||||
transactions=[Transaction.from_dict(t) for t in (d.get("transactions") or [])]
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Transaction types
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionTypeCreateRequest:
|
||||
txn_type: str
|
||||
version: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {"txn_type": self.txn_type}
|
||||
if self.version:
|
||||
d["version"] = self.version
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionTypeCreateResponse:
|
||||
success: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionTypeCreateResponse":
|
||||
return cls(success=bool(d.get("success", False)))
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionType:
|
||||
version: str = ""
|
||||
created: int = 0
|
||||
modified: int = 0
|
||||
txn_type: str = ""
|
||||
contract_id: str = ""
|
||||
custom_indexes: List[Any] = field(default_factory=list)
|
||||
active_since_block: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionType":
|
||||
return cls(
|
||||
version=d.get("version", ""),
|
||||
created=d.get("created", 0),
|
||||
modified=d.get("modified", 0),
|
||||
txn_type=d.get("txn_type", ""),
|
||||
contract_id=d.get("contract_id", ""),
|
||||
custom_indexes=d.get("custom_indexes") or [],
|
||||
active_since_block=d.get("active_since_block", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransactionListResponse:
|
||||
"""Response for listing transaction types (key: ``transactionTypes``)."""
|
||||
|
||||
transaction_types: List[TransactionType] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "TransactionListResponse":
|
||||
return cls(
|
||||
transaction_types=[
|
||||
TransactionType.from_dict(t) for t in (d.get("transactionTypes") or [])
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Smart contracts
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmartContractCreateRequest:
|
||||
environment: str
|
||||
transaction_type: str
|
||||
execution_order: str
|
||||
environment_variables: Optional[Dict[str, str]] = None
|
||||
secrets: Optional[Dict[str, str]] = None
|
||||
remote: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"environment": self.environment,
|
||||
"transactionType": self.transaction_type,
|
||||
"executionOrder": self.execution_order,
|
||||
}
|
||||
if self.environment_variables:
|
||||
d["environmentVariables"] = self.environment_variables
|
||||
if self.secrets:
|
||||
d["secret"] = self.secrets
|
||||
if self.remote:
|
||||
d["remote"] = self.remote
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmartContractUpdateRequest:
|
||||
enabled: bool = False
|
||||
environment_variables: Optional[Dict[str, str]] = None
|
||||
secrets: Optional[Dict[str, str]] = None
|
||||
version: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {"enabled": self.enabled}
|
||||
if self.version:
|
||||
d["version"] = self.version
|
||||
if self.environment_variables:
|
||||
d["environmentVariables"] = self.environment_variables
|
||||
if self.secrets:
|
||||
d["secret"] = self.secrets
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrpcConnectionInfo:
|
||||
address: str = ""
|
||||
api_key: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "GrpcConnectionInfo":
|
||||
return cls(address=d.get("address", ""), api_key=d.get("apiKey", ""))
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmartContractExecutionInfo:
|
||||
type: str = ""
|
||||
executable_path: str = ""
|
||||
executable_working_directory: str = ""
|
||||
executable_hash: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "SmartContractExecutionInfo":
|
||||
return cls(
|
||||
type=d.get("type", ""),
|
||||
executable_path=d.get("executablePath", ""),
|
||||
executable_working_directory=d.get("executableWorkingDirectory", ""),
|
||||
executable_hash=d.get("executableHash", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SmartContract:
|
||||
id: str = ""
|
||||
created: int = 0
|
||||
modified: int = 0
|
||||
version: str = ""
|
||||
environment: str = ""
|
||||
transaction_type: str = ""
|
||||
execution_order: str = ""
|
||||
execution_info: Optional[SmartContractExecutionInfo] = None
|
||||
env_vars: Dict[str, str] = field(default_factory=dict)
|
||||
secrets: List[str] = field(default_factory=list)
|
||||
grpc_connection_info: Optional[GrpcConnectionInfo] = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "SmartContract":
|
||||
exec_info = d.get("executionInfo")
|
||||
grpc = d.get("grpcConnectionInfo")
|
||||
return cls(
|
||||
id=d.get("id", ""),
|
||||
created=d.get("created", 0),
|
||||
modified=d.get("modified", 0),
|
||||
version=d.get("version", ""),
|
||||
environment=d.get("environment", ""),
|
||||
transaction_type=d.get("transactionType", ""),
|
||||
execution_order=d.get("executionOrder", ""),
|
||||
execution_info=(
|
||||
SmartContractExecutionInfo.from_dict(exec_info) if exec_info else None
|
||||
),
|
||||
env_vars=d.get("envVars") or {},
|
||||
secrets=d.get("secrets") or [],
|
||||
grpc_connection_info=(GrpcConnectionInfo.from_dict(grpc) if grpc else None),
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Blocks
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockProof:
|
||||
scheme: str = ""
|
||||
proof: str = ""
|
||||
nonce: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "BlockProof":
|
||||
return cls(
|
||||
scheme=d.get("scheme", ""),
|
||||
proof=d.get("proof", ""),
|
||||
nonce=d.get("nonce", 0),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Block:
|
||||
version: str = ""
|
||||
id: str = ""
|
||||
timestamp: str = ""
|
||||
prev_id: str = ""
|
||||
prev_proof: str = ""
|
||||
transactions: List[str] = field(default_factory=list)
|
||||
proof: BlockProof = field(default_factory=BlockProof)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "Block":
|
||||
return cls(
|
||||
version=d.get("version", ""),
|
||||
id=d.get("block_id", ""),
|
||||
timestamp=d.get("timestamp", ""),
|
||||
prev_id=d.get("prev_id", ""),
|
||||
prev_proof=d.get("prev_proof", ""),
|
||||
transactions=d.get("transactions") or [],
|
||||
proof=BlockProof.from_dict(d.get("proof") or {}),
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# System / generic
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemStatus:
|
||||
id: str = ""
|
||||
level: int = 0
|
||||
url: str = ""
|
||||
hash_algo: str = ""
|
||||
scheme: str = ""
|
||||
version: str = ""
|
||||
encryption_algo: str = ""
|
||||
indexing_enabled: bool = False
|
||||
# Level 5 node fields
|
||||
funded: str = ""
|
||||
broadcast_interval: str = ""
|
||||
network: str = ""
|
||||
interchain_wallet: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "SystemStatus":
|
||||
return cls(
|
||||
id=d.get("id", ""),
|
||||
level=d.get("level", 0),
|
||||
url=d.get("url", ""),
|
||||
hash_algo=d.get("hashAlgo", ""),
|
||||
scheme=d.get("scheme", ""),
|
||||
version=d.get("version", ""),
|
||||
encryption_algo=d.get("encryptionAlgo", ""),
|
||||
indexing_enabled=bool(d.get("indexingEnabled", False)),
|
||||
funded=d.get("funded", ""),
|
||||
broadcast_interval=d.get("broadcastInterval", ""),
|
||||
network=d.get("network", ""),
|
||||
interchain_wallet=d.get("interchainWallet", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SuccessResponse:
|
||||
success: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "SuccessResponse":
|
||||
return cls(success=bool(d.get("success", False)))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListResponse:
|
||||
items: List[Any] = field(default_factory=list)
|
||||
total_count: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "ListResponse":
|
||||
return cls(items=d.get("items") or [], total_count=d.get("total_count", 0))
|
||||
17
prime_sdk/system.py
Normal file
17
prime_sdk/system.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""System endpoints (health, status)."""
|
||||
|
||||
from .client import Client
|
||||
from .models import SystemStatus
|
||||
|
||||
|
||||
class SystemClient:
|
||||
def __init__(self, client: Client):
|
||||
self._client = client
|
||||
|
||||
def health(self) -> None:
|
||||
"""Check system health. Raises ``DragonchainAPIError`` if unhealthy."""
|
||||
self._client.get("/api/v1/health")
|
||||
|
||||
def status(self) -> SystemStatus:
|
||||
"""Get system status."""
|
||||
return self._client.get("/api/v1/status", SystemStatus)
|
||||
43
prime_sdk/transaction.py
Normal file
43
prime_sdk/transaction.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Transaction endpoints."""
|
||||
|
||||
from .client import CONTENT_TYPE_JSON, Client
|
||||
from .models import (
|
||||
ListTransactionsResponse,
|
||||
Transaction,
|
||||
TransactionBulkRequest,
|
||||
TransactionBulkResponse,
|
||||
TransactionCreateRequest,
|
||||
TransactionCreateResponse,
|
||||
)
|
||||
|
||||
|
||||
class TransactionClient:
|
||||
def __init__(self, client: Client):
|
||||
self._client = client
|
||||
|
||||
def create(self, req: TransactionCreateRequest) -> TransactionCreateResponse:
|
||||
"""Submit a new transaction and return immediately with the assigned ID.
|
||||
|
||||
This does NOT wait for the transaction to be included in a block. Block
|
||||
processing happens asynchronously on a ~5-second cycle. If you need the
|
||||
block ID (e.g. for interchain verification), poll ``get`` until
|
||||
``header.block_id`` is populated.
|
||||
"""
|
||||
return self._client.post(
|
||||
"/api/v1/transaction", CONTENT_TYPE_JSON, req, TransactionCreateResponse
|
||||
)
|
||||
|
||||
def create_bulk(self, req: TransactionBulkRequest) -> TransactionBulkResponse:
|
||||
"""Submit multiple transactions and return immediately with their IDs.
|
||||
|
||||
Like ``create``, this does not wait for block inclusion.
|
||||
"""
|
||||
return self._client.post(
|
||||
"/api/v1/transaction/bulk", CONTENT_TYPE_JSON, req, TransactionBulkResponse
|
||||
)
|
||||
|
||||
def get(self, transaction_id: str) -> Transaction:
|
||||
return self._client.get(f"/api/v1/transaction/{transaction_id}", Transaction)
|
||||
|
||||
def list(self) -> ListTransactionsResponse:
|
||||
return self._client.get("/api/v1/transaction/", ListTransactionsResponse)
|
||||
38
prime_sdk/transaction_type.py
Normal file
38
prime_sdk/transaction_type.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Transaction-type endpoints."""
|
||||
|
||||
from .client import CONTENT_TYPE_JSON, Client
|
||||
from .models import (
|
||||
SuccessResponse,
|
||||
TransactionListResponse,
|
||||
TransactionType,
|
||||
TransactionTypeCreateRequest,
|
||||
TransactionTypeCreateResponse,
|
||||
)
|
||||
|
||||
|
||||
class TransactionTypeClient:
|
||||
def __init__(self, client: Client):
|
||||
self._client = client
|
||||
|
||||
def create(
|
||||
self, req: TransactionTypeCreateRequest
|
||||
) -> TransactionTypeCreateResponse:
|
||||
return self._client.post(
|
||||
"/api/v1/transaction-type",
|
||||
CONTENT_TYPE_JSON,
|
||||
req,
|
||||
TransactionTypeCreateResponse,
|
||||
)
|
||||
|
||||
def get(self, txn_type: str) -> TransactionType:
|
||||
return self._client.get(
|
||||
f"/api/v1/transaction-type/{txn_type}", TransactionType
|
||||
)
|
||||
|
||||
def list(self) -> TransactionListResponse:
|
||||
return self._client.get("/api/v1/transaction-types", TransactionListResponse)
|
||||
|
||||
def delete(self, txn_type: str) -> SuccessResponse:
|
||||
return self._client.delete(
|
||||
f"/api/v1/transaction-type/{txn_type}", SuccessResponse
|
||||
)
|
||||
Reference in New Issue
Block a user