Skip to content

Conversation

mastilver
Copy link
Contributor

@mastilver mastilver commented Sep 2, 2025

Summary by CodeRabbit

  • New Features

    • Solv strategy APYs (e.g., BTC+ and partner protocols) exposed when pool data is absent.
    • Ethereum (mainnet) registration for Solv tokens so mainnet-registered tokens appear across chains.
    • Per-token incentive points now combine strategy-level and token-level weights.
  • Improvements

    • Faster incentive loading by fetching external pool data and Solv APYs in parallel.
    • Prefers pool APYs, falls back to Solv APYs or zero APY while preserving points.
    • Normalized reward-token resolution and a time-limited APY override for a hybrid BTC pool.

Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds mainnet (Ethereum) support for SOLV tokens, maps Solv token addresses to strategy labels, parallelizes DefiLlama and Solv APY fetches, resolves reward tokens across Bob/Optimism/Mainnet, computes per-token incentive points, applies a HybridBTC 2% base-APY override before a fixed date, and falls back to Solv or zero-APY when needed.

Changes

Cohort / File(s) Summary of edits
Strategy incentives and APY integration
sdk/src/gateway/strategy.ts
Add tokenToSolvStrategyMap and hybridBTCEndDate; parallelize fetching of DefiLlama pools and Solv GraphQL APYs via Promise.all; compute per-token points as project + token points; prefer DefiLlama pool data (with HybridBTC 2% override before end date); fallback to Solv APYs by mapped strategy or zero-APY; add private helpers getDefillamaPools(), getSolvAPYs(), and resolveTokens() to build pool/APY maps and map reward token addresses across chains.
Token registry and chain support
sdk/src/gateway/tokens.ts
Add ethereum (mainnet) token address support and ethereumTokens; import mainnet from viem/chains; register mainnet in SYMBOL_LOOKUP/ADDRESS_LOOKUP; extend token entry typing to allow an ethereum address and ingest mainnet addresses when present.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Client
    participant Strategy as getTokensIncentives()
    participant Llama as getDefillamaPools()
    participant Solv as getSolvAPYs()
    participant Addr as resolveTokens()/ADDRESS_LOOKUP

    Client->>Strategy: request incentives for tokens
    par Parallel fetch
        Strategy->>Llama: fetch DefiLlama pools (filter Bob)
        Strategy->>Solv: fetch Solv GraphQL APYs (BTC+ and protocols)
    end
    Strategy->>Strategy: compute points = projectPointsIncentives + tokensPointsIncentives

    alt DefiLlama pool found
        Note right of Strategy #DDEBF7: If pool is HybridBTC and now < hybridBTCEndDate → apyBase = 2%
        Strategy->>Addr: resolve pool.rewardTokens to public addresses (Bob/Optimism/Mainnet)
        Strategy->>Client: return {apyBase, apyReward, rewardTokens, points}
    else solv strategy mapped
        Strategy->>Addr: resolve solvAPYs[strategy].rewardTokens
        Strategy->>Client: return {apyBase, apyReward, rewardTokens, points}
    else No match
        Strategy->>Client: return {apyBase: 0, apyReward: 0, rewardTokens: [], points}
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • gregdhill
  • nakul1010
  • danielsimao

Poem

Hop hop, I fetch APYs two by two,
Llama hums while Solv sings true,
Points in my pouch, rewards take flight,
Hybrid carrots gleam tonight,
A rabbit cheers the yields — chew-chew! 🥕🐇


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fa71cca and 1d7e531.

📒 Files selected for processing (1)
  • sdk/src/gateway/strategy.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/src/gateway/strategy.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-yield-to-solv-tokens

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

vercel bot commented Sep 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
bob-docs Ready Ready Preview Comment Sep 4, 2025 8:37am

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Summary of Changes

Hello @mastilver, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request integrates Solv strategy APY data into the existing system, expanding the coverage of Annual Percentage Yields for various tokens. The changes enable the application to retrieve and display APYs for Solv-related assets, providing users with more comprehensive and accurate incentive information.

Highlights

  • New Solv Strategy Mapping: Introduced a new map, tokenToSolvStrategyMap, to associate specific Solv token addresses (like Solv BTC+ and SolvBTC.JUP) with their corresponding strategy names.
  • Enhanced Token Incentives Retrieval: The getTokensIncentives method has been updated to concurrently fetch APY data from both DefiLlama and a new Solv-specific API, allowing for a broader range of APY calculations.
  • Dedicated Solv APY Fetcher: A new private method, getSolvAPYs, was added to handle the fetching and processing of APY data specifically from Solv's GraphQL endpoint, including error handling.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request adds functionality to fetch APYs for Solv strategies. The implementation introduces parallel fetching of data which is good for performance. However, my review has identified a couple of critical issues related to error handling that could lead to application crashes. Specifically, there's a lack of validation for API responses from the Solv service and insufficient checks before accessing potentially undefined data. I've also included a medium-severity suggestion to improve the robustness of how the GraphQL request is constructed. Addressing these points will make the new functionality more resilient.

