1+ const { queryContract } = require ( '../helper/chain/cosmos' ) ;
2+ const { transformBalances } = require ( '../helper/portedTokens' ) ;
3+
4+ const FACTORY_CONTRACT = 'zig1xx3aupmgv3ce537c0yce8zzd3sz567syaltr2tdehu3y803yz6gsc6tz85' ;
5+ const API_CALL_DELAY = 10 ; // Rate limit throttling for public REST API
6+ const MAX_RETRIES = 3 ;
7+ const RETRY_DELAY = 1000 ; // Initial retry delay in ms
8+
9+ const STZIG_DENOM = 'coin.zig109f7g2rzl2aqee7z6gffn8kfe9cpqx0mjkk7ethmx8m2hq4xpe9snmaam2.stzig' ;
10+
11+ // Valdora Staker contract address (https://docs.valdora.finance/smart-contracts)
12+ const VALDORA_STAKER_CONTRACT = 'zig18nnde5tpn76xj3wm53n0tmuf3q06nruj3p6kdemcllzxqwzkpqzqk7ue55' ;
13+
14+ /**
15+ * Fetches the conversion rate from stZIG to uZIG using the redeem-side quote from the Valdora Staker contract
16+ *
17+ * How it works:
18+ * 1. Query the contract with reverse_st_zig_price: "If I redeem X uZIG worth of stZIG, how much stZIG do I get?"
19+ * 2. The contract responds with stzig_amount (amount of stZIG received for probe_uzig uZIG)
20+ * 3. We invert this: uzig_per_stzig = probe_uzig / stzig_amount
21+ *
22+ * We use a large probe amount (1,000 ZIG) for precision:
23+ * - Larger probe = more precision in integer division
24+ * - Reduces rounding errors when calculating the ratio
25+ * - Result is scaled by 1e6 to maintain precision (returns ratio * 1,000,000)
26+ *
27+ * Returns the ratio of uZIG per 1 stZIG, scaled by 1e6 (or null if query fails)
28+ * Example: if 1 stZIG = 0.989 ZIG, returns ~989,000
29+ */
30+ async function fetchUzigPerStzig ( ) {
31+ // Use 1,000 ZIG (1 billion uZIG) as probe for better precision in integer math
32+ // This is the amount of uZIG we're "simulating" a redeem with
33+ const probeUzig = 1_000_000_000 ; // 1,000 ZIG in uZIG (base units with 6 decimals)
34+
35+ // Query: "If I redeem this much uZIG, how much stZIG do I get?"
36+ // The contract uses reverse pricing logic (redeem path)
37+ const { stzig_amount } = await queryContract ( {
38+ contract : VALDORA_STAKER_CONTRACT ,
39+ chain : 'zigchain' ,
40+ data : { reverse_st_zig_price : { amount : String ( probeUzig ) } } ,
41+ } ) ;
42+
43+ if ( ! stzig_amount || stzig_amount === '0' ) return null ;
44+
45+ // Calculate: uzig_per_stzig = probe_uzig / stzig_amount
46+ // We scale by 1e6 to maintain precision: (probe * 1e6) / stzig_amount
47+ // This gives us the ratio scaled by 1,000,000
48+ // Example: if probe=1e9 and stzig_amount=989580475, then ratio_scaled = ~1,010,528
49+ return ( BigInt ( probeUzig ) * 1_000_000n ) / BigInt ( stzig_amount ) ;
50+ }
51+
52+ function sleep ( ms ) {
53+ return new Promise ( resolve => setTimeout ( resolve , ms ) ) ;
54+ }
55+
56+ async function withRetry ( fn , maxAttempts = MAX_RETRIES ) {
57+ for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) {
58+ try {
59+ return await fn ( ) ;
60+ } catch ( error ) {
61+ if ( attempt === maxAttempts ) {
62+ throw error ;
63+ }
64+ const delay = RETRY_DELAY * attempt ; // Exponential backoff
65+ console . error ( `Attempt ${ attempt } failed, retrying in ${ delay } ms:` , error . message ) ;
66+ await sleep ( delay ) ;
67+ }
68+ }
69+ }
70+
71+ async function getAllPairs ( ) {
72+ const allPairs = [ ] ;
73+ let startAfter = undefined ;
74+ const limit = 30 ;
75+
76+ while ( true ) {
77+ const query = { pairs : { limit } } ;
78+ if ( startAfter ) {
79+ query . pairs . start_after = startAfter ;
80+ }
81+
82+ try {
83+ const response = await withRetry ( ( ) => queryContract ( {
84+ contract : FACTORY_CONTRACT ,
85+ chain : 'zigchain' ,
86+ data : query
87+ } ) ) ;
88+
89+ if ( ! response . pairs || response . pairs . length === 0 ) {
90+ break ;
91+ }
92+
93+ allPairs . push ( ...response . pairs ) ;
94+
95+ const lastPair = response . pairs [ response . pairs . length - 1 ] ;
96+ const nextCursor = { asset_infos : lastPair . asset_infos , pair_type : lastPair . pair_type } ;
97+ if ( response . pairs . length < limit ) {
98+ break ;
99+ }
100+ if ( startAfter && JSON . stringify ( startAfter ) === JSON . stringify ( nextCursor ) ) {
101+ break ;
102+ }
103+ startAfter = nextCursor ;
104+ await sleep ( API_CALL_DELAY ) ;
105+ } catch ( error ) {
106+ console . error ( 'Failed to fetch pairs from factory:' , error ) ;
107+ throw error ;
108+ }
109+ }
110+
111+ return allPairs ;
112+ }
113+
114+ async function getPoolInfo ( contractAddr ) {
115+ return await withRetry ( ( ) => queryContract ( {
116+ contract : contractAddr ,
117+ chain : 'zigchain' ,
118+ data : { pool : { } }
119+ } ) ) ;
120+ }
121+
122+ function getAssetKey ( assetInfo ) {
123+ if ( assetInfo . native_token ) {
124+ return assetInfo . native_token . denom ;
125+ } else if ( assetInfo . token ) {
126+ return assetInfo . token . contract_addr ;
127+ }
128+ return null ;
129+ }
130+
131+ async function tvl ( api ) {
132+ try {
133+ const pairs = await getAllPairs ( ) ;
134+
135+ for ( const pair of pairs ) {
136+ const poolInfo = await getPoolInfo ( pair . contract_addr ) ;
137+ if ( ! poolInfo || ! poolInfo . assets ) {
138+ continue ;
139+ }
140+
141+ for ( const asset of poolInfo . assets ) {
142+ const assetKey = getAssetKey ( asset . info ) ;
143+ if ( assetKey && asset . amount ) {
144+ api . add ( assetKey , asset . amount ) ;
145+ }
146+ }
147+
148+ await sleep ( API_CALL_DELAY ) ;
149+ }
150+ const balances = api . getBalances ( ) ;
151+
152+ // Convert stZIG balances to uZIG equivalent for TVL calculation
153+ const stzigKeyRaw = STZIG_DENOM ;
154+ const stzigKeyPrefixed = `zigchain:${ STZIG_DENOM } ` ;
155+ const stzigBalStr = balances [ stzigKeyPrefixed ] || balances [ stzigKeyRaw ] ;
156+
157+ if ( stzigBalStr ) {
158+ // Fetch the current conversion rate from on-chain quote (redeem path)
159+ const ratioScaled = await fetchUzigPerStzig ( ) ;
160+
161+ if ( ratioScaled ) {
162+ // Convert stZIG balance to uZIG equivalent
163+ // Formula: uzig_equivalent = (stzig_balance * ratio_scaled) / 1_000_000
164+ // We divide by 1_000_000 to remove the scaling we added in fetchUzigPerStzig
165+ const stzigBal = BigInt ( stzigBalStr ) ;
166+ const uzigEq = ( stzigBal * ratioScaled ) / 1_000_000n ;
167+
168+ // Remove stZIG from balances (we've converted it to uZIG)
169+ delete balances [ stzigKeyPrefixed ] ;
170+ delete balances [ stzigKeyRaw ] ;
171+
172+ // Add the uZIG equivalent to the existing uZIG balance
173+ const uzigKey = 'zigchain:uzig' ;
174+ const currentUzig = balances [ uzigKey ] ? BigInt ( balances [ uzigKey ] ) : 0n ;
175+ balances [ uzigKey ] = ( currentUzig + uzigEq ) . toString ( ) ;
176+ }
177+ }
178+ return transformBalances ( 'zigchain' , balances ) ;
179+ } catch ( error ) {
180+ console . error ( 'Error calculating TVL:' , error ) ;
181+ throw error ;
182+ }
183+ }
184+
185+ module . exports = {
186+ timetravel : false ,
187+ misrepresentedTokens : false ,
188+ methodology : 'TVL is calculated by summing all assets locked in OroSwap liquidity pools on ZIGChain' ,
189+ zigchain : { tvl }
190+ } ;
0 commit comments