Skip to content

fix: rebuild token whitelists from studio catalog with complete metadata#532

Merged
chrisli30 merged 6 commits into
mainfrom
staging
May 5, 2026
Merged

fix: rebuild token whitelists from studio catalog with complete metadata#532
chrisli30 merged 6 commits into
mainfrom
staging

Conversation

@chrisli30
Copy link
Copy Markdown
Member

  • fix: rebuild token whitelists from studio catalog

The whitelist loader (core/taskengine/token_metadata.go) keys cache entries on
TokenMetadata.Id (json:"id") but the legacy files used "address", so every
entry parsed with Id="" and overwrote a single cache slot — only 1 of N
tokens actually became reachable. Rebuilt the four chain catalogs from
studio's app/lib/erc20 (the authoritative UI source), keeping just
{id, name, symbol, decimals} and dropping incomplete or duplicate placeholders.

  • ethereum.json: 99 -> 30 entries, all complete
  • base.json: 134 -> 23 entries, all complete
  • base-sepolia.json: 19 -> 4 entries, all complete (incl. real Circle USDC w/ decimals: 6)
  • sepolia.json: 6 -> 6 entries, unchanged

Token enrichment now returns proper name/symbol/decimals for whitelisted
tokens — Base Sepolia balance lookups will no longer surface USDC as
"Unknown Token / UNKNOWN".

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

  • fix: relabel notification errors heading on success runs

When a workflow ends with status=success but a branch condition skipped a
node, the channel formatters were unconditionally rendering the skip
description under "What Went Wrong" — misleading because nothing actually
went wrong. The deterministic plain-text body composer already gates on
status=="failed"; the channel formatters now match that semantic by
switching the heading to "Skipped Steps" when status=="success", and
keeping "What Went Wrong" for failed/error runs.

Applies to all four formatters (telegram, discord, plaintext, email).
Test updated to assert "Skipped Steps" / no "What Went Wrong" for the
success-with-branch-skipped case.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

chrisli30 and others added 2 commits May 5, 2026 02:04
The whitelist loader (core/taskengine/token_metadata.go) keys cache entries on
TokenMetadata.Id (json:"id") but the legacy files used "address", so every
entry parsed with Id="" and overwrote a single cache slot — only 1 of N
tokens actually became reachable. Rebuilt the four chain catalogs from
studio's app/lib/erc20 (the authoritative UI source), keeping just
{id, name, symbol, decimals} and dropping incomplete or duplicate placeholders.

- ethereum.json:      99 -> 30 entries, all complete
- base.json:         134 -> 23 entries, all complete
- base-sepolia.json:  19 ->  4 entries, all complete (incl. real Circle USDC w/ decimals: 6)
- sepolia.json:        6 ->  6 entries, unchanged

Token enrichment now returns proper name/symbol/decimals for whitelisted
tokens — Base Sepolia balance lookups will no longer surface USDC as
"Unknown Token / UNKNOWN".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a workflow ends with status=success but a branch condition skipped a
node, the channel formatters were unconditionally rendering the skip
description under "What Went Wrong" — misleading because nothing actually
went wrong. The deterministic plain-text body composer already gates on
status=="failed"; the channel formatters now match that semantic by
switching the heading to "Skipped Steps" when status=="success", and
keeping "What Went Wrong" for failed/error runs.

Applies to all four formatters (telegram, discord, plaintext, email).
Test updated to assert "Skipped Steps" / no "What Went Wrong" for the
success-with-branch-skipped case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 5, 2026

Code Review

Overview

Two independent bug fixes:

  1. Token whitelist cache bug — JSON files used \"address\" as the field name, but TokenMetadata has json:\"id\". Every token parsed with Id=\"\", so all N entries collided on the same cache key and only the last one survived. The fix renames the field to \"id\" across all four chain catalogs and takes the opportunity to remove placeholder/malformed entries.

  2. Notification heading mislead — When a workflow succeeded but a branch condition skipped a node, all four channel formatters (Telegram, Discord, Plaintext, Email) rendered the skipped-node description under "What Went Wrong", which implies failure. The fix switches to "Skipped Steps" when status == \"success\".

Both fixes are correct and clearly motivated.


Token Whitelist Fix

Address case normalization is safe.
token_metadata.go:194 normalises on write (strings.ToLower(token.Id)) and GetTokenMetadata:217 normalises on lookup — so the mixed-case EIP-55 checksummed addresses in the new JSON files (e.g. 0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9) will resolve correctly. No latent case-sensitivity bug here.

