Skip to content

Commit 507498e

Browse files
feat(zenrock): add historical query support with x-cosmos-block-height header
- Add timestampToBlockHeight function with hybrid estimation + binary search to ensure accuracy within 60 seconds - Add apiRequest helper function to pass x-cosmos-block-height header for all API calls to api.diamond.zenrocklabs.io - Update zcashTvl to use historical queries for timestamps > 1 hour ago - Update bitcoin tvl to pass blockHeight to zenrock fetcher - Update zenrock fetcher to accept blockHeight parameter and use header for all treasury API endpoints - Support historical queries for last ~274k blocks (~16 days) - Enable timetravel: true for DefiLlama historical charts
1 parent b9df5fc commit 507498e

File tree

2 files changed

+177
-34
lines changed

2 files changed

+177
-34
lines changed

projects/helper/bitcoin-book/fetchers.js

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,27 @@ module.exports = {
242242
})
243243
return Array.from(new Set(staticAddresses))
244244
},
245-
zenrock: async () => {
245+
zenrock: async (blockHeight = null) => {
246246
const ZRCHAIN_WALLETS_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/zenbtc_wallets';
247247
const ZENBTC_PARAMS_API = 'https://api.diamond.zenrocklabs.io/zenbtc/params';
248248
const ZRCHAIN_KEY_BY_ID_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/key_by_id';
249249

250-
return getConfig('zenrock/addresses', undefined, {
250+
// Helper function to make API requests with optional height header
251+
async function apiRequest(url, height = null) {
252+
const options = {};
253+
if (height) {
254+
options.headers = {
255+
'x-cosmos-block-height': String(height)
256+
};
257+
}
258+
259+
return await get(url, options);
260+
}
261+
262+
// Use cache key that includes block height for historical queries
263+
const cacheKey = blockHeight ? `zenrock/addresses/${blockHeight}` : 'zenrock/addresses';
264+
265+
return getConfig(cacheKey, undefined, {
251266
fetcher: async () => {
252267
async function getBitcoinAddresses() {
253268
const btcAddresses = [];
@@ -258,7 +273,11 @@ module.exports = {
258273
if (nextKey) {
259274
url += `?pagination.key=${encodeURIComponent(nextKey)}`;
260275
}
261-
const { data } = await axios.get(url);
276+
const data = await apiRequest(url, blockHeight);
277+
if (!data) {
278+
// Historical state not available, return empty array
279+
return [];
280+
}
262281
if (data.zenbtc_wallets && Array.isArray(data.zenbtc_wallets)) {
263282
for (const walletGroup of data.zenbtc_wallets) {
264283
if (walletGroup.wallets && Array.isArray(walletGroup.wallets)) {
@@ -282,10 +301,17 @@ module.exports = {
282301
async function getChangeAddresses() {
283302
const changeAddresses = [];
284303

285-
const { data: paramsData } = await axios.get(ZENBTC_PARAMS_API);
304+
const paramsData = await apiRequest(ZENBTC_PARAMS_API, blockHeight);
305+
if (!paramsData) {
306+
// Historical state not available, return empty array
307+
return [];
308+
}
286309
const changeAddressKeyIDs = paramsData.params?.changeAddressKeyIDs || [];
287310
for (const keyID of changeAddressKeyIDs) {
288-
const { data: keyData } = await axios.get(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`);
311+
const keyData = await apiRequest(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`, blockHeight);
312+
if (!keyData) {
313+
continue; // Skip this key if historical state not available
314+
}
289315
if (keyData.wallets && Array.isArray(keyData.wallets)) {
290316
for (const wallet of keyData.wallets) {
291317
if (wallet.type === 'WALLET_TYPE_BTC_MAINNET' && wallet.address) {

projects/zenrock/index.js

Lines changed: 146 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,164 @@
11
const { sumTokens: sumBitcoinTokens } = require('../helper/chain/bitcoin');
22
const { zenrock } = require('../helper/bitcoin-book/fetchers');
3+
const { get } = require('../helper/http');
4+
const sdk = require('@defillama/sdk');
5+
6+
const ZRCHAIN_RPC = 'https://rpc.diamond.zenrocklabs.io';
7+
const ZRCHAIN_API = 'https://api.diamond.zenrocklabs.io';
8+
9+
// Chain launch timestamp (genesis block): 2024-11-20T17:39:07Z
10+
const GENESIS_TIMESTAMP = 1732124347;
11+
12+
// Cache for block height lookups to avoid repeated RPC calls
13+
const blockHeightCache = new Map();
14+
15+
async function timestampToBlockHeight(timestamp) {
16+
// Ensure timestamp is not before chain launch
17+
if (timestamp < GENESIS_TIMESTAMP) {
18+
return 1; // Return genesis block height
19+
}
20+
21+
// Check cache first (cache by day to reduce API calls)
22+
const dayKey = Math.floor(timestamp / 86400);
23+
if (blockHeightCache.has(dayKey)) {
24+
return blockHeightCache.get(dayKey);
25+
}
26+
27+
// Get latest block height
28+
const status = await get(`${ZRCHAIN_RPC}/status`);
29+
const latestHeight = parseInt(status.result.sync_info.latest_block_height);
30+
31+
// Get latest block timestamp
32+
const latestBlock = await get(`${ZRCHAIN_RPC}/block?height=${latestHeight}`);
33+
const latestBlockTime = new Date(latestBlock.result.block.header.time).getTime() / 1000;
34+
35+
// Estimate block height (5 seconds per block for zrchain)
36+
const avgBlockTime = 5; // seconds
37+
const genesisTime = latestBlockTime - (latestHeight * avgBlockTime);
38+
let estimatedHeight = Math.max(1, Math.min(
39+
Math.floor((timestamp - genesisTime) / avgBlockTime),
40+
latestHeight
41+
));
42+
43+
// Refine estimate to ensure accuracy within 60 seconds
44+
const targetAccuracy = 60; // seconds
45+
let low = Math.max(1, estimatedHeight - 1000); // Search range: ±1000 blocks
46+
let high = Math.min(latestHeight, estimatedHeight + 1000);
47+
let bestHeight = estimatedHeight;
48+
let bestDiff = Infinity;
49+
50+
// Binary search to find block within target accuracy
51+
while (low <= high) {
52+
const mid = Math.floor((low + high) / 2);
53+
const block = await get(`${ZRCHAIN_RPC}/block?height=${mid}`);
54+
const blockTime = new Date(block.result.block.header.time).getTime() / 1000;
55+
const diff = Math.abs(blockTime - timestamp);
56+
57+
if (diff < bestDiff) {
58+
bestDiff = diff;
59+
bestHeight = mid;
60+
}
61+
62+
// If within target accuracy, we're done
63+
if (diff <= targetAccuracy) {
64+
estimatedHeight = mid;
65+
break;
66+
}
67+
68+
if (blockTime < timestamp) {
69+
low = mid + 1;
70+
} else {
71+
high = mid - 1;
72+
}
73+
}
74+
75+
// Use best height found if we didn't find one within accuracy
76+
if (bestDiff > targetAccuracy) {
77+
estimatedHeight = bestHeight;
78+
}
79+
80+
// Cache the result
81+
blockHeightCache.set(dayKey, estimatedHeight);
82+
83+
// Limit cache size
84+
if (blockHeightCache.size > 100) {
85+
const firstKey = blockHeightCache.keys().next().value;
86+
blockHeightCache.delete(firstKey);
87+
}
88+
89+
return estimatedHeight;
90+
}
91+
92+
// Helper function to make API requests with optional height header
93+
async function apiRequest(url, blockHeight = null) {
94+
const options = {};
95+
if (blockHeight) {
96+
options.headers = {
97+
'x-cosmos-block-height': String(blockHeight)
98+
};
99+
}
100+
101+
try {
102+
return await get(url, options);
103+
} catch (error) {
104+
// If historical query fails (code 13 - nil pointer), return null
105+
// This indicates historical state is not available
106+
const errorStr = error.message || error.toString() || '';
107+
if (errorStr.includes('code 13') || errorStr.includes('nil pointer')) {
108+
return null;
109+
}
110+
throw error;
111+
}
112+
}
113+
114+
async function tvl(api) {
115+
// Extract timestamp from api parameter
116+
const timestamp = api?.timestamp;
117+
const now = Date.now() / 1000;
118+
119+
// Determine block height for historical queries
120+
let blockHeight = null;
121+
if (timestamp && (now - timestamp) > 3600) {
122+
blockHeight = await timestampToBlockHeight(timestamp);
123+
}
3124

4-
/**
5-
* Queries Bitcoin balances for all zrchain treasury and change addresses
6-
* Returns balances object with Bitcoin TVL
7-
*/
8-
async function tvl() {
9125
// Fetch all protocol addresses (treasury + change) from the bitcoin-book fetcher
10-
const allAddresses = await zenrock();
126+
// Pass blockHeight for historical queries
127+
const allAddresses = await zenrock(blockHeight);
11128

12129
if (allAddresses.length === 0) {
13130
return { bitcoin: '0' };
14131
}
15132

16133
// Use Bitcoin helper to sum balances for all addresses
134+
// Pass timestamp if available for historical queries
17135
const balances = {};
18-
await sumBitcoinTokens({ balances, owners: allAddresses });
136+
await sumBitcoinTokens({ balances, owners: allAddresses, timestamp });
19137

20138
return balances;
21139
}
22140

23-
/**
24-
* Queries Zcash balances for all zrchain treasury and change addresses
25-
* Returns balances object with Zcash TVL
26-
*
27-
* NOTE: Currently using supply-based approach as a test implementation.
28-
* The address-based approach is commented out below for future use.
29-
*/
30-
async function zcashTvl() {
31-
// Fetch custodied amount from DCT supply endpoint
32-
const { get } = require('../helper/http');
33-
const { getConfig } = require('../helper/cache');
34-
const sdk = require('@defillama/sdk');
35-
36-
const DCT_SUPPLY_API = 'https://api.diamond.zenrocklabs.io/dct/supply';
37-
38-
const supplyData = await getConfig('zenrock/dct_supply', DCT_SUPPLY_API, {
39-
fetcher: async () => {
40-
const response = await get(DCT_SUPPLY_API);
41-
return response;
42-
}
43-
});
44-
141+
async function zcashTvl(api) {
45142
const balances = {};
46143

144+
// Extract timestamp from api parameter
145+
const timestamp = api?.timestamp;
146+
const now = Date.now() / 1000;
147+
148+
// Determine block height for historical queries
149+
let blockHeight = null;
150+
if (timestamp && (now - timestamp) > 3600) {
151+
blockHeight = await timestampToBlockHeight(timestamp);
152+
}
153+
154+
// Fetch custodied amount from DCT supply endpoint with height header
155+
const supplyData = await apiRequest(`${ZRCHAIN_API}/dct/supply`, blockHeight);
156+
157+
// If historical query failed (state not available), return empty balances
158+
if (!supplyData) {
159+
return balances;
160+
}
161+
47162
// Find ASSET_ZENZEC in supplies array
48163
const zenZecSupply = supplyData.supplies?.find(
49164
item => item.supply?.asset === 'ASSET_ZENZEC'
@@ -62,6 +177,8 @@ async function zcashTvl() {
62177
}
63178

64179
module.exports = {
180+
timetravel: true,
181+
start: GENESIS_TIMESTAMP,
65182
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.',
66183
bitcoin: {
67184
tvl,

0 commit comments

Comments
 (0)