/** * Tezos relay: listen TezosBridgeInitiated -> (mock) Tezos mint/transfer -> confirmTransaction. */ import { ethers } from 'ethers'; import { config } from './config.js'; import { TezosAdapterABI } from './abis.js'; import * as metrics from './metrics.js'; export class TezosRelayService { constructor(logger) { this.logger = logger; this.provider = null; this.adapter = null; this.signer = null; this.queue = []; this.inFlight = 0; } async start() { if (!config.tezosAdapterAddress) { throw new Error('TEZOS_ADAPTER_ADDRESS required'); } if (!config.oraclePrivateKey) { throw new Error('TEZOS_RELAY_ORACLE_KEY or PRIVATE_KEY required'); } this.provider = new ethers.JsonRpcProvider(config.sourceChain.rpcUrl); this.signer = new ethers.Wallet(config.oraclePrivateKey, this.provider); this.adapter = new ethers.Contract(config.tezosAdapterAddress, TezosAdapterABI, this.signer); this.logger.info('Tezos relay started', { adapter: config.tezosAdapterAddress }); this.adapter.on('TezosBridgeInitiated', (requestId, sender, token, amount, destination) => { this.queue.push({ requestId, sender, token, amount, destination }); metrics.incrementEventsDetected(); metrics.setPendingRequests(this.queue.length + this.inFlight); this.processQueue(); }); } async processQueue() { while (this.queue.length > 0 && this.inFlight < config.maxConcurrent) { const item = this.queue.shift(); this.inFlight++; metrics.setPendingRequests(this.queue.length + this.inFlight); this.handleRequest(item).finally(() => { this.inFlight--; metrics.setPendingRequests(this.queue.length + this.inFlight); }); } } async handleRequest({ requestId, sender, token, amount, destination }) { try { let tezosTxHash; if (config.mockTezosRelay) { tezosTxHash = `mock_${requestId.slice(0, 10)}_${Date.now()}`; this.logger.info('Mock Tezos tx', { requestId, destination, tezosTxHash }); } else { tezosTxHash = await this.performTezosMintOrTransfer(requestId, sender, token, amount, destination); } await this.adapter.confirmTransaction(requestId, tezosTxHash); metrics.incrementConfirmationsSubmitted(); } catch (err) { this.logger.error('confirmTransaction failed', { requestId, error: err.message }); metrics.incrementConfirmationsFailed(); } } /** * Perform actual Tezos mint/transfer via Taquito or Tezos RPC. * Real mint: set MOCK_TEZOS_RELAY=false, TEZOS_MINTER_ADDRESS, TEZOS_RPC_URL, TEZOS_ORACLE_SECRET_KEY (edsk...). * When @taquito/taquito is installed and config is set, calls minter contract mintOrTransfer(destination, amount); otherwise returns mock hash. */ async performTezosMintOrTransfer(requestId, sender, token, amount, destination) { const { config } = await import('./config.js'); if (!config.tezosRpcUrl) { return `mock_${requestId.slice(0, 10)}_${Date.now()}`; } try { const { Tezos } = await import('@taquito/taquito'); const { InMemorySigner } = await import('@taquito/signer'); const tezosSecret = process.env.TEZOS_ORACLE_SECRET_KEY || config.oraclePrivateKey; if (config.tezosMinterAddress && tezosSecret) { Tezos.setProvider({ rpc: config.tezosRpcUrl }); Tezos.setSignerProvider(await InMemorySigner.fromSecretKey(tezosSecret)); const contract = await Tezos.wallet.at(config.tezosMinterAddress); const op = await contract.methods.mintOrTransfer(destination, amount.toString()).send(); await op.confirmation(1); return op.hash; } } catch (e) { this.logger.warn('Taquito mint/transfer failed, using mock hash', { error: e.message }); } return `mock_${requestId.slice(0, 10)}_${Date.now()}`; } }