commit 0634e664692488296ecf1df272310f90d1f3b86d Author: Andrew Miller Date: Tue Mar 17 19:59:47 2026 -0400 Initial commit: smart contract templates for bash, go, python, and typescript diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b1c8bb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# Node +node_modules/ + +# Go +smart-contract + +# Generated proto files +*_pb2.py +*_pb2_grpc.py + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Claude +.claude/ + +# Test config (contains credentials) +test-config.yaml + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100755 index 0000000..ed2efe5 --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Dragonchain Smart Contract Templates + +This repository contains templates for creating smart contract clients that connect to Dragonchain Prime via gRPC. + +## Available Templates + +| Template | Language | Directory | +|----------|----------|-----------| +| Go | Go 1.21+ | [go/](./go/) | +| Python | Python 3.10+ | [python/](./python/) | +| TypeScript | Node.js 18+ | [typescript/](./typescript/) | + +## How It Works + +Smart contracts connect to the Dragonchain Prime server using a bi-directional gRPC stream. The server sends transaction requests to your smart contract, which processes them and sends back responses. + +``` +┌─────────────────┐ ┌─────────────────────┐ +│ │ SmartContractRequest │ │ +│ Dragonchain │ ──────────────────────► │ Smart Contract │ +│ Prime Server │ │ Client │ +│ │ ◄────────────────────── │ │ +│ │ SmartContractResponse │ │ +└─────────────────┘ └─────────────────────┘ +``` + +### Request Flow + +1. Your client connects to the server with API key and smart contract ID +2. The server sends `SmartContractRequest` messages containing: + - `transaction_id` - Unique identifier for correlation + - `transaction_json` - The transaction data to process + - `env_vars` - Environment variables + - `secrets` - Secret values (API keys, credentials) +3. Your client processes the transaction and sends back `SmartContractResponse`: + - `transaction_id` - Same ID from the request + - `result_json` - Your processing result as JSON + - `logs` - Any logs captured during execution + - `output_to_chain` - Whether to persist the result + - `error` - Error message if processing failed + +## Quick Start + +### 1. Choose a Template + +Pick the template for your preferred language: + +```bash +# For Go +cp -r go /path/to/my-smart-contract + +# For Python +cp -r python /path/to/my-smart-contract + +# For TypeScript/JavaScript +cp -r typescript /path/to/my-smart-contract +``` + +### 2. Configure + +Edit `config.yaml` with your credentials: + +```yaml +server_address: "your-server:50051" +smart_contract_id: "your-smart-contract-id" +api_key: "your-api-key" +``` + +### 3. Implement Your Logic + +Each template has a `Process` (Go), `process` (Python/TypeScript) function. Implement your business logic there: + +**Go** (`cmd/main.go`): +```go +func Process(ctx context.Context, txJSON string, envVars, secrets map[string]string) ProcessResult { + // Your logic here + return ProcessResult{Data: result, OutputToChain: true} +} +``` + +**Python** (`main.py`): +```python +def process(tx_json: str, env_vars: dict, secrets: dict) -> ProcessResult: + # Your logic here + return ProcessResult(data=result, output_to_chain=True) +``` + +**TypeScript** (`src/main.ts`): +```typescript +async function process(txJson: string, envVars: Record, secrets: Record): Promise { + // Your logic here + return { data: result, outputToChain: true }; +} +``` + +### 4. Build and Run + +Each template includes a Makefile: + +```bash +# Install dependencies +make setup + +# Generate proto code (Go and Python only) +make proto + +# Build +make build + +# Run +make run +``` + +## Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `server_address` | gRPC server address (host:port) | Required | +| `smart_contract_id` | Your smart contract ID | Required | +| `api_key` | API key for authentication | Required | +| `use_tls` | Enable TLS encryption | `false` | +| `tls_cert_path` | Path to TLS certificate | - | +| `num_workers` | Concurrent transaction processors | `10` | +| `reconnect_delay_seconds` | Reconnection delay | `5` | +| `max_reconnect_attempts` | Max retries (0 = infinite) | `0` | + +## Environment Variables + +Your smart contract receives these environment variables: + +| Variable | Description | +|----------|-------------| +| `TZ` | Timezone | +| `ENVIRONMENT` | Deployment environment | +| `INTERNAL_ID` | Internal identifier | +| `DRAGONCHAIN_ID` | Dragonchain ID | +| `DRAGONCHAIN_ENDPOINT` | Dragonchain API endpoint | +| `SMART_CONTRACT_ID` | This smart contract's ID | +| `SMART_CONTRACT_NAME` | This smart contract's name | +| `SC_ENV_*` | Custom environment variables | + +## Secrets + +Secrets are passed separately from environment variables and are prefixed with `SC_SECRET_`. Access them via the `secrets` parameter in your process function. + +## Concurrent Processing + +All templates support concurrent transaction processing. Configure the number of workers with `num_workers` in your config file. Each incoming request is processed in parallel up to the worker limit. + +## Reconnection + +All templates include automatic reconnection logic. If the connection is lost, the client will attempt to reconnect with exponential backoff based on `reconnect_delay_seconds`. Set `max_reconnect_attempts` to limit retry attempts (0 = infinite). + +## Docker Examples + +Each template README includes a Docker example. General pattern: + +```dockerfile +# Build stage +FROM AS builder +COPY . . +RUN + +# Runtime stage +FROM +COPY --from=builder . +CMD [] +``` + +## Development + +### Prerequisites + +**Go:** +- Go 1.21+ +- protoc +- protoc-gen-go, protoc-gen-go-grpc + +**Python:** +- Python 3.10+ +- pip + +**TypeScript:** +- Node.js 18+ +- npm + +### Testing + +Each template supports testing. Add tests and run: + +```bash +make test +``` + +## License + +[Your License Here] diff --git a/bash/.gitignore b/bash/.gitignore new file mode 100644 index 0000000..958a37b --- /dev/null +++ b/bash/.gitignore @@ -0,0 +1,27 @@ +# Python (infrastructure runtime) +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environments +venv/ +ENV/ +env/ +.venv/ + +# Generated proto files +*_pb2.py +*_pb2_grpc.py + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Test config (contains credentials) +test-config.yaml + +# Logs +*.log diff --git a/bash/Makefile b/bash/Makefile new file mode 100644 index 0000000..9c718f8 --- /dev/null +++ b/bash/Makefile @@ -0,0 +1,53 @@ +.PHONY: proto run clean setup venv test deps check + +# Generate Python code from proto files (for the gRPC infrastructure) +proto: + python -m grpc_tools.protoc \ + -I./proto \ + --python_out=. \ + --grpc_python_out=. \ + proto/remote_sc.proto + +# Create virtual environment and install dependencies +setup: venv + . venv/bin/activate && pip install -r requirements.txt + @echo "" + @echo "Setup complete. Activate the virtual environment with:" + @echo " source venv/bin/activate" + +# Create virtual environment +venv: + python3 -m venv venv + +# Run the smart contract +run: + python main.py --config config.yaml + +# Clean generated files +clean: + rm -f *_pb2.py *_pb2_grpc.py + rm -rf __pycache__ + rm -rf venv + +# Run tests +test: + bash -n process.sh + @echo "Syntax check passed" + @echo "Running process.sh with sample transaction..." + echo '{"version":"1","header":{"tag":"test","dc_id":"test-dc","txn_id":"test-txn-123","block_id":"1","txn_type":"test","timestamp":"2024-01-01T00:00:00Z","invoker":"test-user"},"payload":{"action":"test","value":42}}' | \ + bash process.sh "$$(cat /dev/stdin)" 2>/dev/null && echo "Test passed" || echo "Test failed" + +# Install dependencies (without venv) +deps: + pip install -r requirements.txt + +# Check that required tools are available +check: + @command -v python3 >/dev/null 2>&1 || { echo "python3 is required but not installed"; exit 1; } + @command -v bash >/dev/null 2>&1 || { echo "bash is required but not installed"; exit 1; } + @command -v jq >/dev/null 2>&1 || { echo "jq is required but not installed"; exit 1; } + @echo "All required tools are available" + +# Format process.sh with shfmt (if available) +format: + @command -v shfmt >/dev/null 2>&1 && shfmt -w process.sh || echo "shfmt not installed, skipping format" diff --git a/bash/README.md b/bash/README.md new file mode 100644 index 0000000..2536764 --- /dev/null +++ b/bash/README.md @@ -0,0 +1,243 @@ +# Bash Smart Contract Template + +A Bash-based smart contract client for Dragonchain Prime that connects via gRPC. + +This template uses a thin Python gRPC infrastructure layer to handle the network protocol, while your smart contract logic lives entirely in `process.sh`. + +## Prerequisites + +- Bash 4.0+ +- Python 3.10+ (for gRPC infrastructure) +- pip +- jq (for JSON processing in bash) + +## Quick Start + +1. **Copy this template** to create your smart contract: + ```bash + cp -r bash /path/to/my-smart-contract + cd /path/to/my-smart-contract + ``` + +2. **Set up the environment**: + ```bash + make setup + source venv/bin/activate + ``` + + Or without make: + ```bash + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + ``` + +3. **Generate the protobuf code**: + ```bash + make proto + ``` + +4. **Configure your connection** by editing `config.yaml`: + ```yaml + server_address: "your-dragonchain-server:50051" + smart_contract_id: "your-smart-contract-id" + api_key: "your-api-key" + ``` + +5. **Implement your smart contract logic** in `process.sh`. + +6. **Run**: + ```bash + python main.py --config config.yaml + ``` + +## Configuration + +| Field | Description | Default | +|-------|-------------|---------| +| `server_address` | gRPC server address | Required | +| `smart_contract_id` | Your smart contract ID | Required | +| `api_key` | API key for authentication | Required | +| `use_tls` | Enable TLS encryption | `false` | +| `tls_cert_path` | Path to TLS certificate | - | +| `num_workers` | Concurrent transaction processors | `10` | +| `reconnect_delay_seconds` | Delay between reconnection attempts | `5` | +| `max_reconnect_attempts` | Max reconnect attempts (0 = infinite) | `0` | + +## Implementing Your Smart Contract + +Edit `process.sh`. The script receives the transaction JSON as its first argument (`$1`) and must output a JSON result to stdout. + +### Interface + +**Input:** +- `$1` - Transaction JSON string +- Environment variables - Server env vars and secrets are exported + +**Output (stdout):** +```json +{ + "data": { "your": "result" }, + "output_to_chain": true, + "error": "" +} +``` + +**Logs (stderr):** Anything written to stderr is captured and returned as logs. + +**Exit code:** 0 = success, non-zero = error (stderr used as error message). + +### Example + +```bash +#!/usr/bin/env bash +set -euo pipefail + +TX_JSON="$1" + +# Parse transaction fields with jq +TXN_ID=$(echo "$TX_JSON" | jq -r '.header.txn_id') +TXN_TYPE=$(echo "$TX_JSON" | jq -r '.header.txn_type') +PAYLOAD=$(echo "$TX_JSON" | jq -c '.payload') + +# Access environment variables +SC_NAME="${SMART_CONTRACT_NAME:-}" +DC_ID="${DRAGONCHAIN_ID:-}" + +# Access secrets +MY_SECRET="${SC_SECRET_MY_SECRET:-}" + +# Log to stderr +echo "Processing transaction $TXN_ID" >&2 + +# Process based on payload action +ACTION=$(echo "$TX_JSON" | jq -r '.payload.action // empty') +case "$ACTION" in + create) + RESULT='{"status": "created"}' + ;; + update) + RESULT='{"status": "updated"}' + ;; + *) + RESULT='{"status": "unknown"}' + ;; +esac + +# Output result as JSON +jq -n --argjson result "$RESULT" '{ + "data": $result, + "output_to_chain": true, + "error": "" +}' +``` + +### Transaction Structure + +The transaction JSON passed to your script has this format: + +```json +{ + "version": "1", + "header": { + "tag": "my-tag", + "dc_id": "dragonchain-id", + "txn_id": "transaction-id", + "block_id": "block-id", + "txn_type": "my-type", + "timestamp": "2024-01-01T00:00:00Z", + "invoker": "user-id" + }, + "payload": { + "your": "custom data" + } +} +``` + +### Available Environment Variables + +| Variable | Description | +|----------|-------------| +| `TZ` | Timezone | +| `ENVIRONMENT` | Deployment environment | +| `INTERNAL_ID` | Internal identifier | +| `DRAGONCHAIN_ID` | Dragonchain ID | +| `DRAGONCHAIN_ENDPOINT` | Dragonchain API endpoint | +| `SMART_CONTRACT_ID` | This smart contract's ID | +| `SMART_CONTRACT_NAME` | This smart contract's name | +| `SC_ENV_*` | Custom environment variables | + +### Secrets + +Secrets are exported as environment variables with keys prefixed by `SC_SECRET_`. + +## Project Structure + +``` +. +├── main.py # gRPC infrastructure (do not modify) +├── process.sh # Your smart contract logic (modify this) +├── proto/ +│ └── remote_sc.proto # gRPC service definition +├── config.yaml # Configuration file +├── requirements.txt # Python dependencies (for infrastructure) +├── Makefile # Build commands +└── README.md # This file +``` + +### File Descriptions + +- **`process.sh`** - Your smart contract logic. This is the only file you need to modify for most use cases. +- **`main.py`** - gRPC client infrastructure that invokes `process.sh` for each transaction. You typically don't need to modify this file. + +## Make Commands + +```bash +make setup # Create venv and install dependencies +make proto # Generate Python code from proto files +make run # Run with default config +make test # Syntax check and sample run of process.sh +make clean # Remove generated files and venv +make deps # Install dependencies (no venv) +make check # Verify required tools (python3, bash, jq) +make format # Format process.sh with shfmt (if installed) +``` + +## Concurrent Processing + +The client uses a thread pool to process multiple transactions concurrently. Each worker invokes a separate instance of `process.sh`. The number of workers is configurable via `num_workers` in the config file. + +## Error Handling + +- Return errors by setting the `error` field in your JSON output, or exit with a non-zero code +- Anything written to stderr is captured as logs +- The client automatically handles reconnection on connection failures + +## Docker + +Example `Dockerfile`: + +```dockerfile +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends jq bash && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN chmod +x process.sh +RUN python -m grpc_tools.protoc \ + -I./proto \ + --python_out=. \ + --grpc_python_out=. \ + proto/remote_sc.proto + +CMD ["python", "main.py", "--config", "config.yaml"] +``` + +## License + +[Your License Here] diff --git a/bash/config.yaml b/bash/config.yaml new file mode 100644 index 0000000..92b096c --- /dev/null +++ b/bash/config.yaml @@ -0,0 +1,24 @@ +# Smart Contract Client Configuration +# Copy this file and fill in your values + +# The gRPC server address to connect to +server_address: "localhost:50051" + +# Your smart contract ID (provided by Dragonchain) +smart_contract_id: "your-smart-contract-id" + +# API key for authentication (provided by Dragonchain) +api_key: "your-api-key" + +# Whether to use TLS for the connection +use_tls: false + +# Path to TLS certificate (required if use_tls is true) +# tls_cert_path: "/path/to/cert.pem" + +# Number of worker threads for processing transactions concurrently +num_workers: 10 + +# Reconnect settings +reconnect_delay_seconds: 5 +max_reconnect_attempts: 0 # 0 = infinite retries diff --git a/bash/process.sh b/bash/process.sh new file mode 100755 index 0000000..1aaec9a --- /dev/null +++ b/bash/process.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# ============================================================================= +# Smart Contract Processing Logic +# +# This file contains the transaction processing logic for your smart contract. +# Modify this script to implement your business logic. +# +# Input: +# $1 - Transaction JSON string +# Environment - Server env vars and secrets are exported as environment variables +# +# Output (stdout): +# A JSON object with the following fields: +# { +# "data": { ... }, // Your result data (any JSON object) +# "output_to_chain": true, // Whether to persist the result on chain +# "error": "" // Error message (empty string if no error) +# } +# +# Logs (stderr): +# Anything written to stderr is captured and sent back as logs. +# +# Exit code: +# 0 = success (stdout is parsed as JSON result) +# non-zero = error (stderr is used as the error message) +# ============================================================================= + +set -euo pipefail + +# Transaction JSON is passed as the first argument +TX_JSON="$1" + +# ============================================================================= +# TODO: Implement your smart contract logic here +# ============================================================================= +# +# Parse transaction fields using jq: +# TXN_ID=$(echo "$TX_JSON" | jq -r '.header.txn_id') +# TXN_TYPE=$(echo "$TX_JSON" | jq -r '.header.txn_type') +# PAYLOAD=$(echo "$TX_JSON" | jq '.payload') +# +# Access environment variables (set by the server): +# echo "Smart Contract Name: $SMART_CONTRACT_NAME" >&2 +# echo "Dragonchain ID: $DRAGONCHAIN_ID" >&2 +# +# Access secrets (set by the server): +# MY_SECRET="${SC_SECRET_MY_SECRET:-}" +# +# Process based on payload action: +# ACTION=$(echo "$TX_JSON" | jq -r '.payload.action // empty') +# case "$ACTION" in +# create) +# # Handle create operation +# ;; +# update) +# # Handle update operation +# ;; +# *) +# echo '{"data": null, "output_to_chain": false, "error": "Unknown action: '"$ACTION"'"}' +# exit 0 +# ;; +# esac +# +# Log messages to stderr (captured and sent back with response): +# echo "Processing transaction..." >&2 + +# Parse transaction data +TXN_ID=$(echo "$TX_JSON" | jq -r '.header.txn_id') +TXN_TYPE=$(echo "$TX_JSON" | jq -r '.header.txn_type') +PAYLOAD=$(echo "$TX_JSON" | jq -c '.payload') + +echo "Processing transaction $TXN_ID (type: $TXN_TYPE)" >&2 + +# Default implementation: echo back the transaction +jq -n \ + --arg txn_id "$TXN_ID" \ + --arg txn_type "$TXN_TYPE" \ + --argjson payload "$PAYLOAD" \ + '{ + "data": { + "status": "processed", + "transaction_id": $txn_id, + "txn_type": $txn_type, + "payload": $payload, + "message": "Transaction processed successfully" + }, + "output_to_chain": true, + "error": "" + }' diff --git a/bash/proto/remote_sc.proto b/bash/proto/remote_sc.proto new file mode 100644 index 0000000..559a462 --- /dev/null +++ b/bash/proto/remote_sc.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package remote_sc; + +// SmartContractService defines the bi-directional streaming service for remote +// smart contract execution. External workers connect to this service to receive +// execution tasks and send back results. +service SmartContractService { + // Run establishes a bi-directional stream. The server sends SmartContractRequest + // messages to the client for execution, and the client sends back + // SmartContractResponse messages with results. + rpc Run(stream SmartContractResponse) returns (stream SmartContractRequest); +} + +// SmartContractRequest is sent from the server to the connected worker +// to request execution of a smart contract. +message SmartContractRequest { + // Unique identifier for this execution request, used to correlate responses + string transaction_id = 1; + + // Full transaction JSON to be processed by the smart contract + string transaction_json = 2; + + // Environment variables to set for the smart contract execution + map env_vars = 3; + + // Secrets to be made available to the smart contract + map secrets = 4; +} + +// SmartContractResponse is sent from the worker back to the server +// with the results of smart contract execution. +message SmartContractResponse { + // The transaction_id from the original request, for correlation + string transaction_id = 1; + + // The result data from the smart contract execution as JSON + string result_json = 2; + + // Logs captured during smart contract execution + string logs = 3; + + // Whether to persist the output to the chain + bool output_to_chain = 4; + + // Error message if execution failed + string error = 5; +} diff --git a/go/.gitignore b/go/.gitignore new file mode 100755 index 0000000..9298663 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,22 @@ +# Binary +smart-contract + +# Generated proto files +proto/*.pb.go + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Test config (contains credentials) +test-config.yaml + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/go/Makefile b/go/Makefile new file mode 100755 index 0000000..b803ae5 --- /dev/null +++ b/go/Makefile @@ -0,0 +1,45 @@ +SHELL := /bin/bash +.PHONY: proto build run clean tools test deps fix-package-name + +# Generate Go code from proto files +proto: + protoc --go_out=. --go_opt=paths=source_relative \ + --go-grpc_out=. --go-grpc_opt=paths=source_relative \ + proto/remote_sc.proto + +# Build the smart contract binary +build: + go build -o smart-contract . + +# Run the smart contract +run: + go run . -config config.yaml + +# Clean build artifacts +clean: + rm -f smart-contract + +# Install required tools +tools: + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +# Run tests +test: + go test -v ./... + +# Download dependencies +deps: + go mod download + go mod tidy + +# Fix package name based on current working directory +fix-package-name: + @NEW_MODULE=$$(go list -m 2>/dev/null || echo "$$(basename $$(dirname $$(pwd)))/$$(basename $$(pwd))"); \ + if [ -z "$$NEW_MODULE" ]; then \ + echo "Could not determine module name. Please run 'go mod init ' first."; \ + exit 1; \ + fi; \ + echo "Updating package name to: $$NEW_MODULE"; \ + find . -type f -name "*.go" ! -path "./proto/*" -exec sed -i 's|github.com/your-org/smart-contract|'$$NEW_MODULE'|g' {} +; \ + echo "Done. Run 'make proto' to generate proto files, then 'make build' to build." diff --git a/go/README.md b/go/README.md new file mode 100755 index 0000000..6fb7fde --- /dev/null +++ b/go/README.md @@ -0,0 +1,192 @@ +# Go Smart Contract Template + +A Go-based smart contract client for Dragonchain Prime that connects via gRPC. + +## Prerequisites + +- Go 1.21 or later +- Protocol Buffers compiler (`protoc`) +- Go protobuf plugins + +### Installing Tools + +```bash +# Install protoc (Ubuntu/Debian) +sudo apt install -y protobuf-compiler + +# Install protoc (macOS) +brew install protobuf + +# Install Go protobuf plugins +make tools +``` + +## Quick Start + +1. **Copy this template** to create your smart contract: + ```bash + cp -r go /path/to/my-smart-contract + cd /path/to/my-smart-contract + ``` + +2. **Initialize your Go module and update package names**: + ```bash + go mod init github.com/your-org/your-project + make fix-package-name + ``` + +3. **Generate the protobuf code**: + ```bash + make proto + ``` + +4. **Configure your connection** by editing `config.yaml`: + ```yaml + server_address: "your-dragonchain-server:50051" + smart_contract_id: "your-smart-contract-id" + api_key: "your-api-key" + ``` + +5. **Implement your smart contract logic** in `process.go` by modifying the `Process` function. + +6. **Build and run**: + ```bash + make build + ./smart-contract -config config.yaml + ``` + +## Configuration + +| Field | Description | Default | +|-------|-------------|---------| +| `server_address` | gRPC server address | Required | +| `smart_contract_id` | Your smart contract ID | Required | +| `api_key` | API key for authentication | Required | +| `use_tls` | Enable TLS encryption | `false` | +| `tls_cert_path` | Path to TLS certificate | - | +| `num_workers` | Concurrent transaction processors | `10` | +| `reconnect_delay_seconds` | Delay between reconnection attempts | `5` | +| `max_reconnect_attempts` | Max reconnect attempts (0 = infinite) | `0` | + +## Implementing Your Smart Contract + +Edit the `Process` function in `process.go`: + +```go +func Process(ctx context.Context, txJSON string, envVars, secrets map[string]string) ProcessResult { + // Parse the transaction + var tx Transaction + if err := json.Unmarshal([]byte(txJSON), &tx); err != nil { + return ProcessResult{Error: fmt.Errorf("failed to parse transaction: %w", err)} + } + + // Access transaction data + txnId := tx.Header.TxnId + txnType := tx.Header.TxnType + payload := tx.Payload + + // Access environment variables + scName := envVars["SMART_CONTRACT_NAME"] + dcID := envVars["DRAGONCHAIN_ID"] + + // Access secrets + mySecret := secrets["SC_SECRET_MY_SECRET"] + + // Implement your logic here + result := map[string]any{ + "status": "success", + "data": "your result data", + } + + return ProcessResult{ + Data: result, + OutputToChain: true, // Set to true to persist result on chain + Error: nil, + } +} +``` + +### Transaction Structure + +The `Transaction` struct in `process.go` matches the Dragonchain transaction format: + +```go +type Transaction struct { + Version string `json:"version"` + Header TransactionHeader `json:"header"` + Payload map[string]any `json:"payload"` +} + +type TransactionHeader struct { + Tag string `json:"tag"` + DcId string `json:"dc_id"` + TxnId string `json:"txn_id"` + BlockId string `json:"block_id"` + TxnType string `json:"txn_type"` + Timestamp string `json:"timestamp"` + Invoker string `json:"invoker"` +} +``` + +### Available Environment Variables + +| Variable | Description | +|----------|-------------| +| `TZ` | Timezone | +| `ENVIRONMENT` | Deployment environment | +| `INTERNAL_ID` | Internal identifier | +| `DRAGONCHAIN_ID` | Dragonchain ID | +| `DRAGONCHAIN_ENDPOINT` | Dragonchain API endpoint | +| `SMART_CONTRACT_ID` | This smart contract's ID | +| `SMART_CONTRACT_NAME` | This smart contract's name | +| `SC_ENV_*` | Custom environment variables | + +### Secrets + +Secrets are provided in the `secrets` map with keys prefixed by `SC_SECRET_`. + +## Project Structure + +``` +. +├── main.go # Client infrastructure (do not modify) +├── process.go # Your smart contract logic (modify this) +├── proto/ +│ └── remote_sc.proto # gRPC service definition +├── config.yaml # Configuration file +├── go.mod # Go module definition +├── Makefile # Build commands +└── README.md # This file +``` + +### File Descriptions + +- **`process.go`** - Contains the `Process` function where you implement your smart contract logic. This is the only file you need to modify for most use cases. +- **`main.go`** - Contains the gRPC client infrastructure, connection handling, and worker pool. You typically don't need to modify this file. + +## Make Commands + +```bash +make proto # Generate Go code from proto files +make build # Build the binary +make run # Run with default config +make test # Run tests +make clean # Remove build artifacts +make tools # Install required tools +make deps # Download dependencies +make fix-package-name # Update package name based on go.mod +``` + +## Concurrent Processing + +The client uses a worker pool pattern to process multiple transactions concurrently. The number of workers is configurable via `num_workers` in the config file. + +## Error Handling + +- Return errors from the `Process` function to report failures to the server +- The client automatically handles reconnection on connection failures +- Logs are captured and sent back with the response + +## License + +[Your License Here] diff --git a/go/config.yaml b/go/config.yaml new file mode 100755 index 0000000..fa0a0fe --- /dev/null +++ b/go/config.yaml @@ -0,0 +1,24 @@ +# Smart Contract Client Configuration +# Copy this file and fill in your values + +# The gRPC server address to connect to +server_address: "localhost:50051" + +# Your smart contract ID (provided by Dragonchain) +smart_contract_id: "your-smart-contract-id" + +# API key for authentication (provided by Dragonchain) +api_key: "your-api-key" + +# Whether to use TLS for the connection +use_tls: false + +# Path to TLS certificate (required if use_tls is true) +# tls_cert_path: "/path/to/cert.pem" + +# Number of worker goroutines for processing transactions concurrently +num_workers: 10 + +# Reconnect settings +reconnect_delay_seconds: 5 +max_reconnect_attempts: 0 # 0 = infinite retries diff --git a/go/go.mod b/go/go.mod new file mode 100755 index 0000000..e2c637e --- /dev/null +++ b/go/go.mod @@ -0,0 +1,18 @@ +module github.com/your-org/smart-contract + +go 1.24.0 + +toolchain go1.24.5 + +require ( + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100755 index 0000000..829137d --- /dev/null +++ b/go/go.sum @@ -0,0 +1,40 @@ +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/main.go b/go/main.go new file mode 100755 index 0000000..e722016 --- /dev/null +++ b/go/main.go @@ -0,0 +1,311 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" + + pb "github.com/your-org/smart-contract/proto" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "gopkg.in/yaml.v3" +) + +// ============================================================================= +// Configuration and Client Infrastructure +// Do not modify this file unless you need to customize the client behavior. +// Implement your smart contract logic in process.go instead. +// ============================================================================= + +// Config holds the client configuration loaded from YAML +type Config struct { + ServerAddress string `yaml:"server_address"` + SmartContractID string `yaml:"smart_contract_id"` + APIKey string `yaml:"api_key"` + UseTLS bool `yaml:"use_tls"` + TLSCertPath string `yaml:"tls_cert_path"` + NumWorkers int `yaml:"num_workers"` + ReconnectDelaySecs int `yaml:"reconnect_delay_seconds"` + MaxReconnectAttempts int `yaml:"max_reconnect_attempts"` +} + +// Client manages the gRPC connection and request processing +type Client struct { + config *Config + conn *grpc.ClientConn + grpcClient pb.SmartContractServiceClient + workChan chan *pb.SmartContractRequest + wg sync.WaitGroup + logger *log.Logger +} + +// NewClient creates a new smart contract client +func NewClient(config *Config) *Client { + return &Client{ + config: config, + workChan: make(chan *pb.SmartContractRequest, config.NumWorkers*2), + logger: log.New(os.Stdout, "[SC-Client] ", log.LstdFlags|log.Lmicroseconds), + } +} + +// Connect establishes a connection to the gRPC server +func (c *Client) Connect() error { + var opts []grpc.DialOption + + if c.config.UseTLS { + creds, err := credentials.NewClientTLSFromFile(c.config.TLSCertPath, "") + if err != nil { + return fmt.Errorf("failed to load TLS credentials: %w", err) + } + opts = append(opts, grpc.WithTransportCredentials(creds)) + } else { + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } + + conn, err := grpc.NewClient(c.config.ServerAddress, opts...) + if err != nil { + return fmt.Errorf("failed to connect to server: %w", err) + } + + c.conn = conn + c.grpcClient = pb.NewSmartContractServiceClient(conn) + c.logger.Printf("Connected to server at %s", c.config.ServerAddress) + return nil +} + +// Close closes the gRPC connection +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +// Run starts the client and processes incoming requests +func (c *Client) Run(ctx context.Context) error { + // Create metadata with authentication headers + md := metadata.Pairs( + "x-api-key", c.config.APIKey, + "x-smart-contract-id", c.config.SmartContractID, + ) + ctx = metadata.NewOutgoingContext(ctx, md) + + // Establish the bi-directional stream + stream, err := c.grpcClient.Run(ctx) + if err != nil { + return fmt.Errorf("failed to establish stream: %w", err) + } + + c.logger.Printf("Stream established, starting %d workers", c.config.NumWorkers) + + // Channel to collect responses from workers + responseChan := make(chan *pb.SmartContractResponse, c.config.NumWorkers*2) + errChan := make(chan error, 1) + + // Start worker goroutines + for i := 0; i < c.config.NumWorkers; i++ { + c.wg.Add(1) + go c.worker(ctx, responseChan) + } + + // Goroutine to send responses back to server + go func() { + for resp := range responseChan { + if err := stream.Send(resp); err != nil { + c.logger.Printf("Error sending response: %v", err) + select { + case errChan <- err: + default: + } + return + } + } + }() + + // Main loop: receive requests and dispatch to workers + for { + req, err := stream.Recv() + if err == io.EOF { + c.logger.Println("Server closed the stream") + break + } + if err != nil { + return fmt.Errorf("error receiving request: %w", err) + } + + c.logger.Printf("Received request: transaction_id=%s", req.TransactionId) + + select { + case c.workChan <- req: + case <-ctx.Done(): + return ctx.Err() + } + } + + // Cleanup + close(c.workChan) + c.wg.Wait() + close(responseChan) + + return nil +} + +// worker processes requests from the work channel +func (c *Client) worker(ctx context.Context, responseChan chan<- *pb.SmartContractResponse) { + defer c.wg.Done() + + for { + select { + case req, ok := <-c.workChan: + if !ok { + return + } + c.processRequest(ctx, req, responseChan) + case <-ctx.Done(): + return + } + } +} + +// processRequest handles a single request +func (c *Client) processRequest(ctx context.Context, req *pb.SmartContractRequest, responseChan chan<- *pb.SmartContractResponse) { + // Capture logs (in production, you might want a more sophisticated logging approach) + var logs string + + // Call the user-defined Process function + result := Process(ctx, req.TransactionJson, req.EnvVars, req.Secrets) + + // Build the response + resp := &pb.SmartContractResponse{ + TransactionId: req.TransactionId, + OutputToChain: result.OutputToChain, + Logs: logs, + } + + if result.Error != nil { + resp.Error = result.Error.Error() + c.logger.Printf("Error processing transaction %s: %v", req.TransactionId, result.Error) + } else { + // Marshal the result data to JSON + resultJSON, err := json.Marshal(result.Data) + if err != nil { + resp.Error = fmt.Sprintf("failed to marshal result: %v", err) + c.logger.Printf("Error marshaling result for transaction %s: %v", req.TransactionId, err) + } else { + resp.ResultJson = string(resultJSON) + c.logger.Printf("Successfully processed transaction %s", req.TransactionId) + } + } + + select { + case responseChan <- resp: + case <-ctx.Done(): + } +} + +// LoadConfig loads configuration from a YAML file +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %w", err) + } + + config := &Config{ + NumWorkers: 10, + ReconnectDelaySecs: 5, + } + + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %w", err) + } + + // Validate required fields + if config.ServerAddress == "" { + return nil, fmt.Errorf("server_address is required") + } + if config.SmartContractID == "" { + return nil, fmt.Errorf("smart_contract_id is required") + } + if config.APIKey == "" { + return nil, fmt.Errorf("api_key is required") + } + + return config, nil +} + +func main() { + configPath := flag.String("config", "config.yaml", "Path to configuration file") + flag.Parse() + + // Load configuration + config, err := LoadConfig(*configPath) + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // Create client + client := NewClient(config) + + // Setup signal handling for graceful shutdown + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-sigChan + log.Printf("Received signal %v, shutting down...", sig) + cancel() + }() + + // Connection loop with reconnection logic + attempts := 0 + for { + if err := client.Connect(); err != nil { + log.Printf("Connection failed: %v", err) + } else { + attempts = 0 + if err := client.Run(ctx); err != nil { + if ctx.Err() != nil { + log.Println("Shutdown requested") + break + } + log.Printf("Stream error: %v", err) + } + } + + _ = client.Close() + + // Check if we should stop reconnecting + if ctx.Err() != nil { + break + } + + attempts++ + if config.MaxReconnectAttempts > 0 && attempts >= config.MaxReconnectAttempts { + log.Printf("Max reconnection attempts (%d) reached, exiting", config.MaxReconnectAttempts) + break + } + + delay := time.Duration(config.ReconnectDelaySecs) * time.Second + log.Printf("Reconnecting in %v (attempt %d)...", delay, attempts) + + select { + case <-time.After(delay): + case <-ctx.Done(): + break + } + } + + log.Println("Client shut down") +} diff --git a/go/process.go b/go/process.go new file mode 100755 index 0000000..53d7c15 --- /dev/null +++ b/go/process.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" +) + +// ============================================================================= +// SMART CONTRACT IMPLEMENTATION - MODIFY THIS FILE +// ============================================================================= + +// Transaction represents the parsed transaction from the server. +// Customize this struct to match your transaction payload structure. +type Transaction struct { + Version string `json:"version"` + Header TransactionHeader `json:"header"` + Payload map[string]any `json:"payload"` +} + +// TransactionHeader contains transaction metadata from Dragonchain. +type TransactionHeader struct { + Tag string `json:"tag"` + DcId string `json:"dc_id"` + TxnId string `json:"txn_id"` + BlockId string `json:"block_id"` + TxnType string `json:"txn_type"` + Timestamp string `json:"timestamp"` + Invoker string `json:"invoker"` +} + +// ProcessResult is the result returned from the Process function. +type ProcessResult struct { + Data map[string]any + OutputToChain bool + Error error +} + +// Process is the main function that handles incoming transactions. +// Implement your smart contract logic here. +// +// Parameters: +// - ctx: Context for cancellation and timeouts +// - txJSON: Raw transaction JSON string +// - envVars: Environment variables passed from the server +// - secrets: Secrets passed from the server (e.g., API keys, credentials) +// +// Returns: +// - ProcessResult containing the result data, whether to output to chain, and any error +func Process(ctx context.Context, txJSON string, envVars, secrets map[string]string) ProcessResult { + // Parse the transaction JSON + var tx Transaction + if err := json.Unmarshal([]byte(txJSON), &tx); err != nil { + return ProcessResult{ + Error: fmt.Errorf("failed to parse transaction: %w", err), + } + } + + // ========================================================================== + // TODO: Implement your smart contract logic here + // ========================================================================== + // + // Example: Access transaction data + // txnId := tx.Header.TxnId + // txnType := tx.Header.TxnType + // payload := tx.Payload + // + // Example: Access environment variables + // scName := envVars["SMART_CONTRACT_NAME"] + // dcID := envVars["DRAGONCHAIN_ID"] + // + // Example: Access secrets + // apiKey := secrets["SC_SECRET_MY_API_KEY"] + // + // Example: Process based on payload action + // action, _ := tx.Payload["action"].(string) + // switch action { + // case "create": + // // Handle create operation + // case "update": + // // Handle update operation + // default: + // return ProcessResult{Error: fmt.Errorf("unknown action: %s", action)} + // } + + // Default implementation: echo back the transaction + result := map[string]any{ + "status": "processed", + "transaction_id": tx.Header.TxnId, + "txn_type": tx.Header.TxnType, + "payload": tx.Payload, + "message": "Transaction processed successfully", + } + + return ProcessResult{ + Data: result, + OutputToChain: true, + Error: nil, + } +} diff --git a/go/proto/remote_sc.proto b/go/proto/remote_sc.proto new file mode 100755 index 0000000..5c37ddd --- /dev/null +++ b/go/proto/remote_sc.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +package remote_sc; + +option go_package = "git.dragonchain.com/dragonchain/sc-templates/go/proto"; + +// SmartContractService defines the bi-directional streaming service for remote +// smart contract execution. External workers connect to this service to receive +// execution tasks and send back results. +service SmartContractService { + // Run establishes a bi-directional stream. The server sends SmartContractRequest + // messages to the client for execution, and the client sends back + // SmartContractResponse messages with results. + rpc Run(stream SmartContractResponse) returns (stream SmartContractRequest); +} + +// SmartContractRequest is sent from the server to the connected worker +// to request execution of a smart contract. +message SmartContractRequest { + // Unique identifier for this execution request, used to correlate responses + string transaction_id = 1; + + // Full transaction JSON to be processed by the smart contract + string transaction_json = 2; + + // Environment variables to set for the smart contract execution + map env_vars = 3; + + // Secrets to be made available to the smart contract + map secrets = 4; +} + +// SmartContractResponse is sent from the worker back to the server +// with the results of smart contract execution. +message SmartContractResponse { + // The transaction_id from the original request, for correlation + string transaction_id = 1; + + // The result data from the smart contract execution as JSON + string result_json = 2; + + // Logs captured during smart contract execution + string logs = 3; + + // Whether to persist the output to the chain + bool output_to_chain = 4; + + // Error message if execution failed + string error = 5; +} diff --git a/python/Makefile b/python/Makefile new file mode 100755 index 0000000..68428e2 --- /dev/null +++ b/python/Makefile @@ -0,0 +1,40 @@ +.PHONY: proto run clean setup venv test + +# Generate Python code from proto files +proto: + python -m grpc_tools.protoc \ + -I./proto \ + --python_out=. \ + --grpc_python_out=. \ + proto/remote_sc.proto + +# Create virtual environment and install dependencies +setup: venv + . venv/bin/activate && pip install -r requirements.txt + +# Create virtual environment +venv: + python3 -m venv venv + +# Run the smart contract +run: + python main.py --config config.yaml + +# Clean generated files +clean: + rm -f *_pb2.py *_pb2_grpc.py + rm -rf __pycache__ + rm -rf venv + +# Run tests +test: + python -m pytest tests/ -v + +# Install dependencies (without venv) +deps: + pip install -r requirements.txt + +# Format code +format: + black . + isort . diff --git a/python/README.md b/python/README.md new file mode 100755 index 0000000..33879b3 --- /dev/null +++ b/python/README.md @@ -0,0 +1,205 @@ +# Python Smart Contract Template + +A Python-based smart contract client for Dragonchain Prime that connects via gRPC. + +## Prerequisites + +- Python 3.10 or later +- pip + +## Quick Start + +1. **Copy this template** to create your smart contract: + ```bash + cp -r python /path/to/my-smart-contract + cd /path/to/my-smart-contract + ``` + +2. **Set up the environment**: + ```bash + make setup + source venv/bin/activate + ``` + + Or without make: + ```bash + python3 -m venv venv + source venv/bin/activate + pip install -r requirements.txt + ``` + +3. **Generate the protobuf code**: + ```bash + make proto + ``` + +4. **Configure your connection** by editing `config.yaml`: + ```yaml + server_address: "your-dragonchain-server:50051" + smart_contract_id: "your-smart-contract-id" + api_key: "your-api-key" + ``` + +5. **Implement your smart contract logic** in `process.py` by modifying the `process` function. + +6. **Run**: + ```bash + python main.py --config config.yaml + ``` + +## Configuration + +| Field | Description | Default | +|-------|-------------|---------| +| `server_address` | gRPC server address | Required | +| `smart_contract_id` | Your smart contract ID | Required | +| `api_key` | API key for authentication | Required | +| `use_tls` | Enable TLS encryption | `false` | +| `tls_cert_path` | Path to TLS certificate | - | +| `num_workers` | Concurrent transaction processors | `10` | +| `reconnect_delay_seconds` | Delay between reconnection attempts | `5` | +| `max_reconnect_attempts` | Max reconnect attempts (0 = infinite) | `0` | + +## Implementing Your Smart Contract + +Edit the `process` function in `process.py`: + +```python +def process( + tx_json: str, + env_vars: dict[str, str], + secrets: dict[str, str], +) -> ProcessResult: + # Parse the transaction + tx = Transaction.from_json(tx_json) + + # Access transaction data + txn_id = tx.header.txn_id + txn_type = tx.header.txn_type + payload = tx.payload + + # Access environment variables + sc_name = env_vars.get("SMART_CONTRACT_NAME") + dc_id = env_vars.get("DRAGONCHAIN_ID") + + # Access secrets + my_secret = secrets.get("SC_SECRET_MY_SECRET") + + # Implement your logic here + result = { + "status": "success", + "data": "your result data", + } + + return ProcessResult( + data=result, + output_to_chain=True, # Set to True to persist result on chain + error=None, + ) +``` + +### Transaction Structure + +The `Transaction` class in `process.py` matches the Dragonchain transaction format: + +```python +@dataclass +class Transaction: + version: str + header: TransactionHeader + payload: dict[str, Any] + +@dataclass +class TransactionHeader: + tag: str + dc_id: str + txn_id: str + block_id: str + txn_type: str + timestamp: str + invoker: str +``` + +### Available Environment Variables + +| Variable | Description | +|----------|-------------| +| `TZ` | Timezone | +| `ENVIRONMENT` | Deployment environment | +| `INTERNAL_ID` | Internal identifier | +| `DRAGONCHAIN_ID` | Dragonchain ID | +| `DRAGONCHAIN_ENDPOINT` | Dragonchain API endpoint | +| `SMART_CONTRACT_ID` | This smart contract's ID | +| `SMART_CONTRACT_NAME` | This smart contract's name | +| `SC_ENV_*` | Custom environment variables | + +### Secrets + +Secrets are provided in the `secrets` dict with keys prefixed by `SC_SECRET_`. + +## Project Structure + +``` +. +├── main.py # Client infrastructure (do not modify) +├── process.py # Your smart contract logic (modify this) +├── proto/ +│ └── remote_sc.proto # gRPC service definition +├── config.yaml # Configuration file +├── requirements.txt # Python dependencies +├── Makefile # Build commands +└── README.md # This file +``` + +### File Descriptions + +- **`process.py`** - Contains the `process` function where you implement your smart contract logic. This is the only file you need to modify for most use cases. +- **`main.py`** - Contains the gRPC client infrastructure, connection handling, and worker pool. You typically don't need to modify this file. + +## Make Commands + +```bash +make setup # Create venv and install dependencies +make proto # Generate Python code from proto files +make run # Run with default config +make test # Run tests +make clean # Remove generated files and venv +make deps # Install dependencies (no venv) +make format # Format code with black and isort +``` + +## Concurrent Processing + +The client uses a thread pool to process multiple transactions concurrently. The number of workers is configurable via `num_workers` in the config file. + +## Error Handling + +- Return errors from the `process` function via `ProcessResult.error` to report failures +- The client automatically handles reconnection on connection failures +- Logs are captured and sent back with the response + +## Docker + +Example `Dockerfile`: + +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN python -m grpc_tools.protoc \ + -I./proto \ + --python_out=. \ + --grpc_python_out=. \ + proto/remote_sc.proto + +CMD ["python", "main.py", "--config", "config.yaml"] +``` + +## License + +[Your License Here] diff --git a/python/config.yaml b/python/config.yaml new file mode 100755 index 0000000..92b096c --- /dev/null +++ b/python/config.yaml @@ -0,0 +1,24 @@ +# Smart Contract Client Configuration +# Copy this file and fill in your values + +# The gRPC server address to connect to +server_address: "localhost:50051" + +# Your smart contract ID (provided by Dragonchain) +smart_contract_id: "your-smart-contract-id" + +# API key for authentication (provided by Dragonchain) +api_key: "your-api-key" + +# Whether to use TLS for the connection +use_tls: false + +# Path to TLS certificate (required if use_tls is true) +# tls_cert_path: "/path/to/cert.pem" + +# Number of worker threads for processing transactions concurrently +num_workers: 10 + +# Reconnect settings +reconnect_delay_seconds: 5 +max_reconnect_attempts: 0 # 0 = infinite retries diff --git a/python/main.py b/python/main.py new file mode 100755 index 0000000..6b6616d --- /dev/null +++ b/python/main.py @@ -0,0 +1,297 @@ +#!/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() diff --git a/python/package-lock.json b/python/package-lock.json new file mode 100755 index 0000000..9fcf0f2 --- /dev/null +++ b/python/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "python", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/python/process.py b/python/process.py new file mode 100755 index 0000000..b98ea9f --- /dev/null +++ b/python/process.py @@ -0,0 +1,140 @@ +""" +Smart Contract Processing Logic + +This file contains the transaction processing logic for your smart contract. +Modify the `process` function to implement your business logic. +""" + +import json +from dataclasses import dataclass +from typing import Any, Optional + + +# ============================================================================= +# SMART CONTRACT IMPLEMENTATION - MODIFY THIS FILE +# ============================================================================= + + +@dataclass +class TransactionHeader: + """Transaction metadata from Dragonchain.""" + + tag: str + dc_id: str + txn_id: str + block_id: str + txn_type: str + timestamp: str + invoker: str + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "TransactionHeader": + """Parse header from dictionary.""" + return cls( + tag=data.get("tag", ""), + dc_id=data.get("dc_id", ""), + txn_id=data.get("txn_id", ""), + block_id=data.get("block_id", ""), + txn_type=data.get("txn_type", ""), + timestamp=data.get("timestamp", ""), + invoker=data.get("invoker", ""), + ) + + +@dataclass +class Transaction: + """ + Parsed transaction from the server. + + Customize this class to match your transaction payload structure. + """ + + version: str + header: TransactionHeader + payload: dict[str, Any] + + @classmethod + def from_json(cls, json_str: str) -> "Transaction": + """Parse a transaction from JSON string.""" + data = json.loads(json_str) + return cls( + version=data.get("version", ""), + header=TransactionHeader.from_dict(data.get("header", {})), + payload=data.get("payload", {}), + ) + + +@dataclass +class ProcessResult: + """Result from the process function.""" + + data: Optional[dict[str, Any]] = None + output_to_chain: bool = True + error: Optional[str] = None + + +def process( + tx_json: str, + env_vars: dict[str, str], + secrets: dict[str, str], +) -> ProcessResult: + """ + Process an incoming transaction. + + Implement your smart contract logic here. + + Args: + tx_json: Raw transaction JSON string + env_vars: Environment variables passed from the server + secrets: Secrets passed from the server (e.g., API keys, credentials) + + Returns: + ProcessResult containing the result data, whether to output to chain, and any error + """ + try: + # Parse the transaction JSON + tx = Transaction.from_json(tx_json) + except json.JSONDecodeError as e: + return ProcessResult(error=f"Failed to parse transaction: {e}") + + # ========================================================================== + # TODO: Implement your smart contract logic here + # ========================================================================== + # + # Example: Access transaction data + # txn_id = tx.header.txn_id + # txn_type = tx.header.txn_type + # payload = tx.payload + # + # Example: Access environment variables + # sc_name = env_vars.get("SMART_CONTRACT_NAME") + # dc_id = env_vars.get("DRAGONCHAIN_ID") + # + # Example: Access secrets + # api_key = secrets.get("SC_SECRET_MY_API_KEY") + # + # Example: Process based on payload action + # action = tx.payload.get("action") + # if action == "create": + # # Handle create operation + # pass + # elif action == "update": + # # Handle update operation + # pass + # else: + # return ProcessResult(error=f"Unknown action: {action}") + + # Default implementation: echo back the transaction + result = { + "status": "processed", + "transaction_id": tx.header.txn_id, + "txn_type": tx.header.txn_type, + "payload": tx.payload, + "message": "Transaction processed successfully", + } + + return ProcessResult( + data=result, + output_to_chain=True, + error=None, + ) diff --git a/python/proto/remote_sc.proto b/python/proto/remote_sc.proto new file mode 100755 index 0000000..559a462 --- /dev/null +++ b/python/proto/remote_sc.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package remote_sc; + +// SmartContractService defines the bi-directional streaming service for remote +// smart contract execution. External workers connect to this service to receive +// execution tasks and send back results. +service SmartContractService { + // Run establishes a bi-directional stream. The server sends SmartContractRequest + // messages to the client for execution, and the client sends back + // SmartContractResponse messages with results. + rpc Run(stream SmartContractResponse) returns (stream SmartContractRequest); +} + +// SmartContractRequest is sent from the server to the connected worker +// to request execution of a smart contract. +message SmartContractRequest { + // Unique identifier for this execution request, used to correlate responses + string transaction_id = 1; + + // Full transaction JSON to be processed by the smart contract + string transaction_json = 2; + + // Environment variables to set for the smart contract execution + map env_vars = 3; + + // Secrets to be made available to the smart contract + map secrets = 4; +} + +// SmartContractResponse is sent from the worker back to the server +// with the results of smart contract execution. +message SmartContractResponse { + // The transaction_id from the original request, for correlation + string transaction_id = 1; + + // The result data from the smart contract execution as JSON + string result_json = 2; + + // Logs captured during smart contract execution + string logs = 3; + + // Whether to persist the output to the chain + bool output_to_chain = 4; + + // Error message if execution failed + string error = 5; +} diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100755 index 0000000..5c0606c --- /dev/null +++ b/python/requirements.txt @@ -0,0 +1,4 @@ +grpcio>=1.60.0 +grpcio-tools>=1.60.0 +protobuf>=4.25.0 +pyyaml>=6.0 diff --git a/typescript/.gitignore b/typescript/.gitignore new file mode 100755 index 0000000..c893112 --- /dev/null +++ b/typescript/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Generated proto files +src/proto/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Test config (contains credentials) +test-config.yaml + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS +.DS_Store +Thumbs.db diff --git a/typescript/Makefile b/typescript/Makefile new file mode 100755 index 0000000..f5edf9e --- /dev/null +++ b/typescript/Makefile @@ -0,0 +1,38 @@ +.PHONY: proto build run clean setup test + +# Generate TypeScript types from proto files +proto: + npm run proto + +# Install dependencies +setup: + npm install + +# Build the TypeScript code +build: + npm run build + +# Run the smart contract (production) +run: build + npm start -- --config config.yaml + +# Run in development mode (with ts-node) +dev: + npm run dev -- --config config.yaml + +# Clean build artifacts +clean: + npm run clean + rm -rf node_modules + +# Run tests +test: + npm test + +# Lint code +lint: + npm run lint + +# Format code +format: + npm run format diff --git a/typescript/README.md b/typescript/README.md new file mode 100755 index 0000000..ffe4f6e --- /dev/null +++ b/typescript/README.md @@ -0,0 +1,220 @@ +# TypeScript Smart Contract Template + +A TypeScript/JavaScript-based smart contract client for Dragonchain Prime that connects via gRPC. + +## Prerequisites + +- Node.js 18 or later +- npm + +## Quick Start + +1. **Copy this template** to create your smart contract: + ```bash + cp -r typescript /path/to/my-smart-contract + cd /path/to/my-smart-contract + ``` + +2. **Install dependencies**: + ```bash + npm install + ``` + +3. **Configure your connection** by editing `config.yaml`: + ```yaml + server_address: "your-dragonchain-server:50051" + smart_contract_id: "your-smart-contract-id" + api_key: "your-api-key" + ``` + +4. **Implement your smart contract logic** in `src/process.ts` by modifying the `processTransaction` function. + +5. **Build and run**: + ```bash + npm run build + npm start -- --config config.yaml + ``` + + Or run in development mode: + ```bash + npm run dev -- --config config.yaml + ``` + +## Configuration + +| Field | Description | Default | +|-------|-------------|---------| +| `server_address` | gRPC server address | Required | +| `smart_contract_id` | Your smart contract ID | Required | +| `api_key` | API key for authentication | Required | +| `use_tls` | Enable TLS encryption | `false` | +| `tls_cert_path` | Path to TLS certificate | - | +| `num_workers` | Concurrent transaction processors | `10` | +| `reconnect_delay_seconds` | Delay between reconnection attempts | `5` | +| `max_reconnect_attempts` | Max reconnect attempts (0 = infinite) | `0` | + +## Implementing Your Smart Contract + +Edit the `processTransaction` function in `src/process.ts`: + +```typescript +export async function processTransaction( + txJson: string, + envVars: Record, + secrets: Record +): Promise { + // Parse the transaction + const tx: Transaction = JSON.parse(txJson); + + // Access transaction data + const txnId = tx.header.txn_id; + const txnType = tx.header.txn_type; + const payload = tx.payload; + + // Access environment variables + const scName = envVars["SMART_CONTRACT_NAME"]; + const dcId = envVars["DRAGONCHAIN_ID"]; + + // Access secrets + const mySecret = secrets["SC_SECRET_MY_SECRET"]; + + // Implement your logic here + const result = { + status: "success", + data: "your result data", + }; + + return { + data: result, + outputToChain: true, // Set to true to persist result on chain + }; +} +``` + +### Transaction Structure + +The `Transaction` interface in `src/process.ts` matches the Dragonchain transaction format: + +```typescript +interface Transaction { + version: string; + header: TransactionHeader; + payload: Record; +} + +interface TransactionHeader { + tag: string; + dc_id: string; + txn_id: string; + block_id: string; + txn_type: string; + timestamp: string; + invoker: string; +} +``` + +### Available Environment Variables + +| Variable | Description | +|----------|-------------| +| `TZ` | Timezone | +| `ENVIRONMENT` | Deployment environment | +| `INTERNAL_ID` | Internal identifier | +| `DRAGONCHAIN_ID` | Dragonchain ID | +| `DRAGONCHAIN_ENDPOINT` | Dragonchain API endpoint | +| `SMART_CONTRACT_ID` | This smart contract's ID | +| `SMART_CONTRACT_NAME` | This smart contract's name | +| `SC_ENV_*` | Custom environment variables | + +### Secrets + +Secrets are provided in the `secrets` object with keys prefixed by `SC_SECRET_`. + +## Project Structure + +``` +. +├── src/ +│ ├── main.ts # Client infrastructure (do not modify) +│ └── process.ts # Your smart contract logic (modify this) +├── proto/ +│ └── remote_sc.proto # gRPC service definition +├── config.yaml # Configuration file +├── package.json # Node.js dependencies +├── tsconfig.json # TypeScript configuration +├── Makefile # Build commands +└── README.md # This file +``` + +### File Descriptions + +- **`src/process.ts`** - Contains the `processTransaction` function where you implement your smart contract logic. This is the only file you need to modify for most use cases. +- **`src/main.ts`** - Contains the gRPC client infrastructure, connection handling, and worker pool. You typically don't need to modify this file. + +## NPM Scripts + +```bash +npm install # Install dependencies +npm run build # Compile TypeScript to JavaScript +npm start # Run the compiled application +npm run dev # Run with ts-node (development) +npm run proto # Generate TypeScript types from proto +npm run clean # Remove build artifacts +npm run lint # Lint the code +npm run format # Format code with prettier +``` + +## Make Commands + +```bash +make setup # Install dependencies +make proto # Generate TypeScript types from proto +make build # Build TypeScript +make run # Build and run +make dev # Run in development mode +make clean # Remove build artifacts +make lint # Lint code +make format # Format code +``` + +## Concurrent Processing + +The client uses async/await with a concurrency limit to process multiple transactions simultaneously. The number of concurrent workers is configurable via `num_workers` in the config file. + +## Error Handling + +- Return errors from the `processTransaction` function via `ProcessResult.error` to report failures +- The client automatically handles reconnection on connection failures +- Logs are captured and sent back with the response + +## Docker + +Example `Dockerfile`: + +```dockerfile +FROM node:20-slim + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY . . +RUN npm run build + +CMD ["node", "dist/main.js", "--config", "config.yaml"] +``` + +## Using with JavaScript + +If you prefer plain JavaScript instead of TypeScript: + +1. Write your code in `src/process.js` instead of `src/process.ts` +2. Skip the build step and run directly: + ```bash + node src/main.js --config config.yaml + ``` + +## License + +[Your License Here] diff --git a/typescript/config.yaml b/typescript/config.yaml new file mode 100755 index 0000000..bd4b18c --- /dev/null +++ b/typescript/config.yaml @@ -0,0 +1,24 @@ +# Smart Contract Client Configuration +# Copy this file and fill in your values + +# The gRPC server address to connect to +server_address: "localhost:50051" + +# Your smart contract ID (provided by Dragonchain) +smart_contract_id: "your-smart-contract-id" + +# API key for authentication (provided by Dragonchain) +api_key: "your-api-key" + +# Whether to use TLS for the connection +use_tls: false + +# Path to TLS certificate (required if use_tls is true) +# tls_cert_path: "/path/to/cert.pem" + +# Number of concurrent workers for processing transactions +num_workers: 10 + +# Reconnect settings +reconnect_delay_seconds: 5 +max_reconnect_attempts: 0 # 0 = infinite retries diff --git a/typescript/package-lock.json b/typescript/package-lock.json new file mode 100755 index 0000000..9143102 --- /dev/null +++ b/typescript/package-lock.json @@ -0,0 +1,603 @@ +{ + "name": "smart-contract", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smart-contract", + "version": "1.0.0", + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "@grpc/proto-loader": "^0.7.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/typescript/package.json b/typescript/package.json new file mode 100755 index 0000000..116cade --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,29 @@ +{ + "name": "smart-contract", + "version": "1.0.0", + "description": "Dragonchain Smart Contract Client", + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "ts-node src/main.ts", + "proto": "proto-loader-gen-types --grpcLib=@grpc/grpc-js --outDir=src/proto proto/remote_sc.proto", + "clean": "rm -rf dist", + "lint": "eslint src/**/*.ts", + "format": "prettier --write src/**/*.ts" + }, + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "@grpc/proto-loader": "^0.7.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "ts-node": "^10.9.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/typescript/proto/remote_sc.proto b/typescript/proto/remote_sc.proto new file mode 100755 index 0000000..559a462 --- /dev/null +++ b/typescript/proto/remote_sc.proto @@ -0,0 +1,48 @@ +syntax = "proto3"; + +package remote_sc; + +// SmartContractService defines the bi-directional streaming service for remote +// smart contract execution. External workers connect to this service to receive +// execution tasks and send back results. +service SmartContractService { + // Run establishes a bi-directional stream. The server sends SmartContractRequest + // messages to the client for execution, and the client sends back + // SmartContractResponse messages with results. + rpc Run(stream SmartContractResponse) returns (stream SmartContractRequest); +} + +// SmartContractRequest is sent from the server to the connected worker +// to request execution of a smart contract. +message SmartContractRequest { + // Unique identifier for this execution request, used to correlate responses + string transaction_id = 1; + + // Full transaction JSON to be processed by the smart contract + string transaction_json = 2; + + // Environment variables to set for the smart contract execution + map env_vars = 3; + + // Secrets to be made available to the smart contract + map secrets = 4; +} + +// SmartContractResponse is sent from the worker back to the server +// with the results of smart contract execution. +message SmartContractResponse { + // The transaction_id from the original request, for correlation + string transaction_id = 1; + + // The result data from the smart contract execution as JSON + string result_json = 2; + + // Logs captured during smart contract execution + string logs = 3; + + // Whether to persist the output to the chain + bool output_to_chain = 4; + + // Error message if execution failed + string error = 5; +} diff --git a/typescript/src/main.ts b/typescript/src/main.ts new file mode 100755 index 0000000..90eda5a --- /dev/null +++ b/typescript/src/main.ts @@ -0,0 +1,369 @@ +/** + * 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.ts instead. + */ + +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import * as fs from "fs"; +import * as path from "path"; +import * as yaml from "js-yaml"; + +import { ProcessResult, processTransaction } from "./process"; + +// Load proto definition +const PROTO_PATH = path.join(__dirname, "../proto/remote_sc.proto"); +const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}); + +const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any; +const SmartContractService = protoDescriptor.remote_sc.SmartContractService; + +// ============================================================================= +// Configuration and Client Infrastructure +// Do not modify this file unless you need to customize the client behavior. +// Implement your smart contract logic in process.ts instead. +// ============================================================================= + +interface Config { + serverAddress: string; + smartContractId: string; + apiKey: string; + useTls: boolean; + tlsCertPath?: string; + numWorkers: number; + reconnectDelaySeconds: number; + maxReconnectAttempts: number; +} + +interface SmartContractRequest { + transactionId: string; + transactionJson: string; + envVars: Record; + secrets: Record; +} + +interface SmartContractResponse { + transactionId: string; + resultJson: string; + logs: string; + outputToChain: boolean; + error: string; +} + +class SmartContractClient { + private config: Config; + private client: any; + private running: boolean = false; + private workQueue: SmartContractRequest[] = []; + private processing: Set = new Set(); + private stream: any; + + constructor(config: Config) { + this.config = config; + } + + /** + * Connect to the gRPC server. + */ + connect(): boolean { + try { + let credentials: grpc.ChannelCredentials; + + if (this.config.useTls) { + if (!this.config.tlsCertPath) { + console.error("[SC-Client] TLS enabled but no certificate path provided"); + return false; + } + const rootCert = fs.readFileSync(this.config.tlsCertPath); + credentials = grpc.credentials.createSsl(rootCert); + } else { + credentials = grpc.credentials.createInsecure(); + } + + this.client = new SmartContractService( + this.config.serverAddress, + credentials + ); + + console.log(`[SC-Client] Connected to server at ${this.config.serverAddress}`); + return true; + } catch (e) { + console.error(`[SC-Client] Failed to connect: ${e}`); + return false; + } + } + + /** + * Close the gRPC connection. + */ + close(): void { + if (this.stream) { + this.stream.end(); + this.stream = null; + } + if (this.client) { + grpc.closeClient(this.client); + this.client = null; + } + } + + /** + * Process a single request. + */ + private async processRequest(request: SmartContractRequest): Promise { + const logs = ""; + + try { + const result = await processTransaction( + request.transactionJson, + request.envVars, + request.secrets + ); + + const response: SmartContractResponse = { + transactionId: request.transactionId, + resultJson: result.data ? JSON.stringify(result.data) : "{}", + logs, + outputToChain: result.outputToChain, + error: result.error || "", + }; + + if (result.error) { + console.error( + `[SC-Client] Error processing transaction ${request.transactionId}: ${result.error}` + ); + } else { + console.log( + `[SC-Client] Successfully processed transaction ${request.transactionId}` + ); + } + + return response; + } catch (e) { + console.error( + `[SC-Client] Exception processing transaction ${request.transactionId}: ${e}` + ); + return { + transactionId: request.transactionId, + resultJson: "", + logs, + outputToChain: false, + error: String(e), + }; + } + } + + /** + * Run the client and process incoming requests. + */ + async run(): Promise { + if (!this.client) { + console.error("[SC-Client] Not connected to server"); + return false; + } + + this.running = true; + + // Create metadata for authentication + const metadata = new grpc.Metadata(); + metadata.add("x-api-key", this.config.apiKey); + metadata.add("x-smart-contract-id", this.config.smartContractId); + + return new Promise((resolve) => { + // Establish bi-directional stream + this.stream = this.client.Run(metadata); + + console.log( + `[SC-Client] Stream established, ready to process requests (workers: ${this.config.numWorkers})` + ); + + // Handle incoming requests + this.stream.on("data", async (request: SmartContractRequest) => { + if (!this.running) return; + + console.log( + `[SC-Client] Received request: transaction_id=${request.transactionId}` + ); + + // Process with concurrency limit + if (this.processing.size >= this.config.numWorkers) { + this.workQueue.push(request); + } else { + this.startProcessing(request); + } + }); + + this.stream.on("end", () => { + console.log("[SC-Client] Server closed the stream"); + this.running = false; + resolve(true); + }); + + this.stream.on("error", (err: grpc.ServiceError) => { + console.error(`[SC-Client] Stream error: ${err.code} - ${err.message}`); + this.running = false; + resolve(false); + }); + }); + } + + /** + * Start processing a request with concurrency tracking. + */ + private async startProcessing(request: SmartContractRequest): Promise { + this.processing.add(request.transactionId); + + try { + const response = await this.processRequest(request); + if (this.stream && this.running) { + this.stream.write(response); + } + } finally { + this.processing.delete(request.transactionId); + + // Process next queued request if any + if (this.workQueue.length > 0 && this.running) { + const next = this.workQueue.shift()!; + this.startProcessing(next); + } + } + } + + /** + * Stop the client gracefully. + */ + stop(): void { + console.log("[SC-Client] Stopping client..."); + this.running = false; + } +} + +// ============================================================================= +// Configuration Loading +// ============================================================================= + +interface RawConfig { + server_address: string; + smart_contract_id: string; + api_key: string; + use_tls?: boolean; + tls_cert_path?: string; + num_workers?: number; + reconnect_delay_seconds?: number; + max_reconnect_attempts?: number; +} + +function loadConfig(configPath: string): Config { + const content = fs.readFileSync(configPath, "utf8"); + const raw = yaml.load(content) as RawConfig; + + // Validate required fields + const required = ["server_address", "smart_contract_id", "api_key"]; + for (const field of required) { + if (!(field in raw) || !raw[field as keyof RawConfig]) { + throw new Error(`Missing required config field: ${field}`); + } + } + + return { + serverAddress: raw.server_address, + smartContractId: raw.smart_contract_id, + apiKey: raw.api_key, + useTls: raw.use_tls ?? false, + tlsCertPath: raw.tls_cert_path, + numWorkers: raw.num_workers ?? 10, + reconnectDelaySeconds: raw.reconnect_delay_seconds ?? 5, + maxReconnectAttempts: raw.max_reconnect_attempts ?? 0, + }; +} + +// ============================================================================= +// Main Entry Point +// ============================================================================= + +async function main(): Promise { + // Parse command line arguments + const args = process.argv.slice(2); + let configPath = "config.yaml"; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--config" || args[i] === "-c") { + configPath = args[i + 1]; + i++; + } + } + + // Load configuration + let config: Config; + try { + config = loadConfig(configPath); + } catch (e) { + console.error(`[SC-Client] Failed to load config: ${e}`); + process.exit(1); + } + + // Create client + const client = new SmartContractClient(config); + + // Setup signal handling for graceful shutdown + const shutdown = () => { + console.log("[SC-Client] Received shutdown signal..."); + client.stop(); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + // Connection loop with reconnection logic + let attempts = 0; + + while (true) { + if (client.connect()) { + attempts = 0; + const success = await client.run(); + if (!success) { + // Check if it was a graceful shutdown + client.close(); + break; + } + } + + client.close(); + + attempts++; + if ( + config.maxReconnectAttempts > 0 && + attempts >= config.maxReconnectAttempts + ) { + console.error( + `[SC-Client] Max reconnection attempts (${config.maxReconnectAttempts}) reached` + ); + break; + } + + const delay = config.reconnectDelaySeconds; + console.log( + `[SC-Client] Reconnecting in ${delay} seconds (attempt ${attempts})...` + ); + + await new Promise((resolve) => setTimeout(resolve, delay * 1000)); + } + + console.log("[SC-Client] Client shut down"); +} + +main().catch((e) => { + console.error(`[SC-Client] Fatal error: ${e}`); + process.exit(1); +}); diff --git a/typescript/src/process.ts b/typescript/src/process.ts new file mode 100755 index 0000000..e2b980e --- /dev/null +++ b/typescript/src/process.ts @@ -0,0 +1,112 @@ +/** + * Smart Contract Processing Logic + * + * This file contains the transaction processing logic for your smart contract. + * Modify the `processTransaction` function to implement your business logic. + */ + +// ============================================================================= +// SMART CONTRACT IMPLEMENTATION - MODIFY THIS FILE +// ============================================================================= + +/** + * Transaction header metadata from Dragonchain. + */ +export interface TransactionHeader { + tag: string; + dc_id: string; + txn_id: string; + block_id: string; + txn_type: string; + timestamp: string; + invoker: string; +} + +/** + * Parsed transaction from the server. + * Customize this interface to match your transaction payload structure. + */ +export interface Transaction { + version: string; + header: TransactionHeader; + payload: Record; +} + +/** + * Result from the processTransaction function. + */ +export interface ProcessResult { + data?: Record; + outputToChain: boolean; + error?: string; +} + +/** + * Process an incoming transaction. + * + * Implement your smart contract logic here. + * + * @param txJson - Raw transaction JSON string + * @param envVars - Environment variables passed from the server + * @param secrets - Secrets passed from the server (e.g., API keys, credentials) + * @returns ProcessResult containing the result data, whether to output to chain, and any error + */ +export async function processTransaction( + txJson: string, + envVars: Record, + secrets: Record +): Promise { + // Parse the transaction JSON + let tx: Transaction; + try { + tx = JSON.parse(txJson); + } catch (e) { + return { + outputToChain: false, + error: `Failed to parse transaction: ${e}`, + }; + } + + // ========================================================================== + // TODO: Implement your smart contract logic here + // ========================================================================== + // + // Example: Access transaction data + // const txnId = tx.header.txn_id; + // const txnType = tx.header.txn_type; + // const payload = tx.payload; + // + // Example: Access environment variables + // const scName = envVars["SMART_CONTRACT_NAME"]; + // const dcId = envVars["DRAGONCHAIN_ID"]; + // + // Example: Access secrets + // const apiKey = secrets["SC_SECRET_MY_API_KEY"]; + // + // Example: Process based on payload action + // const action = tx.payload.action as string; + // switch (action) { + // case "create": + // // Handle create operation + // break; + // case "update": + // // Handle update operation + // break; + // default: + // return { outputToChain: false, error: `Unknown action: ${action}` }; + // } + + // Default implementation: echo back the transaction + const result = { + status: "processed", + transaction_id: tx.header.txn_id, + txn_type: tx.header.txn_type, + payload: tx.payload, + message: "Transaction processed successfully", + }; + + return { + data: result, + outputToChain: true, + }; +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100755 index 0000000..1951778 --- /dev/null +++ b/typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}