feat(provider): add optional GoldRush multi-chain token balance enrichment#26
feat(provider): add optional GoldRush multi-chain token balance enrichment#26dinxsh wants to merge 1 commit intoelizaos-plugins:1.xfrom
Conversation
…hment Fixes elizaos-plugins#15 Extends WalletProvider to optionally fetch full token balances across all configured chains via GoldRush API when GOLDRUSH_API_KEY is set. Falls back to existing behavior if key is not present — zero breaking changes. Why GoldRush: - Single API covers 100+ EVM chains - Returns USD values, spam filtering, and decoded token metadata - Parallel fetching across chains in one provider call Configuration: GOLDRUSH_API_KEY=your_key_here # optional Without key: existing behavior unchanged With key: agent context includes full multi-chain token portfolio Tested on: eth-mainnet, base-mainnet, arbitrum-mainnet, matic-mainnet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WalkthroughThis PR adds GoldRush integration to the EVM wallet provider for multi-chain token balance enrichment. A new dependency ( Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Wallet Provider Client
participant Provider as EVM Wallet Provider
participant EVMService as EVM Service
participant GoldRush as GoldRush API
Client->>Provider: get(address)
Provider->>EVMService: fetch cached data
alt EVMService returns data
EVMService-->>Provider: wallet data + text
else EVMService unavailable or no cache
Provider->>EVMService: direct fetch attempt
EVMService-->>Provider: data or fallback
end
alt GOLDRUSH_API_KEY & address available
Provider->>GoldRush: getMultiChainBalances(address)
GoldRush-->>Provider: per-chain token balances
Provider->>Provider: buildGoldRushText(data)
Provider->>Provider: append GoldRush text to result
end
Provider-->>Client: enriched wallet result (text + data + values)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
| const resp = await client.BalanceService.getTokenBalancesForWalletAddress( | ||
| goldrushChain, | ||
| address, | ||
| { quoteCurrency: 'USD', noSpam: true } | ||
| ); | ||
|
|
||
| return { | ||
| chain: goldrushChain, | ||
| items: resp.data?.items ?? [], | ||
| totalUSD: (resp.data?.items ?? []).reduce( | ||
| (sum: number, item: any) => sum + (item.quote ?? 0), | ||
| 0 | ||
| ), | ||
| }; |
There was a problem hiding this comment.
GoldRush API errors silently swallowed
The GoldRush SDK returns a standardized response of {data, error, error_message, error_code}. When the API returns an error (invalid API key, rate limiting, invalid chain name), resp.error will be true and resp.data may be null. The current code only uses resp.data?.items ?? [], which silently treats API errors as "no tokens found."
This means if the API key is invalid or expired, or the account hits rate limits, the agent will silently receive no token data with no indication of failure — making it very difficult to diagnose configuration issues.
| const resp = await client.BalanceService.getTokenBalancesForWalletAddress( | |
| goldrushChain, | |
| address, | |
| { quoteCurrency: 'USD', noSpam: true } | |
| ); | |
| return { | |
| chain: goldrushChain, | |
| items: resp.data?.items ?? [], | |
| totalUSD: (resp.data?.items ?? []).reduce( | |
| (sum: number, item: any) => sum + (item.quote ?? 0), | |
| 0 | |
| ), | |
| }; | |
| const resp = await client.BalanceService.getTokenBalancesForWalletAddress( | |
| goldrushChain, | |
| address, | |
| { quoteCurrency: 'USD', noSpam: true } | |
| ); | |
| if (resp.error) { | |
| elizaLogger.warn(`GoldRush API error for ${goldrushChain}: ${resp.error_message}`); | |
| return null; | |
| } | |
| return { | |
| chain: goldrushChain, | |
| items: resp.data?.items ?? [], | |
| totalUSD: (resp.data?.items ?? []).reduce( | |
| (sum: number, item: any) => sum + (item.quote ?? 0), | |
| 0 | |
| ), | |
| }; |
| for (const result of chainResults) { | ||
| for (const item of result.items) { | ||
| if (!item.balance || item.balance === '0') continue; | ||
| const symbol = item.contract_ticker_symbol ?? '???'; | ||
| const decimals = item.contract_decimals ?? 18; | ||
| const balance = formatUnits(BigInt(item.balance), decimals); | ||
| const formattedBalance = Number(balance).toLocaleString('en-US', { | ||
| maximumFractionDigits: 4, | ||
| }); | ||
| const usdValue = item.quote ?? 0; | ||
| const formattedUSD = `$${usdValue.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; | ||
| lines.push(`${symbol} (${result.chain}): ${formattedBalance} ${symbol} — ${formattedUSD}`); | ||
| totalPortfolioUSD += usdValue; | ||
| } | ||
| } |
There was a problem hiding this comment.
One malformed token breaks all output
The outer try/catch in buildGoldRushText wraps the entire token iteration. If any single token item from the API has an unexpected balance value (e.g., a non-numeric string or a type that BigInt() can't parse), the error will bubble up to the catch block and the function returns '' — discarding data for all tokens across all chains.
Consider wrapping the per-item processing in its own try/catch so that one bad token doesn't wipe out the entire portfolio output:
| for (const result of chainResults) { | |
| for (const item of result.items) { | |
| if (!item.balance || item.balance === '0') continue; | |
| const symbol = item.contract_ticker_symbol ?? '???'; | |
| const decimals = item.contract_decimals ?? 18; | |
| const balance = formatUnits(BigInt(item.balance), decimals); | |
| const formattedBalance = Number(balance).toLocaleString('en-US', { | |
| maximumFractionDigits: 4, | |
| }); | |
| const usdValue = item.quote ?? 0; | |
| const formattedUSD = `$${usdValue.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; | |
| lines.push(`${symbol} (${result.chain}): ${formattedBalance} ${symbol} — ${formattedUSD}`); | |
| totalPortfolioUSD += usdValue; | |
| } | |
| } | |
| for (const result of chainResults) { | |
| for (const item of result.items) { | |
| try { | |
| if (!item.balance || item.balance === '0') continue; | |
| const symbol = item.contract_ticker_symbol ?? '???'; | |
| const decimals = item.contract_decimals ?? 18; | |
| const balance = formatUnits(BigInt(item.balance), decimals); | |
| const formattedBalance = Number(balance).toLocaleString('en-US', { | |
| maximumFractionDigits: 4, | |
| }); | |
| const usdValue = item.quote ?? 0; | |
| const formattedUSD = `$${usdValue.toLocaleString('en-US', { maximumFractionDigits: 0 })}`; | |
| lines.push(`${symbol} (${result.chain}): ${formattedBalance} ${symbol} — ${formattedUSD}`); | |
| totalPortfolioUSD += usdValue; | |
| } catch (itemError) { | |
| elizaLogger.warn(`Skipping malformed token on ${result.chain}: ${itemError}`); | |
| } | |
| } | |
| } |
| const getMultiChainBalances = async (address: string, chains: string[], apiKey: string) => { | ||
| const client = new GoldRushClient(apiKey); |
There was a problem hiding this comment.
New GoldRushClient created on every provider call
evmWalletProvider.get is called on every agent message cycle. Each call creates a brand new GoldRushClient instance inside getMultiChainBalances. The GoldRush SDK client manages internal state for thread pooling, retry logic, and rate limiting — recreating it on every call loses those benefits and adds unnecessary overhead.
Consider instantiating the client once (e.g., at module level or cached by API key) and reusing it across calls.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/providers/wallet.ts (1)
111-116: AvoidNumber()when formatting token balances.
formatUnits()returns decimal strings that can exceed JavaScript's safe numeric range. Coercing them throughNumber()causes precision loss for large token holdings—for example, a balance like123456789012345678901234567890.1234becomes123,456,789,012,345,680,000,000,000,000after rounding.String-safe formatting
- const formattedBalance = Number(balance).toLocaleString('en-US', { - maximumFractionDigits: 4, - }); + const [whole, fraction = ''] = balance.split('.'); + const formattedBalance = `${BigInt(whole).toLocaleString('en-US')}${ + fraction ? `.${fraction.slice(0, 4)}` : '' + }`;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/providers/wallet.ts` around lines 111 - 116, The code coerces the token balance string returned by formatUnits into a Number (in formattedBalance), which loses precision for very large balances; change formattedBalance to use a string-safe formatter instead of Number(): preserve the balance string returned by formatUnits(item.balance, decimals) and format it without numeric coercion (e.g., implement or call a utility like formatTokenBalance(balanceString, { maximumFractionDigits: 4 }) that splits integer/fraction parts and inserts thousands separators, or use a decimal library such as Big/decimal.js to format safely). Update the usage around formatUnits, formattedBalance, and item.balance/decimals to avoid Number() conversions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 25: The plugin manifest (package.json) is missing the new
GOLDRUSH_API_KEY declaration referenced by src/providers/wallet.ts; update the
manifest's agentConfig.pluginParameters to include a GOLDRUSH_API_KEY parameter
(name "GOLDRUSH_API_KEY", type string, add a short description and mark
required/optional per the provider's expectations) so schema-driven UIs and
README-consumers can discover and validate the setting; ensure the parameter key
matches exactly "GOLDRUSH_API_KEY" and appears alongside the other
pluginParameters entries in package.json.
In `@README.md`:
- Around line 38-42: Update the README wording to clarify that GoldRush only
enriches token holdings for chains that are supported by the implementation:
explicitly state that the provider only queries chains listed in CHAIN_MAP (in
src/providers/wallet.ts) and any configured chains not present in CHAIN_MAP are
skipped, and keep the GOLDRUSH_API_KEY note but add this limitation sentence
next to it.
In `@src/providers/wallet.ts`:
- Around line 500-512: The GoldRush enrichment is blocking the provider return;
change the flow so when GOLDRUSH_API_KEY is present you kick off
buildGoldRushText(result.data.address, chainNames, goldRushApiKey)
asynchronously with a bounded timeout (use Promise.race or equivalent) and apply
the returned goldRushText to result.text only if it completes before the
timeout; additionally add a simple cache keyed by address+chainNames+apiKey
(lookup before calling buildGoldRushText and store the rendered text after
success) to prevent repeated fan-out; locate changes around
genChainsFromRuntime, buildGoldRushText and the result merging logic and ensure
the main provider returns immediately if the enrichment does not finish in time.
- Around line 63-91: getMultiChainBalances currently swallows rejected GoldRush
calls and returns only successful balances, causing buildGoldRushText to print a
misleading unconditional total; change getMultiChainBalances to return an object
like { successes: [...], failedChains: [...] } by capturing Promise.allSettled
results (use CHAIN_MAP and GoldRushClient as currently) and populate
failedChains with chain names/ids for any rejected or null entries, and then
update buildGoldRushText to accept that object, surface failedChains to the user
(e.g., list which chains failed) and only compute/print the Total Portfolio
Value when failedChains is empty or otherwise clearly mark the total as partial
when some chains failed.
---
Nitpick comments:
In `@src/providers/wallet.ts`:
- Around line 111-116: The code coerces the token balance string returned by
formatUnits into a Number (in formattedBalance), which loses precision for very
large balances; change formattedBalance to use a string-safe formatter instead
of Number(): preserve the balance string returned by formatUnits(item.balance,
decimals) and format it without numeric coercion (e.g., implement or call a
utility like formatTokenBalance(balanceString, { maximumFractionDigits: 4 })
that splits integer/fraction parts and inserts thousands separators, or use a
decimal library such as Big/decimal.js to format safely). Update the usage
around formatUnits, formattedBalance, and item.balance/decimals to avoid
Number() conversions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f1ed17aa-0ed4-4324-a080-25b3d8250c6e
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
README.mdpackage.jsonsrc/providers/wallet.ts
| "dist" | ||
| ], | ||
| "dependencies": { | ||
| "@covalenthq/client-sdk": "^3.0.5", |
There was a problem hiding this comment.
Add GOLDRUSH_API_KEY to agentConfig.pluginParameters.
src/providers/wallet.ts now reads this setting, but the plugin manifest never declares it. That keeps the new feature out of schema-driven config/UIs and makes the README the only place users can discover it.
Suggested manifest addition
"ETHEREUM_PROVIDER_OPTIMISM": {
"type": "string",
"description": "Custom RPC URL for Optimism mainnet. For other chains, set ETHEREUM_PROVIDER_<CHAINNAME> (or EVM_PROVIDER_<CHAINNAME>) in your .env.",
"required": false,
"sensitive": false
},
+ "GOLDRUSH_API_KEY": {
+ "type": "string",
+ "description": "Optional GoldRush API key used to enrich wallet context with multi-chain token balances.",
+ "required": false,
+ "sensitive": true
+ },
"TEE_MODE": {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 25, The plugin manifest (package.json) is missing the
new GOLDRUSH_API_KEY declaration referenced by src/providers/wallet.ts; update
the manifest's agentConfig.pluginParameters to include a GOLDRUSH_API_KEY
parameter (name "GOLDRUSH_API_KEY", type string, add a short description and
mark required/optional per the provider's expectations) so schema-driven UIs and
README-consumers can discover and validate the setting; ensure the parameter key
matches exactly "GOLDRUSH_API_KEY" and appears alongside the other
pluginParameters entries in package.json.
| # Optional - GoldRush API for multi-chain token balances | ||
| # When set, the wallet provider enriches agent context with full token | ||
| # holdings across all configured chains (USD values, spam filtering). | ||
| # Get your free API key at https://goldrush.dev | ||
| GOLDRUSH_API_KEY=your_goldrush_api_key |
There was a problem hiding this comment.
Clarify that GoldRush does not cover every configured chain.
The text says this enriches holdings across “all configured chains”, but the implementation only queries chains present in src/providers/wallet.ts’s CHAIN_MAP; unsupported configured chains are skipped. Please make that limitation explicit here.
Suggested wording
-# holdings across all configured chains (USD values, spam filtering).
+# holdings across supported configured chains (USD values, spam filtering).
+# Chains without a GoldRush mapping are skipped.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@README.md` around lines 38 - 42, Update the README wording to clarify that
GoldRush only enriches token holdings for chains that are supported by the
implementation: explicitly state that the provider only queries chains listed in
CHAIN_MAP (in src/providers/wallet.ts) and any configured chains not present in
CHAIN_MAP are skipped, and keep the GOLDRUSH_API_KEY note but add this
limitation sentence next to it.
| const getMultiChainBalances = async (address: string, chains: string[], apiKey: string) => { | ||
| const client = new GoldRushClient(apiKey); | ||
|
|
||
| const results = await Promise.allSettled( | ||
| chains.map(async (chain) => { | ||
| const goldrushChain = CHAIN_MAP[chain]; | ||
| if (!goldrushChain) return null; | ||
|
|
||
| const resp = await client.BalanceService.getTokenBalancesForWalletAddress( | ||
| goldrushChain, | ||
| address, | ||
| { quoteCurrency: 'USD', noSpam: true } | ||
| ); | ||
|
|
||
| return { | ||
| chain: goldrushChain, | ||
| items: resp.data?.items ?? [], | ||
| totalUSD: (resp.data?.items ?? []).reduce( | ||
| (sum: number, item: any) => sum + (item.quote ?? 0), | ||
| 0 | ||
| ), | ||
| }; | ||
| }) | ||
| ); | ||
|
|
||
| return results | ||
| .filter((r) => r.status === 'fulfilled' && r.value !== null) | ||
| .map((r) => (r as PromiseFulfilledResult<any>).value); | ||
| }; |
There was a problem hiding this comment.
Don’t label a partial portfolio as complete.
Rejected GoldRush calls are filtered out here, so buildGoldRushText() later emits Total Portfolio Value over only the successful subset. That underreports holdings without any warning when one configured chain fails.
Suggested direction
- return results
- .filter((r) => r.status === 'fulfilled' && r.value !== null)
- .map((r) => (r as PromiseFulfilledResult<any>).value);
+ const chainResults = [];
+ const failedChains: string[] = [];
+
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled' && result.value !== null) {
+ chainResults.push(result.value);
+ return;
+ }
+
+ if (CHAIN_MAP[chains[index]]) {
+ failedChains.push(chains[index]);
+ }
+ });
+
+ return { chainResults, failedChains };Have buildGoldRushText() surface failedChains and avoid printing an unconditional total when the enrichment is partial.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/providers/wallet.ts` around lines 63 - 91, getMultiChainBalances
currently swallows rejected GoldRush calls and returns only successful balances,
causing buildGoldRushText to print a misleading unconditional total; change
getMultiChainBalances to return an object like { successes: [...], failedChains:
[...] } by capturing Promise.allSettled results (use CHAIN_MAP and
GoldRushClient as currently) and populate failedChains with chain names/ids for
any rejected or null entries, and then update buildGoldRushText to accept that
object, surface failedChains to the user (e.g., list which chains failed) and
only compute/print the Total Portfolio Value when failedChains is empty or
otherwise clearly mark the total as partial when some chains failed.
| // Optionally enrich with GoldRush multi-chain token balances | ||
| const goldRushApiKey = runtime.getSetting('GOLDRUSH_API_KEY'); | ||
| if (goldRushApiKey && result.data?.address) { | ||
| const chains = genChainsFromRuntime(runtime); | ||
| const chainNames = Object.keys(chains); | ||
| const goldRushText = await buildGoldRushText( | ||
| result.data.address as string, | ||
| chainNames, | ||
| goldRushApiKey | ||
| ); | ||
| if (goldRushText) { | ||
| result = { ...result, text: result.text + goldRushText }; | ||
| } |
There was a problem hiding this comment.
Keep GoldRush enrichment off the critical path.
With GOLDRUSH_API_KEY set, every provider call now waits for fresh GoldRush requests across all chains before returning. That makes optional enrichment a latency hit on the hot path even though the base wallet data is already ready.
At minimum, bound the enrichment with a timeout
- const goldRushText = await buildGoldRushText(
- result.data.address as string,
- chainNames,
- goldRushApiKey
- );
+ const goldRushText = await Promise.race([
+ buildGoldRushText(result.data.address as string, chainNames, goldRushApiKey),
+ new Promise<string>((resolve) => setTimeout(() => resolve(''), 3_000)),
+ ]);I’d also cache the rendered GoldRush section separately so repeated prompts don’t fan out to the API every time.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/providers/wallet.ts` around lines 500 - 512, The GoldRush enrichment is
blocking the provider return; change the flow so when GOLDRUSH_API_KEY is
present you kick off buildGoldRushText(result.data.address, chainNames,
goldRushApiKey) asynchronously with a bounded timeout (use Promise.race or
equivalent) and apply the returned goldRushText to result.text only if it
completes before the timeout; additionally add a simple cache keyed by
address+chainNames+apiKey (lookup before calling buildGoldRushText and store the
rendered text after success) to prevent repeated fan-out; locate changes around
genChainsFromRuntime, buildGoldRushText and the result merging logic and ensure
the main provider returns immediately if the enrichment does not finish in time.
Fixes #15
Summary
WalletProviderinsrc/providers/wallet.tsto optionally fetch full token balances across all configured chains via GoldRush API whenGOLDRUSH_API_KEYis setPromise.allSettled; per-chain errors are isolated and never block othersWhy GoldRush
noSpam: true), and decoded token metadataConfiguration
Without key: existing behavior unchanged
With key: agent context includes full multi-chain token portfolio appended to the existing wallet output:
Changes
src/providers/wallet.ts— addedCHAIN_MAP,getMultiChainBalances,buildGoldRushTexthelpers; enrichment hook inevmWalletProvider.getpackage.json— added@covalenthq/client-sdktodependenciesREADME.md— documentedGOLDRUSH_API_KEYenv varTest plan
GOLDRUSH_API_KEY: provider output is identical to pre-PR behaviorGOLDRUSH_API_KEY: token holdings section appended for each configured chainCHAIN_MAPare silently skipped🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Greptile Summary
This PR adds optional multi-chain token balance enrichment to the
evmWalletProvidervia the GoldRush (Covalent) API. WhenGOLDRUSH_API_KEYis set, the provider appends detailed token holdings (with USD values and spam filtering) across all configured EVM chains to the agent context. The integration is purely additive — when the key is absent, existing behavior is unchanged.CHAIN_MAP,getMultiChainBalances, andbuildGoldRushTexthelpers tosrc/providers/wallet.tsfor GoldRush API integrationPromise.allSettledfor parallel per-chain fetching with isolated error handlingevmWalletProvider.getto accumulate aresultvariable before optionally enriching it with GoldRush dataresp.errorand logging warningsGoldRushClientis re-instantiated on every provider call — consider caching the client instance for better performanceConfidence Score: 3/5
src/providers/wallet.ts— the GoldRush integration helpers (getMultiChainBalancesandbuildGoldRushText) need improved error handlingImportant Files Changed
@covalenthq/client-sdk^3.0.5 dependency. Appropriate version pinning.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[evmWalletProvider.get] --> B{EVM Service available?} B -- No --> C[directFetchWalletData] B -- Yes --> D{Cached wallet data?} D -- No --> C D -- Yes --> E[Build result from cached data] C --> F{GOLDRUSH_API_KEY set?} E --> F F -- No --> G[Return result as-is] F -- Yes --> H[genChainsFromRuntime] H --> I[buildGoldRushText] I --> J[getMultiChainBalances] J --> K[Map chains via CHAIN_MAP] K --> L[Promise.allSettled: GoldRush API calls] L --> M[Format token holdings text] M --> N[Append GoldRush text to result] N --> GLast reviewed commit: 4f7dacf
(2/5) Greptile learns from your feedback when you react with thumbs up/down!