Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m3s
CI/CD Pipeline / Security Scanning (push) Successful in 2m18s
CI/CD Pipeline / Lint and Format (push) Failing after 34s
CI/CD Pipeline / Terraform Validation (push) Failing after 20s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 22s
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 40s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 49s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 21s
Validation / validate-genesis (push) Successful in 25s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 8s
Validation / validate-smart-contracts (push) Failing after 8s
Validation / validate-security (push) Failing after 1m11s
Validation / validate-documentation (push) Failing after 14s
Verify Deployment / Verify Deployment (push) Failing after 45s
Ship AddressActivityRegistry V1/V2, ISO20022IntakeGateway, Chain138ParticipantSurface, checkpoint hub contracts, checkpoint-core package, aggregator/indexer/sdk services, relay profile guards, M00 diamond bridge facet, and OMNL compliance contracts. Co-authored-by: Cursor <cursoragent@cursor.com>
321 lines
12 KiB
TypeScript
321 lines
12 KiB
TypeScript
import express from 'express';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { ethers } from 'ethers';
|
|
import { checkpointIndexerConfig as cfg } from './config';
|
|
import { enrichLeaves, type LeafRecord } from './tokenEnrich';
|
|
import { merkleVerifyValueWei } from '@dbis/checkpoint-core';
|
|
import { merkleProofs, paymentLeafHash } from './merkle';
|
|
import { jsonSafe } from './serialize';
|
|
import {
|
|
buildAddressIndex,
|
|
getAddressTransactions,
|
|
summarizeAddressActivity,
|
|
type AddressIndex,
|
|
} from './addressIndex';
|
|
import { chainlistSupplement, etherscanV2Api } from './etherscanV2Shim';
|
|
import { chain138ExplorerBase, chain138ExplorerTxUrl } from './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 = express();
|
|
const mainnet = new ethers.JsonRpcProvider(cfg.mainnetRpc);
|
|
const chain138 = new ethers.JsonRpcProvider(cfg.chain138Rpc);
|
|
const hub = cfg.checkpointProxy ? new ethers.Contract(cfg.checkpointProxy, CHECKPOINT_ABI, mainnet) : null;
|
|
const mirror = new ethers.Contract(cfg.legacyMirror, MIRROR_ABI, mainnet);
|
|
|
|
let addressIndexCache: AddressIndex | null = null;
|
|
let addressIndexBuiltAt = 0;
|
|
const ADDRESS_INDEX_TTL_MS = 60_000;
|
|
|
|
function getAddressIndex(): AddressIndex {
|
|
const now = Date.now();
|
|
if (!addressIndexCache || now - addressIndexBuiltAt > ADDRESS_INDEX_TTL_MS) {
|
|
addressIndexCache = buildAddressIndex(cfg.batchPayloadDir);
|
|
addressIndexBuiltAt = now;
|
|
}
|
|
return addressIndexCache;
|
|
}
|
|
|
|
function loadBatchPayload(batchId: string): { batchId?: string; leaves?: LeafRecord[] } | null {
|
|
const p = path.join(cfg.batchPayloadDir, `batch-${batchId}.json`);
|
|
if (!fs.existsSync(p)) return null;
|
|
return JSON.parse(fs.readFileSync(p, 'utf8')) as { batchId?: string; leaves?: LeafRecord[] };
|
|
}
|
|
|
|
async function payloadWithEnrichedLeaves(
|
|
batchId: string,
|
|
raw: { batchId?: string; leaves?: LeafRecord[]; chainId?: number; paymentsRoot?: string } | null
|
|
): Promise<object | null> {
|
|
if (!raw?.leaves?.length) return raw;
|
|
const chainId = raw.chainId ?? 138;
|
|
let leaves = raw.leaves;
|
|
if (cfg.enrichTokenTransfers || cfg.usdEnrichEnabled) {
|
|
leaves = await enrichLeaves(leaves, cfg.blockscoutApi);
|
|
}
|
|
const hashes = leaves.map((l) =>
|
|
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 } = 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: cfg.checkpointProxy || null, batchDir: cfg.batchPayloadDir });
|
|
});
|
|
|
|
/** Etherscan V2 supplement for Chain 138 (unsupported on official chainlist). */
|
|
app.get('/v2/chainlist-supplement', chainlistSupplement);
|
|
app.get('/v2/api', (req, res) => {
|
|
void 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: jsonSafe(header),
|
|
legacy: { mirrorCount: v1Count.toString(), tether: cfg.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: 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 as { leaves?: LeafRecord[] }).leaves;
|
|
const leaf = leaves?.[leafIndex];
|
|
if (!leaf || !(leaf as LeafRecord).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.Contract(cfg.checkpointProxy, VERIFY_ABI, mainnet);
|
|
const l = leaf as LeafRecord;
|
|
const ok = await verifyHub.verifyPaymentInBatch(
|
|
BigInt(batchId),
|
|
[
|
|
l.txHash,
|
|
l.from,
|
|
l.to,
|
|
merkleVerifyValueWei(l),
|
|
BigInt(String(l.blockNumber)),
|
|
BigInt(String(l.blockTimestamp)),
|
|
BigInt(String(l.gasUsed ?? 0)),
|
|
Boolean(l.success),
|
|
],
|
|
(l as { merkleProof: string[] }).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: number | 'latest' = '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 = summarizeAddressActivity(rows);
|
|
res.json({
|
|
address: addr,
|
|
chainId: 138,
|
|
blockTag,
|
|
balanceWei: balance.toString(),
|
|
attestedActivity: activity,
|
|
mainnetSurfaces: {
|
|
transactionMirror: cfg.legacyMirror,
|
|
addressActivityRegistry: cfg.addressActivityRegistry || null,
|
|
checkpointHub: cfg.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 } = 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 = summarizeAddressActivity(rows);
|
|
res.json({
|
|
address: addr,
|
|
chainId: 138,
|
|
summary,
|
|
recent: rows.slice(0, 25).map((r) => ({
|
|
...r,
|
|
chain138ExplorerUrl: chain138ExplorerTxUrl(r.txHash),
|
|
})),
|
|
chain138Explorer: {
|
|
base: chain138ExplorerBase(),
|
|
txUrlTemplate: `${chain138ExplorerBase()}/tx/{txHash}`,
|
|
},
|
|
etherscan: {
|
|
transactionMirrorEvents: `https://etherscan.io/address/${cfg.legacyMirror}#events`,
|
|
addressActivityEvents: cfg.addressActivityRegistry
|
|
? `https://etherscan.io/address/${cfg.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(cfg.port, () => {
|
|
console.log(`checkpoint-indexer listening on :${cfg.port}`);
|
|
});
|