Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions backend/src/routes/deploy.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Router } from 'express';
import { deployContract } from '../services/deployService.js';
import logger from '../utils/logger.js';

const router = Router();

router.post('/deploy', async (req, res) => {
try {
const { wasmPath, network, sourceKey, rpcUrl } = req.body;

if (!wasmPath || typeof wasmPath !== 'string') {
return res.status(400).json({
status: 'error',
message: 'wasmPath is required and must be a string',
});
}

const result = await deployContract({ wasmPath, network, sourceKey, rpcUrl });

if (!result.success) {
return res.status(500).json({
status: 'error',
message: 'Contract deployment failed',
error: result.error,
network: result.network,
durationMs: result.durationMs,
});
}

res.status(201).json({
status: 'success',
contractId: result.contractId,
network: result.network,
durationMs: result.durationMs,
});
} catch (error) {
logger.error('Unexpected error during contract deployment', error);
res.status(500).json({ status: 'error', message: 'Unable to deploy contract' });
}
});

export default router;
2 changes: 2 additions & 0 deletions backend/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import dependenciesRouter from './dependencies.routes.js';
import infrastructureRouter from '../infrastructure/infrastructure.routes.js';
import simulatorRouter from '../simulator/simulator.routes.js';

import deployRouter from './deploy.routes.js';
import webhooksRouter from './webhooks.js';
import adminDLQRouter from './admin/dlq.routes.js';

Expand Down Expand Up @@ -67,6 +68,7 @@ router.use('/osct', osctRouter);
router.use('/simulator', simulatorRouter);
router.use('/playground', playgroundRouter);
router.use('/export', exportRouter);
router.use('/deploy', deployRouter);
router.use('/webhooks', webhooksRouter);
router.use('/admin/dlq', adminDLQRouter);
router.use('/user', userRouter);
Expand Down
110 changes: 110 additions & 0 deletions backend/src/services/deployService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { execFile } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs';
import logger from '../utils/logger.js';

const execFileAsync = promisify(execFile);

export interface DeployRequest {
wasmPath: string;
network?: string;
sourceKey?: string;
rpcUrl?: string;
}

export interface DeployResult {
success: boolean;
contractId?: string;
network: string;
durationMs: number;
error?: string;
}

function resolveNetwork(provided?: string): string {
return provided || process.env.SOROBAN_NETWORK || 'testnet';
}

function resolveRpcUrl(provided?: string): string | undefined {
if (provided) return provided;
const network = resolveNetwork();
const envUrl = process.env.SOROBAN_RPC_URL;
if (envUrl) return envUrl;
const defaults: Record<string, string> = {
local: 'http://localhost:8000/soroban/rpc',
testnet: 'https://soroban-testnet.stellar.org',
mainnet: 'https://soroban-mainnet.stellar.org',
};
return defaults[network];
}

export async function deployContract(request: DeployRequest): Promise<DeployResult> {
const start = process.hrtime.bigint();
const network = resolveNetwork(request.network);

try {
const wasmPath = path.resolve(request.wasmPath);
if (!fs.existsSync(wasmPath)) {
return {
success: false,
network,
durationMs: 0,
error: `WASM file not found: ${wasmPath}`,
};
}

const sourceKey = request.sourceKey || process.env.SOROBAN_SOURCE_KEY;
if (!sourceKey) {
return {
success: false,
network,
durationMs: 0,
error: 'SOROBAN_SOURCE_KEY is required. Set it in environment or pass sourceKey.',
};
}

const args = ['contract', 'deploy', '--wasm', wasmPath, '--source', sourceKey, '--network', network];

const rpcUrl = resolveRpcUrl(request.rpcUrl);
if (rpcUrl) {
args.push('--rpc-url', rpcUrl);
}

logger.info('Invoking soroban contract deploy', { network, wasmPath });

const { stdout, stderr } = await execFileAsync('soroban', args, {
timeout: 120_000,
maxBuffer: 1024 * 1024,
});

const contractId = stdout?.toString().trim();
if (!contractId) {
throw new Error(`Empty response from soroban CLI.\nstderr: ${stderr}`);
}

const end = process.hrtime.bigint();
const durationMs = Number(end - start) / 1_000_000;

logger.info('Contract deployed successfully', { contractId, network, durationMs });

return {
success: true,
contractId,
network,
durationMs,
};
} catch (err: any) {
const end = process.hrtime.bigint();
const durationMs = Number(end - start) / 1_000_000;

const message = err.stderr?.toString().trim() || err.message || 'Unknown error';
logger.error('Contract deployment failed', { error: message, network, durationMs });

return {
success: false,
network,
durationMs,
error: message,
};
}
}