Initial commit: smart contract templates for bash, go, python, and typescript
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user