Skip to content
Open
Show file tree
Hide file tree
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
11 changes: 9 additions & 2 deletions src/handlers/triggerEnrichment.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {
getLatestYieldForPool,
getYieldOffset,
getYieldAvg30d,
getVolatility30d,
getYieldLendBorrow,
} = require('../queries/yield');
const { getStat } = require('../queries/stat');
Expand Down Expand Up @@ -87,11 +88,17 @@ const main = async () => {
}
}

// add 30d avg apy
const avgApy30d = await getYieldAvg30d();
// add 30d avg apy and volatility metrics
const [avgApy30d, volatilityData] = await Promise.all([
getYieldAvg30d(),
getVolatility30d(),
]);
dataEnriched = dataEnriched.map((p) => ({
...p,
apyMean30d: avgApy30d[p.configID] ?? null,
apyMedian30d: volatilityData[p.configID]?.apyMedian30d ?? null,
apyStd30d: volatilityData[p.configID]?.apyStd30d ?? null,
apyCv30d: volatilityData[p.configID]?.apyCv30d ?? null,
}));
Comment on lines +91 to 102
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider isolating getVolatility30d failure so it doesn't crash the entire enrichment pipeline.

If the volatility materialized view is missing, stale, or the query fails for any reason, Promise.all will reject and the entire enrichment handler aborts — no pools data gets written to S3. Since volatility metrics are additive/non-critical, a failure there shouldn't take down the whole pipeline.

Suggested resilient approach
-  const [avgApy30d, volatilityData] = await Promise.all([
-    getYieldAvg30d(),
-    getVolatility30d(),
-  ]);
+  const [avgApy30d, volatilityData] = await Promise.all([
+    getYieldAvg30d(),
+    getVolatility30d().catch((err) => {
+      console.log('Failed to fetch volatility data, defaulting to empty', err);
+      return {};
+    }),
+  ]);

This way, if getVolatility30d fails, volatilityData defaults to {} and all pools simply get null for the three volatility fields via the existing ?? null fallback.

📝 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
// add 30d avg apy and volatility metrics
const [avgApy30d, volatilityData] = await Promise.all([
getYieldAvg30d(),
getVolatility30d(),
]);
dataEnriched = dataEnriched.map((p) => ({
...p,
apyMean30d: avgApy30d[p.configID] ?? null,
apyMedian30d: volatilityData[p.configID]?.apyMedian30d ?? null,
apyStd30d: volatilityData[p.configID]?.apyStd30d ?? null,
apyCv30d: volatilityData[p.configID]?.apyCv30d ?? null,
}));
// add 30d avg apy and volatility metrics
const [avgApy30d, volatilityData] = await Promise.all([
getYieldAvg30d(),
getVolatility30d().catch((err) => {
console.log('Failed to fetch volatility data, defaulting to empty', err);
return {};
}),
]);
dataEnriched = dataEnriched.map((p) => ({
...p,
apyMean30d: avgApy30d[p.configID] ?? null,
apyMedian30d: volatilityData[p.configID]?.apyMedian30d ?? null,
apyStd30d: volatilityData[p.configID]?.apyStd30d ?? null,
apyCv30d: volatilityData[p.configID]?.apyCv30d ?? null,
}));
🤖 Prompt for AI Agents
In `@src/handlers/triggerEnrichment.js` around lines 91 - 102, The Promise.all
call makes the whole enrichment pipeline fail if getVolatility30d rejects;
change to a resilient pattern (e.g., use Promise.allSettled or catch
getVolatility30d separately) so getYieldAvg30d still resolves and volatilityData
defaults to an empty object on error; specifically, keep calling getYieldAvg30d
as before, call getVolatility30d in a way that captures errors and sets
volatilityData = {} on failure, then continue the dataEnriched =
dataEnriched.map(...) step so apyMedian30d/apyStd30d/apyCv30d fall back to null.


// add info about stablecoin, exposure etc.
Expand Down
33 changes: 33 additions & 0 deletions src/queries/yield.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,38 @@ const getYieldAvg30d = async () => {
return responseObject;
};