Malformed placeholder addresses correctly removed.
The old base.json contained obviously synthetic addresses like 0xd4d042C6c716c0d6C2a5B7F5b7c8C8E6e6b8b8b8 (Maple Finance) and 0x5a6b6a9c7826e2b4b8b8b8b8b8b8b8b8b8b8b8b8 (MediBloc). Removing these is correct — they would never resolve on-chain.

Minor inconsistency: WETH name in ethereum.json.
The new WETH entry uses \"name\": \"WETH\" (the symbol) while every other entry and every other chain file uses a descriptive name (\"Wrapped Ether\"). The value is technically valid but inconsistent with the rest of the catalog.

// ethereum.json — should probably be:
{
  "name": "Wrapped Ether",   // not "WETH"
  "symbol": "WETH",
  "decimals": 18,
  "id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}

sepolia.json is a no-op except field rename + reorder.
The six tokens and their addresses are unchanged — only \"address\"\"id\" and a trailing-newline fix. Confirmed correct.


Notification Heading Fix

Logic is correct and applied consistently.
All four formatters gate on s.Status == \"success\", which is the right predicate: branch skips only appear during successful runs. Failed/error runs keep "What Went Wrong".

No XSS risk in the email formatter.

sb.WriteString(fmt.Sprintf(`<h3 style="...">%s</h3>`, heading))

heading is always one of two string literals, never user-supplied data — safe.

Test gap: Discord, Plaintext, and Email formatters are not tested for this behaviour.
The updated test in TestFormatTelegramFromStructured_PRDFormat (lines 859–880) asserts \"Skipped Steps:\" and not \"What Went Wrong:\" for the success-with-errors case, which is good. But summarizer_format_test.go has no equivalent tests for Discord, Plaintext, or Email. The logic is identical in all four so the risk is low, but a copy-paste error or future regression in one of those formatters would go undetected. Consider adding parallel table-driven cases (even minimal ones) for the other three.

Minor style note.
The fmt import was added to Discord, Plaintext, and Telegram solely for fmt.Sprintf on a two-part string. String concatenation avoids the import:

// instead of:
sb.WriteString(fmt.Sprintf("\n**%s:**\n", heading))

// could be:
sb.WriteString("\n**" + heading + ":**\n")

fmt.Sprintf is not wrong here, just slightly heavier than necessary. Not a blocker.


Summary

Area Status
Root cause of cache bug ✅ Correctly identified and fixed
Address normalisation safety ✅ Confirmed safe — loader normalises to lowercase
Malformed addresses removed ✅ Good cleanup
Heading logic (all 4 formatters) ✅ Consistent and correct
Telegram test updated
Discord / Plaintext / Email test coverage ⚠️ Missing for the new heading behaviour
WETH name inconsistency in ethereum.json 🔹 Minor (cosmetic only)

The two bugs are real and the fixes are sound. The main follow-up worth doing is adding at least one test case per untested formatter for the success + Errors path.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes token whitelist loading by rebuilding chain token catalogs to use the id field expected by TokenMetadata.Id, and updates notification channel formatters to avoid “What Went Wrong” framing on successful runs where steps were only skipped by branch conditions.

Changes:

  • Rebuilt token whitelist JSON catalogs (Ethereum/Base/Sepolia/Base Sepolia) to use {id, name, symbol, decimals} so whitelist cache keys are populated correctly.
  • Updated Telegram/Discord/plaintext/email structured-summary formatters to render the skipped-steps section under “Skipped Steps” when status=="success".
  • Updated Telegram formatter test expectations for the success-with-branch-skipped scenario.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
token_whitelist/sepolia.json Migrates entries from address to id and refreshes token set ordering/content.
token_whitelist/ethereum.json Rebuilds mainnet token catalog to the {id,name,symbol,decimals} schema used by the loader.
token_whitelist/base.json Rebuilds Base token catalog to the {id,name,symbol,decimals} schema used by the loader.
token_whitelist/base-sepolia.json Rebuilds Base Sepolia token catalog to the {id,name,symbol,decimals} schema used by the loader.
core/taskengine/summarizer_format_test.go Adjusts Telegram formatter test to expect “Skipped Steps” and to forbid “What Went Wrong” on success-with-skips.
core/taskengine/summarizer_format_telegram.go Switches the errors-section heading based on Summary.Status.
core/taskengine/summarizer_format_plaintext.go Switches the errors-section heading based on Summary.Status (adds fmt import).
core/taskengine/summarizer_format_email.go Switches the errors-section heading in analysisHtml based on Summary.Status.
core/taskengine/summarizer_format_discord.go Switches the errors-section heading based on Summary.Status (adds fmt import).
Comments suppressed due to low confidence (1)

core/taskengine/summarizer_format_email.go:311

  • In the email HTML renderer, when Status=="success" the section heading becomes "Skipped Steps", but each entry is still prefixed with a "✗" marker. That marker reads like a failure indicator and undermines the intent of the relabel. Consider switching the per-line glyph based on status (e.g., use a neutral bullet/skip icon when status is success) so success-with-skips doesn’t look like an error list.
		heading := "What Went Wrong"
		if s.Status == "success" {
			heading = "Skipped Steps"
		}
		sb.WriteString(`<div style="margin-bottom: 20px;">`)
		sb.WriteString(fmt.Sprintf(`<h3 style="margin: 0 0 8px 0; font-size: 16px;">%s</h3>`, heading))
		for _, err := range s.Errors {
			sb.WriteString("<p style=\"margin: 0 0 4px 0;\">✗ ")
			sb.WriteString(formatBackticksForChannel(err, "email"))
			sb.WriteString("</p>")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 60 to 68
// Errors section — heading depends on status. On a successful run the only
// entries here are branch-skipped nodes, so use "Skipped Steps" instead.
if len(s.Errors) > 0 {
sb.WriteString("\n**What Went Wrong:**\n")
heading := "What Went Wrong"
if s.Status == "success" {
heading = "Skipped Steps"
}
sb.WriteString(fmt.Sprintf("\n**%s:**\n", heading))
for _, err := range s.Errors {
Comment on lines +54 to +61
// Errors section — heading depends on status. On a successful run the only
// entries here are branch-skipped nodes, so use "Skipped Steps" instead.
if len(s.Errors) > 0 {
sb.WriteString("\nWhat Went Wrong:\n")
heading := "What Went Wrong"
if s.Status == "success" {
heading = "Skipped Steps"
}
sb.WriteString(fmt.Sprintf("\n%s:\n", heading))
chrisli30 and others added 2 commits May 5, 2026 02:28
Reverts 404b882. The original framing was correct after all: when a
workflow ends with status=success but the main on-chain logic was
skipped by a branch condition (e.g. a balance gate caused a transfer
loop to be bypassed), the workflow did NOT deliver its intended
outcome. Surfacing those skips under "What Went Wrong" is the right
signal to the user, even though no error was thrown.

Restores the unconditional "What Went Wrong" heading in all four
channel formatters (telegram, discord, plaintext, email) and reverts
the test assertion accordingly. No helper extraction is kept since
the logic collapses back to a single literal heading per channel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the descriptive-name convention used by every other catalog
entry (and every other chain file). Symbol stays "WETH". Same change
applied upstream in studio/app/lib/erc20/ethereum.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chrisli30 chrisli30 changed the title fix: rebuild token whitelists from studio catalog, relabel skipped-steps heading fix: rebuild token whitelists from studio catalog with complete metadata May 5, 2026
chrisli30 and others added 2 commits May 5, 2026 02:39
The legacy token_whitelist/ethereum.json had 99 fully-complete entries
(all four fields populated) — only the field key was wrong (address vs
id), so the wholesale replacement with studio's 30-entry catalog dropped
74 valid tokens (BNB, TONCOIN, USDe, weETH, NEAR, OKB, BUIDL, ONDO,
WLD, FDUSD, etc.).

Re-merge those 74 legacy-only entries (by lowercased id, studio wins on
overlap) so the whitelist still recognises them — they short-circuit the
ERC-20 RPC fallback in TokenEnrichmentService.GetTokenMetadata for
balance lookups.

Net counts after merge:
- ethereum.json:    30 -> 104 entries (+74 legacy-only)
- sepolia.json:     6 -> 6 (full overlap with studio, no change)
- base.json:        23 (unchanged — legacy entries lacked decimals)
- base-sepolia.json: 4 (unchanged — same)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LoadWhitelist used to silently accept any token entry. The original
"address" → "id" field-key bug went undetected because every entry
parsed with id="" and overwrote a single cache slot — visible only
later when balance lookups returned "Unknown Token / UNKNOWN".

Surface two classes of issue at startup so future regressions show up
before they degrade balance lookups:

- Empty id → log Warn, skip the entry (don't pollute cache[""]).
- decimals=0 → log Warn (still cached, since some tokens may legitimately
  have 0 decimals; but most will be a missing-field bug).

Loader now also reports loaded/skipped counts in the debug summary so
sync issues are visible without grepping for individual warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chrisli30 chrisli30 merged commit 2776686 into main May 5, 2026
18 checks passed
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