Comment on lines 234 to 240
if (solvStrategy) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: [],
points,
};

Choose a reason for hiding this comment

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

critical

If getSolvAPYs() fails and returns an empty object, solvAPYs[solvStrategy] will be undefined. Attempting to access properties on it (e.g., .apyBase) will cause a runtime TypeError. You should add a check to ensure solvAPYs[solvStrategy] is defined before using it.

Suggested change
if (solvStrategy) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: [],
points,
};
if (solvStrategy && solvAPYs[solvStrategy]) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: [],
points,
};
}

Comment on lines 439 to 456
const data = await res.json();

const apys = {
// TODO: should include Solv token as reward token
'BTC+': {
apyBase: Number(data.data.btcPlusStats.baseApy),
apyReward: Number(data.data.btcPlusStats.rewardApy),
}
}

data.data.lsts.details.forEach((pool) => {
apys[pool.protocol] = {
apyBase: Number(pool.apy),
apyReward: 0,
}
});

return apys;

Choose a reason for hiding this comment

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

critical

The code proceeds to parse the JSON response and access its properties (data.data.btcPlusStats) without first checking if the fetch request was successful (e.g., res.ok). If the API returns an error status code (like a 500 or 404), this will likely cause a crash when trying to access properties on a non-existent or error-structured response object. You should always check the response status and validate the data structure before using it.

            if (!res.ok) {
                const errorBody = await res.text();
                throw new Error(`Failed to fetch APY data from Solv: ${res.status} ${res.statusText} - ${errorBody}`);
            }
            const data = await res.json();

            if (!data.data?.btcPlusStats || !data.data?.lsts?.details) {
                console.warn('Received unexpected data structure from Solv API', data);
                return {};
            }

            const apys = {
                // TODO: should include Solv token as reward token
                'BTC+': {
                    apyBase: Number(data.data.btcPlusStats.baseApy),
                    apyReward: Number(data.data.btcPlusStats.rewardApy),
                }
            }

            data.data.lsts.details.forEach((pool) => {
                apys[pool.protocol] = {
                    apyBase: Number(pool.apy),
                    apyReward: 0,
                }
            });

            return apys;

Comment on lines 408 to 437
const query =`
query SolvAPYs {
btcPlusStats(stageNo: 1) {
baseApy
rewardApy
tvl
}
lsts {
details {
protocol
apy
estApy
tvlUsd
}
}
}
`.split("\n").join(" ");

const res = await fetch("https://sft-api.com/graphql", {
"headers": {
"content-type": "application/json",
"Authorization": "solv",
},
"body": `{
"operationName": "SolvAPYs",
"variables": {},
"query": "${query}"
}`,
"method": "POST",
});

Choose a reason for hiding this comment

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

medium

The GraphQL request body is constructed by manually creating a JSON string and manipulating the query string. This approach is fragile and can be simplified. A more robust and readable approach is to build a JavaScript object for the body and then serialize it using JSON.stringify. This also makes the .split("\n").join(" ") transformation on the query string unnecessary.

            const query =`
                query SolvAPYs {
                    btcPlusStats(stageNo: 1) {
                        baseApy
                        rewardApy
                        tvl
                    }
                    lsts {
                        details {
                            protocol
                            apy
                            estApy
                            tvlUsd
                        }
                    }
                }
            `;

            const res = await fetch("https://sft-api.com/graphql", {
                method: "POST",
                headers: {
                    "content-type": "application/json",
                    "Authorization": "solv",
                },
                body: JSON.stringify({
                    operationName: "SolvAPYs",
                    variables: {},
                    query,
                }),
            });

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
sdk/src/gateway/strategy.ts (2)

196-203: Good parallelization; consider lightweight caching.

These two network calls are re-done per request; add a short TTL cache (e.g., 60s) to reduce latency and rate-limit exposure.


163-175: Match Solv strategy addresses with tokens.ts entries and use checksummed casing

  • 0x4ca70811e831db42072cba1f0d03496ef126faad not found in tokens.ts; verify it’s correct and add with identical casing.
  • Strategy.ts lowercases 0x6b062aa7f5fc52b530cb13967ae2e6bc0d8dd3e4, but tokens.ts uses 0x6b062AA7F5FC52b530Cb13967aE2E6bc0D8Dd3E4; update to checksum casing.
