Skip to content

Instantly share code, notes, and snippets.

@uF4No
Created May 22, 2026 11:36
Show Gist options
  • Select an option

  • Save uF4No/92284a48fde0f1fae88bb465dc409f9b to your computer and use it in GitHub Desktop.

Select an option

Save uF4No/92284a48fde0f1fae88bb465dc409f9b to your computer and use it in GitHub Desktop.
L1 hashes for L2 tx
/**
* Run:
* node standalone/tx-batch-info-unstable/index.mjs <txHash>
* or set TX_HASH in the env file and run without arguments.
*
* Required env variables:
* ADMIN_PRIVATE_KEY
* PRIVIDIUM_RPC_URL
* PRIVIDIUM_CHAIN_ID
* PRIVIDIUM_SIWE_DOMAIN
*
* Optional env variables:
* TX_HASH
* PRIVIDIUM_API_BASE_URL
* ETH_SEPOLIA_RPC_URL
*/
import { existsSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { config as loadEnv } from 'dotenv'
import {
decodeFunctionResult,
encodeFunctionData,
keccak256,
parseAbi,
toBytes,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
const __dirname = dirname(fileURLToPath(import.meta.url))
const envPath = resolve(__dirname, '.env')
if (existsSync(envPath)) {
loadEnv({ path: envPath })
} else {
loadEnv()
}
const txHash = process.argv[2] ?? requiredEnv('TX_HASH')
const rpcUrl = requiredEnv('PRIVIDIUM_RPC_URL')
const apiBaseUrl = trimTrailingSlash(process.env.PRIVIDIUM_API_BASE_URL || deriveApiBaseUrl(rpcUrl))
const expectedChainId = Number(requiredEnv('PRIVIDIUM_CHAIN_ID'))
const siweDomain = requiredEnv('PRIVIDIUM_SIWE_DOMAIN')
const l1RpcUrl = process.env.ETH_SEPOLIA_RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com'
const privateKey = process.env.ADMIN_PRIVATE_KEY || process.env.PRIVATE_KEY
if (!privateKey) {
throw new Error('Missing ADMIN_PRIVATE_KEY')
}
const account = privateKeyToAccount(normalizePrivateKey(privateKey))
const auth = await authenticate({
apiBaseUrl,
domain: siweDomain,
address: account.address,
signMessage: async (message) => account.signMessage({ message }),
})
const chainIdHex = await rpcCall({
rpcUrl,
token: auth.token,
method: 'eth_chainId',
params: [],
})
const chainId = hexToNumber(chainIdHex)
if (chainId !== expectedChainId) {
throw new Error(`Chain ID mismatch: expected ${expectedChainId}, got ${chainId} (${chainIdHex})`)
}
const transaction = await rpcCall({
rpcUrl,
token: auth.token,
method: 'eth_getTransactionByHash',
params: [txHash],
})
if (!transaction) {
throw new Error(`Transaction not found: ${txHash}`)
}
const blockNumber = toNullableNumber(transaction.blockNumber)
if (blockNumber === null) {
throw new Error(`Transaction ${txHash} is not included in a block yet`)
}
const batchByBlock = await rpcCall({
rpcUrl,
token: auth.token,
method: 'unstable_getBatchByBlockNumber',
params: [blockNumber],
})
const batchNumber = toNullableNumber(batchByBlock?.batch_info?.batch_number)
const executeSettlementBlock = toNullableNumber(batchByBlock?.execute_sl_block_number)
if (batchNumber === null) {
throw new Error(`unstable_getBatchByBlockNumber did not return a batch number for block ${blockNumber}`)
}
const bridgehubAddress = await rpcCall({
rpcUrl,
token: auth.token,
method: 'zks_getBridgehubContract',
params: [],
})
const l1LogResolution = await resolveL1TransactionsFromLogs({
l1RpcUrl,
bridgehubAddress,
l2ChainId: BigInt(chainId),
l1BatchNumber: BigInt(batchNumber),
executeSettlementBlock: executeSettlementBlock === null ? null : BigInt(executeSettlementBlock),
})
const summary = {
network: {
rpcUrl,
chainId,
l1RpcUrl,
},
txHash,
l2BlockNumber: blockNumber,
l1BatchNumber: batchNumber,
executeSettlementBlock,
bridgehubAddress,
l1ChainContractAddress: l1LogResolution.l1ChainContractAddress,
batchL1Transactions: {
commitTxHash: l1LogResolution.commit?.transactionHash ?? null,
proveTxHash: l1LogResolution.prove?.transactionHash ?? null,
executeTxHash: l1LogResolution.execute?.transactionHash ?? null,
},
}
console.log(JSON.stringify(summary, null, 2))
async function authenticate({ apiBaseUrl, domain, address, signMessage }) {
const siwe = await requestSiweMessage({ apiBaseUrl, address, domain })
const signature = await signMessage(siwe.msg)
const session = await apiJson(`${apiBaseUrl}/api/auth/login/crypto-native`, {
method: 'POST',
headers: jsonHeaders(),
body: JSON.stringify({
message: siwe.msg,
signature,
nonceToken: siwe.nonceToken,
}),
})
return {
...session,
domain: siwe.domain,
}
}
async function requestSiweMessage({ apiBaseUrl, address, domain }) {
try {
const result = await apiJson(`${apiBaseUrl}/api/siwe-messages`, {
method: 'POST',
headers: jsonHeaders(),
body: JSON.stringify({ address, domain }),
})
return { ...result, domain }
} catch (error) {
const validDomains = parseValidDomains(error)
if (validDomains.length === 0 || validDomains.includes(domain)) {
throw error
}
const fallbackDomain = validDomains[0]
const result = await apiJson(`${apiBaseUrl}/api/siwe-messages`, {
method: 'POST',
headers: jsonHeaders(),
body: JSON.stringify({ address, domain: fallbackDomain }),
})
return { ...result, domain: fallbackDomain }
}
}
async function rpcCall({ rpcUrl, token, method, params }) {
const response = await fetch(rpcUrl, {
method: 'POST',
headers: {
...jsonHeaders(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({
jsonrpc: '2.0',
id: method,
method,
params,
}),
})
const payload = await response.json()
if (!response.ok) {
throw new Error(`RPC ${method} failed with HTTP ${response.status}: ${JSON.stringify(payload)}`)
}
if (payload.error) {
throw new Error(`RPC ${method} failed: ${JSON.stringify(payload.error)}`)
}
return payload.result
}
async function resolveL1TransactionsFromLogs({
l1RpcUrl,
bridgehubAddress,
l2ChainId,
l1BatchNumber,
executeSettlementBlock,
}) {
const l1ChainContractAddress = await resolveL1ChainContractAddress({
l1RpcUrl,
bridgehubAddress,
l2ChainId,
})
const latestBlockHex = await rpcCall({
rpcUrl: l1RpcUrl,
method: 'eth_blockNumber',
params: [],
})
const latestBlock = BigInt(latestBlockHex)
const anchorBlock = executeSettlementBlock === null || executeSettlementBlock > latestBlock
? latestBlock
: executeSettlementBlock
const batchTopic = numberToTopic(l1BatchNumber)
const eventTopics = {
blockCommit: topicHash('BlockCommit(uint256,bytes32,bytes32)'),
blocksVerification: topicHash('BlocksVerification(uint256,uint256)'),
blockExecution: topicHash('BlockExecution(uint256,bytes32,bytes32)'),
}
const execute = await findNearestLog({
l1RpcUrl,
address: l1ChainContractAddress,
anchorBlock,
direction: 'both',
topics: [eventTopics.blockExecution, batchTopic],
})
const prove = await findNearestLog({
l1RpcUrl,
address: l1ChainContractAddress,
anchorBlock,
direction: 'backward',
topics: [eventTopics.blocksVerification, null, batchTopic],
})
const commit = await findNearestLog({
l1RpcUrl,
address: l1ChainContractAddress,
anchorBlock,
direction: 'backward',
topics: [eventTopics.blockCommit, batchTopic],
})
return {
l1ChainContractAddress,
commit,
prove,
execute,
}
}
async function resolveL1ChainContractAddress({ l1RpcUrl, bridgehubAddress, l2ChainId }) {
const candidates = [
'function getZKChain(uint256) view returns (address)',
'function getHyperchain(uint256) view returns (address)',
]
for (const signature of candidates) {
const abi = parseAbi([signature])
const functionName = abi[0].name
const data = encodeFunctionData({
abi,
functionName,
args: [l2ChainId],
})
try {
const raw = await rpcCall({
rpcUrl: l1RpcUrl,
method: 'eth_call',
params: [{ to: bridgehubAddress, data }, 'latest'],
})
const result = decodeFunctionResult({
abi,
functionName,
data: raw,
})
if (result && result !== zeroAddress()) {
return result
}
} catch {
continue
}
}
throw new Error(`Unable to resolve L1 chain contract from Bridgehub ${bridgehubAddress}`)
}
async function findNearestLog({ l1RpcUrl, address, anchorBlock, direction, topics }) {
const maxRange = 50000n
const latestBlockHex = await rpcCall({
rpcUrl: l1RpcUrl,
method: 'eth_blockNumber',
params: [],
})
const latestBlock = BigInt(latestBlockHex)
if (direction === 'both') {
const exact = await getLogs({
l1RpcUrl,
address,
fromBlock: anchorBlock,
toBlock: anchorBlock,
topics,
})
if (exact.length > 0) return exact[exact.length - 1]
for (let offset = 1n; offset <= latestBlock; offset += maxRange) {
const candidateLogs = []
const backwardTo = anchorBlock >= offset ? anchorBlock - offset : -1n
if (backwardTo >= 0n) {
const backwardFrom = backwardTo >= maxRange - 1n ? backwardTo - (maxRange - 1n) : 0n
const backwardLogs = await getLogs({
l1RpcUrl,
address,
fromBlock: backwardFrom,
toBlock: backwardTo,
topics,
})
candidateLogs.push(...backwardLogs)
}
const forwardFrom = anchorBlock + offset
if (forwardFrom <= latestBlock) {
const forwardTo = forwardFrom + maxRange - 1n <= latestBlock
? forwardFrom + maxRange - 1n
: latestBlock
const forwardLogs = await getLogs({
l1RpcUrl,
address,
fromBlock: forwardFrom,
toBlock: forwardTo,
topics,
})
candidateLogs.push(...forwardLogs)
}
if (candidateLogs.length > 0) {
return pickClosestLog(candidateLogs, anchorBlock)
}
if (backwardTo < 0n && forwardFrom > latestBlock) break
if (backwardTo >= 0n && backwardTo < maxRange && forwardFrom > latestBlock) break
}
return null
}
for (let end = anchorBlock; end >= 0n; end = end >= maxRange ? end - maxRange : -1n) {
const start = end >= maxRange - 1n ? end - (maxRange - 1n) : 0n
const logs = await getLogs({
l1RpcUrl,
address,
fromBlock: start,
toBlock: end,
topics,
})
if (logs.length > 0) {
return logs.reduce((latest, current) => {
if (!latest) return current
return BigInt(current.blockNumber) > BigInt(latest.blockNumber) ? current : latest
}, null)
}
if (start === 0n) break
}
return null
}
async function getLogs({ l1RpcUrl, address, fromBlock, toBlock, topics }) {
return rpcCall({
rpcUrl: l1RpcUrl,
method: 'eth_getLogs',
params: [{
address,
fromBlock: bigintToHex(fromBlock),
toBlock: bigintToHex(toBlock),
topics,
}],
})
}
function pickClosestLog(logs, anchorBlock) {
return logs.reduce((closest, current) => {
if (!closest) return current
const currentDistance = absBigInt(BigInt(current.blockNumber) - anchorBlock)
const closestDistance = absBigInt(BigInt(closest.blockNumber) - anchorBlock)
if (currentDistance < closestDistance) return current
if (currentDistance > closestDistance) return closest
return BigInt(current.logIndex) > BigInt(closest.logIndex) ? current : closest
}, null)
}
async function apiJson(url, init) {
const response = await fetch(url, init)
const text = await response.text()
if (!response.ok) {
throw new Error(`Request failed for ${url}: ${response.status} ${text}`)
}
return JSON.parse(text)
}
function parseValidDomains(error) {
if (!(error instanceof Error)) return []
const match = error.message.match(/Valid values are:\s*(.+)$/)
if (!match) return []
return match[1]
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
function deriveApiBaseUrl(rpcUrl) {
const url = new URL(rpcUrl)
url.pathname = url.pathname.replace(/\/rpc\/?$/, '')
return url.toString().replace(/\/+$/, '')
}
function topicHash(signature) {
return keccak256(toBytes(signature))
}
function numberToTopic(value) {
return `0x${value.toString(16).padStart(64, '0')}`
}
function bigintToHex(value) {
return `0x${value.toString(16)}`
}
function absBigInt(value) {
return value < 0n ? -value : value
}
function zeroAddress() {
return '0x0000000000000000000000000000000000000000'
}
function toNullableNumber(value) {
if (value == null) return null
if (typeof value === 'number') return value
if (typeof value === 'string' && value.startsWith('0x')) return hexToNumber(value)
return Number(value)
}
function hexToNumber(value) {
return Number(BigInt(value))
}
function normalizePrivateKey(value) {
return value.startsWith('0x') ? value : `0x${value}`
}
function trimTrailingSlash(value) {
return value.replace(/\/+$/, '')
}
function jsonHeaders() {
return {
'Content-Type': 'application/json',
}
}
function requiredEnv(name) {
const value = process.env[name]
if (!value) {
throw new Error(`Missing ${name}`)
}
return value
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment