Initial commit: smart contract templates for bash, go, python, and typescript

This commit is contained in:
2026-03-17 19:59:47 -04:00
commit 0634e66469
35 changed files with 3794 additions and 0 deletions

311
go/main.go Executable file
View 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")
}