// get 30day volatility metrics from materialized view
const getVolatility30d = async () => {
const conn = await connect();

const query = `
SELECT
"configID",
apy_median_30d as "apyMedian30d",
apy_std_30d as "apyStd30d",
cv_30d as "apyCv30d"
FROM volatility
`;

const response = await conn.query(query);

if (!response) {
return new AppError("Couldn't get volatility data", 404);
}

// reformat to object keyed by configID
const responseObject = {};
for (const p of response) {
responseObject[p.configID] = {
apyMedian30d: p.apyMedian30d,
apyStd30d: p.apyStd30d,
apyCv30d: p.apyCv30d,
};
}

return responseObject;
};
Comment on lines +321 to +351
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

return new AppError(...) silently swallows the error instead of propagating it.

This replicates a pre-existing pattern in the file, but it's worth calling out: return new AppError(...) hands an AppError object back to the caller as a successful return value rather than throwing. In triggerEnrichment.js, Promise.all won't catch this, so volatilityData could silently become an AppError instance instead of the expected object.

In practice, this is partially mitigated by the optional chaining (volatilityData[p.configID]?.apyMedian30d ?? null) which will default to null for every pool. However, this means a silent total loss of volatility data with no log or alert.

Consider throwing or logging on error:

Suggested fix
   const response = await conn.query(query);
 
   if (!response) {
-    return new AppError("Couldn't get volatility data", 404);
+    throw new AppError("Couldn't get volatility data", 404);
   }

Note: The same return new AppError(...) pattern exists in every other query function in this file. A follow-up to convert them all to throw (or at least add logging) would improve reliability.

📝 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
// get 30day volatility metrics from materialized view
const getVolatility30d = async () => {
const conn = await connect();
const query = `
SELECT
"configID",
apy_median_30d as "apyMedian30d",
apy_std_30d as "apyStd30d",
cv_30d as "apyCv30d"
FROM volatility
`;
const response = await conn.query(query);
if (!response) {
return new AppError("Couldn't get volatility data", 404);
}
// reformat to object keyed by configID
const responseObject = {};
for (const p of response) {
responseObject[p.configID] = {
apyMedian30d: p.apyMedian30d,
apyStd30d: p.apyStd30d,
apyCv30d: p.apyCv30d,
};
}
return responseObject;
};
// get 30day volatility metrics from materialized view
const getVolatility30d = async () => {
const conn = await connect();
const query = `
SELECT
"configID",
apy_median_30d as "apyMedian30d",
apy_std_30d as "apyStd30d",
cv_30d as "apyCv30d"
FROM volatility
`;
const response = await conn.query(query);
if (!response) {
throw new AppError("Couldn't get volatility data", 404);
}
// reformat to object keyed by configID
const responseObject = {};
for (const p of response) {
responseObject[p.configID] = {
apyMedian30d: p.apyMedian30d,
apyStd30d: p.apyStd30d,
apyCv30d: p.apyCv30d,
};
}
return responseObject;
};
🤖 Prompt for AI Agents
In `@src/queries/yield.js` around lines 321 - 351, The getVolatility30d function
currently returns new AppError(...) on failure which treats the error as a
successful return value; change this to throw the AppError (or throw a plain
Error) so callers can catch it, and optionally log the failure before throwing;
locate the early check in getVolatility30d (after const response = await
conn.query(query)) and replace the "return new AppError(...)" with throwing the
error (e.g., throw new AppError(...)) so Promise.all consumers and
triggerEnrichment.js will receive a rejection instead of an AppError object.


// multi row insert query generator
const buildInsertYieldQuery = (payload) => {
// note: even though apyBase and apyReward are optional fields
Expand Down Expand Up @@ -356,4 +388,5 @@ module.exports = {
getYieldLendBorrow,
buildInsertYieldQuery,
getYieldAvg30d,
getVolatility30d,
};
3 changes: 3 additions & 0 deletions src/utils/enrichedColumns.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const poolsResponseColumns = [
'il7d',
'apyBase7d',
'apyMean30d',
'apyMedian30d',
'apyStd30d',
'apyCv30d',
'volumeUsd1d',
'volumeUsd7d',
'apyBaseInception',
Expand Down
Loading