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);
|
||||
});
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user