Skip to content
Merged
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
173 changes: 173 additions & 0 deletions src/adaptors/zest-v2/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
const axios = require('axios');
const {
callReadOnlyFunction,
contractPrincipalCV,
} = require('@stacks/transactions');
const { StacksMainnet } = require('@stacks/network');

const DEPLOYER = 'SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7';

const POOLS = [
{
symbol: 'STX',
vaultContract: 'v0-vault-stx',
assetAddress: 'SP1A27KFY4XERQCCRCARCYD1CC5N7M6688BSYADJ7',
contractName: 'wstx',
decimals: 6,
priceKeys: ['coingecko:blockstack'],
},
{
symbol: 'sBTC',
vaultContract: 'v0-vault-sbtc',
assetAddress: 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4',
contractName: 'sbtc-token',
decimals: 8,
priceKeys: [
'stacks:SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token',
'coingecko:bitcoin',
],
},
{
symbol: 'stSTX',
vaultContract: 'v0-vault-ststx',
assetAddress: 'SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG',
contractName: 'ststx-token',
decimals: 6,
priceKeys: [
'stacks:SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token',
'coingecko:blockstack',
],
},
{
symbol: 'USDC',
vaultContract: 'v0-vault-usdc',
assetAddress: 'SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE',
contractName: 'usdcx',
decimals: 6,
priceKeys: [
'stacks:SP120SBRBQJ00MCWS7TM5R8WJNTTKD5K0HFRC2CNE.usdcx',
'coingecko:usd-coin',
],
},
{
symbol: 'USDH',
vaultContract: 'v0-vault-usdh',
assetAddress: 'SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG',
contractName: 'usdh-token-v1',
decimals: 8,
priceKeys: [
'stacks:SPN5AKG35QZSK2M8GAMR4AFX45659RJHDW353HSG.usdh-token-v1',
'coingecko:usd-coin',
],
},
{
symbol: 'stSTXbtc',
vaultContract: 'v0-vault-ststxbtc',
assetAddress: 'SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG',
contractName: 'ststxbtc-token-v2',
decimals: 6,
priceKeys: [
'stacks:SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststxbtc-token-v2',
'coingecko:blockstack',
],
},
];

