Initial commit: smart contract templates for bash, go, python, and typescript
This commit is contained in:
57
.gitignore
vendored
Executable file
57
.gitignore
vendored
Executable file
@@ -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
|
||||||
197
README.md
Executable file
197
README.md
Executable file
@@ -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<string, string>, secrets: Record<string, string>): Promise<ProcessResult> {
|
||||||
|
// 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 <base-image> AS builder
|
||||||
|
COPY . .
|
||||||
|
RUN <build-commands>
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM <runtime-image>
|
||||||
|
COPY --from=builder <artifacts> .
|
||||||
|
CMD [<run-command>]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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]
|
||||||
27
bash/.gitignore
vendored
Normal file
27
bash/.gitignore
vendored
Normal file
@@ -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
|
||||||
53
bash/Makefile
Normal file
53
bash/Makefile
Normal file
@@ -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"
|
||||||
243
bash/README.md
Normal file
243
bash/README.md
Normal file
@@ -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]
|
||||||
24
bash/config.yaml
Normal file
24
bash/config.yaml
Normal file
@@ -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
|
||||||
89
bash/process.sh
Executable file
89
bash/process.sh
Executable file
@@ -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": ""
|
||||||
|
}'
|
||||||
48
bash/proto/remote_sc.proto
Normal file
48
bash/proto/remote_sc.proto
Normal file
@@ -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<string, string> env_vars = 3;
|
||||||
|
|
||||||
|
// Secrets to be made available to the smart contract
|
||||||
|
map<string, string> 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;
|
||||||
|
}
|
||||||
22
go/.gitignore
vendored
Executable file
22
go/.gitignore
vendored
Executable file
@@ -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
|
||||||
45
go/Makefile
Executable file
45
go/Makefile
Executable file
@@ -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 <module-name>' 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."
|
||||||
192
go/README.md
Executable file
192
go/README.md
Executable file
@@ -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]
|
||||||
24
go/config.yaml
Executable file
24
go/config.yaml
Executable file
@@ -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
|
||||||
18
go/go.mod
Executable file
18
go/go.mod
Executable file
@@ -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
|
||||||
|
)
|
||||||
40
go/go.sum
Executable file
40
go/go.sum
Executable file
@@ -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=
|
||||||
311
go/main.go
Executable file
311
go/main.go
Executable file
@@ -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")
|
||||||
|
}
|
||||||
100
go/process.go
Executable file
100
go/process.go
Executable file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
50
go/proto/remote_sc.proto
Executable file
50
go/proto/remote_sc.proto
Executable file
@@ -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<string, string> env_vars = 3;
|
||||||
|
|
||||||
|
// Secrets to be made available to the smart contract
|
||||||
|
map<string, string> 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;
|
||||||
|
}
|
||||||
40
python/Makefile
Executable file
40
python/Makefile
Executable file
@@ -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 .
|
||||||
205
python/README.md
Executable file
205
python/README.md
Executable file
@@ -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]
|
||||||
24
python/config.yaml
Executable file
24
python/config.yaml
Executable file
@@ -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
|
||||||
297
python/main.py
Executable file
297
python/main.py
Executable file
@@ -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()
|
||||||
6
python/package-lock.json
generated
Executable file
6
python/package-lock.json
generated
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "python",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
140
python/process.py
Executable file
140
python/process.py
Executable file
@@ -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,
|
||||||
|
)
|
||||||
48
python/proto/remote_sc.proto
Executable file
48
python/proto/remote_sc.proto
Executable file
@@ -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<string, string> env_vars = 3;
|
||||||
|
|
||||||
|
// Secrets to be made available to the smart contract
|
||||||
|
map<string, string> 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;
|
||||||
|
}
|
||||||
4
python/requirements.txt
Executable file
4
python/requirements.txt
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
grpcio>=1.60.0
|
||||||
|
grpcio-tools>=1.60.0
|
||||||
|
protobuf>=4.25.0
|
||||||
|
pyyaml>=6.0
|
||||||
28
typescript/.gitignore
vendored
Executable file
28
typescript/.gitignore
vendored
Executable file
@@ -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
|
||||||
38
typescript/Makefile
Executable file
38
typescript/Makefile
Executable file
@@ -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
|
||||||
220
typescript/README.md
Executable file
220
typescript/README.md
Executable file
@@ -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<string, string>,
|
||||||
|
secrets: Record<string, string>
|
||||||
|
): Promise<ProcessResult> {
|
||||||
|
// 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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
24
typescript/config.yaml
Executable file
24
typescript/config.yaml
Executable file
@@ -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
|
||||||
603
typescript/package-lock.json
generated
Executable file
603
typescript/package-lock.json
generated
Executable file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
typescript/package.json
Executable file
29
typescript/package.json
Executable file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
48
typescript/proto/remote_sc.proto
Executable file
48
typescript/proto/remote_sc.proto
Executable file
@@ -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<string, string> env_vars = 3;
|
||||||
|
|
||||||
|
// Secrets to be made available to the smart contract
|
||||||
|
map<string, string> 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;
|
||||||
|
}
|
||||||
369
typescript/src/main.ts
Executable file
369
typescript/src/main.ts
Executable file
@@ -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<string, string>;
|
||||||
|
secrets: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> = 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<SmartContractResponse> {
|
||||||
|
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<boolean> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
112
typescript/src/process.ts
Executable file
112
typescript/src/process.ts
Executable file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result from the processTransaction function.
|
||||||
|
*/
|
||||||
|
export interface ProcessResult {
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
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<string, string>,
|
||||||
|
secrets: Record<string, string>
|
||||||
|
): Promise<ProcessResult> {
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
19
typescript/tsconfig.json
Executable file
19
typescript/tsconfig.json
Executable file
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user