import fs from 'fs'; import path from 'path'; import process from 'process'; import { execFileSync } from 'child_process'; import { fileURLToPath } from 'url'; import dotenv from 'dotenv'; import { ethers } from 'ethers'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const serviceRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(serviceRoot, '..', '..', '..'); const stateDir = path.join(serviceRoot, 'state'); const defaultStatePath = path.join(stateDir, 'relay-state.json'); const defaultHealthPath = path.join(repoRoot, 'reports', 'status', 'solana-relay-worker-health.json'); dotenv.config({ path: path.join(repoRoot, '.env') }); dotenv.config({ path: path.join(repoRoot, 'smom-dbis-138/.env') }); dotenv.config({ path: path.join(process.env.HOME || '', '.secure-secrets', 'private-keys.env'), override: true }); function readJson(filePath, fallback) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return fallback; } } function writeJson(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`); } function loadRuntime() { const runtimePath = process.env.SOLANA_RELAY_RUNTIME_CONFIG ? path.resolve(process.cwd(), process.env.SOLANA_RELAY_RUNTIME_CONFIG) : path.join(repoRoot, 'config', 'solana-relay-runtime.json'); return { path: runtimePath, config: JSON.parse(fs.readFileSync(runtimePath, 'utf8')) }; } function normalizeAddress(value) { return ethers.getAddress(String(value).toLowerCase()); } function rawToUiAmount(rawValue, decimals) { const raw = BigInt(rawValue); const base = 10n ** BigInt(decimals); const whole = raw / base; const fraction = raw % base; if (fraction === 0n) return whole.toString(); const fractionString = fraction.toString().padStart(decimals, '0').replace(/0+$/, ''); return `${whole.toString()}.${fractionString}`; } function decodeRecipient(destination, recipientBytes) { if (destination && destination.trim() !== '') return destination.trim(); if (!recipientBytes || recipientBytes === '0x') return null; return ethers.toUtf8String(recipientBytes).trim(); } function runLocalCommand(command, args) { return execFileSync(command, args, { cwd: repoRoot, encoding: 'utf8', env: process.env }).trim(); } function ensureAta(mint, wallet, keypairPath) { try { runLocalCommand('spl-token', ['create-account', mint, '--owner', wallet, '--fee-payer', keypairPath]); } catch { // No-op when the associated token account already exists. } } function mintToDestination(mint, uiAmount, wallet, keypairPath) { ensureAta(mint, wallet, keypairPath); for (let attempt = 0; attempt < 3; attempt += 1) { try { const output = runLocalCommand('spl-token', [ 'mint', mint, uiAmount, '--mint-authority', keypairPath, '--recipient-owner', wallet ]); const match = output.match(/Signature:\s+(\S+)/); if (match) return match[1]; } catch { // Retry with a fresh blockhash on transient Solana RPC errors. } } throw new Error(`Unable to parse Solana mint signature for ${mint}`); } function confirmSolanaSignature(signature) { try { return runLocalCommand('solana', ['confirm', signature, '--output', 'json']); } catch (error) { throw new Error(`Solana confirmation failed for ${signature}: ${error.message}`); } } function buildFulfillmentId(requestId, solanaSymbol, solanaMint) { return ethers.keccak256( ethers.solidityPacked(['bytes32', 'string', 'string'], [requestId, solanaSymbol, solanaMint]) ); } async function main() { const once = process.argv.includes('--once'); const runtimeBundle = loadRuntime(); const runtime = runtimeBundle.config; const rpcUrl = process.env.RPC_URL_138 || process.env.CHAIN138_RPC_URL || runtime.sourceChain.rpcEnv; const privateKey = process.env.PRIVATE_KEY; if (!privateKey || privateKey.includes('${')) { throw new Error('PRIVATE_KEY is required'); } const provider = new ethers.JsonRpcProvider(rpcUrl); const signer = new ethers.Wallet(privateKey, provider); const adapterAddress = normalizeAddress(runtime.sourceChain.adapterAddress); const adapterAbi = [ 'function confirmTransaction(bytes32 requestId,string txSignature,uint256 finalizedSlot,bytes32 fulfillmentId)', 'function getBridgeStatus(bytes32 requestId) view returns (tuple(address sender,address token,uint256 amount,bytes destinationData,bytes32 requestId,uint8 status,uint256 createdAt,uint256 completedAt))', 'event SolanaBridgeInitiated(bytes32 indexed requestId,address indexed sender,address indexed token,uint256 amount,string destination,bytes recipient)' ]; const adapter = new ethers.Contract(adapterAddress, adapterAbi, signer); const interfaceDecoder = adapter.interface; const assetBySourceToken = new Map( (runtime.assets || []).map((asset) => [normalizeAddress(asset.sourceToken), asset]) ); const state = readJson(defaultStatePath, { lastBlock: 0, processed: {} }); const latestBlock = await provider.getBlockNumber(); const fromBlock = Math.max(0, Number(state.lastBlock || 0) + 1); const health = { generatedAt: new Date().toISOString(), runtimeConfig: runtimeBundle.path, adapterAddress, signer: signer.address, fromBlock, latestBlock, processedThisRun: [], mode: once ? 'once' : 'poll' }; if (fromBlock > latestBlock) { state.lastBlock = latestBlock; writeJson(defaultStatePath, state); writeJson(defaultHealthPath, health); return; } const logs = await provider.getLogs({ address: adapterAddress, fromBlock, toBlock: latestBlock, topics: [ethers.id('SolanaBridgeInitiated(bytes32,address,address,uint256,string,bytes)')] }); for (const log of logs) { const parsed = interfaceDecoder.parseLog(log); const requestId = parsed.args.requestId; if (state.processed[requestId]) continue; const request = await adapter.getBridgeStatus(requestId); if (Number(request.status) !== 1) continue; const sourceToken = normalizeAddress(parsed.args.token); const asset = assetBySourceToken.get(sourceToken); if (!asset) { throw new Error(`No Solana runtime asset for source token ${sourceToken}`); } const recipientWallet = decodeRecipient(parsed.args.destination, parsed.args.recipient); if (!recipientWallet) { throw new Error(`Missing Solana recipient for request ${requestId}`); } const uiAmount = rawToUiAmount(request.amount.toString(), asset.decimals); const solanaSignature = mintToDestination( asset.solanaMint, uiAmount, recipientWallet, runtime.solana.keypairPath ); confirmSolanaSignature(solanaSignature); const fulfillmentId = buildFulfillmentId(requestId, asset.solanaSymbol, asset.solanaMint); const confirmTx = await adapter.confirmTransaction( requestId, solanaSignature, BigInt(runtime.solana.confirmationFinality), fulfillmentId, { type: 0, gasPrice: BigInt(process.env.CHAIN138_TX_GAS_PRICE || '200000') } ); const confirmReceipt = await confirmTx.wait(); state.processed[requestId] = { blockNumber: log.blockNumber, sourceToken, solanaSymbol: asset.solanaSymbol, solanaMint: asset.solanaMint, recipientWallet, requestAmountRaw: request.amount.toString(), solanaSignature, confirmTxHash: confirmReceipt.hash, processedAt: new Date().toISOString() }; health.processedThisRun.push({ requestId, solanaSymbol: asset.solanaSymbol, solanaSignature, confirmTxHash: confirmReceipt.hash }); } state.lastBlock = latestBlock; writeJson(defaultStatePath, state); writeJson(defaultHealthPath, health); } main().catch((error) => { writeJson(defaultHealthPath, { generatedAt: new Date().toISOString(), status: 'error', error: error.message }); console.error(error); process.exit(1); });