const tokenToSolvStrategyMap: ReadonlyMap<string, 'BTC+' | 'Jupiter'> = new Map([...]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 85f09eb and f2621dd.

📒 Files selected for processing (1)
  • sdk/src/gateway/strategy.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk/src/gateway/strategy.ts (2)
sdk/src/gateway/tokens.ts (2)
  • tokenToStrategyTypeMap (398-405)
  • ADDRESS_LOOKUP (359-359)
sdk/src/gateway/types.ts (1)
  • DefiLlamaPool (525-535)
🪛 GitHub Actions: SDK
sdk/src/gateway/strategy.ts

[warning] 1-1: Code style issues found in the above file. Run Prettier with --write to fix.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (3)
sdk/src/gateway/strategy.ts (3)

208-212: Points composition LGTM.

Combines project- and token-level incentives cleanly with sensible fallbacks.


244-248: Reasonable zero-APY fallback for unknown tokens.

Keeps incentives points while avoiding undefined fields.


405-461: Make Solv GraphQL handling resilient and typed.

  • Annotate getSolvAPYs with
    : Promise<Record<string, { apyBase: number; apyReward: number }>>.
  • Use JSON.stringify for the request body instead of manual string interpolation.
  • Check res.ok before parsing, and guard with optional chaining (json?.data?.…) around btcPlusStats and lsts.details.
  • Coalesce pool.apy and pool.estApy (e.g. Number(pool.apy ?? pool.estApy ?? 0)).
  • Verify that stageNo: 1 is the intended production stage.

Comment on lines 232 to 241
const solvStrategy = tokenToSolvStrategyMap.get(tokenAddress);

if (solvStrategy) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: [],
points,
};
}
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

Guard against missing Solv APY entries (crash risk).

If Solv API fails (returns {}), solvAPYs[solvStrategy] is undefined and property access throws.

Apply:

