Skip to content

Commit 4217332

Browse files
Add Historical Query Support for Zenrock DefiLlama Adapter (#16948)
1 parent 4389e05 commit 4217332

File tree

2 files changed

+173
-34
lines changed

2 files changed

+173
-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,38 @@ 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+
try {
260+
return await get(url, options);
261+
} catch (error) {
262+
// If historical query fails (code 13 - nil pointer), throw descriptive error
263+
// This indicates either: (1) the module was not present on-chain at this block height,
264+
// or (2) historical state has been pruned (varies by module)
265+
const errorStr = error.message || error.toString() || '';
266+
if (errorStr.includes('code 13') || errorStr.includes('nil pointer')) {
267+
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).`);
268+
}
269+
throw error;
270+
}
271+
}
272+
273+
// Use cache key that includes block height for historical queries
274+
const cacheKey = blockHeight ? `zenrock/addresses/${blockHeight}` : 'zenrock/addresses';
275+
276+
return getConfig(cacheKey, undefined, {
251277
fetcher: async () => {
252278
async function getBitcoinAddresses() {
253279
const btcAddresses = [];
@@ -258,7 +284,7 @@ module.exports = {
258284
if (nextKey) {
259285
url += `?pagination.key=${encodeURIComponent(nextKey)}`;
260286
}
261-
const { data } = await axios.get(url);
287+
const data = await apiRequest(url, blockHeight);
262288
if (data.zenbtc_wallets && Array.isArray(data.zenbtc_wallets)) {
263289
for (const walletGroup of data.zenbtc_wallets) {
264290
if (walletGroup.wallets && Array.isArray(walletGroup.wallets)) {
@@ -282,10 +308,10 @@ module.exports = {
282308
async function getChangeAddresses() {
283309
const changeAddresses = [];
284310

285-
const { data: paramsData } = await axios.get(ZENBTC_PARAMS_API);
311+
const paramsData = await apiRequest(ZENBTC_PARAMS_API, blockHeight);
286312
const changeAddressKeyIDs = paramsData.params?.changeAddressKeyIDs || [];
287313
for (const keyID of changeAddressKeyIDs) {
288-
const { data: keyData } = await axios.get(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`);
314+
const keyData = await apiRequest(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`, blockHeight);
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: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,160 @@
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+
throw new Error(`Timestamp ${timestamp} is before chain genesis (${GENESIS_TIMESTAMP})`);
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), throw descriptive error
105+
// This indicates either: (1) the module was not present on-chain at this block height,
106+
// or (2) historical state has been pruned (varies by module - DCT was launched on 2025-10-31)
107+
const errorStr = error.message || error.toString() || '';
108+
if (errorStr.includes('code 13') || errorStr.includes('nil pointer')) {
109+
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).`);
110+
}
111+
throw error;
112+
}
113+
}
114+
115+
async function tvl(api) {
116+
// Extract timestamp from api parameter
117+
const timestamp = api?.timestamp;
118+
const now = Date.now() / 1000;
119+
120+
// Determine block height for historical queries
121+
let blockHeight = null;
122+
if (timestamp && (now - timestamp) > 3600) {
123+
blockHeight = await timestampToBlockHeight(timestamp);
124+
}
3125

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

12130
if (allAddresses.length === 0) {
13131
return { bitcoin: '0' };
14132
}
15133

16134
// Use Bitcoin helper to sum balances for all addresses
135+
// Pass timestamp if available for historical queries
17136
const balances = {};
18-
await sumBitcoinTokens({ balances, owners: allAddresses });
137+
await sumBitcoinTokens({ balances, owners: allAddresses, timestamp });
19138

20139
return balances;
21140
}
22141

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-
142+
async function zcashTvl(api) {
45143
const balances = {};
46144

145+
// Extract timestamp from api parameter
146+
const timestamp = api?.timestamp;
147+
const now = Date.now() / 1000;
148+
149+
// Determine block height for historical queries
150+
let blockHeight = null;
151+
if (timestamp && (now - timestamp) > 3600) {
152+
blockHeight = await timestampToBlockHeight(timestamp);
153+
}
154+
155+
// Fetch custodied amount from DCT supply endpoint with height header
156+
const supplyData = await apiRequest(`${ZRCHAIN_API}/dct/supply`, blockHeight);
157+
47158
// Find ASSET_ZENZEC in supplies array
48159
const zenZecSupply = supplyData.supplies?.find(
49160
item => item.supply?.asset === 'ASSET_ZENZEC'
@@ -62,6 +173,8 @@ async function zcashTvl() {
62173
}
63174

64175
module.exports = {
176+
timetravel: true,
177+
start: GENESIS_TIMESTAMP,
65178
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.',
66179
bitcoin: {
67180
tvl,

0 commit comments

Comments
 (0)