298 lines
9.3 KiB
Python
Executable File
298 lines
9.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Dragonchain Smart Contract Client
|
|
|
|
A gRPC client that connects to Dragonchain Prime server to process
|
|
smart contract transactions.
|
|
|
|
Do not modify this file unless you need to customize the client behavior.
|
|
Implement your smart contract logic in process.py instead.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import queue
|
|
import signal
|
|
import sys
|
|
import threading
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from dataclasses import dataclass
|
|
from typing import Any, Optional
|
|
|
|
import grpc
|
|
import yaml
|
|
|
|
import remote_sc_pb2 as pb
|
|
import remote_sc_pb2_grpc as pb_grpc
|
|
from process import ProcessResult, process
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
)
|
|
logger = logging.getLogger("SmartContract")
|
|
|
|
|
|
# =============================================================================
|
|
# Configuration and Client Infrastructure
|
|
# Do not modify this file unless you need to customize the client behavior.
|
|
# Implement your smart contract logic in process.py instead.
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
"""Client configuration loaded from YAML."""
|
|
|
|
server_address: str
|
|
smart_contract_id: str
|
|
api_key: str
|
|
use_tls: bool = False
|
|
tls_cert_path: Optional[str] = None
|
|
num_workers: int = 10
|
|
reconnect_delay_seconds: int = 5
|
|
max_reconnect_attempts: int = 0 # 0 = infinite
|
|
|
|
|
|
class SmartContractClient:
|
|
"""gRPC client for smart contract execution."""
|
|
|
|
def __init__(self, config: Config):
|
|
self.config = config
|
|
self.channel: Optional[grpc.Channel] = None
|
|
self.stub: Optional[pb_grpc.SmartContractServiceStub] = None
|
|
self.running = False
|
|
self.work_queue: queue.Queue = queue.Queue()
|
|
self.response_queue: queue.Queue = queue.Queue()
|
|
self.executor: Optional[ThreadPoolExecutor] = None
|
|
|
|
def connect(self) -> bool:
|
|
"""Establish connection to the gRPC server."""
|
|
try:
|
|
if self.config.use_tls:
|
|
if not self.config.tls_cert_path:
|
|
logger.error("TLS enabled but no certificate path provided")
|
|
return False
|
|
with open(self.config.tls_cert_path, "rb") as f:
|
|
creds = grpc.ssl_channel_credentials(f.read())
|
|
self.channel = grpc.secure_channel(self.config.server_address, creds)
|
|
else:
|
|
self.channel = grpc.insecure_channel(self.config.server_address)
|
|
|
|
self.stub = pb_grpc.SmartContractServiceStub(self.channel)
|
|
logger.info(f"Connected to server at {self.config.server_address}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect: {e}")
|
|
return False
|
|
|
|
def close(self):
|
|
"""Close the gRPC connection."""
|
|
if self.channel:
|
|
self.channel.close()
|
|
self.channel = None
|
|
self.stub = None
|
|
|
|
def _response_generator(self):
|
|
"""Generator that yields responses from the response queue."""
|
|
while self.running:
|
|
try:
|
|
response = self.response_queue.get(timeout=1.0)
|
|
if response is None:
|
|
break
|
|
yield response
|
|
except queue.Empty:
|
|
continue
|
|
|
|
def _process_request(self, request: pb.SmartContractRequest):
|
|
"""Process a single request and queue the response."""
|
|
logs = ""
|
|
|
|
try:
|
|
result = process(
|
|
tx_json=request.transaction_json,
|
|
env_vars=dict(request.env_vars),
|
|
secrets=dict(request.secrets),
|
|
)
|
|
|
|
response = pb.SmartContractResponse(
|
|
transaction_id=request.transaction_id,
|
|
output_to_chain=result.output_to_chain,
|
|
logs=logs,
|
|
)
|
|
|
|
if result.error:
|
|
response.error = result.error
|
|
logger.error(
|
|
f"Error processing transaction {request.transaction_id}: {result.error}"
|
|
)
|
|
else:
|
|
response.result_json = json.dumps(result.data) if result.data else "{}"
|
|
logger.info(f"Successfully processed transaction {request.transaction_id}")
|
|
|
|
except Exception as e:
|
|
response = pb.SmartContractResponse(
|
|
transaction_id=request.transaction_id,
|
|
error=str(e),
|
|
logs=logs,
|
|
)
|
|
logger.exception(f"Exception processing transaction {request.transaction_id}")
|
|
|
|
self.response_queue.put(response)
|
|
|
|
def _worker(self):
|
|
"""Worker thread that processes requests from the queue."""
|
|
while self.running:
|
|
try:
|
|
request = self.work_queue.get(timeout=1.0)
|
|
if request is None:
|
|
break
|
|
self._process_request(request)
|
|
except queue.Empty:
|
|
continue
|
|
|
|
def run(self) -> bool:
|
|
"""Run the client and process incoming requests."""
|
|
if not self.stub:
|
|
logger.error("Not connected to server")
|
|
return False
|
|
|
|
self.running = True
|
|
self.executor = ThreadPoolExecutor(max_workers=self.config.num_workers)
|
|
|
|
# Start worker threads
|
|
workers = []
|
|
for _ in range(self.config.num_workers):
|
|
future = self.executor.submit(self._worker)
|
|
workers.append(future)
|
|
|
|
logger.info(f"Started {self.config.num_workers} worker threads")
|
|
|
|
# Create metadata for authentication
|
|
metadata = [
|
|
("x-api-key", self.config.api_key),
|
|
("x-smart-contract-id", self.config.smart_contract_id),
|
|
]
|
|
|
|
try:
|
|
# Establish bi-directional stream
|
|
stream = self.stub.Run(self._response_generator(), metadata=metadata)
|
|
|
|
logger.info("Stream established, waiting for requests...")
|
|
|
|
# Receive and dispatch requests
|
|
for request in stream:
|
|
if not self.running:
|
|
break
|
|
logger.info(f"Received request: transaction_id={request.transaction_id}")
|
|
self.work_queue.put(request)
|
|
|
|
logger.info("Server closed the stream")
|
|
return True
|
|
|
|
except grpc.RpcError as e:
|
|
logger.error(f"gRPC error: {e.code()} - {e.details()}")
|
|
return False
|
|
except Exception as e:
|
|
logger.exception(f"Error in run loop: {e}")
|
|
return False
|
|
finally:
|
|
self.running = False
|
|
|
|
# Signal workers to stop
|
|
for _ in range(self.config.num_workers):
|
|
self.work_queue.put(None)
|
|
self.response_queue.put(None)
|
|
|
|
# Wait for workers to finish
|
|
if self.executor:
|
|
self.executor.shutdown(wait=True)
|
|
|
|
def stop(self):
|
|
"""Stop the client gracefully."""
|
|
logger.info("Stopping client...")
|
|
self.running = False
|
|
|
|
|
|
def load_config(path: str) -> Config:
|
|
"""Load configuration from a YAML file."""
|
|
with open(path, "r") as f:
|
|
data = yaml.safe_load(f)
|
|
|
|
# Validate required fields
|
|
required = ["server_address", "smart_contract_id", "api_key"]
|
|
for field in required:
|
|
if field not in data or not data[field]:
|
|
raise ValueError(f"Missing required config field: {field}")
|
|
|
|
return Config(
|
|
server_address=data["server_address"],
|
|
smart_contract_id=data["smart_contract_id"],
|
|
api_key=data["api_key"],
|
|
use_tls=data.get("use_tls", False),
|
|
tls_cert_path=data.get("tls_cert_path"),
|
|
num_workers=data.get("num_workers", 10),
|
|
reconnect_delay_seconds=data.get("reconnect_delay_seconds", 5),
|
|
max_reconnect_attempts=data.get("max_reconnect_attempts", 0),
|
|
)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Dragonchain Smart Contract Client")
|
|
parser.add_argument(
|
|
"--config",
|
|
"-c",
|
|
default="config.yaml",
|
|
help="Path to configuration file",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
# Load configuration
|
|
try:
|
|
config = load_config(args.config)
|
|
except Exception as e:
|
|
logger.error(f"Failed to load config: {e}")
|
|
sys.exit(1)
|
|
|
|
# Create client
|
|
client = SmartContractClient(config)
|
|
|
|
# Setup signal handling for graceful shutdown
|
|
def signal_handler(signum, frame):
|
|
logger.info(f"Received signal {signum}, shutting down...")
|
|
client.stop()
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# Connection loop with reconnection logic
|
|
attempts = 0
|
|
while True:
|
|
if client.connect():
|
|
attempts = 0
|
|
if not client.run():
|
|
if not client.running:
|
|
logger.info("Shutdown requested")
|
|
break
|
|
|
|
client.close()
|
|
|
|
attempts += 1
|
|
if config.max_reconnect_attempts > 0 and attempts >= config.max_reconnect_attempts:
|
|
logger.error(f"Max reconnection attempts ({config.max_reconnect_attempts}) reached")
|
|
break
|
|
|
|
delay = config.reconnect_delay_seconds
|
|
logger.info(f"Reconnecting in {delay} seconds (attempt {attempts})...")
|
|
time.sleep(delay)
|
|
|
|
logger.info("Client shut down")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|