synor/sdk/python/synor_wallet/client.py
Gulshan Yadav 59a7123535 feat(sdk): implement Phase 1 SDKs for Wallet, RPC, and Storage
Implements comprehensive SDK support for three core services across
four programming languages (JavaScript/TypeScript, Python, Go, Rust).

## New SDKs

### Wallet SDK
- Key management (create, import, export)
- Transaction signing
- Message signing and verification
- Balance and UTXO queries
- Stealth address support

### RPC SDK
- Block and transaction queries
- Chain state information
- Fee estimation
- Mempool information
- WebSocket subscriptions for real-time updates

### Storage SDK
- Content upload and download
- Pinning operations
- CAR file support
- Directory management
- Gateway URL generation

## Shared Infrastructure

- JSON Schema definitions for all 11 services
- Common type definitions (Address, Amount, UTXO, etc.)
- Unified error handling patterns
- Builder patterns for configuration

## Package Updates

- JavaScript: Updated to @synor/sdk with module exports
- Python: Updated to synor-sdk with websockets dependency
- Go: Added gorilla/websocket dependency
- Rust: Added base64, urlencoding, multipart support

## Fixes

- Fixed Tensor Default trait implementation
- Fixed ProcessorType enum casing
2026-01-27 00:46:24 +05:30

547 lines
15 KiB
Python

"""Synor Wallet Client."""
from typing import Any, Optional
import httpx
from .types import (
WalletConfig,
Network,
WalletType,
Wallet,
CreateWalletResult,
StealthAddress,
Transaction,
TransactionInput,
TransactionOutput,
SignedTransaction,
SignedMessage,
UTXO,
Balance,
TokenBalance,
BalanceResponse,
FeeEstimate,
Priority,
GetUtxosOptions,
ImportWalletOptions,
BuildTransactionOptions,
)
class WalletError(Exception):
"""Synor Wallet SDK error."""
def __init__(
self,
message: str,
status_code: Optional[int] = None,
code: Optional[str] = None,
):
super().__init__(message)
self.status_code = status_code
self.code = code
class SynorWallet:
"""
Synor Wallet SDK client.
Example:
>>> async with SynorWallet(api_key="sk_...") as wallet:
... result = await wallet.create_wallet()
... print(f"Address: {result.wallet.address}")
... print(f"Mnemonic: {result.mnemonic}") # Store securely!
...
... balance = await wallet.get_balance(result.wallet.address)
... print(f"Balance: {balance.native.total}")
"""
def __init__(
self,
api_key: str,
endpoint: str = "https://wallet.synor.cc/api/v1",
network: Network = Network.MAINNET,
timeout: float = 30.0,
debug: bool = False,
derivation_path: Optional[str] = None,
):
self.config = WalletConfig(
api_key=api_key,
endpoint=endpoint,
network=network,
timeout=timeout,
debug=debug,
derivation_path=derivation_path,
)
self._client = httpx.AsyncClient(
base_url=endpoint,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"X-Network": network.value,
},
timeout=timeout,
)
async def __aenter__(self) -> "SynorWallet":
return self
async def __aexit__(self, *args: Any) -> None:
await self.close()
async def close(self) -> None:
"""Close the client."""
await self._client.aclose()
async def create_wallet(
self,
wallet_type: WalletType = WalletType.STANDARD,
) -> CreateWalletResult:
"""
Create a new wallet.
Args:
wallet_type: Type of wallet to create
Returns:
Created wallet and mnemonic phrase
"""
response = await self._request(
"POST",
"/wallets",
{
"type": wallet_type.value,
"network": self.config.network.value,
"derivationPath": self.config.derivation_path,
},
)
wallet = self._parse_wallet(response["wallet"])
return CreateWalletResult(wallet=wallet, mnemonic=response["mnemonic"])
async def import_wallet(self, options: ImportWalletOptions) -> Wallet:
"""
Import a wallet from mnemonic phrase.
Args:
options: Import options including mnemonic
Returns:
Imported wallet
"""
response = await self._request(
"POST",
"/wallets/import",
{
"mnemonic": options.mnemonic,
"passphrase": options.passphrase,
"type": options.type.value,
"network": self.config.network.value,
"derivationPath": self.config.derivation_path,
},
)
return self._parse_wallet(response["wallet"])
async def get_wallet(self, wallet_id: str) -> Wallet:
"""
Get wallet by ID.
Args:
wallet_id: Wallet ID
Returns:
Wallet details
"""
response = await self._request("GET", f"/wallets/{wallet_id}")
return self._parse_wallet(response["wallet"])
async def list_wallets(self) -> list[Wallet]:
"""
List all wallets for this account.
Returns:
List of wallets
"""
response = await self._request("GET", "/wallets")
return [self._parse_wallet(w) for w in response["wallets"]]
async def get_address(self, wallet_id: str, index: int = 0) -> str:
"""
Get address at a specific index for a wallet.
Args:
wallet_id: Wallet ID
index: Derivation index
Returns:
Address at the index
"""
response = await self._request(
"GET", f"/wallets/{wallet_id}/addresses/{index}"
)
return response["address"]
async def get_stealth_address(self, wallet_id: str) -> StealthAddress:
"""
Generate a stealth address for receiving private payments.
Args:
wallet_id: Wallet ID
Returns:
Stealth address details
"""
response = await self._request("POST", f"/wallets/{wallet_id}/stealth")
sa = response["stealthAddress"]
return StealthAddress(
address=sa["address"],
view_key=sa["viewKey"],
spend_key=sa["spendKey"],
ephemeral_key=sa.get("ephemeralKey"),
)
async def sign_transaction(
self,
wallet_id: str,
transaction: Transaction,
) -> SignedTransaction:
"""
Sign a transaction.
Args:
wallet_id: Wallet ID
transaction: Transaction to sign
Returns:
Signed transaction
"""
response = await self._request(
"POST",
f"/wallets/{wallet_id}/sign",
{"transaction": self._serialize_transaction(transaction)},
)
st = response["signedTransaction"]
return SignedTransaction(
raw=st["raw"],
txid=st["txid"],
size=st["size"],
weight=st.get("weight"),
)
async def sign_message(
self,
wallet_id: str,
message: str,
format: str = "text",
) -> SignedMessage:
"""
Sign a message.
Args:
wallet_id: Wallet ID
message: Message to sign
format: Message format (text, hex, base64)
Returns:
Signed message
"""
response = await self._request(
"POST",
f"/wallets/{wallet_id}/sign-message",
{"message": message, "format": format},
)
return SignedMessage(
signature=response["signature"],
public_key=response["publicKey"],
address=response["address"],
)
async def verify_message(
self,
message: str,
signature: str,
address: str,
) -> bool:
"""
Verify a signed message.
Args:
message: Original message
signature: Signature to verify
address: Expected signer address
Returns:
True if signature is valid
"""
response = await self._request(
"POST",
"/verify-message",
{"message": message, "signature": signature, "address": address},
)
return response["valid"]
async def get_balance(
self,
address: str,
include_tokens: bool = False,
) -> BalanceResponse:
"""
Get balance for an address.
Args:
address: Address to check
include_tokens: Include token balances
Returns:
Balance information
"""
params = {"includeTokens": str(include_tokens).lower()}
response = await self._request(
"GET", f"/balances/{address}", params=params
)
native = Balance(
confirmed=response["native"]["confirmed"],
unconfirmed=response["native"]["unconfirmed"],
total=response["native"]["total"],
)
tokens = []
if "tokens" in response:
tokens = [
TokenBalance(
token=t["token"],
symbol=t["symbol"],
decimals=t["decimals"],
balance=t["balance"],
)
for t in response["tokens"]
]
return BalanceResponse(native=native, tokens=tokens)
async def get_utxos(
self,
address: str,
options: Optional[GetUtxosOptions] = None,
) -> list[UTXO]:
"""
Get UTXOs for an address.
Args:
address: Address to query
options: Query options
Returns:
List of UTXOs
"""
params: dict[str, str] = {}
if options:
if options.min_confirmations:
params["minConfirmations"] = str(options.min_confirmations)
if options.min_amount:
params["minAmount"] = options.min_amount
response = await self._request(
"GET", f"/utxos/{address}", params=params
)
return [self._parse_utxo(u) for u in response["utxos"]]
async def build_transaction(
self,
wallet_id: str,
options: BuildTransactionOptions,
) -> Transaction:
"""
Build a transaction (without signing).
Args:
wallet_id: Wallet ID
options: Transaction building options
Returns:
Unsigned transaction
"""
data: dict[str, Any] = {
"to": options.to,
"amount": options.amount,
}
if options.fee_rate is not None:
data["feeRate"] = options.fee_rate
if options.utxos:
data["utxos"] = [
{"txid": u.txid, "vout": u.vout, "amount": u.amount}
for u in options.utxos
]
if options.change_address:
data["changeAddress"] = options.change_address
response = await self._request(
"POST", f"/wallets/{wallet_id}/build-tx", data
)
return self._parse_transaction(response["transaction"])
async def send_transaction(
self,
wallet_id: str,
options: BuildTransactionOptions,
) -> SignedTransaction:
"""
Build and sign a transaction in one step.
Args:
wallet_id: Wallet ID
options: Transaction building options
Returns:
Signed transaction
"""
tx = await self.build_transaction(wallet_id, options)
return await self.sign_transaction(wallet_id, tx)
async def estimate_fee(
self,
priority: Priority = Priority.MEDIUM,
) -> FeeEstimate:
"""
Estimate transaction fee.
Args:
priority: Priority level
Returns:
Fee estimate
"""
response = await self._request(
"GET", "/fees/estimate", params={"priority": priority.value}
)
return FeeEstimate(
priority=Priority(response["priority"]),
fee_rate=response["feeRate"],
estimated_blocks=response["estimatedBlocks"],
)
async def get_all_fee_estimates(self) -> list[FeeEstimate]:
"""
Get all fee estimates.
Returns:
Fee estimates for all priority levels
"""
response = await self._request("GET", "/fees/estimate/all")
return [
FeeEstimate(
priority=Priority(e["priority"]),
fee_rate=e["feeRate"],
estimated_blocks=e["estimatedBlocks"],
)
for e in response["estimates"]
]
async def _request(
self,
method: str,
path: str,
data: Optional[dict[str, Any]] = None,
params: Optional[dict[str, str]] = None,
) -> dict[str, Any]:
"""Make an API request."""
if self.config.debug:
print(f"[SynorWallet] {method} {path}")
response = await self._client.request(
method,
path,
json=data,
params=params,
)
if response.status_code >= 400:
error = (
response.json()
if response.content
else {"message": response.reason_phrase}
)
raise WalletError(
error.get("message", "Request failed"),
response.status_code,
error.get("code"),
)
return response.json()
def _parse_wallet(self, data: dict[str, Any]) -> Wallet:
"""Parse wallet from API response."""
return Wallet(
id=data["id"],
address=data["address"],
public_key=data["publicKey"],
type=WalletType(data["type"]),
created_at=data["createdAt"],
)
def _parse_utxo(self, data: dict[str, Any]) -> UTXO:
"""Parse UTXO from API response."""
return UTXO(
txid=data["txid"],
vout=data["vout"],
amount=data["amount"],
address=data["address"],
confirmations=data["confirmations"],
script_pub_key=data.get("scriptPubKey"),
)
def _parse_transaction(self, data: dict[str, Any]) -> Transaction:
"""Parse transaction from API response."""
inputs = [
TransactionInput(
txid=i["txid"],
vout=i["vout"],
amount=i["amount"],
script_sig=i.get("scriptSig"),
)
for i in data["inputs"]
]
outputs = [
TransactionOutput(
address=o["address"],
amount=o["amount"],
script_pub_key=o.get("scriptPubKey"),
)
for o in data["outputs"]
]
return Transaction(
version=data["version"],
inputs=inputs,
outputs=outputs,
lock_time=data.get("lockTime", 0),
fee=data.get("fee"),
)
def _serialize_transaction(self, tx: Transaction) -> dict[str, Any]:
"""Serialize transaction for API request."""
return {
"version": tx.version,
"inputs": [
{
"txid": i.txid,
"vout": i.vout,
"amount": i.amount,
}
for i in tx.inputs
],
"outputs": [
{
"address": o.address,
"amount": o.amount,
}
for o in tx.outputs
],
"lockTime": tx.lock_time,
}