"""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, }