-            if (solvStrategy) {
-                return {
-                    apyBase: solvAPYs[solvStrategy].apyBase,
-                    apyReward: solvAPYs[solvStrategy].apyReward,
-                    rewardTokens: [],
-                    points,
-                };
-            }
+            if (solvStrategy) {
+                const solv = solvAPYs?.[solvStrategy];
+                return {
+                    apyBase: solv?.apyBase ?? 0,
+                    apyReward: solv?.apyReward ?? 0,
+                    rewardTokens: [],
+                    points,
+                };
+            }
📝 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
const solvStrategy = tokenToSolvStrategyMap.get(tokenAddress);
if (solvStrategy) {
return {
apyBase: solvAPYs[solvStrategy].apyBase,
apyReward: solvAPYs[solvStrategy].apyReward,
rewardTokens: [],
points,
};
}
const solvStrategy = tokenToSolvStrategyMap.get(tokenAddress);
if (solvStrategy) {
- return {
- apyBase: solvAPYs[solvStrategy].apyBase,
- apyReward: solvAPYs[solvStrategy].apyReward,
- rewardTokens: [],
- points,
const solv = solvAPYs?.[solvStrategy];
return {
apyBase: solv?.apyBase ?? 0,
apyReward: solv?.apyReward ?? 0,
rewardTokens: [],
points,
};
}
🤖 Prompt for AI Agents
In sdk/src/gateway/strategy.ts around lines 232 to 241, the code assumes
solvAPYs[solvStrategy] always exists and will throw if the Solv API returned an
empty object; guard by checking whether solvAPYs and solvAPYs[solvStrategy] are
defined before accessing properties, and if missing return safe defaults (e.g.,
apyBase: 0, apyReward: 0, rewardTokens: [], points) or otherwise handle the
missing entry (early return or fallback), optionally emit a warning/log when the
APY entry is absent.

Comment on lines 252 to 260
private async getDefillamaPools() {
const res = await fetch('https://yields.llama.fi/pools');

const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
const defillamaPoolMap = new Map<string, DefiLlamaPool>(
defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
);
return defillamaPoolMap;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Harden DefiLlama fetch: handle network errors and chain name variance.

A thrown fetch will reject Promise.all and bubble. Add try/catch, res.ok check, and accept BOB/Bob.

Apply:

-    private async getDefillamaPools() {
-        const res = await fetch('https://yields.llama.fi/pools');
-
-        const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
-        const defillamaPoolMap = new Map<string, DefiLlamaPool>(
-            defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
-        );
-        return defillamaPoolMap;
-    }
+    private async getDefillamaPools() {
+        try {
+            const res = await fetch('https://yields.llama.fi/pools');
+            if (!res.ok) return new Map();
+            const body = await res.json();
+            const defillamaPools: DefiLlamaPool[] = Array.isArray(body?.data) ? body.data : [];
+            const defillamaPoolMap = new Map<string, DefiLlamaPool>(
+                defillamaPools
+                    .filter((pool) => pool.chain === 'Bob' || pool.chain === 'BOB')
+                    .map((pool) => [pool.pool, pool])
+            );
+            return defillamaPoolMap;
+        } catch (e) {
+            console.error('Failed to fetch DefiLlama pools', e);
+            return new Map();
+        }
+    }

To confirm the exact chain casing returned today:


🏁 Script executed:

#!/bin/bash
curl -s https://yields.llama.fi/pools | jq -r '.data[].chain' | sort -u | rg -n 'BOB|Bob|bob' -n

Length of output: 113


Normalize and strictly match ‘bob’ when filtering DefiLlama pools
Replace the current casing check to avoid matching “Boba”:

sdk/src/gateway/strategy.ts (around line 255):

- .filter((pool) => pool.chain === 'Bob' || pool.chain === 'BOB')
+ .filter((pool) => pool.chain.toLowerCase() === 'bob')

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk/src/gateway/strategy.ts around lines 252 to 260, the filter currently
checks pool.chain === 'Bob' which can incorrectly match variants like "Boba";
update the filter to normalize the chain string and strictly compare to 'bob'
(e.g. use pool.chain?.toLowerCase() === 'bob') to ensure only exact "bob"
matches and guard against null/undefined chain values.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sdk/src/gateway/tokens.ts (2)

431-439: getTokenDecimals does not support optimism/ethereum

With ethereum support added, this function returns undefined for tokens not on BOB/BOB Sepolia. Use the generic address lookup.

Apply:

-export function getTokenDecimals(tokenAddress: Address): number | undefined {
-    const normalizedAddress = tokenAddress.toString().toLowerCase();
-
-    const token =
-        bobTokens.find((t) => t.tokens.bob.address.toLowerCase() === normalizedAddress) ||
-        bobSepoliaTokens.find((t) => t.tokens['bob-sepolia'].address.toLowerCase() === normalizedAddress);
-
-    return token?.decimals;
-}
+export function getTokenDecimals(tokenAddress: Address): number | undefined {
+    const normalized = tokenAddress.toString().toLowerCase();
+    for (const map of Object.values(ADDRESS_LOOKUP)) {
+        const t = map[normalized];
+        if (t) return t.decimals;
+    }
+    return undefined;
+}

441-449: getTokenAddress is case-sensitive on symbols; hardens error path

Lowercase the symbol input and guard missing chain buckets.

 export function getTokenAddress(chainId: number, token: string): Address {
     if (isAddress(token)) {
         return token;
-    } else if (SYMBOL_LOOKUP[chainId][token]) {
-        return SYMBOL_LOOKUP[chainId][token].address;
+    } else if (SYMBOL_LOOKUP[chainId]?.[token.toLowerCase()]) {
+        return SYMBOL_LOOKUP[chainId][token.toLowerCase()]!.address;
     } else {
-        throw new Error('Unknown output token');
+        throw new Error(`Unknown token "${token}" on chainId ${chainId}`);
     }
 }
🧹 Nitpick comments (1)
sdk/src/gateway/tokens.ts (1)

358-361: Avoid drift by typing chain keys once

Define a ChainKey union and use Partial<Record<ChainKey, { address: string }>> to prevent future omissions.

Proposed type:

type ChainKey = 'bob' | 'bob-sepolia' | 'optimism' | 'ethereum';

type ChainAddresses = Partial<Record<ChainKey, { address: string }>>;

Then update the TOKENS shape to:

tokens: ChainAddresses;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f2621dd and 79128ff.

⛔ Files ignored due to path filters (1)
  • assets/SOLV.svg is excluded by !**/*.svg
📒 Files selected for processing (2)
  • sdk/src/gateway/strategy.ts (4 hunks)
  • sdk/src/gateway/tokens.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/src/gateway/strategy.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (4)
sdk/src/gateway/tokens.ts (4)

3-3: Importing mainnet: LGTM

Import aligns with usage of mainnet.id below.


367-367: Including ethereumTokens in TOKENS: LGTM


382-382: Initialize SYMBOL_LOOKUP for mainnet: LGTM


387-387: Initialize ADDRESS_LOOKUP for mainnet: LGTM

Comment on lines +169 to +181
const ethereumTokens = [
{
name: 'Solv',
symbol: 'SOLV',
decimals: 18,
tokens: {
ethereum: {
address: '0x04830a96a23ea718faa695a5aae74695aae3a23f',
},
},
logoURI: 'https://raw.githubusercontent.com/bob-collective/bob/master/assets/SOLV.svg',
},
];
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify SOLV mainnet address/decimals before merge

Please confirm 0x04830a96a23ea718faa695a5aae74695aae3a23f and decimals: 18 are correct for SOLV on Ethereum.


🌐 Web query:

Confirm the official Ethereum mainnet contract address and decimals for the SOLV token (Solv Protocol).

💡 Result:

The Solv Protocol SOLV token on Ethereum (as listed on Etherscan) is: 0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7 — decimals: 18. [1]

Note: there are other unrelated contracts named "SOLV" on Etherscan (e.g. 0xeb6b6...); Solv's docs state SOLV governance operates on BNB Chain (so please confirm which chain/token you mean if you need official project confirmation). [2]


Incorrect SOLV token address on Ethereum mainnet

The address configured in sdk/src/gateway/tokens.ts (0x04830a96a23ea718faa695a5aae74695aae3a23f) does not match the official SOLV (Solv Protocol) contract on Ethereum mainnet, which is 0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7. The decimals: 18 value is correct.

🤖 Prompt for AI Agents
In sdk/src/gateway/tokens.ts around lines 169-181, the SOLV token entry uses an
incorrect Ethereum mainnet address; update the ethereum.tokens.ethereum.address
value to the official SOLV mainnet contract
0xD1D8c3f9a5609027A1c7cEF070aB9c51b99DCFf7 (preserve the checksummed casing),
keep decimals: 18 and existing metadata (name, symbol, logoURI) unchanged, then
run any token/address validation tests or linting to ensure the new address is
valid in the codebase.

Comment on lines +417 to +419
if (token.tokens.ethereum) {
addToken(token.tokens.ethereum.address, token, mainnet.id);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Mainnet registration path: LGTM. Ensure strategy wiring covers SOLV

The registration works. Please verify the new Solv strategy map consumes these mainnet entries.


🏁 Script executed:

#!/bin/bash
# Verify any strategy maps include the SOLV mainnet address.
rg -nC2 -i "0x04830a96a23ea718faa695a5aae74695aae3a23f|tokenToSolvStrategyMap|solv" --type ts

Length of output: 42793


Add SOLV mapping to tokenToSolvStrategyMap

tokenToSolvStrategyMap in sdk/src/gateway/strategy.ts currently only includes SolvBTC+ and Jupiter. It’s missing the SOLV mainnet address (0x04830a96a23ea718faa695a5aae74695aae3a23f), so SOLV won’t receive its APY strategy. Add an entry mapping that address to the correct Solv strategy ID.

🤖 Prompt for AI Agents
In sdk/src/gateway/tokens.ts around lines 417 to 419, tokenToSolvStrategyMap is
missing the SOLV mainnet address mapping; add an entry that maps the SOLV
mainnet address 0x04830a96a23ea718faa695a5aae74695aae3a23f to the correct Solv
strategy ID (use the existing Solv strategy constant/ID used for Solv tokens),
ensure the address is normalized the same way other keys are (lowercase or
checksummed) and add/update any related exports/tests so SOLV receives its APY
strategy.

@mastilver
Copy link
Contributor Author

getEnrichedTokens now returns

  {
    "chainId": 60808,
    "address": "0x4ca70811e831db42072cba1f0d03496ef126faad",
    "name": "BTC+",
    "symbol": "BTC+",
    "decimals": 18,
    "logoURI": "https://raw.githubusercontent.com/bob-collective/bob/master/assets/BTC_plus.svg",
    "apyBase": 5.14,
    "apyReward": 1.1043,
    "rewardTokens": [
      {
        "chainId": 1,
        "address": "0x04830a96a23ea718faa695a5aae74695aae3a23f",
        "name": "Solv",
        "symbol": "SOLV",
        "decimals": 18,
        "logoURI": "https://raw.githubusercontent.com/bob-collective/bob/master/assets/SOLV.svg"
      }
    ],
    "points": [
      {
        "id": "bob",
        "name": "Bob Spice"
      }
    ],
    "tvl": 122400.92027686277
  },
  {
    "chainId": 60808,
    "address": "0x6b062aa7f5fc52b530cb13967ae2e6bc0d8dd3e4",
    "name": "SolvBTC Jupiter",
    "symbol": "SolvBTC.JUP",
    "decimals": 18,
    "logoURI": "https://raw.githubusercontent.com/bob-collective/bob/master/assets/solvBTC_Jupiter.svg",
    "apyBase": 7.48,
    "apyReward": 0,
    "rewardTokens": [],
    "points": [
      {
        "id": "bob",
        "name": "Bob Spice"
      }
    ],
    "tvl": 21.7111820406662
  },

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
sdk/src/gateway/strategy.ts (4)

221-230: Crash risk: missing Solv APY entry access.

If Solv API returns {} or the key is absent, solvAPYs[solvStrategy] is undefined and property access throws.

Apply:

-            if (solvStrategy) {
-                return {
-                    apyBase: solvAPYs[solvStrategy].apyBase,
-                    apyReward: solvAPYs[solvStrategy].apyReward,
-                    rewardTokens: this.resolveTokens(solvAPYs[solvStrategy].rewardTokens),
-                    points,
-                };
-            }
+            if (solvStrategy) {
+                const solv = solvAPYs?.[solvStrategy];
+                return {
+                    apyBase: solv?.apyBase ?? 0,
+                    apyReward: solv?.apyReward ?? 0,
+                    rewardTokens: this.resolveTokens(solv?.rewardTokens),
+                    points,
+                };
+            }

241-249: Harden DefiLlama fetch and normalize chain casing.

Add try/catch, check res.ok, validate shape, and compare pool.chain.toLowerCase() === 'bob'.

Apply:

-    private async getDefillamaPools() {
-        const res = await fetch('https://yields.llama.fi/pools');
-
-        const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
-        const defillamaPoolMap = new Map<string, DefiLlamaPool>(
-            defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
-        );
-        return defillamaPoolMap;
-    }
+    private async getDefillamaPools(): Promise<Map<string, DefiLlamaPool>> {
+        try {
+            const res = await fetch('https://yields.llama.fi/pools');
+            if (!res.ok) return new Map();
+            const body = await res.json();
+            const defillamaPools: DefiLlamaPool[] = Array.isArray(body?.data) ? body.data : [];
+            const map = new Map<string, DefiLlamaPool>(
+                defillamaPools
+                    .filter((pool) => pool?.chain?.toLowerCase() === 'bob')
+                    .map((pool) => [pool.pool, pool])
+            );
+            return map;
+        } catch (e) {
+            console.error('Failed to fetch DefiLlama pools', e);
+            return new Map();
+        }
+    }

251-264: Guard ADDRESS_LOOKUP accesses and return a typed array.

Current code can throw if a chain bucket is missing; also filter(Boolean) loses type info.

Apply:

-    private resolveTokens(tokens: string[] | undefined | null): Token[] {
+    private resolveTokens(tokens: string[] | undefined | null): Token[] {
         if (!tokens) {
             return [];
         }
 
-        return tokens
-            .map(
-                (addr) =>
-                    ADDRESS_LOOKUP[bob.id][addr.toLowerCase()] ||
-                    ADDRESS_LOOKUP[optimism.id][addr.toLowerCase()] ||
-                    ADDRESS_LOOKUP[mainnet.id][addr.toLowerCase()]
-            )
-            .filter(Boolean);
+        return tokens
+            .map((addr) => {
+                const a = addr.toLowerCase();
+                return (
+                    ADDRESS_LOOKUP[bob.id]?.[a] ??
+                    ADDRESS_LOOKUP[optimism.id]?.[a] ??
+                    ADDRESS_LOOKUP[mainnet.id]?.[a]
+                );
+            })
+            .filter((t): t is Token => Boolean(t));
     }

410-467: Make Solv GraphQL request robust and typed.

Check res.ok, validate shape, and build the body with JSON.stringify instead of manual string interpolation. Add an explicit return type.

Apply:

-    private async getSolvAPYs() {
-        try {
-            const query = `
+    private async getSolvAPYs(): Promise<Record<string, { apyBase: number; apyReward: number; rewardTokens?: string[] }>> {
+        try {
+            const query = `
                 query SolvAPYs {
                     btcPlusStats(stageNo: 1) {
                         baseApy
                         rewardApy
                         tvl
                     }
                     lsts {
                         details {
                             protocol
                             apy
                             estApy
                             tvlUsd
                         }
                     }
                 }
-            `
-                .split('\n')
-                .join(' ');
-
-            const res = await fetch('https://sft-api.com/graphql', {
-                headers: {
-                    'content-type': 'application/json',
-                    Authorization: 'solv',
-                },
-                body: `{
-                    "operationName": "SolvAPYs",
-                    "variables": {},
-                    "query": "${query}"
-                }`,
-                method: 'POST',
-            });
-
-            const data = await res.json();
-
-            const apys = {
+            `;
+
+            const res = await fetch('https://sft-api.com/graphql', {
+                method: 'POST',
+                headers: {
+                    'content-type': 'application/json',
+                    Authorization: 'solv',
+                },
+                body: JSON.stringify({
+                    operationName: 'SolvAPYs',
+                    variables: {},
+                    query,
+                }),
+            });
+            if (!res.ok) {
+                const errorBody = await res.text().catch(() => '');
+                throw new Error(`Solv APY fetch failed: ${res.status} ${res.statusText} ${errorBody}`);
+            }
+            const data = await res.json();
+            if (!data?.data?.btcPlusStats || !data?.data?.lsts?.details) {
+                console.warn('Unexpected Solv API shape', data);
+                return {};
+            }
+
+            const apys: Record<string, { apyBase: number; apyReward: number; rewardTokens?: string[] }> = {
                 'BTC+': {
                     apyBase: Number(data.data.btcPlusStats.baseApy),
                     apyReward: Number(data.data.btcPlusStats.rewardApy),
                     rewardTokens: ['0x04830a96a23ea718faa695a5aae74695aae3a23f'],
                 },
             };
 
             data.data.lsts.details.forEach((pool) => {
                 apys[pool.protocol] = {
                     apyBase: Number(pool.apy),
                     apyReward: 0,
                 };
             });
 
             return apys;
         } catch (err) {
             console.error('Failed to fetch APY data from Solv', err);
             return {};
         }
     }

Optional timeout (nice-to-have):

const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 8_000);
try { /* fetch with signal: ctrl.signal */ } finally { clearTimeout(t); }
🧹 Nitpick comments (1)
sdk/src/gateway/strategy.ts (1)

210-219: Avoid magic pool ID; extract a named constant.

Improves readability and prevents drift if the ID changes.

Apply:

-                    // HACK: set HybridBTC APY to 2%
-                    apyBase:
-                        defillamaPoolId === 'e8bfea35-ff6d-48db-aa08-51599b363219' ? 2 : (defillamaPool?.apyBase ?? 0),
+                    // HACK: set HybridBTC APY to 2%
+                    apyBase:
+                        defillamaPoolId === HYBRID_BTC_POOL_ID ? HYBRID_BTC_OVERRIDE_APY : (defillamaPool?.apyBase ?? 0),

Add near the top (outside the shown range):

const HYBRID_BTC_POOL_ID = 'e8bfea35-ff6d-48db-aa08-51599b363219';
const HYBRID_BTC_OVERRIDE_APY = 2;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 79128ff and 702db62.

📒 Files selected for processing (1)
  • sdk/src/gateway/strategy.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk/src/gateway/strategy.ts (2)
sdk/src/gateway/tokens.ts (2)
  • tokenToStrategyTypeMap (422-429)
  • ADDRESS_LOOKUP (377-377)
sdk/src/gateway/types.ts (2)
  • DefiLlamaPool (525-535)
  • Token (32-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (2)
sdk/src/gateway/strategy.ts (2)

11-11: Import usage looks good.

mainnet is used in resolveTokens; no issues.


163-174: Solv strategy map looks correct.

Keys are lowercased; aligns with tokenAddress = token.toLowerCase().

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
sdk/src/gateway/strategy.ts (4)

225-234: Guard against missing Solv APY entry (crash risk).

If the Solv API returns {} or a missing key, solvAPYs[solvStrategy] is undefined and property access throws.

-            if (solvStrategy) {
-                return {
-                    apyBase: solvAPYs[solvStrategy].apyBase,
-                    apyReward: solvAPYs[solvStrategy].apyReward,
-                    rewardTokens: this.resolveTokens(solvAPYs[solvStrategy].rewardTokens),
-                    points,
-                };
-            }
+            if (solvStrategy) {
+                // Optional: case-normalize if Solv protocol labels vary
+                const key = solvStrategy;
+                const s = solvAPYs?.[key];
+                return {
+                    apyBase: s?.apyBase ?? 0,
+                    apyReward: s?.apyReward ?? 0,
+                    rewardTokens: this.resolveTokens(s?.rewardTokens),
+                    points,
+                };
+            }

414-471: Make Solv GraphQL fetch robust and validate shape.

Check res.ok, build the JSON body with JSON.stringify, and guard against missing fields. Optionally normalize protocol keys and fall back to estApy when apy is null.

-    private async getSolvAPYs() {
-        try {
-            const query = `
-                query SolvAPYs {
-                    btcPlusStats(stageNo: 1) {
-                        baseApy
-                        rewardApy
-                        tvl
-                    }
-                    lsts {
-                        details {
-                            protocol
-                            apy
-                            estApy
-                            tvlUsd
-                        }
-                    }
-                }
-            `
-                .split('\n')
-                .join(' ');
-
-            const res = await fetch('https://sft-api.com/graphql', {
-                headers: {
-                    'content-type': 'application/json',
-                    Authorization: 'solv',
-                },
-                body: `{
-                    "operationName": "SolvAPYs",
-                    "variables": {},
-                    "query": "${query}"
-                }`,
-                method: 'POST',
-            });
-
-            const data = await res.json();
-
-            const apys = {
-                'BTC+': {
-                    apyBase: Number(data.data.btcPlusStats.baseApy),
-                    apyReward: Number(data.data.btcPlusStats.rewardApy),
-                    rewardTokens: ['0x04830a96a23ea718faa695a5aae74695aae3a23f'],
-                },
-            };
-
-            data.data.lsts.details.forEach((pool) => {
-                apys[pool.protocol] = {
-                    apyBase: Number(pool.apy),
-                    apyReward: 0,
-                };
-            });
-
-            return apys;
-        } catch (err) {
-            console.error('Failed to fetch APY data from Solv', err);
-            return {};
-        }
-    }
+    private async getSolvAPYs() {
+        try {
+            const query = `
+                query SolvAPYs {
+                    btcPlusStats(stageNo: 1) {
+                        baseApy
+                        rewardApy
+                        tvl
+                    }
+                    lsts {
+                        details {
+                            protocol
+                            apy
+                            estApy
+                            tvlUsd
+                        }
+                    }
+                }
+            `;
+
+            const res = await fetch('https://sft-api.com/graphql', {
+                method: 'POST',
+                headers: {
+                    'content-type': 'application/json',
+                    Authorization: 'solv',
+                },
+                body: JSON.stringify({
+                    operationName: 'SolvAPYs',
+                    variables: {},
+                    query,
+                }),
+            });
+            if (!res.ok) {
+                const body = await res.text().catch(() => '');
+                throw new Error(`Solv GraphQL ${res.status} ${res.statusText} ${body}`.trim());
+            }
+            const data = await res.json();
+            const btc = data?.data?.btcPlusStats;
+            const lsts: Array<{ protocol?: string; apy?: number | string | null; estApy?: number | string | null }> =
+                data?.data?.lsts?.details ?? [];
+            if (!btc || !Array.isArray(lsts)) return {};
+
+            const apys: Record<string, { apyBase: number; apyReward: number; rewardTokens?: string[] }> = {
+                'BTC+': {
+                    apyBase: Number(btc.baseApy ?? 0),
+                    apyReward: Number(btc.rewardApy ?? 0),
+                    // SOLV token
+                    rewardTokens: ['0x04830a96a23ea718faa695a5aae74695aae3a23f'],
+                },
+            };
+
+            lsts.forEach((pool) => {
+                const key = (pool?.protocol ?? '').toString();
+                if (!key) return;
+                apys[key] = {
+                    apyBase: Number(pool.apy ?? pool.estApy ?? 0),
+                    apyReward: 0,
+                };
+            });
+            return apys;
+        } catch (err) {
+            console.error('Failed to fetch APY data from Solv', err);
+            return {};
+        }
+    }

245-253: Harden DefiLlama fetch and normalize chain match.

Network errors can throw; current filter misses 'BOB' and risks “Boba” false positives later. Use try/catch and strict lowercase equality.

-    private async getDefillamaPools() {
-        const res = await fetch('https://yields.llama.fi/pools');
-
-        const defillamaPools: DefiLlamaPool[] = res.ok ? (await res.json()).data : [];
-        const defillamaPoolMap = new Map<string, DefiLlamaPool>(
-            defillamaPools.filter((pool) => pool.chain === 'Bob').map((pool) => [pool.pool, pool])
-        );
-        return defillamaPoolMap;
-    }
+    private async getDefillamaPools() {
+        try {
+            const res = await fetch('https://yields.llama.fi/pools');
+            if (!res.ok) return new Map<string, DefiLlamaPool>();
+            const body = await res.json();
+            const defillamaPools: DefiLlamaPool[] = Array.isArray(body?.data) ? body.data : [];
+            const defillamaPoolMap = new Map<string, DefiLlamaPool>(
+                defillamaPools
+                    .filter((pool) => pool?.chain?.toLowerCase?.() === 'bob')
+                    .map((pool) => [pool.pool, pool])
+            );
+            return defillamaPoolMap;
+        } catch (e) {
+            console.error('Failed to fetch DefiLlama pools', e);
+            return new Map<string, DefiLlamaPool>();
+        }
+    }

212-223: HBTC override deviates from product ask; set 0% after 2025-09-08.

Per prior note: “if it's past September 8th return 0 otherwise 2”. Current code falls back to DefiLlama after the date. Apply:

-                    // HACK: set HybridBTC APY to 2% until 2025-09-08
-                    apyBase:
-                        defillamaPoolId === 'e8bfea35-ff6d-48db-aa08-51599b363219' && new Date() < hybridBTCEndDate
-                            ? 2
-                            : (defillamaPool?.apyBase ?? 0),
+                    // HACK: HybridBTC fixed APY window: 2% before cutoff, 0% after
+                    apyBase:
+                        defillamaPoolId === 'e8bfea35-ff6d-48db-aa08-51599b363219'
+                            ? (new Date() < hybridBTCEndDate ? 2 : 0)
+                            : (defillamaPool?.apyBase ?? 0),

Optional: use a HYBRID_BTC_POOL_ID constant.

🧹 Nitpick comments (2)
sdk/src/gateway/strategy.ts (2)

163-174: Verify Solv protocol key names match API exactly.

tokenToSolvStrategyMap uses 'BTC+' and 'Jupiter'. Ensure these exactly match data.lsts.details[].protocol from Solv; otherwise the lookup will miss. If names can vary, normalize both sides (e.g., lowercase).

Would you like me to normalize keys end-to-end?


176-176: Name/tighten the HBTC cutoff constants.

Consider extracting a HYBRID_BTC_POOL_ID constant alongside hybridBTCEndDate to avoid the hardcoded string later and make intent explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ac55476 and fa71cca.

📒 Files selected for processing (1)
  • sdk/src/gateway/strategy.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
sdk/src/gateway/strategy.ts (2)
sdk/src/gateway/tokens.ts (2)
  • tokenToStrategyTypeMap (422-429)
  • ADDRESS_LOOKUP (377-377)
sdk/src/gateway/types.ts (2)
  • DefiLlamaPool (525-535)
  • Token (32-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test
🔇 Additional comments (2)
sdk/src/gateway/strategy.ts (2)

11-11: Import looks correct.

mainnet is used in resolveTokens; good addition.


204-208: Points composition is clean.

Merges project- and token-level incentives as intended.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants