diff --git a/projects/helper/bitcoin-book/fetchers.js b/projects/helper/bitcoin-book/fetchers.js index 5334e1d270..220b0b27a9 100644 --- a/projects/helper/bitcoin-book/fetchers.js +++ b/projects/helper/bitcoin-book/fetchers.js @@ -242,12 +242,38 @@ module.exports = { }) return Array.from(new Set(staticAddresses)) }, - zenrock: async () => { + zenrock: async (blockHeight = null) => { const ZRCHAIN_WALLETS_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/zenbtc_wallets'; const ZENBTC_PARAMS_API = 'https://api.diamond.zenrocklabs.io/zenbtc/params'; const ZRCHAIN_KEY_BY_ID_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/key_by_id'; - return getConfig('zenrock/addresses', undefined, { + // Helper function to make API requests with optional height header + async function apiRequest(url, height = null) { + const options = {}; + if (height) { + options.headers = { + 'x-cosmos-block-height': String(height) + }; + } + + try { + return await get(url, options); + } catch (error) { + // If historical query fails (code 13 - nil pointer), throw descriptive error + // This indicates either: (1) the module was not present on-chain at this block height, + // or (2) historical state has been pruned (varies by module) + const errorStr = error.message || error.toString() || ''; + if (errorStr.includes('code 13') || errorStr.includes('nil pointer')) { + throw new Error(`Historical data unavailable for block height ${height}. The module may not have been present on-chain at this block height, or historical state may have been pruned (varies by module).`); + } + throw error; + } + } + + // Use cache key that includes block height for historical queries + const cacheKey = blockHeight ? `zenrock/addresses/${blockHeight}` : 'zenrock/addresses'; + + return getConfig(cacheKey, undefined, { fetcher: async () => { async function getBitcoinAddresses() { const btcAddresses = []; @@ -258,7 +284,7 @@ module.exports = { if (nextKey) { url += `?pagination.key=${encodeURIComponent(nextKey)}`; } - const { data } = await axios.get(url); + const data = await apiRequest(url, blockHeight); if (data.zenbtc_wallets && Array.isArray(data.zenbtc_wallets)) { for (const walletGroup of data.zenbtc_wallets) { if (walletGroup.wallets && Array.isArray(walletGroup.wallets)) { @@ -282,10 +308,10 @@ module.exports = { async function getChangeAddresses() { const changeAddresses = []; - const { data: paramsData } = await axios.get(ZENBTC_PARAMS_API); + const paramsData = await apiRequest(ZENBTC_PARAMS_API, blockHeight); const changeAddressKeyIDs = paramsData.params?.changeAddressKeyIDs || []; for (const keyID of changeAddressKeyIDs) { - const { data: keyData } = await axios.get(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`); + const keyData = await apiRequest(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`, blockHeight); if (keyData.wallets && Array.isArray(keyData.wallets)) { for (const wallet of keyData.wallets) { if (wallet.type === 'WALLET_TYPE_BTC_MAINNET' && wallet.address) { diff --git a/projects/zenrock/index.js b/projects/zenrock/index.js index 256a7a2ccc..6ac1c6e395 100644 --- a/projects/zenrock/index.js +++ b/projects/zenrock/index.js @@ -1,49 +1,160 @@ const { sumTokens: sumBitcoinTokens } = require('../helper/chain/bitcoin'); const { zenrock } = require('../helper/bitcoin-book/fetchers'); +const { get } = require('../helper/http'); +const sdk = require('@defillama/sdk'); + +const ZRCHAIN_RPC = 'https://rpc.diamond.zenrocklabs.io'; +const ZRCHAIN_API = 'https://api.diamond.zenrocklabs.io'; + +// Chain launch timestamp (genesis block): 2024-11-20T17:39:07Z +const GENESIS_TIMESTAMP = 1732124347; + +// Cache for block height lookups to avoid repeated RPC calls +const blockHeightCache = new Map(); + +async function timestampToBlockHeight(timestamp) { + // Ensure timestamp is not before chain launch + if (timestamp < GENESIS_TIMESTAMP) { + throw new Error(`Timestamp ${timestamp} is before chain genesis (${GENESIS_TIMESTAMP})`); + } + + // Check cache first (cache by day to reduce API calls) + const dayKey = Math.floor(timestamp / 86400); + if (blockHeightCache.has(dayKey)) { + return blockHeightCache.get(dayKey); + } + + // Get latest block height + const status = await get(`${ZRCHAIN_RPC}/status`); + const latestHeight = parseInt(status.result.sync_info.latest_block_height); + + // Get latest block timestamp + const latestBlock = await get(`${ZRCHAIN_RPC}/block?height=${latestHeight}`); + const latestBlockTime = new Date(latestBlock.result.block.header.time).getTime() / 1000; + + // Estimate block height (5 seconds per block for zrchain) + const avgBlockTime = 5; // seconds + const genesisTime = latestBlockTime - (latestHeight * avgBlockTime); + let estimatedHeight = Math.max(1, Math.min( + Math.floor((timestamp - genesisTime) / avgBlockTime), + latestHeight + )); + + // Refine estimate to ensure accuracy within 60 seconds + const targetAccuracy = 60; // seconds + let low = Math.max(1, estimatedHeight - 1000); // Search range: ±1000 blocks + let high = Math.min(latestHeight, estimatedHeight + 1000); + let bestHeight = estimatedHeight; + let bestDiff = Infinity; + + // Binary search to find block within target accuracy + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const block = await get(`${ZRCHAIN_RPC}/block?height=${mid}`); + const blockTime = new Date(block.result.block.header.time).getTime() / 1000; + const diff = Math.abs(blockTime - timestamp); + + if (diff < bestDiff) { + bestDiff = diff; + bestHeight = mid; + } + + // If within target accuracy, we're done + if (diff <= targetAccuracy) { + estimatedHeight = mid; + break; + } + + if (blockTime < timestamp) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + // Use best height found if we didn't find one within accuracy + if (bestDiff > targetAccuracy) { + estimatedHeight = bestHeight; + } + + // Cache the result + blockHeightCache.set(dayKey, estimatedHeight); + + // Limit cache size + if (blockHeightCache.size > 100) { + const firstKey = blockHeightCache.keys().next().value; + blockHeightCache.delete(firstKey); + } + + return estimatedHeight; +} + +// Helper function to make API requests with optional height header +async function apiRequest(url, blockHeight = null) { + const options = {}; + if (blockHeight) { + options.headers = { + 'x-cosmos-block-height': String(blockHeight) + }; + } + + try { + return await get(url, options); + } catch (error) { + // If historical query fails (code 13 - nil pointer), throw descriptive error + // This indicates either: (1) the module was not present on-chain at this block height, + // or (2) historical state has been pruned (varies by module - DCT was launched on 2025-10-31) + const errorStr = error.message || error.toString() || ''; + if (errorStr.includes('code 13') || errorStr.includes('nil pointer')) { + throw new Error(`Historical data unavailable for block height ${blockHeight}. The module may not have been present on-chain at this block height, or historical state may have been pruned (varies by module).`); + } + throw error; + } +} + +async function tvl(api) { + // Extract timestamp from api parameter + const timestamp = api?.timestamp; + const now = Date.now() / 1000; + + // Determine block height for historical queries + let blockHeight = null; + if (timestamp && (now - timestamp) > 3600) { + blockHeight = await timestampToBlockHeight(timestamp); + } -/** - * Queries Bitcoin balances for all zrchain treasury and change addresses - * Returns balances object with Bitcoin TVL - */ -async function tvl() { // Fetch all protocol addresses (treasury + change) from the bitcoin-book fetcher - const allAddresses = await zenrock(); + // Pass blockHeight for historical queries + const allAddresses = await zenrock(blockHeight); if (allAddresses.length === 0) { return { bitcoin: '0' }; } // Use Bitcoin helper to sum balances for all addresses + // Pass timestamp if available for historical queries const balances = {}; - await sumBitcoinTokens({ balances, owners: allAddresses }); + await sumBitcoinTokens({ balances, owners: allAddresses, timestamp }); return balances; } -/** - * Queries Zcash balances for all zrchain treasury and change addresses - * Returns balances object with Zcash TVL - * - * NOTE: Currently using supply-based approach as a test implementation. - * The address-based approach is commented out below for future use. - */ -async function zcashTvl() { - // Fetch custodied amount from DCT supply endpoint - const { get } = require('../helper/http'); - const { getConfig } = require('../helper/cache'); - const sdk = require('@defillama/sdk'); - - const DCT_SUPPLY_API = 'https://api.diamond.zenrocklabs.io/dct/supply'; - - const supplyData = await getConfig('zenrock/dct_supply', DCT_SUPPLY_API, { - fetcher: async () => { - const response = await get(DCT_SUPPLY_API); - return response; - } - }); - +async function zcashTvl(api) { const balances = {}; + // Extract timestamp from api parameter + const timestamp = api?.timestamp; + const now = Date.now() / 1000; + + // Determine block height for historical queries + let blockHeight = null; + if (timestamp && (now - timestamp) > 3600) { + blockHeight = await timestampToBlockHeight(timestamp); + } + + // Fetch custodied amount from DCT supply endpoint with height header + const supplyData = await apiRequest(`${ZRCHAIN_API}/dct/supply`, blockHeight); + // Find ASSET_ZENZEC in supplies array const zenZecSupply = supplyData.supplies?.find( item => item.supply?.asset === 'ASSET_ZENZEC' @@ -62,6 +173,8 @@ async function zcashTvl() { } module.exports = { + timetravel: true, + start: GENESIS_TIMESTAMP, methodology: 'zrchain locks native assets through its decentralized MPC network. zenBTC, Zenrock\'s flagship product, is a yield-bearing wrapped Bitcoin issued on Solana and EVM chains. TVL represents the total Bitcoin locked in zrchain treasury addresses. All zenBTC is fully backed by native Bitcoin, with the price of zenBTC anticipated to increase as yield payments are made continuously.', bitcoin: { tvl,