Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions projects/helper/bitcoin-book/fetchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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)) {
Expand All @@ -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) {
Expand Down
171 changes: 142 additions & 29 deletions projects/zenrock/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down
Loading