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