"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const ethers_1 = require("ethers"); const config_1 = require("./config"); const tokenEnrich_1 = require("./tokenEnrich"); const checkpoint_core_1 = require("@dbis/checkpoint-core"); const merkle_1 = require("./merkle"); const serialize_1 = require("./serialize"); const addressIndex_1 = require("./addressIndex"); const etherscanV2Shim_1 = require("./etherscanV2Shim"); const chain138Explorer_1 = require("./chain138Explorer"); const CHECKPOINT_ABI = [ 'function getLatestBatchId() view returns (uint64)', 'function latestCheckpointBlock() view returns (uint256)', 'function getLatestCheckpoint() view returns (tuple(uint64,uint64,uint64,uint256,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint16,uint32,uint64,address,bytes32))', 'function getCheckpoint(uint64) view returns (tuple(uint64,uint64,uint64,uint256,uint256,uint256,bytes32,bytes32,bytes32,bytes32,uint16,uint32,uint64,address,bytes32))', 'function isTxIncluded(bytes32) view returns (bool,uint64)', ]; const MIRROR_ABI = ['function getMirroredTransactionCount() view returns (uint256)']; const app = (0, express_1.default)(); const mainnet = new ethers_1.ethers.JsonRpcProvider(config_1.checkpointIndexerConfig.mainnetRpc); const chain138 = new ethers_1.ethers.JsonRpcProvider(config_1.checkpointIndexerConfig.chain138Rpc); const hub = config_1.checkpointIndexerConfig.checkpointProxy ? new ethers_1.ethers.Contract(config_1.checkpointIndexerConfig.checkpointProxy, CHECKPOINT_ABI, mainnet) : null; const mirror = new ethers_1.ethers.Contract(config_1.checkpointIndexerConfig.legacyMirror, MIRROR_ABI, mainnet); let addressIndexCache = null; let addressIndexBuiltAt = 0; const ADDRESS_INDEX_TTL_MS = 60_000; function getAddressIndex() { const now = Date.now(); if (!addressIndexCache || now - addressIndexBuiltAt > ADDRESS_INDEX_TTL_MS) { addressIndexCache = (0, addressIndex_1.buildAddressIndex)(config_1.checkpointIndexerConfig.batchPayloadDir); addressIndexBuiltAt = now; } return addressIndexCache; } function loadBatchPayload(batchId) { const p = path.join(config_1.checkpointIndexerConfig.batchPayloadDir, `batch-${batchId}.json`); if (!fs.existsSync(p)) return null; return JSON.parse(fs.readFileSync(p, 'utf8')); } async function payloadWithEnrichedLeaves(batchId, raw) { if (!raw?.leaves?.length) return raw; const chainId = raw.chainId ?? 138; let leaves = raw.leaves; if (config_1.checkpointIndexerConfig.enrichTokenTransfers || config_1.checkpointIndexerConfig.usdEnrichEnabled) { leaves = await (0, tokenEnrich_1.enrichLeaves)(leaves, config_1.checkpointIndexerConfig.blockscoutApi); } const hashes = leaves.map((l) => (0, merkle_1.paymentLeafHash)(chainId, { txHash: String(l.txHash), from: String(l.from), to: String(l.to), value: String(l.value ?? '0'), nativeValueWei: l.nativeValueWei != null ? String(l.nativeValueWei) : undefined, onChainValueWei: l.onChainValueWei != null ? String(l.onChainValueWei) : undefined, tokenValue: l.tokenValue != null ? String(l.tokenValue) : undefined, blockNumber: Number(l.blockNumber), blockTimestamp: Number(l.blockTimestamp), gasUsed: String(l.gasUsed ?? '0'), success: Boolean(l.success), })); const { root, proofs } = (0, merkle_1.merkleProofs)(hashes); const leavesWithProofs = leaves.map((l, i) => ({ ...l, leafHash: hashes[i], merkleProof: proofs[i], })); return { ...raw, batchId: raw.batchId ?? batchId, leaves: leavesWithProofs, merkleRootComputed: root, merkleRootMatches: raw.paymentsRoot ? root.toLowerCase() === String(raw.paymentsRoot).toLowerCase() : null, walletAttestationCopy: 'Balances and payments as of Chain 138 checkpoint block in this batch, attested on Ethereum mainnet.', }; } app.get('/health', (_req, res) => { res.json({ ok: true, proxy: config_1.checkpointIndexerConfig.checkpointProxy || null, batchDir: config_1.checkpointIndexerConfig.batchPayloadDir }); }); /** Etherscan V2 supplement for Chain 138 (unsupported on official chainlist). */ app.get('/v2/chainlist-supplement', etherscanV2Shim_1.chainlistSupplement); app.get('/v2/api', (req, res) => { void (0, etherscanV2Shim_1.etherscanV2Api)(req, res); }); /** Public batch JSON for contentURI resolution (CHECKPOINT_PAYLOAD_PUBLIC_URL). */ app.get('/v1/payload/batch-:batchId.json', (req, res) => { const raw = loadBatchPayload(req.params.batchId); if (!raw) return res.status(404).json({ error: 'batch not found' }); res.type('json').send(JSON.stringify(raw, null, 2)); }); app.get('/v1/checkpoint/latest', async (_req, res) => { if (!hub) return res.status(503).json({ error: 'CHAIN138_MAINNET_CHECKPOINT_PROXY not set' }); try { const batchId = await hub.getLatestBatchId(); const header = await hub.getLatestCheckpoint(); let v1Count = 0n; try { v1Count = await mirror.getMirroredTransactionCount(); } catch { v1Count = 0n; } const raw = batchId > 0n ? loadBatchPayload(batchId.toString()) : null; const payload = raw ? await payloadWithEnrichedLeaves(batchId.toString(), raw) : null; res.json({ source: 'v2', batchId: batchId.toString(), header: (0, serialize_1.jsonSafe)(header), legacy: { mirrorCount: v1Count.toString(), tether: config_1.checkpointIndexerConfig.legacyTether }, payload, }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/checkpoint/:batchId', async (req, res) => { if (!hub) return res.status(503).json({ error: 'proxy not set' }); try { const batchId = req.params.batchId; const header = await hub.getCheckpoint(BigInt(batchId)); const raw = loadBatchPayload(batchId); const payload = raw ? await payloadWithEnrichedLeaves(batchId, raw) : null; res.json({ batchId, header: (0, serialize_1.jsonSafe)(header), payload }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/tx/:txHash/attestation', async (req, res) => { if (!hub) return res.status(503).json({ error: 'proxy not set' }); try { const txHash = req.params.txHash; const [included, batchId] = await hub.isTxIncluded(txHash); const raw = included && batchId > 0n ? loadBatchPayload(batchId.toString()) : null; const payload = raw ? await payloadWithEnrichedLeaves(batchId.toString(), raw) : null; res.json({ txHash, included, batchId: batchId.toString(), payload }); } catch (e) { res.status(500).json({ error: String(e) }); } }); /** Wallet helper: verify leaf against on-chain hub (client may also use @dbis/checkpoint-sdk). */ app.get('/v1/checkpoint/:batchId/verify/:leafIndex', async (req, res) => { if (!hub) return res.status(503).json({ error: 'proxy not set' }); try { const batchId = req.params.batchId; const leafIndex = parseInt(req.params.leafIndex, 10); const raw = loadBatchPayload(batchId); if (!raw?.leaves?.length || leafIndex < 0 || leafIndex >= raw.leaves.length) { return res.status(404).json({ error: 'leaf not found' }); } const payload = await payloadWithEnrichedLeaves(batchId, raw); const leaves = payload.leaves; const leaf = leaves?.[leafIndex]; if (!leaf || !leaf.merkleProof) { return res.status(404).json({ error: 'proof missing' }); } const VERIFY_ABI = [ 'function verifyPaymentInBatch(uint64 batchId, tuple(bytes32,address,address,uint256,uint256,uint64,uint256,bool) leaf, bytes32[] proof) view returns (bool)', ]; const verifyHub = new ethers_1.ethers.Contract(config_1.checkpointIndexerConfig.checkpointProxy, VERIFY_ABI, mainnet); const l = leaf; const ok = await verifyHub.verifyPaymentInBatch(BigInt(batchId), [ l.txHash, l.from, l.to, (0, checkpoint_core_1.merkleVerifyValueWei)(l), BigInt(String(l.blockNumber)), BigInt(String(l.blockTimestamp)), BigInt(String(l.gasUsed ?? 0)), Boolean(l.success), ], l.merkleProof); res.json({ batchId, leafIndex, verified: ok, leaf }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/account/:address', async (req, res) => { try { const addr = req.params.address; let blockTag = 'latest'; if (req.query.atBatch) { if (!hub) return res.status(503).json({ error: 'proxy not set' }); const h = await hub.getCheckpoint(BigInt(String(req.query.atBatch))); blockTag = Number(h.checkpointBlock); } else if (hub) { blockTag = Number(await hub.latestCheckpointBlock()); } const balance = await chain138.getBalance(addr, blockTag); const index = getAddressIndex(); const rows = index.byAddress[addr.toLowerCase()] ?? []; const activity = (0, addressIndex_1.summarizeAddressActivity)(rows); res.json({ address: addr, chainId: 138, blockTag, balanceWei: balance.toString(), attestedActivity: activity, mainnetSurfaces: { transactionMirror: config_1.checkpointIndexerConfig.legacyMirror, addressActivityRegistry: config_1.checkpointIndexerConfig.addressActivityRegistry || null, checkpointHub: config_1.checkpointIndexerConfig.checkpointProxy || null, }, walletAttestationCopy: 'Native balance as of Chain 138 checkpoint block. Attested payments (ETH + USD) via checkpoint batches on Ethereum mainnet.', }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/account/:address/transactions', async (req, res) => { try { const addr = req.params.address; const limit = Math.min(parseInt(String(req.query.limit ?? '50'), 10) || 50, 200); const offset = parseInt(String(req.query.offset ?? '0'), 10) || 0; const index = getAddressIndex(); const { total, items } = (0, addressIndex_1.getAddressTransactions)(index, addr, limit, offset); res.json({ address: addr, chainId: 138, total, limit, offset, items, indexMeta: { builtAt: index.builtAt, batchCount: index.batchCount, txCount: index.txCount, }, }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/account/:address/activity', async (req, res) => { try { const addr = req.params.address; const index = getAddressIndex(); const rows = index.byAddress[addr.toLowerCase()] ?? []; const summary = (0, addressIndex_1.summarizeAddressActivity)(rows); res.json({ address: addr, chainId: 138, summary, recent: rows.slice(0, 25).map((r) => ({ ...r, chain138ExplorerUrl: (0, chain138Explorer_1.chain138ExplorerTxUrl)(r.txHash), })), chain138Explorer: { base: (0, chain138Explorer_1.chain138ExplorerBase)(), txUrlTemplate: `${(0, chain138Explorer_1.chain138ExplorerBase)()}/tx/{txHash}`, }, etherscan: { transactionMirrorEvents: `https://etherscan.io/address/${config_1.checkpointIndexerConfig.legacyMirror}#events`, addressActivityEvents: config_1.checkpointIndexerConfig.addressActivityRegistry ? `https://etherscan.io/address/${config_1.checkpointIndexerConfig.addressActivityRegistry}#events` : null, }, }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.get('/v1/tx/:txHash/logs', async (req, res) => { try { const txHash = req.params.txHash; const receipt = await chain138.getTransactionReceipt(txHash); if (!receipt) return res.status(404).json({ error: 'receipt not found on chain 138' }); res.json({ txHash, chainId: 138, blockNumber: receipt.blockNumber, status: receipt.status, gasUsed: receipt.gasUsed.toString(), logCount: receipt.logs.length, logs: receipt.logs.map((log, i) => ({ index: i, address: log.address, topics: log.topics, data: log.data, })), }); } catch (e) { res.status(500).json({ error: String(e) }); } }); app.listen(config_1.checkpointIndexerConfig.port, () => { console.log(`checkpoint-indexer listening on :${config_1.checkpointIndexerConfig.port}`); });