/** * Chain adapter for Tezos mainnet (chainId 1729). * Uses TzKT API for read operations. sendTransaction requires Taquito/injection for production. */ import type { IChainAdapter, ChainAdapterConfig, NormalizedReceipt, NormalizedLog, SendTransactionResult, } from './types.js'; import { getChainConfig, TEZOS_CHAIN_ID } from './config.js'; const TZKt_BASE = 'https://api.tzkt.io'; export class TezosChainAdapter implements IChainAdapter { private config: ChainAdapterConfig; private baseUrl: string; constructor(rpcUrls?: string[]) { const cfg = getChainConfig(TEZOS_CHAIN_ID); if (!cfg) throw new Error('Tezos chain config not found'); this.config = rpcUrls?.length ? { ...cfg, rpcUrls } : cfg; this.baseUrl = this.config.rpcUrls[0].replace(/\/$/, ''); } getChainId(): number { return this.config.chainId; } getConfig(): ChainAdapterConfig { return this.config; } async getBlockNumber(): Promise { const res = await fetch(`${this.baseUrl}/v1/blocks/count`); if (!res.ok) throw new Error(`TzKT blocks/count failed: ${res.status}`); const count = await res.json(); return Number(count); } async getBlock(blockNumber: number): Promise<{ number: number; hash: string; parentHash: string; timestamp: number } | null> { const res = await fetch(`${this.baseUrl}/v1/blocks/${blockNumber}`); if (!res.ok) return null; const b = await res.json(); return { number: b.level, hash: b.hash ?? '', parentHash: b.previousHash ?? '', timestamp: new Date(b.timestamp).getTime() / 1000, }; } async sendTransaction(signedTxHex: string): Promise { const hex = signedTxHex.startsWith('0x') ? signedTxHex.slice(2) : signedTxHex; const bytes = Buffer.from(hex, 'hex'); const rpcUrl = process.env.TEZOS_RPC_INJECT_URL ?? 'https://mainnet.api.tez.ie'; const res = await fetch(`${rpcUrl}/injection/operation`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: bytes, signal: AbortSignal.timeout(15000), }); if (!res.ok) { const err = await res.text(); throw new Error(`Tezos injection failed: ${res.status} ${err}`); } const opHash = await res.text(); return { hash: opHash.trim(), from: '', nonce: 0 }; } async getTransactionReceipt(txHash: string): Promise { const res = await fetch(`${this.baseUrl}/v1/operations/transactions/${txHash}`); if (!res.ok) return null; const op = await res.json(); if (Array.isArray(op)) { const t = op[0]; if (!t) return null; return { chainId: this.config.chainId, transactionHash: t.hash, blockNumber: BigInt(t.level ?? 0), blockHash: t.block ?? '', transactionIndex: 0, from: t.sender?.address ?? '', to: t.target?.address ?? null, gasUsed: BigInt(t.gasUsed ?? 0), cumulativeGasUsed: BigInt(t.gasUsed ?? 0), contractAddress: t.target?.address ?? null, logsBloom: '', status: t.status === 'applied' ? 1 : 0, root: null, }; } return { chainId: this.config.chainId, transactionHash: op.hash, blockNumber: BigInt(op.level ?? 0), blockHash: op.block ?? '', transactionIndex: 0, from: op.sender?.address ?? '', to: op.target?.address ?? null, gasUsed: BigInt(op.gasUsed ?? 0), cumulativeGasUsed: BigInt(op.gasUsed ?? 0), contractAddress: op.target?.address ?? null, logsBloom: '', status: op.status === 'applied' ? 1 : 0, root: null, }; } async getLogs( _fromBlock: number, _toBlock: number, _address?: string, _topics?: string[] ): Promise { return []; } async detectReorg(blockNumber: number, expectedBlockHash: string): Promise { const block = await this.getBlock(blockNumber); if (!block) return true; return block.hash !== expectedBlockHash; } async healthCheck(): Promise { try { await fetch(`${this.baseUrl}/v1/blocks/head`); return true; } catch { return false; } } } export function createAdapterTezos(rpcUrls?: string[]): TezosChainAdapter { return new TezosChainAdapter(rpcUrls); }