diff --git a/sdk/python/examples/ZETACHAIN_README.md b/sdk/python/examples/ZETACHAIN_README.md new file mode 100644 index 00000000..c21d3652 --- /dev/null +++ b/sdk/python/examples/ZETACHAIN_README.md @@ -0,0 +1,357 @@ +# ZetaChain Integration with dstack + +This directory contains examples demonstrating how to use dstack's Trusted Execution Environment (TEE) with ZetaChain. + +## Overview + +dstack provides hardware-based secure key management using Intel TDX technology. ZetaChain is an EVM-compatible blockchain focused on cross-chain interoperability. Together, they enable: + +- **Secure Cross-Chain Applications**: Build omnichain apps with TEE security guarantees +- **Confidential Key Management**: Private keys never leave the TEE hardware +- **Verifiable Execution**: Cryptographic proof that code runs in genuine TEE +- **Deterministic Wallets**: Reproducible accounts across deployments + +## Installation + +### Basic Installation + +```bash +# Install dstack SDK with ZetaChain support +pip install "dstack-sdk[zetachain]" +``` + +### Full Installation (All Blockchains) + +```bash +# Install with support for Ethereum, Solana, and ZetaChain +pip install "dstack-sdk[all]" +``` + +### Development Installation + +```bash +# Install from source +cd sdk/python +pip install -e ".[zetachain]" +``` + +## Quick Start + +### Basic Account Creation + +```python +from dstack_sdk import DstackClient +from dstack_sdk.zetachain import to_account_secure + +# Initialize dstack client (connects to TEE) +client = DstackClient() + +# Derive a deterministic key for ZetaChain +key = client.get_key('zetachain/mainnet', 'wallet') + +# Convert to ZetaChain account +account = to_account_secure(key) + +print(f"ZetaChain Address: {account.address}") +# Private key is secure in TEE and never exposed! +``` + +### Check Balance + +```python +from web3 import Web3 +from dstack_sdk import DstackClient +from dstack_sdk.zetachain import to_account_secure + +# Create account +client = DstackClient() +key = client.get_key('zetachain/testnet', 'wallet') +account = to_account_secure(key) + +# Connect to ZetaChain testnet +w3 = Web3(Web3.HTTPProvider('https://zetachain-athens-evm.blockpi.network/v1/rpc/public')) + +# Check balance +balance = w3.eth.get_balance(account.address) +print(f"Balance: {w3.from_wei(balance, 'ether')} ZETA") +``` + +### Sign and Send Transaction + +```python +from web3 import Web3 +from dstack_sdk import DstackClient +from dstack_sdk.zetachain import to_account_secure + +# Setup +client = DstackClient() +key = client.get_key('zetachain/testnet', 'wallet') +account = to_account_secure(key) +w3 = Web3(Web3.HTTPProvider('https://zetachain-athens-evm.blockpi.network/v1/rpc/public')) + +# Prepare transaction +tx = { + 'from': account.address, + 'to': '0x...', + 'value': w3.to_wei(1, 'ether'), + 'gas': 21000, + 'gasPrice': w3.eth.gas_price, + 'nonce': w3.eth.get_transaction_count(account.address), + 'chainId': 7001, # ZetaChain Athens Testnet +} + +# Sign with TEE-protected key +signed_tx = w3.eth.account.sign_transaction(tx, account.key) + +# Send transaction +tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) +print(f"Transaction: {tx_hash.hex()}") +``` + +## Running the Demo + +The demo application showcases all features: + +```bash +# Run the comprehensive demo +python examples/zetachain_demo.py +``` + +This demo includes: +1. Basic account creation with TEE security +2. Balance checking on ZetaChain network +3. Transaction signing with TEE-protected keys +4. Multiple deterministic accounts +5. Cross-chain account management +6. TEE attestation with ZetaChain +7. Async operations + +## ZetaChain Networks + +### Mainnet + +```python +RPC_URL = "https://zetachain-evm.blockpi.network/v1/rpc/public" +CHAIN_ID = 7000 +EXPLORER = "https://explorer.zetachain.com" +``` + +### Testnet (Athens) + +```python +RPC_URL = "https://zetachain-athens-evm.blockpi.network/v1/rpc/public" +CHAIN_ID = 7001 +EXPLORER = "https://athens.explorer.zetachain.com" +FAUCET = "https://labs.zetachain.com/get-zeta" +``` + +## Advanced Features + +### Deterministic Key Derivation + +```python +# Same path and subject always produce same account +client = DstackClient() + +# Create two accounts for different purposes +trading_key = client.get_key('zetachain/mainnet', 'trading') +governance_key = client.get_key('zetachain/mainnet', 'governance') + +trading_account = to_account_secure(trading_key) +governance_account = to_account_secure(governance_key) + +# Accounts are different but deterministic +print(f"Trading: {trading_account.address}") +print(f"Governance: {governance_account.address}") +``` + +### Cross-Chain Key Management + +```python +from dstack_sdk import DstackClient +from dstack_sdk.ethereum import to_account_secure as eth_to_account +from dstack_sdk.solana import to_keypair_secure +from dstack_sdk.zetachain import to_account_secure as zeta_to_account + +client = DstackClient() + +# Derive keys for different chains +eth_account = eth_to_account(client.get_key('ethereum/mainnet', 'wallet')) +sol_keypair = to_keypair_secure(client.get_key('solana/mainnet', 'wallet')) +zeta_account = zeta_to_account(client.get_key('zetachain/mainnet', 'wallet')) + +print(f"Ethereum: {eth_account.address}") +print(f"Solana: {sol_keypair.pubkey()}") +print(f"ZetaChain: {zeta_account.address}") +``` + +### TEE Attestation + +```python +from dstack_sdk import DstackClient +from dstack_sdk.zetachain import to_account_secure + +client = DstackClient() + +# Create account +key = client.get_key('zetachain/mainnet', 'wallet') +account = to_account_secure(key) + +# Get TEE attestation quote +# This proves the account was created in genuine TEE hardware +report_data = account.address.encode()[:64] +quote = client.get_quote(report_data) + +print(f"Quote size: {len(quote.quote)} bytes") +print(f"Event log entries: {len(quote.event_log)}") +# Quote can be verified to prove TEE execution +``` + +### Async Operations + +```python +import asyncio +from dstack_sdk import AsyncDstackClient +from dstack_sdk.zetachain import to_account_secure + +async def create_accounts(): + async with AsyncDstackClient() as client: + # Derive multiple keys concurrently + keys = await asyncio.gather( + client.get_key('zetachain/mainnet', 'wallet-1'), + client.get_key('zetachain/mainnet', 'wallet-2'), + client.get_key('zetachain/testnet', 'wallet-1'), + ) + + # Convert to accounts + accounts = [to_account_secure(key) for key in keys] + for account in accounts: + print(account.address) + +asyncio.run(create_accounts()) +``` + +## Security Best Practices + +### āœ… DO + +- **Use `to_account_secure`** instead of `to_account` (better security) +- **Keep path and subject names organized** for easy key management +- **Use different paths for different environments** (mainnet vs testnet) +- **Verify TEE quotes** when accepting keys from other parties +- **Use hardware attestation** for critical operations + +### āŒ DON'T + +- **Don't extract private keys** from the TEE if possible +- **Don't use `get_tls_key`** for blockchain accounts (it shows deprecation warning) +- **Don't reuse accounts** across different security contexts +- **Don't skip attestation verification** in production + +## Troubleshooting + +### Connection Issues + +```python +# If you can't connect to dstack, check if TEE/simulator is running +from dstack_sdk import DstackClient + +try: + client = DstackClient() + info = client.get_info() + print(f"Connected! Version: {info}") +except Exception as e: + print(f"Connection failed: {e}") + print("Make sure dstack TEE or simulator is running") +``` + +### Using Simulator for Development + +```bash +# Download dstack simulator +# macOS +curl -LO https://github.com/Dstack-TEE/dstack/releases/latest/download/dstack-sim-darwin-x86_64.tar.gz +tar xf dstack-sim-darwin-x86_64.tar.gz + +# Linux +curl -LO https://github.com/Dstack-TEE/dstack/releases/latest/download/dstack-sim-linux-x86_64.tar.gz +tar xf dstack-sim-linux-x86_64.tar.gz + +# Run simulator +./dstack-simulator + +# In another terminal, run your code +python examples/zetachain_demo.py +``` + +## Resources + +### dstack Resources + +- **Documentation**: https://docs.phala.network/dstack +- **GitHub**: https://github.com/Dstack-TEE/dstack +- **PyPI**: https://pypi.org/project/dstack-sdk/ + +### ZetaChain Resources + +- **Documentation**: https://docs.zetachain.com/ +- **Explorer (Mainnet)**: https://explorer.zetachain.com/ +- **Explorer (Testnet)**: https://athens.explorer.zetachain.com/ +- **Testnet Faucet**: https://labs.zetachain.com/get-zeta +- **GitHub**: https://github.com/zeta-chain + +### Related Projects + +- **Phala Network**: https://phala.network/ +- **Flashbots**: https://flashbots.net/ +- **Web3.py**: https://web3py.readthedocs.io/ + +## Use Cases + +### 1. Confidential Cross-Chain DEX + +Build a decentralized exchange that: +- Stores trading strategies in TEE +- Executes cross-chain swaps via ZetaChain +- Provides cryptographic proof of fair execution + +### 2. Secure Omnichain Wallet + +Create a wallet that: +- Manages keys for multiple chains in single TEE +- Uses ZetaChain for cross-chain transfers +- Never exposes private keys to frontend + +### 3. Cross-Chain DeFi Strategies + +Implement strategies that: +- Monitor prices across multiple chains +- Execute arbitrage via ZetaChain connectors +- Keep strategy logic confidential in TEE + +### 4. Verifiable Random Oracles + +Build oracles that: +- Generate random numbers in TEE +- Provide attestation of randomness +- Bridge results to ZetaChain and connected chains + +## Contributing + +We welcome contributions! Please: + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## License + +Apache-2.0 License - see LICENSE file for details. + +## Support + +- **Issues**: https://github.com/Dstack-TEE/dstack/issues +- **Discussions**: https://github.com/Dstack-TEE/dstack/discussions +- **Discord**: https://discord.gg/phala-network diff --git a/sdk/python/examples/zetachain_demo.py b/sdk/python/examples/zetachain_demo.py new file mode 100644 index 00000000..6cc0dbe1 --- /dev/null +++ b/sdk/python/examples/zetachain_demo.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Ā© 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +"""ZetaChain + dstack Integration Demo + +This example demonstrates how to use dstack's secure key derivation with ZetaChain. +It shows: +1. Connecting to dstack TEE environment +2. Deriving deterministic ZetaChain keys +3. Using Web3 to interact with ZetaChain networks +4. Cross-chain capabilities with confidential computing + +Requirements: + pip install "dstack-sdk[zetachain]" +""" + +import asyncio +from web3 import Web3 +from web3.middleware import geth_poa_middleware + +from dstack_sdk import DstackClient +from dstack_sdk.zetachain import to_account_secure + + +# ZetaChain Network Configuration +ZETACHAIN_NETWORKS = { + "mainnet": { + "rpc": "https://zetachain-evm.blockpi.network/v1/rpc/public", + "chain_id": 7000, + "explorer": "https://explorer.zetachain.com", + }, + "testnet": { + "rpc": "https://zetachain-athens-evm.blockpi.network/v1/rpc/public", + "chain_id": 7001, + "explorer": "https://athens.explorer.zetachain.com", + }, +} + + +def get_web3_client(network="testnet"): + """Create Web3 client for ZetaChain network.""" + w3 = Web3(Web3.HTTPProvider(ZETACHAIN_NETWORKS[network]["rpc"])) + # Add PoA middleware if needed + w3.middleware_onion.inject(geth_poa_middleware, layer=0) + return w3 + + +def demo_basic_account_creation(): + """Demo: Create ZetaChain account from dstack TEE.""" + print("=" * 60) + print("Demo 1: Basic ZetaChain Account Creation with TEE Security") + print("=" * 60) + + # Initialize dstack client (connects to TEE) + client = DstackClient() + + # Derive a deterministic key for ZetaChain mainnet + # Keys are unique per path and subject, but deterministic + print("\nšŸ“Š Deriving key from TEE...") + key_response = client.get_key("zetachain/mainnet", "wallet-1") + + # Convert to ZetaChain account + account = to_account_secure(key_response) + + print(f"\nāœ… ZetaChain Account Created:") + print(f" Address: {account.address}") + print(f" šŸ” Private key is secure in TEE, never exposed!") + + return account + + +def demo_check_balance(network="testnet"): + """Demo: Check ZetaChain balance.""" + print("\n" + "=" * 60) + print("Demo 2: Check Balance on ZetaChain Network") + print("=" * 60) + + # Create account + client = DstackClient() + key_response = client.get_key(f"zetachain/{network}", "wallet-1") + account = to_account_secure(key_response) + + # Connect to ZetaChain + w3 = get_web3_client(network) + + print(f"\n🌐 Connected to ZetaChain {network.upper()}") + print(f" Chain ID: {w3.eth.chain_id}") + print(f" Block Number: {w3.eth.block_number}") + + # Check balance + balance_wei = w3.eth.get_balance(account.address) + balance_zeta = w3.from_wei(balance_wei, "ether") + + print(f"\nšŸ’° Account Balance:") + print(f" Address: {account.address}") + print(f" Balance: {balance_zeta} ZETA") + + if balance_zeta == 0 and network == "testnet": + print( + f"\nšŸ’” Get testnet ZETA from faucet: https://labs.zetachain.com/get-zeta" + ) + + return account, w3 + + +def demo_sign_transaction(network="testnet"): + """Demo: Sign transaction with TEE-protected key.""" + print("\n" + "=" * 60) + print("Demo 3: Sign Transaction with TEE Security") + print("=" * 60) + + # Create account + client = DstackClient() + key_response = client.get_key(f"zetachain/{network}", "wallet-1") + account = to_account_secure(key_response) + + # Connect to ZetaChain + w3 = get_web3_client(network) + + # Prepare transaction (example - not actually sent) + tx = { + "from": account.address, + "to": "0x0000000000000000000000000000000000000000", # Example address + "value": w3.to_wei(0.01, "ether"), + "gas": 21000, + "gasPrice": w3.eth.gas_price, + "nonce": w3.eth.get_transaction_count(account.address), + "chainId": w3.eth.chain_id, + } + + print(f"\nšŸ“ Transaction Details:") + print(f" From: {tx['from']}") + print(f" To: {tx['to']}") + print(f" Value: {w3.from_wei(tx['value'], 'ether')} ZETA") + print(f" Gas: {tx['gas']}") + + # Sign transaction (key never leaves TEE!) + print("\nšŸ” Signing transaction in TEE...") + signed_tx = w3.eth.account.sign_transaction(tx, account.key) + + print(f"āœ… Transaction Signed!") + print(f" Hash: {signed_tx.hash.hex()}") + print(f" Signature: {signed_tx.signature.hex()[:64]}...") + + print("\nšŸ’” Transaction is signed but not sent (demo only)") + + return signed_tx + + +def demo_multi_account(): + """Demo: Create multiple accounts for different purposes.""" + print("\n" + "=" * 60) + print("Demo 4: Multiple Deterministic Accounts") + print("=" * 60) + + client = DstackClient() + + # Different paths create different accounts + accounts = {} + purposes = ["wallet-1", "wallet-2", "trading", "governance"] + + print("\nšŸ“Š Creating multiple accounts from same TEE:") + + for purpose in purposes: + key_response = client.get_key("zetachain/mainnet", purpose) + account = to_account_secure(key_response) + accounts[purpose] = account + print(f" {purpose:12} -> {account.address}") + + print( + "\nāœ… Each account is deterministic and can be recreated with same path/purpose" + ) + + return accounts + + +def demo_cross_chain_compatibility(): + """Demo: Same dstack client for multiple chains.""" + print("\n" + "=" * 60) + print("Demo 5: Cross-Chain Account Management") + print("=" * 60) + + client = DstackClient() + + print("\n🌐 Creating accounts across different chains:") + + # ZetaChain + zeta_key = client.get_key("zetachain/mainnet", "wallet") + zeta_account = to_account_secure(zeta_key) + print(f" ZetaChain: {zeta_account.address}") + + # Can also create Ethereum accounts (if ethereum module available) + try: + from dstack_sdk.ethereum import to_account_secure as eth_to_account + + eth_key = client.get_key("ethereum/mainnet", "wallet") + eth_account = eth_to_account(eth_key) + print(f" Ethereum: {eth_account.address}") + except ImportError: + print(" Ethereum: (install dstack-sdk[ethereum] to enable)") + + # And Solana (if solana module available) + try: + from dstack_sdk.solana import to_keypair_secure + + sol_key = client.get_key("solana/mainnet", "wallet") + sol_keypair = to_keypair_secure(sol_key) + print(f" Solana: {sol_keypair.pubkey()}") + except ImportError: + print(" Solana: (install dstack-sdk[solana] to enable)") + + print( + "\nāœ… Single TEE environment manages keys for all chains securely!" + ) + + +def demo_get_quote(): + """Demo: Get TEE attestation quote.""" + print("\n" + "=" * 60) + print("Demo 6: TEE Attestation with ZetaChain Account") + print("=" * 60) + + client = DstackClient() + + # Create account + key_response = client.get_key("zetachain/mainnet", "wallet") + account = to_account_secure(key_response) + + # Get attestation quote (proves execution in TEE) + print(f"\nšŸ” Getting TEE attestation quote...") + report_data = account.address.encode()[:64] # Use address as report data + quote_response = client.get_quote(report_data) + + print(f"āœ… TEE Quote Generated:") + print(f" Quote size: {len(quote_response.quote)} bytes") + print(f" Event log entries: {len(quote_response.event_log)}") + print(f" Report data: {account.address}") + + print( + "\nšŸ’” This quote proves the account was created in genuine TEE hardware!" + ) + + return quote_response + + +async def demo_async_operations(): + """Demo: Async operations with dstack.""" + print("\n" + "=" * 60) + print("Demo 7: Async Operations") + print("=" * 60) + + from dstack_sdk import AsyncDstackClient + + # Create async client + async with AsyncDstackClient() as client: + print("\nšŸ“Š Running async key derivation...") + + # Derive multiple keys concurrently + keys = await asyncio.gather( + client.get_key("zetachain/mainnet", "wallet-1"), + client.get_key("zetachain/mainnet", "wallet-2"), + client.get_key("zetachain/testnet", "wallet-1"), + ) + + print(f"āœ… Derived {len(keys)} keys concurrently!") + + # Convert to accounts + accounts = [to_account_secure(key) for key in keys] + for i, account in enumerate(accounts, 1): + print(f" Account {i}: {account.address}") + + +def main(): + """Run all demos.""" + print("\n" + "šŸš€ " * 20) + print("ZetaChain + dstack TEE Integration Demo") + print("šŸš€ " * 20) + + try: + # Basic demos + demo_basic_account_creation() + demo_check_balance("testnet") + demo_sign_transaction("testnet") + demo_multi_account() + demo_cross_chain_compatibility() + demo_get_quote() + + # Async demo + print("\nā³ Running async demo...") + asyncio.run(demo_async_operations()) + + print("\n" + "=" * 60) + print("āœ… All demos completed successfully!") + print("=" * 60) + + print("\nšŸ’” Key Benefits:") + print(" • Keys are secure in TEE hardware") + print(" • Deterministic key derivation") + print(" • Cryptographic proof of TEE execution") + print(" • Cross-chain compatibility") + print(" • No private key exposure") + + except Exception as e: + print(f"\nāŒ Error: {e}") + print( + "\nšŸ’” Make sure you have dstack TEE environment running or simulator available" + ) + print(" See: https://docs.phala.network/dstack/getting-started") + + +if __name__ == "__main__": + main() diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index e0ba5d33..9ef39d0b 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -23,8 +23,10 @@ license = {text = "Apache-2.0"} [project.optional-dependencies] solana = ["solders"] ethereum = ["web3"] +zetachain = ["web3"] sol = ["solders"] eth = ["web3"] +zeta = ["web3"] all = ["solders", "web3"] [build-system] @@ -41,6 +43,9 @@ solana = [ ethereum = [ "web3", ] +zetachain = [ + "web3", +] [tool.mypy] python_version = "3.10" @@ -134,4 +139,7 @@ solana = [ ] ethereum = [ "web3", +] +zetachain = [ + "web3", ] \ No newline at end of file diff --git a/sdk/python/src/dstack_sdk/zetachain.py b/sdk/python/src/dstack_sdk/zetachain.py new file mode 100644 index 00000000..28fc654b --- /dev/null +++ b/sdk/python/src/dstack_sdk/zetachain.py @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: Ā© 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +"""ZetaChain helpers for deriving accounts from dstack keys. + +Use with ``dstack_sdk.DstackClient`` responses to create ZetaChain +accounts for signing and transacting on ZetaChain networks. + +ZetaChain is fully EVM-compatible, so this module uses the same +eth_account library as the Ethereum integration. +""" + +import hashlib +import warnings + +from eth_account import Account +from eth_account.signers.local import LocalAccount + +from .dstack_client import GetKeyResponse +from .dstack_client import GetTlsKeyResponse + + +def to_account(get_key_response: GetKeyResponse | GetTlsKeyResponse) -> LocalAccount: + """Create a ZetaChain account from DstackClient key response. + + DEPRECATED: Use to_account_secure instead. This method has security concerns. + Current implementation uses raw key material without proper hashing. + + Args: + get_key_response: Response from get_key() or get_tls_key() + + Returns: + Account: ZetaChain account object (EVM-compatible) + + Example: + >>> from dstack_sdk import DstackClient + >>> from dstack_sdk.zetachain import to_account + >>> client = DstackClient() + >>> key = client.get_key('zetachain/mainnet', 'wallet') + >>> account = to_account(key) + >>> print(f"ZetaChain address: {account.address}") + + """ + if isinstance(get_key_response, GetTlsKeyResponse): + warnings.warn( + "to_account: Please don't use getTlsKey method to get key, use getKey instead.", + DeprecationWarning, + stacklevel=2, + ) + key_bytes = get_key_response.as_uint8array(32) + return Account.from_key(key_bytes) # type: ignore[no-any-return] + else: # GetKeyResponse + return Account.from_key(get_key_response.decode_key()) # type: ignore[no-any-return] + + +def to_account_secure( + get_key_response: GetKeyResponse | GetTlsKeyResponse, +) -> LocalAccount: + """Create a ZetaChain account using SHA256 of full key material for security. + + This is the recommended method for creating ZetaChain accounts from dstack keys. + + Args: + get_key_response: Response from get_key() or get_tls_key() + + Returns: + LocalAccount: ZetaChain account object with enhanced security + + Example: + >>> from dstack_sdk import DstackClient + >>> from dstack_sdk.zetachain import to_account_secure + >>> from web3 import Web3 + >>> + >>> # Initialize dstack client + >>> client = DstackClient() + >>> + >>> # Derive a deterministic key for ZetaChain + >>> key = client.get_key('zetachain/mainnet', 'wallet') + >>> account = to_account_secure(key) + >>> + >>> # Use with Web3 for ZetaChain + >>> w3 = Web3(Web3.HTTPProvider('https://zetachain-evm.blockpi.network/v1/rpc/public')) + >>> balance = w3.eth.get_balance(account.address) + >>> print(f"Balance: {w3.from_wei(balance, 'ether')} ZETA") + + """ + if isinstance(get_key_response, GetTlsKeyResponse): + warnings.warn( + "to_account_secure: Please don't use getTlsKey method to get key, use getKey instead.", + DeprecationWarning, + stacklevel=2, + ) + try: + # Hash the complete key material with SHA256 + key_bytes = get_key_response.as_uint8array() + hashed_key = hashlib.sha256(key_bytes).digest() + return Account.from_key(hashed_key) # type: ignore[no-any-return] + except Exception as e: + raise RuntimeError( + "to_account_secure: missing SHA256 support, please upgrade your system" + ) from e + else: # GetKeyResponse + return Account.from_key(get_key_response.decode_key()) # type: ignore[no-any-return] diff --git a/sdk/python/tests/test_zetachain.py b/sdk/python/tests/test_zetachain.py new file mode 100644 index 00000000..65ef59fb --- /dev/null +++ b/sdk/python/tests/test_zetachain.py @@ -0,0 +1,161 @@ +# SPDX-FileCopyrightText: Ā© 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +import warnings + +from eth_account.signers.local import LocalAccount +import pytest + +from dstack_sdk import GetKeyResponse +from dstack_sdk.zetachain import to_account +from dstack_sdk.zetachain import to_account_secure + + +@pytest.mark.asyncio +async def test_async_to_account(): + """Test async to_account with ZetaChain.""" + # Use mock GetKeyResponse instead of actual server call + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + assert isinstance(mock_result, GetKeyResponse) + account = to_account(mock_result) + assert isinstance(account, LocalAccount) + # Verify it's a valid Ethereum-compatible address (0x + 40 hex chars) + assert account.address.startswith("0x") + assert len(account.address) == 42 + + +def test_sync_to_account(): + """Test sync to_account with ZetaChain.""" + # Use mock GetKeyResponse instead of actual server call + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + assert isinstance(mock_result, GetKeyResponse) + account = to_account(mock_result) + assert isinstance(account, LocalAccount) + # Verify it's a valid Ethereum-compatible address + assert account.address.startswith("0x") + assert len(account.address) == 42 + + +@pytest.mark.asyncio +async def test_async_to_account_secure(): + """Test async to_account_secure with ZetaChain.""" + # Use mock GetKeyResponse instead of actual server call + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + assert isinstance(mock_result, GetKeyResponse) + account = to_account_secure(mock_result) + assert isinstance(account, LocalAccount) + # Verify it's a valid Ethereum-compatible address + assert account.address.startswith("0x") + assert len(account.address) == 42 + + +def test_sync_to_account_secure(): + """Test sync to_account_secure with ZetaChain.""" + # Use mock GetKeyResponse instead of actual server call + mock_result = GetKeyResponse( + key="1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + signature_chain=["sig1", "sig2"], + ) + assert isinstance(mock_result, GetKeyResponse) + account = to_account_secure(mock_result) + assert isinstance(account, LocalAccount) + # Verify it's a valid Ethereum-compatible address + assert account.address.startswith("0x") + assert len(account.address) == 42 + + +def test_to_account_with_tls_key(): + """Test to_account with TLS key response (should show warning).""" + from dstack_sdk import GetTlsKeyResponse + + # Use mock TLS key response instead of actual server call + mock_result = GetTlsKeyResponse( + key="""-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKONKWRjMvhgxHDmr +SY7zfjPHe3Qp8vCO9HqjzjqhXNKhRANCAAT5XHKyj7JRGHl2nQ2SltGKjQ3A7MPJ +/7JDkUxMNYhTxKqYdJZ6l1C8XrjKc7SFsVJhYgdJjLzQ3xKJz6l5jKzQ +-----END PRIVATE KEY-----""", + certificate_chain=["cert1", "cert2"], + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + account = to_account(mock_result) + + assert isinstance(account, LocalAccount) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "Please don't use getTlsKey method" in str(w[0].message) + + +def test_to_account_secure_with_tls_key(): + """Test to_account_secure with TLS key response (should show warning).""" + from dstack_sdk import GetTlsKeyResponse + + # Use mock TLS key response instead of actual server call + mock_result = GetTlsKeyResponse( + key="""-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKONKWRjMvhgxHDmr +SY7zfjPHe3Qp8vCO9HqjzjqhXNKhRANCAAT5XHKyj7JRGHl2nQ2SltGKjQ3A7MPJ +/7JDkUxMNYhTxKqYdJZ6l1C8XrjKc7SFsVJhYgdJjLzQ3xKJz6l5jKzQ +-----END PRIVATE KEY-----""", + certificate_chain=["cert1", "cert2"], + ) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + account = to_account_secure(mock_result) + + assert isinstance(account, LocalAccount) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "Please don't use getTlsKey method" in str(w[0].message) + + +def test_deterministic_keys(): + """Test that same key input produces same ZetaChain account.""" + # Same key should produce same account + key_hex = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + + mock_result1 = GetKeyResponse( + key=key_hex, + signature_chain=["sig1"], + ) + mock_result2 = GetKeyResponse( + key=key_hex, + signature_chain=["sig1"], + ) + + account1 = to_account_secure(mock_result1) + account2 = to_account_secure(mock_result2) + + # Same key should produce same address + assert account1.address == account2.address + + +def test_different_keys_produce_different_accounts(): + """Test that different keys produce different ZetaChain accounts.""" + mock_result1 = GetKeyResponse( + key="1111111111111111111111111111111111111111111111111111111111111111", + signature_chain=["sig1"], + ) + mock_result2 = GetKeyResponse( + key="2222222222222222222222222222222222222222222222222222222222222222", + signature_chain=["sig1"], + ) + + account1 = to_account_secure(mock_result1) + account2 = to_account_secure(mock_result2) + + # Different keys should produce different addresses + assert account1.address != account2.address