Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
154 changes: 154 additions & 0 deletions projects/helper/bitcoin-book/fetchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,158 @@ module.exports = {
})
return Array.from(new Set(staticAddresses))
},
zenrock: async () => {
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, {
fetcher: async () => {
async function getBitcoinAddresses() {
const btcAddresses = [];
let nextKey = null;
try {
while (true) {
let url = ZRCHAIN_WALLETS_API;
if (nextKey) {
url += `?pagination.key=${encodeURIComponent(nextKey)}`;
}
const { data } = await axios.get(url);
if (data.zenbtc_wallets && Array.isArray(data.zenbtc_wallets)) {
for (const walletGroup of data.zenbtc_wallets) {
if (walletGroup.wallets && Array.isArray(walletGroup.wallets)) {
for (const wallet of walletGroup.wallets) {
if (wallet.type === 'WALLET_TYPE_BTC_MAINNET' && wallet.address) {
btcAddresses.push(wallet.address);
}
}
}
}
}
if (data.pagination && data.pagination.next_key) {
nextKey = data.pagination.next_key;
} else {
break;
}
}
return btcAddresses;
} catch (error) {
sdk.log(`Error fetching Bitcoin addresses from zrchain API: ${error.message}`);
return [];
}
}

async function getChangeAddresses() {
const changeAddresses = [];
try {
const { data: paramsData } = await axios.get(ZENBTC_PARAMS_API);
const changeAddressKeyIDs = paramsData.params?.changeAddressKeyIDs || [];
for (const keyID of changeAddressKeyIDs) {
try {
const { data: keyData } = await axios.get(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_BTC_MAINNET/`);
if (keyData.wallets && Array.isArray(keyData.wallets)) {
for (const wallet of keyData.wallets) {
if (wallet.type === 'WALLET_TYPE_BTC_MAINNET' && wallet.address) {
changeAddresses.push(wallet.address);
}
}
}
} catch (error) {
sdk.log(`Error fetching change address for key ID ${keyID}: ${error.message}`);
}
}
return changeAddresses;
} catch (error) {
sdk.log(`Error fetching change addresses from zenbtc params: ${error.message}`);
return [];
}
}

const [btcAddresses, changeAddresses] = await Promise.all([
getBitcoinAddresses(),
getChangeAddresses(),
]);
const allAddresses = [...btcAddresses, ...changeAddresses];
sdk.log(`Zenrock: Fetched ${btcAddresses.length} treasury addresses + ${changeAddresses.length} change addresses = ${allAddresses.length} total`);
return allAddresses;
}
});
},
zenrockDCT: async () => {
const ZRCHAIN_WALLETS_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/zenbtc_wallets';
const DCT_PARAMS_API = 'https://api.diamond.zenrocklabs.io/dct/params';
const ZRCHAIN_KEY_BY_ID_API = 'https://api.diamond.zenrocklabs.io/zrchain/treasury/key_by_id';

return getConfig('zenrock/dct_addresses', undefined, {
fetcher: async () => {
async function getZcashAddresses() {
const zecAddresses = [];
let nextKey = null;
try {
while (true) {
let url = ZRCHAIN_WALLETS_API;
if (nextKey) {
url += `?pagination.key=${encodeURIComponent(nextKey)}`;
}
const { data } = await axios.get(url);
if (data.zenbtc_wallets && Array.isArray(data.zenbtc_wallets)) {
for (const walletGroup of data.zenbtc_wallets) {
if (walletGroup.wallets && Array.isArray(walletGroup.wallets)) {
for (const wallet of walletGroup.wallets) {
if (wallet.type === 'WALLET_TYPE_ZCASH_MAINNET' && wallet.address) {
zecAddresses.push(wallet.address);
}
}
}
}
}
if (data.pagination && data.pagination.next_key) {
nextKey = data.pagination.next_key;
} else {
break;
}
}
return zecAddresses;
} catch (error) {
sdk.log(`Error fetching Zcash addresses from zrchain API: ${error.message}`);
return [];
}
}

async function getChangeAddresses() {
const changeAddresses = [];
try {
const { data: paramsData } = await axios.get(DCT_PARAMS_API);
const changeAddressKeyIDs = paramsData.params?.assets?.[0]?.change_address_key_ids || [];
for (const keyID of changeAddressKeyIDs) {
try {
const { data: keyData } = await axios.get(`${ZRCHAIN_KEY_BY_ID_API}/${keyID}/WALLET_TYPE_ZCASH_MAINNET/`);
if (keyData.wallets && Array.isArray(keyData.wallets)) {
for (const wallet of keyData.wallets) {
if (wallet.type === 'WALLET_TYPE_ZCASH_MAINNET' && wallet.address) {
changeAddresses.push(wallet.address);
}
}
}
} catch (error) {
sdk.log(`Error fetching change address for key ID ${keyID}: ${error.message}`);
}
}
return changeAddresses;
} catch (error) {
sdk.log(`Error fetching change addresses from dct params: ${error.message}`);
return [];
}
}

const [zecAddresses, changeAddresses] = await Promise.all([
getZcashAddresses(),
getChangeAddresses(),
]);
const allAddresses = [...zecAddresses, ...changeAddresses];
sdk.log(`Zenrock DCT: Fetched ${zecAddresses.length} treasury addresses + ${changeAddresses.length} change addresses = ${allAddresses.length} total`);
return allAddresses;
}
});
},
}
174 changes: 174 additions & 0 deletions projects/helper/chain/zcash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
const sdk = require('@defillama/sdk')
const { get, post } = require('../http')
const { getEnv } = require('../env')
const { getUniqueAddresses } = require('../tokenMapping')
const { RateLimiter } = require("limiter");
const { sliceIntoChunks, sleep } = require('../utils');

// Zcash block explorer API endpoints
const url = addr => 'https://api.zcha.in/v2/mainnet/accounts/' + addr
const url2 = addr => 'https://api.zcashnetwork.io/api/v1/addresses/' + addr + '/balance'

const delay = 3 * 60 * 60 // 3 hours
const balancesNow = {}

const zcashCacheEnv = getEnv('ZCASH_CACHE_API')

const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 10_000 });

async function cachedZECBalCall(owners, retriesLeft = 2) {
try {
const res = await post(zcashCacheEnv, { addresses: owners, network: 'ZEC' })
return res
} catch (e) {
console.error('cachedZECBalCall error', e.toString())
if (retriesLeft > 0) {
return await cachedZECBalCall(owners, retriesLeft - 1)
}
throw e
}
}

async function getCachedZcashBalances(owners) {
const chunks = sliceIntoChunks(owners, 700)
sdk.log('zcash cache api call: ', owners.length, chunks.length)
let sum = 0
let i = 0
for (const chunk of chunks) {
const res = await cachedZECBalCall(chunk)
sdk.log(i++, sum / 1e8, res / 1e8, chunk.length)
sum += +res
}
return sum
}

async function _sumTokensBlockchain({ balances = {}, owners = [], forceCacheUse, }) {
if (zcashCacheEnv && owners.length > 51) {
if (owners.length > 1000) forceCacheUse = true
try {
const res = await getCachedZcashBalances(owners)
sdk.util.sumSingleBalance(balances, 'zcash', res / 1e8)
return balances

} catch (e) {
if (forceCacheUse) throw e
sdk.log('zcash cache error', e.toString())
}
}
console.time('zcash' + owners.length + '___' + owners[0])
const STEP = 10 // Smaller batches for Zcash API
for (let i = 0; i < owners.length; i += STEP) {
const chunk = owners.slice(i, i + STEP)
// Query addresses individually since batch APIs aren't reliable
for (const addr of chunk) {
try {
const balance = await getBalanceNow(addr)
sdk.util.sumSingleBalance(balances, 'zcash', balance)
} catch (err) {
sdk.log('zcash balance error', addr, err.toString())
}
}
await sleep(2000) // Rate limiting
}

console.timeEnd('zcash' + owners.length + '___' + owners[0])
return balances
}

const withLimiter = (fn, tokensToRemove = 1) => async (...args) => {
await limiter.removeTokens(tokensToRemove);
return fn(...args);
}

const sumTokensBlockchain = withLimiter(_sumTokensBlockchain)

async function getBalanceNow(addr) {
if (balancesNow[addr]) return balancesNow[addr]
try {
// Try zcha.in API first - returns balance in Zatoshi (smallest unit, like satoshis)
const response = await get(url(addr))
if (response && (response.balance !== undefined || response.balance !== null)) {
balancesNow[addr] = (response.balance || 0) / 1e8
return balancesNow[addr]
}
} catch (e) {
sdk.log('zcha.in zcash balance error', addr, e.toString())
}

try {
// Fallback to zcashnetwork.io API
const response = await get(url2(addr))
if (response && (response.balance !== undefined || response.balance !== null)) {
balancesNow[addr] = (response.balance || 0) / 1e8
return balancesNow[addr]
}
} catch (e) {
sdk.log('zcashnetwork.io balance error', addr, e.toString())
}

// Default to 0 if no balance found
balancesNow[addr] = 0
return balancesNow[addr]
}

async function sumTokens({ balances = {}, owners = [], timestamp, forceCacheUse, }) {
if (typeof timestamp === "object" && timestamp.timestamp) timestamp = timestamp.timestamp
owners = getUniqueAddresses(owners, 'zcash')
const now = Date.now() / 1e3

if (!timestamp || (now - timestamp) < delay) {
try {
await sumTokensBlockchain({ balances, owners, forceCacheUse })
return balances
} catch (e) {
sdk.log('zcash sumTokens error', e.toString())
}
}
if (forceCacheUse) throw new Error('timestamp is too old, cant pull with forceCacheUse flag set')

for (const addr of owners)
sdk.util.sumSingleBalance(balances, 'zcash', await getBalance(addr, timestamp))
return balances
}

// get archive ZEC balance
async function getBalance(addr, timestamp) {
try {
const endpoint = url(addr) + '/transactions'
const response = await get(endpoint)
const txs = response.data?.[addr]?.transactions || []

let balance = 0
for (const tx of txs) {
if (tx.time && tx.time <= timestamp) {
// Process outputs (received)
if (tx.outputs) {
for (const output of tx.outputs) {
if (output.recipient === addr) {
balance += (output.value || 0) / 1e8
}
}
}
// Process inputs (spent)
if (tx.inputs) {
for (const input of tx.inputs) {
if (input.recipient === addr) {
balance -= (input.value || 0) / 1e8
}
}
}
}
}

return balance
} catch (e) {
sdk.log('zcash getBalance error', addr, e.toString())
// Fallback to current balance if historical lookup fails
return await getBalanceNow(addr)
}
}

module.exports = {
sumTokens
}

1 change: 1 addition & 0 deletions projects/helper/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const ENV_KEYS = [
'RPC_PROXY_URL',
'BLACKSAIL_API_KEY',
'BITCOIN_CACHE_API',
'ZCASH_CACHE_API',
'DEBANK_API_KEY',
'SMARDEX_SUBGRAPH_API_KEY',
'PROXY_AUTH',
Expand Down
1 change: 1 addition & 0 deletions projects/helper/sumTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const helpers = {
"near": require("./chain/near"),
"bitcoin": require("./chain/bitcoin"),
"litecoin": require("./chain/litecoin"),
"zcash": require("./chain/zcash"),
"polkadot": require("./chain/polkadot"),
"acala": require("./chain/acala"),
"bifrost": require("./chain/bifrost"),
Expand Down
2 changes: 1 addition & 1 deletion projects/helper/tokenMapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const ibcChains = ['ibc', 'terra', 'terra2', 'crescent', 'osmosis', 'kujira', 's
'kopi', 'elys', "pryzm", "mantra", 'agoric', 'band',
'celestia', 'dydx', 'carbon', 'milkyway', 'regen', 'sommelier', 'stride', 'prom', 'babylon', 'xion'
]
const caseSensitiveChains = [...ibcChains, ...svmChains, 'tezos', 'ton', 'algorand', 'aptos', 'near', 'bitcoin', 'waves', 'tron', 'litecoin', 'polkadot', 'ripple', 'elrond', 'cardano', 'stacks', 'sui', 'ergo', 'mvc', 'renec', 'doge', 'stellar', 'massa',
const caseSensitiveChains = [...ibcChains, ...svmChains, 'tezos', 'ton', 'algorand', 'aptos', 'near', 'bitcoin', 'waves', 'tron', 'litecoin', 'polkadot', 'ripple', 'elrond', 'cardano', 'stacks', 'sui', 'ergo', 'mvc', 'renec', 'doge', 'stellar', 'massa', 'zcash',
'eclipse', 'acala', 'aelf', 'aeternity', 'alephium', 'bifrost', 'bittensor', 'verus',
]

Expand Down
Loading
Loading