Some checks failed
Deploy to Phoenix / deploy (push) Has been cancelled
- ADD_CHAIN138_TO_LEDGER_LIVE: Ledger form done; public code review repo bis-innovations/LedgerLive; init/push commands - CONTRACT_DEPLOYMENT_RUNBOOK: Chain 138 gas price 1 gwei, 36-addr check, TransactionMirror workaround - CONTRACT_*: AddressMapper, MirrorManager deployed 2026-02-12; 36-address on-chain check - NEXT_STEPS_FOR_YOU: Ledger done; steps completable now (no LAN); run-completable-tasks-from-anywhere - MASTER_INDEX, OPERATOR_OPTIONAL, SMART_CONTRACTS_INVENTORY_SIMPLE: updates - LEDGER_BLOCKCHAIN_INTEGRATION_COMPLETE: bis-innovations/LedgerLive reference Co-authored-by: Cursor <cursoragent@cursor.com>
141 lines
4.2 KiB
TypeScript
141 lines
4.2 KiB
TypeScript
/**
|
|
* 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<number> {
|
|
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<SendTransactionResult> {
|
|
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<NormalizedReceipt | null> {
|
|
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<NormalizedLog[]> {
|
|
return [];
|
|
}
|
|
|
|
async detectReorg(blockNumber: number, expectedBlockHash: string): Promise<boolean> {
|
|
const block = await this.getBlock(blockNumber);
|
|
if (!block) return true;
|
|
return block.hash !== expectedBlockHash;
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
try {
|
|
await fetch(`${this.baseUrl}/v1/blocks/head`);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createAdapterTezos(rpcUrls?: string[]): TezosChainAdapter {
|
|
return new TezosChainAdapter(rpcUrls);
|
|
}
|