async function fetchPrices() {
const keys = [...new Set(POOLS.flatMap((p) => p.priceKeys))].join(',');
const url = `https://coins.llama.fi/prices/current/${keys}`;
const { data } = await axios.get(url);
return data.coins;
Comment on lines +76 to +80
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual code in the file
cat -n src/adaptors/zest-v2/index.js | head -100

Repository: DefiLlama/yield-server

Length of output: 3469


🏁 Script executed:

# Check if there's any error handling around the axios call
rg -A 10 -B 5 "fetchPrices" src/adaptors/zest-v2/index.js

Repository: DefiLlama/yield-server

Length of output: 906


🏁 Script executed:

# Look for how fetchPrices is called and if there's any wrapper error handling
rg "fetchPrices\(\)" src/adaptors/zest-v2/index.js

Repository: DefiLlama/yield-server

Length of output: 136


🌐 Web query:

axios default timeout behavior javascript library

💡 Result:

  • Axios’ default timeout is 0, which means no timeout (it will wait indefinitely unless something else aborts the request). [1][2]
  • timeout is milliseconds before Axios aborts the request (when set to a positive number). [1]
  • You can set it per request (axios.get(url, { timeout: 5000 })), globally (axios.defaults.timeout = 5000), or per instance (instance.defaults.timeout = 5000). [1][2]
  • For cases where a request may “hang” due to connection issues, Axios recommends combining timeout with cancellation (e.g., AbortController / AbortSignal.timeout(...)). [3]

Sources:
[1] Axios docs – Request Config (timeout: 1000, // default is 0 (no timeout)) (axios-http.com)
[2] Axios docs – Config defaults (notes default timeout is 0) (axios.rest)
[3] Axios docs – Cancellation (timeout vs cancellation, AbortSignal.timeout) (axios-http.com)

Citations:


Add a timeout to the price request.

axios.get() defaults to no timeout, so a hung llama.fi call will block the entire adapter indefinitely before any pools are emitted.

🛠️ Proposed fix
 async function fetchPrices() {
   const keys = [...new Set(POOLS.flatMap((p) => p.priceKeys))].join(',');
   const url = `https://coins.llama.fi/prices/current/${keys}`;
-  const { data } = await axios.get(url);
+  const { data } = await axios.get(url, { timeout: 10_000 });
   return data.coins;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adaptors/zest-v2/index.js` around lines 76 - 80, fetchPrices uses
axios.get without a timeout which can hang the adapter; update fetchPrices to
pass a timeout option to axios.get (e.g., axios.get(url, { timeout: <ms> })) so
the request fails fast, and handle the error path (catch) to avoid blocking pool
emission. Make changes in the fetchPrices function where keys, url, and
axios.get are used so the request includes a timeout value and errors are
handled appropriately.

}

function getPrice(prices, priceKeys) {
for (const key of priceKeys) {
if (prices[key]?.price) return { price: prices[key].price, key };
}
return null;
}

async function fetchApys(pool, network) {
const result = await callReadOnlyFunction({
contractAddress: DEPLOYER,
contractName: 'v0-1-data',
functionName: 'get-asset-apys',
network,
functionArgs: [contractPrincipalCV(pool.assetAddress, pool.contractName)],
senderAddress: DEPLOYER,
});

const tupleData = result.value.data;
const supplyApy = Number(tupleData['supply-apy'].value) / 100;
const borrowApy = Number(tupleData['borrow-apy'].value) / 100;

return { supplyApy, borrowApy };
}

async function fetchTotalAssets(pool, network) {
const result = await callReadOnlyFunction({
contractAddress: DEPLOYER,
contractName: pool.vaultContract,
functionName: 'get-total-assets',
network,
functionArgs: [],
senderAddress: DEPLOYER,
});

const rawAmount = Number(result.value.value);
return rawAmount / Math.pow(10, pool.decimals);
}

async function getZestV2Pools() {
try {
const network = new StacksMainnet();
const chain = 'Stacks';

const prices = await fetchPrices();

const results = [];

for (const pool of POOLS) {
try {
const priceResult = getPrice(prices, pool.priceKeys);
if (!priceResult) {
console.log(`Skipping ${pool.symbol}: price not available`);
continue;
}

const [apys, totalAssets] = await Promise.all([
fetchApys(pool, network),
fetchTotalAssets(pool, network),
]);

const tvlUsd = totalAssets * priceResult.price;

results.push({
pool: `${DEPLOYER}.${pool.vaultContract}-${chain}`.toLowerCase(),
chain: chain,
project: 'zest-v2',
symbol: pool.symbol,
tvlUsd: tvlUsd,
apyBase: apys.supplyApy,
apyBaseBorrow: apys.borrowApy,
underlyingTokens: [`${pool.assetAddress}.${pool.contractName}`],
token: `${pool.assetAddress}.${pool.contractName}`,
url: 'https://app.zestprotocol.com/market/main',
});
} catch (error) {
console.log(`Error processing pool ${pool.symbol}: ${error.message}`);
}
}

return results;
} catch (error) {
console.log(`Error in getZestV2Pools: ${error.message}`);
return [];
Comment on lines +163 to +165
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't mask adapter-wide failures as “zero pools”.

Returning [] here turns a transient upstream/RPC failure into a valid empty dataset, which can temporarily drop all Zest v2 markets from downstream consumers.

🛠️ Proposed fix
   } catch (error) {
-    console.log(`Error in getZestV2Pools: ${error.message}`);
-    return [];
+    console.error(`Error in getZestV2Pools: ${error.message}`);
+    throw error;
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.log(`Error in getZestV2Pools: ${error.message}`);
return [];
} catch (error) {
console.error(`Error in getZestV2Pools: ${error.message}`);
throw error;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/adaptors/zest-v2/index.js` around lines 164 - 166, The catch block inside
getZestV2Pools currently logs the error and returns [], which masks adapter-wide
failures as an empty dataset; change it to propagate the failure instead of
returning an empty array by removing the "return []" and rethrowing (or throwing
a new Error with contextual message) after logging so upstream can handle/retry
the error; ensure you reference getZestV2Pools in src/adaptors/zest-v2/index.js
when making the change.

}
}

module.exports = {
timetravel: false,
apy: getZestV2Pools,
url: 'https://app.zestprotocol.com/market/main',
};