feat(token-price-oracle): add multi-source price feeds#1002
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughAdds Chainlink, Pyth Hermes, Binance, and OKX price feeds, expands configuration and fallback validation, and pre-registers three devnet test tokens during genesis. Documentation, environment examples, CLI wiring, provider tests, and closed-loop verification reports are included. ChangesMulti-source Token Price Feeds
Devnet TokenRegistry Initialization
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant DeveloperGenesis
participant TokenRegistryStorage
participant PriceUpdater
participant FallbackPriceFeed
participant ChainlinkOrPyth
participant BinanceOrOKX
DeveloperGenesis->>TokenRegistryStorage: Pre-register token metadata and supported IDs
PriceUpdater->>FallbackPriceFeed: GetBatchTokenPrices(tokenIDs)
FallbackPriceFeed->>ChainlinkOrPyth: Request oracle prices
ChainlinkOrPyth-->>FallbackPriceFeed: Return validated prices or an incomplete batch
FallbackPriceFeed->>BinanceOrOKX: Request fallback prices when needed
BinanceOrOKX-->>FallbackPriceFeed: Return exchange prices
FallbackPriceFeed-->>PriceUpdater: Return accepted token prices
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
token-price-oracle/client/chainlink_feed_test.go (1)
63-69: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winSingle-case test with potential precision loss in
TestChainlinkAnswerToFloat.The test verifies only one conversion scenario (123456789000 → 1234.56789) and relies on
Float64()conversion, which truncates to ~17 significant digits. The implementation usesbig.Float.SetPrec(256)for high-precision arithmetic, but the test doesn't validate precision preservation or edge cases:
decimals = 0(no scaling)decimals = 18(extreme precision, common for ERC-20 tokens)- Large answer values where Float64 conversion may lose precision
- Answer = 1 with various decimal positions
Adding parameterized test cases for different decimal values would increase confidence in correct price scaling.
📋 Suggested parameterized test cases
func TestChainlinkAnswerToFloat(t *testing.T) { tests := []struct { name string answer *big.Int decimals uint8 expected float64 }{ { name: "standard 8 decimals", answer: big.NewInt(123456789000), decimals: 8, expected: 1234.56789, }, { name: "no decimals", answer: big.NewInt(2000), decimals: 0, expected: 2000, }, { name: "18 decimals (ERC-20 standard)", answer: big.NewInt(1_000_000_000_000_000_000), // 1 token with 18 decimals decimals: 18, expected: 1.0, }, { name: "single unit", answer: big.NewInt(1), decimals: 8, expected: 0.00000001, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { price := chainlinkAnswerToFloat(tt.answer, tt.decimals) got, _ := price.Float64() if got != tt.expected { t.Fatalf("chainlinkAnswerToFloat(%s, %d) = %v, want %v", tt.answer.String(), tt.decimals, got, tt.expected) } }) } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@token-price-oracle/client/chainlink_feed_test.go` around lines 63 - 69, The TestChainlinkAnswerToFloat function only tests a single case and does not validate the chainlinkAnswerToFloat implementation across edge cases and different decimal positions. Refactor TestChainlinkAnswerToFloat into a parameterized test by creating a test cases table with fields for test name, answer value as *big.Int, decimals as uint8, and expected float64 result. Iterate through the test cases using t.Run() and verify the chainlinkAnswerToFloat function correctly handles scenarios including: decimals=0 (no scaling), decimals=18 (ERC-20 standard), large answer values, and answer=1 with various decimal positions. This ensures the high-precision arithmetic in the implementation is properly validated across different scaling scenarios.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@token-price-oracle/client/chainlink_feed_test.go`:
- Around line 9-61: The TestValidateChainlinkRound test is missing coverage for
critical validation paths in validateChainlinkRound: future timestamp
validation, nil parameter checks, and staleness boundary conditions. Add four
new test cases to the tests table: one for future updatedAt values beyond the
maxStaleness window, separate cases for nil values in each of the four input
parameters (answer, updatedAt, roundID, answeredInRound), and one for the exact
staleness boundary condition where updatedAt is exactly maxStaleness in the
past, with appropriate wantErr values for each.
In `@token-price-oracle/client/chainlink_feed.go`:
- Around line 243-245: The future timestamp validation in the condition check at
line 243 is too permissive because it allows timestamps up to now plus
maxStaleness in the future. Change the condition to reject any updatedAt
timestamp that is after the current time (now), rather than allowing it to be up
to maxStaleness into the future. Replace now.Add(maxStaleness) with just now in
the After() comparison to ensure future-dated timestamps are properly rejected.
In `@token-price-oracle/updater/factory.go`:
- Around line 118-121: The log.Info statement at line 120 logs the raw
ChainlinkRPC URL directly, which exposes potentially sensitive API keys or
authentication credentials. Create a helper function called redactRPCForLog that
parses the RPC URL and returns only the scheme and host portion (e.g.,
"https://example.com"), stripping out any credentials or path-based API keys.
Then update the log.Info call to use the redactRPCForLog function on
cfg.ChainlinkRPC before logging it in the "rpc" field.
---
Nitpick comments:
In `@token-price-oracle/client/chainlink_feed_test.go`:
- Around line 63-69: The TestChainlinkAnswerToFloat function only tests a single
case and does not validate the chainlinkAnswerToFloat implementation across edge
cases and different decimal positions. Refactor TestChainlinkAnswerToFloat into
a parameterized test by creating a test cases table with fields for test name,
answer value as *big.Int, decimals as uint8, and expected float64 result.
Iterate through the test cases using t.Run() and verify the
chainlinkAnswerToFloat function correctly handles scenarios including:
decimals=0 (no scaling), decimals=18 (ERC-20 standard), large answer values, and
answer=1 with various decimal positions. This ensures the high-precision
arithmetic in the implementation is properly validated across different scaling
scenarios.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 238ff072-6b3c-4840-af5f-4bf64cbaa274
📒 Files selected for processing (10)
token-price-oracle/README.mdtoken-price-oracle/client/chainlink_feed.gotoken-price-oracle/client/chainlink_feed_test.gotoken-price-oracle/client/price_feed.gotoken-price-oracle/config/config.gotoken-price-oracle/docker-compose.ymltoken-price-oracle/env.exampletoken-price-oracle/flags/flags.gotoken-price-oracle/local.shtoken-price-oracle/updater/factory.go
| func TestValidateChainlinkRound(t *testing.T) { | ||
| now := time.Unix(1_700_000_000, 0) | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| answer *big.Int | ||
| updatedAt *big.Int | ||
| roundID *big.Int | ||
| answeredInRound *big.Int | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "valid", | ||
| answer: big.NewInt(2000_00000000), | ||
| updatedAt: big.NewInt(now.Add(-5 * time.Minute).Unix()), | ||
| roundID: big.NewInt(10), | ||
| answeredInRound: big.NewInt(10), | ||
| }, | ||
| { | ||
| name: "non-positive answer", | ||
| answer: big.NewInt(0), | ||
| updatedAt: big.NewInt(now.Add(-5 * time.Minute).Unix()), | ||
| roundID: big.NewInt(10), | ||
| answeredInRound: big.NewInt(10), | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "stale", | ||
| answer: big.NewInt(2000_00000000), | ||
| updatedAt: big.NewInt(now.Add(-2 * time.Hour).Unix()), | ||
| roundID: big.NewInt(10), | ||
| answeredInRound: big.NewInt(10), | ||
| wantErr: true, | ||
| }, | ||
| { | ||
| name: "answered in old round", | ||
| answer: big.NewInt(2000_00000000), | ||
| updatedAt: big.NewInt(now.Add(-5 * time.Minute).Unix()), | ||
| roundID: big.NewInt(10), | ||
| answeredInRound: big.NewInt(9), | ||
| wantErr: true, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| err := validateChainlinkRound(tt.answer, tt.updatedAt, tt.roundID, tt.answeredInRound, time.Hour, now) | ||
| if (err != nil) != tt.wantErr { | ||
| t.Fatalf("validateChainlinkRound() error = %v, wantErr %v", err, tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
Incomplete test coverage for validateChainlinkRound validation logic.
The table-driven test covers valid, non-positive answer, stale, and answered-in-old-round cases, but misses critical validation paths shown in the implementation (Context snippet 2):
- Future time validation (line 243 in Context snippet 2):
updated.After(now.Add(maxStaleness))— no test exercises the case whereupdatedAtis too far in the future. - Nil value checks (lines 237-239): The function returns an error if any of the four inputs is
nil, but the test never passesnilvalues. - Staleness boundary: No test for the edge case where
now.Sub(updated) == maxStaleness(exactly at the threshold).
These gaps reduce confidence in round validation, especially for malformed or edge-case data from Chainlink feeds.
📋 Suggested additional test cases
{
name: "future time",
answer: big.NewInt(2000_00000000),
updatedAt: big.NewInt(now.Add(2 * time.Hour).Unix()), // beyond maxStaleness into future
roundID: big.NewInt(10),
answeredInRound: big.NewInt(10),
wantErr: true,
},
{
name: "nil answer",
answer: nil,
updatedAt: big.NewInt(now.Add(-5 * time.Minute).Unix()),
roundID: big.NewInt(10),
answeredInRound: big.NewInt(10),
wantErr: true,
},
{
name: "nil updatedAt",
answer: big.NewInt(2000_00000000),
updatedAt: nil,
roundID: big.NewInt(10),
answeredInRound: big.NewInt(10),
wantErr: true,
},
{
name: "exactly at staleness boundary",
answer: big.NewInt(2000_00000000),
updatedAt: big.NewInt(now.Add(-1 * time.Hour).Unix()), // exactly maxStaleness ago
roundID: big.NewInt(10),
answeredInRound: big.NewInt(10),
// Behavior depends on > vs >=; current impl uses >, so this should pass
wantErr: false,
},🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/client/chainlink_feed_test.go` around lines 9 - 61, The
TestValidateChainlinkRound test is missing coverage for critical validation
paths in validateChainlinkRound: future timestamp validation, nil parameter
checks, and staleness boundary conditions. Add four new test cases to the tests
table: one for future updatedAt values beyond the maxStaleness window, separate
cases for nil values in each of the four input parameters (answer, updatedAt,
roundID, answeredInRound), and one for the exact staleness boundary condition
where updatedAt is exactly maxStaleness in the past, with appropriate wantErr
values for each.
| if updated.After(now.Add(maxStaleness)) { | ||
| return fmt.Errorf("updatedAt %s is too far in the future", updated.UTC().Format(time.RFC3339)) | ||
| } |
There was a problem hiding this comment.
Future timestamp validation is too permissive.
Line 243 currently allows updatedAt up to now + maxStaleness; with a 1h staleness window this accepts future-dated rounds that should be rejected.
Proposed fix
func validateChainlinkRound(answer, updatedAt, roundID, answeredInRound *big.Int, maxStaleness time.Duration, now time.Time) error {
@@
updated := time.Unix(updatedAt.Int64(), 0)
- if updated.After(now.Add(maxStaleness)) {
- return fmt.Errorf("updatedAt %s is too far in the future", updated.UTC().Format(time.RFC3339))
+ const maxFutureSkew = 30 * time.Second
+ if updated.After(now.Add(maxFutureSkew)) {
+ return fmt.Errorf("updatedAt %s is too far in the future (max skew %s)", updated.UTC().Format(time.RFC3339), maxFutureSkew)
}
if now.Sub(updated) > maxStaleness {
return fmt.Errorf("price is stale: updatedAt=%s maxStaleness=%s", updated.UTC().Format(time.RFC3339), maxStaleness)
}📝 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.
| if updated.After(now.Add(maxStaleness)) { | |
| return fmt.Errorf("updatedAt %s is too far in the future", updated.UTC().Format(time.RFC3339)) | |
| } | |
| updated := time.Unix(updatedAt.Int64(), 0) | |
| const maxFutureSkew = 30 * time.Second | |
| if updated.After(now.Add(maxFutureSkew)) { | |
| return fmt.Errorf("updatedAt %s is too far in the future (max skew %s)", updated.UTC().Format(time.RFC3339), maxFutureSkew) | |
| } | |
| if now.Sub(updated) > maxStaleness { | |
| return fmt.Errorf("price is stale: updatedAt=%s maxStaleness=%s", updated.UTC().Format(time.RFC3339), maxStaleness) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/client/chainlink_feed.go` around lines 243 - 245, The
future timestamp validation in the condition check at line 243 is too permissive
because it allows timestamps up to now plus maxStaleness in the future. Change
the condition to reject any updatedAt timestamp that is after the current time
(now), rather than allowing it to be up to maxStaleness into the future. Replace
now.Add(maxStaleness) with just now in the After() comparison to ensure
future-dated timestamps are properly rejected.
| log.Info("Chainlink price feed created", | ||
| "type", "chainlink", | ||
| "rpc", cfg.ChainlinkRPC, | ||
| "eth_usd_feed", cfg.ChainlinkETHUSDFeed.Hex(), |
There was a problem hiding this comment.
Do not log the raw Chainlink RPC URL.
Line 120 logs cfg.ChainlinkRPC directly. RPC URLs commonly include API keys/auth material, which can leak via logs.
🔒 Proposed fix
- log.Info("Chainlink price feed created",
+ log.Info("Chainlink price feed created",
"type", "chainlink",
- "rpc", cfg.ChainlinkRPC,
+ "rpc", redactRPCForLog(cfg.ChainlinkRPC),
"eth_usd_feed", cfg.ChainlinkETHUSDFeed.Hex(),
"max_staleness", cfg.ChainlinkMaxStaleness,
"mapping", mapping)// Add near other private helpers in this file.
func redactRPCForLog(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return "<invalid_rpc_url>"
}
return fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/updater/factory.go` around lines 118 - 121, The log.Info
statement at line 120 logs the raw ChainlinkRPC URL directly, which exposes
potentially sensitive API keys or authentication credentials. Create a helper
function called redactRPCForLog that parses the RPC URL and returns only the
scheme and host portion (e.g., "https://example.com"), stripping out any
credentials or path-based API keys. Then update the log.Info call to use the
redactRPCForLog function on cfg.ChainlinkRPC before logging it in the "rpc"
field.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
token-price-oracle/client/pyth_feed.go (1)
21-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUnused write-lock:
muis only ever RLocked.
tokenPriceIDsis populated once in the constructor and never mutated afterward, yetmuis read-locked inGetTokenPrice/GetBatchTokenPrices. There is noLock()call anywhere in the file, so thesync.RWMutexis vestigial and could mislead future maintainers into assuming mutation support exists.Also applies to: 69-72, 109-122
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@token-price-oracle/client/pyth_feed.go` around lines 21 - 31, The sync.RWMutex field mu is never write-locked and protects immutable state only. Remove mu from PythHermesPriceFeed, eliminate the corresponding RLock/RUnlock calls in GetTokenPrice and GetBatchTokenPrices, and retain the existing read behavior without synchronization.token-price-oracle/client/cex_feed.go (1)
96-115: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff
GetBatchTokenPricesissues one sequential HTTP request per token instead of a batch call.Both Binance (
GET /api/v3/ticker/pricewith nosymbolreturns all tickers) and OKX (GET /api/v5/market/tickerwithinstType) support fetching multiple prices in a single call. The current implementation does N sequential round-trips per update cycle, which scales poorly and increases exposure to per-exchange rate limits as the number of mapped tokens grows.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@token-price-oracle/client/cex_feed.go` around lines 96 - 115, Update CEXPriceFeed.GetBatchTokenPrices to fetch all requested token prices through a single exchange batch request instead of calling GetTokenPrice once per token. Use the Binance and OKX batch ticker APIs, map returned symbols to the requested tokenIDs, preserve the existing ETH-price update and per-token skip behavior, and return the assembled prices map.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@token-price-oracle/client/cex_feed.go`:
- Around line 62-94: Update CEXPriceFeed.GetTokenPrice to lazily initialize
ethPrice when it has not yet been set, fetching the ETH price through the
existing CEX price-fetching path instead of returning the “ETH price not
initialized” error. Reuse the same initialization and synchronization behavior
as GetBatchTokenPrices, while preserving the existing token lookup, mapped-price
fetch, and return flow.
In `@token-price-oracle/local.sh`:
- Line 31: Update the commented Pyth API-key argument in the example command to
expand the documented TOKEN_PRICE_ORACLE_PYTH_API_KEY variable instead of
PYTH_API_KEY.
In `@token-price-oracle/README.md`:
- Around line 74-76: Update the README documentation for
TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY to consistently mark it as optional and
include its default value bitget, matching the default assigned in flags.go;
apply the same correction to the other occurrence and leave the CLI behavior
unchanged.
---
Nitpick comments:
In `@token-price-oracle/client/cex_feed.go`:
- Around line 96-115: Update CEXPriceFeed.GetBatchTokenPrices to fetch all
requested token prices through a single exchange batch request instead of
calling GetTokenPrice once per token. Use the Binance and OKX batch ticker APIs,
map returned symbols to the requested tokenIDs, preserve the existing ETH-price
update and per-token skip behavior, and return the assembled prices map.
In `@token-price-oracle/client/pyth_feed.go`:
- Around line 21-31: The sync.RWMutex field mu is never write-locked and
protects immutable state only. Remove mu from PythHermesPriceFeed, eliminate the
corresponding RLock/RUnlock calls in GetTokenPrice and GetBatchTokenPrices, and
retain the existing read behavior without synchronization.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2f1accfd-350f-4a5f-a35c-52ff0f91f2ce
📒 Files selected for processing (11)
token-price-oracle/README.mdtoken-price-oracle/client/cex_feed.gotoken-price-oracle/client/cex_feed_test.gotoken-price-oracle/client/pyth_feed.gotoken-price-oracle/client/pyth_feed_test.gotoken-price-oracle/config/config.gotoken-price-oracle/docker-compose.ymltoken-price-oracle/env.exampletoken-price-oracle/flags/flags.gotoken-price-oracle/local.shtoken-price-oracle/updater/factory.go
| // GetTokenPrice returns token price in USD. | ||
| func (f *CEXPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (*TokenPrice, error) { | ||
| f.mu.RLock() | ||
| symbol, exists := f.tokenMap[tokenID] | ||
| ethPrice := new(big.Float).Copy(f.ethPrice) | ||
| f.mu.RUnlock() | ||
|
|
||
| if !exists { | ||
| return nil, fmt.Errorf("token ID %d not mapped to %s trading pair", tokenID, f.source) | ||
| } | ||
| if ethPrice.Cmp(big.NewFloat(0)) == 0 { | ||
| return nil, fmt.Errorf("ETH price not initialized, please call GetBatchTokenPrices first") | ||
| } | ||
|
|
||
| tokenPrice, err := f.fetchMappedPrice(ctx, symbol) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to fetch %s price for %s: %w", f.source, symbol, err) | ||
| } | ||
|
|
||
| f.log.Info("Fetched price from CEX", | ||
| "source", f.source, | ||
| "token_id", tokenID, | ||
| "symbol", symbol, | ||
| "token_price_usd", tokenPrice.String(), | ||
| "eth_price_usd", ethPrice.String()) | ||
|
|
||
| return &TokenPrice{ | ||
| TokenID: tokenID, | ||
| Symbol: symbol, | ||
| TokenPriceUSD: tokenPrice, | ||
| EthPriceUSD: ethPrice, | ||
| }, nil | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
GetTokenPrice silently depends on GetBatchTokenPrices having run first.
If GetTokenPrice is called before GetBatchTokenPrices initializes ethPrice, it fails with "ETH price not initialized" instead of fetching the ETH price itself. This is a hidden coupling between two public API methods on the same interface (unlike the Chainlink/Pyth feeds, which are self-contained per call) — any future caller that invokes GetTokenPrice standalone (e.g. a health check or per-token refresh) will silently break.
🔧 Proposed fix — lazily initialize ETH price
func (f *CEXPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (*TokenPrice, error) {
f.mu.RLock()
symbol, exists := f.tokenMap[tokenID]
ethPrice := new(big.Float).Copy(f.ethPrice)
f.mu.RUnlock()
if !exists {
return nil, fmt.Errorf("token ID %d not mapped to %s trading pair", tokenID, f.source)
}
if ethPrice.Cmp(big.NewFloat(0)) == 0 {
- return nil, fmt.Errorf("ETH price not initialized, please call GetBatchTokenPrices first")
+ if err := f.updateETHPrice(ctx); err != nil {
+ return nil, fmt.Errorf("failed to initialize ETH price: %w", err)
+ }
+ f.mu.RLock()
+ ethPrice = new(big.Float).Copy(f.ethPrice)
+ f.mu.RUnlock()
}📝 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.
| // GetTokenPrice returns token price in USD. | |
| func (f *CEXPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (*TokenPrice, error) { | |
| f.mu.RLock() | |
| symbol, exists := f.tokenMap[tokenID] | |
| ethPrice := new(big.Float).Copy(f.ethPrice) | |
| f.mu.RUnlock() | |
| if !exists { | |
| return nil, fmt.Errorf("token ID %d not mapped to %s trading pair", tokenID, f.source) | |
| } | |
| if ethPrice.Cmp(big.NewFloat(0)) == 0 { | |
| return nil, fmt.Errorf("ETH price not initialized, please call GetBatchTokenPrices first") | |
| } | |
| tokenPrice, err := f.fetchMappedPrice(ctx, symbol) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to fetch %s price for %s: %w", f.source, symbol, err) | |
| } | |
| f.log.Info("Fetched price from CEX", | |
| "source", f.source, | |
| "token_id", tokenID, | |
| "symbol", symbol, | |
| "token_price_usd", tokenPrice.String(), | |
| "eth_price_usd", ethPrice.String()) | |
| return &TokenPrice{ | |
| TokenID: tokenID, | |
| Symbol: symbol, | |
| TokenPriceUSD: tokenPrice, | |
| EthPriceUSD: ethPrice, | |
| }, nil | |
| } | |
| // GetTokenPrice returns token price in USD. | |
| func (f *CEXPriceFeed) GetTokenPrice(ctx context.Context, tokenID uint16) (*TokenPrice, error) { | |
| f.mu.RLock() | |
| symbol, exists := f.tokenMap[tokenID] | |
| ethPrice := new(big.Float).Copy(f.ethPrice) | |
| f.mu.RUnlock() | |
| if !exists { | |
| return nil, fmt.Errorf("token ID %d not mapped to %s trading pair", tokenID, f.source) | |
| } | |
| if ethPrice.Cmp(big.NewFloat(0)) == 0 { | |
| if err := f.updateETHPrice(ctx); err != nil { | |
| return nil, fmt.Errorf("failed to initialize ETH price: %w", err) | |
| } | |
| f.mu.RLock() | |
| ethPrice = new(big.Float).Copy(f.ethPrice) | |
| f.mu.RUnlock() | |
| } | |
| tokenPrice, err := f.fetchMappedPrice(ctx, symbol) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to fetch %s price for %s: %w", f.source, symbol, err) | |
| } | |
| f.log.Info("Fetched price from CEX", | |
| "source", f.source, | |
| "token_id", tokenID, | |
| "symbol", symbol, | |
| "token_price_usd", tokenPrice.String(), | |
| "eth_price_usd", ethPrice.String()) | |
| return &TokenPrice{ | |
| TokenID: tokenID, | |
| Symbol: symbol, | |
| TokenPriceUSD: tokenPrice, | |
| EthPriceUSD: ethPrice, | |
| }, nil | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/client/cex_feed.go` around lines 62 - 94, Update
CEXPriceFeed.GetTokenPrice to lazily initialize ethPrice when it has not yet
been set, fetching the ETH price through the existing CEX price-fetching path
instead of returning the “ETH price not initialized” error. Reuse the same
initialization and synchronization behavior as GetBatchTokenPrices, while
preserving the existing token lookup, mapped-price fetch, and return flow.
| # --chainlink-max-staleness 1h \ | ||
| # --token-mapping-chainlink "1:0x...,2:0x..." \ | ||
| # --pyth-hermes-base-url https://hermes.pyth.network \ | ||
| # --pyth-api-key "$PYTH_API_KEY" \ |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Use the documented Pyth API-key variable.
This example expands $PYTH_API_KEY, but the documented variable is TOKEN_PRICE_ORACLE_PYTH_API_KEY. Unless callers define both, the copied command passes an empty key.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/local.sh` at line 31, Update the commented Pyth API-key
argument in the example command to expand the documented
TOKEN_PRICE_ORACLE_PYTH_API_KEY variable instead of PYTH_API_KEY.
| | `TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY` | Enabled price feeds in fallback order | | ||
|
|
||
| Each enabled price feed also requires its own mapping/configuration in the feed sections below. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Document feed priority consistently as optional.
flags.go assigns TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY the default bitget, but this README labels it both “Required” and “Optional.” Keep it in the optional table with its default, or remove the CLI default and require it consistently.
Also applies to: 90-90
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@token-price-oracle/README.md` around lines 74 - 76, Update the README
documentation for TOKEN_PRICE_ORACLE_PRICE_FEED_PRIORITY to consistently mark it
as optional and include its default value bitget, matching the default assigned
in flags.go; apply the same correction to the other occurrence and leave the CLI
behavior unchanged.
- Add devnet_tokens.go with BTC/ETH/BGB test token definitions - Automatically register tokens in genesis for devnet environment - Enable token-price-oracle to work out-of-the-box without manual setup - Tokens: BTC (ID=1), ETH (ID=2), BGB (ID=3) - Supports multi-source price feeds: Chainlink, Pyth, Bitget, OKX This ensures complete end-to-end flow from genesis generation to price oracle operation in devnet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Complete setup guide for devnet token pre-registration - Configuration examples for token-price-oracle - Troubleshooting and extension recommendations - Final test summary covering all PRs (1021, 1023, 1002) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary of completed tasks: - PR1021 testing (beacon fallback) - PR1023 testing (layer1-verify metrics) - PR1002 testing with devnet TokenRegistry implementation - Complete documentation and troubleshooting guides Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.
Tip: disable this comment in your organization's Code Review settings.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@DEVNET_TOKENREGISTRY_SETUP.md`:
- Around line 137-147: Update the token-price-oracle docker run command in
DEVNET_TOKENREGISTRY_SETUP.md to assign the explicit container name
token-price-oracle, matching the existing docker logs command. Keep the
remaining run options unchanged.
- Line 54: Replace the concrete TOKEN_PRICE_ORACLE_PRIVATE_KEY value in the
setup documentation with a clearly marked placeholder, and explicitly state that
the key must never be used outside an isolated local devnet. Keep the
surrounding setup instructions intact while making the devnet-only restriction
unmistakable.
In `@ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go`:
- Around line 122-126: The devnet activation flow and its documentation must
agree on whether tokens are immediately updateable. In
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go lines 122-126,
deliberately retain or revise the inactive token initialization and test the
intended behavior; in DEVNET_TOKENREGISTRY_SETUP.md lines 5 and 127-141, remove
the unconditional out-of-the-box update claim or state prerequisites, and place
activation/allowlist setup before oracle startup; in FINAL_TEST_SUMMARY.md lines
131-151 and 274-285, mark transaction logs and complete-loop/merge-readiness
claims as conditional until activation and allowlisting are verified.
- Around line 101-130: Update setTokenInfo to encode token.BalanceSlot as
BalanceSlot plus one when non-zero, while preserving zero as zero, matching
L2TokenRegistry’s getTokenInfo and getTokenInfoByAddress contract. Add a
regression test covering getTokenInfo, reverse lookup, getAllTokenIDs,
getSupportedTokenList, and zero/non-zero balance-slot storage encoding.
In `@TODAY_WORK_SUMMARY.md`:
- Around line 187-201: Reconcile all validation reports to use the pending
devnet RPC checks as the authoritative status: in TODAY_WORK_SUMMARY.md lines
187-201 retain pending states, lines 94-113 label unverified logs as expected
output, lines 263-277 avoid claiming the full loop is complete, and lines
301-309 update the conclusion and merge recommendation; uncheck unrerun
validations in DEVNET_TOKENREGISTRY_SETUP.md lines 159-166 and align the
coverage matrix with actual evidence in FINAL_TEST_SUMMARY.md lines 171-185.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 6845cc95-9f3e-478e-9365-074215486c8f
📒 Files selected for processing (5)
DEVNET_TOKENREGISTRY_SETUP.mdFINAL_TEST_SUMMARY.mdTODAY_WORK_SUMMARY.mdops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.goops/l2-genesis/morph-chain-ops/genesis/layer_two.go
| export TOKEN_PRICE_ORACLE_L2_TOKEN_REGISTRY_ADDRESS=0x5300000000000000000000000000000000000021 | ||
|
|
||
| # 私钥(devnet默认账户) | ||
| export TOKEN_PRICE_ORACLE_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Make the private key unmistakably devnet-only.
This is the well-known local devnet key, not a unique secret, but publishing it as a ready-to-export value makes accidental reuse against a non-local RPC easy. Use a placeholder in generic documentation and explicitly state that it must never be used outside an isolated devnet.
Suggested documentation change
-export TOKEN_PRICE_ORACLE_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+export TOKEN_PRICE_ORACLE_PRIVATE_KEY=<DEVNET_ONLY_PRIVATE_KEY>📝 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.
| export TOKEN_PRICE_ORACLE_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | |
| export TOKEN_PRICE_ORACLE_PRIVATE_KEY=<DEVNET_ONLY_PRIVATE_KEY> |
🧰 Tools
🪛 Betterleaks (1.6.1)
[high] 54-54: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@DEVNET_TOKENREGISTRY_SETUP.md` at line 54, Replace the concrete
TOKEN_PRICE_ORACLE_PRIVATE_KEY value in the setup documentation with a clearly
marked placeholder, and explicitly state that the key must never be used outside
an isolated local devnet. Keep the surrounding setup instructions intact while
making the devnet-only restriction unmistakable.
Source: Linters/SAST tools
| docker run -d \ | ||
| --network docker_default \ | ||
| --env-file devnet.env \ | ||
| morph/token-price-oracle:latest | ||
| ``` | ||
|
|
||
| ### 4. 验证价格更新 | ||
|
|
||
| ```bash | ||
| # 监控日志 | ||
| docker logs -f token-price-oracle |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Name the Docker container used by the log commands.
docker run does not specify --name, but the guide later executes docker logs -f token-price-oracle. Docker will assign a random name, so this troubleshooting command fails.
Also applies to: 246-247
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@DEVNET_TOKENREGISTRY_SETUP.md` around lines 137 - 147, Update the
token-price-oracle docker run command in DEVNET_TOKENREGISTRY_SETUP.md to assign
the explicit container name token-price-oracle, matching the existing docker
logs command. Keep the remaining run options unchanged.
| // setTokenInfo sets TokenInfo struct in tokenRegistry mapping | ||
| func setTokenInfo(db vm.StateDB, contractAddr common.Address, registrySlot *big.Int, token DevnetTestToken) error { | ||
| // Calculate base slot: keccak256(abi.encode(tokenID, registrySlot)) | ||
| tokenIDBytes := common.LeftPadBytes(big.NewInt(int64(token.TokenID)).Bytes(), 32) | ||
| slotBytes := common.LeftPadBytes(registrySlot.Bytes(), 32) | ||
| baseSlot := crypto.Keccak256Hash(append(tokenIDBytes, slotBytes...)) | ||
|
|
||
| // TokenInfo struct layout (compact storage): | ||
| // slot+0: tokenAddress (address, 20 bytes) + first 12 bytes of balanceSlot | ||
| // slot+1: remaining 20 bytes of balanceSlot | ||
| // slot+2: isActive (bool, 1 byte) + decimals (uint8, 1 byte) in lowest 2 bytes | ||
| // slot+3: scale (uint256, 32 bytes) | ||
|
|
||
| // Slot+0: pack tokenAddress (20 bytes) into lowest bytes | ||
| slot0Value := new(big.Int).SetBytes(token.TokenAddress.Bytes()) | ||
| db.SetState(contractAddr, baseSlot, common.BigToHash(slot0Value)) | ||
|
|
||
| // Slot+1: balanceSlot (bytes32) | ||
| slot1Key := common.BigToHash(new(big.Int).Add(baseSlot.Big(), big.NewInt(1))) | ||
| db.SetState(contractAddr, slot1Key, token.BalanceSlot) | ||
|
|
||
| // Slot+2: isActive=false (0x00) + decimals (1 byte) | ||
| // Pack as: [31 zeros][decimals][isActive=0] | ||
| slot2Key := common.BigToHash(new(big.Int).Add(baseSlot.Big(), big.NewInt(2))) | ||
| slot2Value := new(big.Int).SetUint64(uint64(token.Decimals) << 8) // decimals in second byte | ||
| db.SetState(contractAddr, slot2Key, common.BigToHash(slot2Value)) | ||
|
|
||
| // Slot+3: scale (uint256) | ||
| slot3Key := common.BigToHash(new(big.Int).Add(baseSlot.Big(), big.NewInt(3))) | ||
| db.SetState(contractAddr, slot3Key, common.BigToHash(token.Scale)) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)devnet_tokens\.go$|layer_two_test\.go$|morph-chain-ops|l2-genesis' | sed -n '1,200p'
echo
echo "devnet_tokens.go size and outline:"
fd -a 'devnet_tokens\.go' . | while read -r f; do
echo "== $f =="
wc -l "$f"
ast-grep outline "$f" || true
done
echo
echo "Relevant devnet_tokens.go sections:"
fd -a 'devnet_tokens\.go' . | while read -r f; do
echo "== $f =="
sed -n '1,240p' "$f" | cat -n
done
echo
echo "layer_two_test.go locations and snippets:"
fd -a 'layer_two_test\.go' . | while read -r f; do
echo "== $f =="
wc -l "$f"
sed -n '1,260p' "$f" | cat -n
doneRepository: morph-l2/morph
Length of output: 16896
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "L2TokenRegistry and token registry references:"
rg -n "TokenRegistry|TokenInfo|getTokenInfo|getAllTokenIDs|tokenRegistration|supportedTokenSet|Enum" ops/morph-chain-ops solidity contracts -S 2>/dev/null || true
echo
echo "Search for TokenRegistry.sol or bindings:"
git ls-files | rg -i 'token(registry|.sol)|TokenRegistry|predeploy|bindings|Token\.sol|TokenRegistry' | sed -n '1,240p'
echo
echo "If any TokenRegistry source, show relevant storage declarations:"
for f in $(git ls-files | rg -i 'TokenRegistry\.sol$|TokenRegistry' | sed -n '1,20p'); do
echo "== $f =="
wc -l "$f"
rg -n "bytes32 balanceSlot|TokenInfo|mapping|getTokenInfo|getAllTokenIDs|supportedTokenSet" "$f" -C 3
doneRepository: morph-l2/morph
Length of output: 35766
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "L2TokenRegistry.sol tokenRegistry/tokenRegistration/supportedTokenSet declarations and relevant storage:"
sed -n '1,80p' contracts/contracts/l2/system/L2TokenRegistry.sol | cat -n
echo
echo "contracts/scripts/oracle-testing/token.go token parsing:"
sed -n '1,180p' contracts/scripts/oracle-testing/token.go | cat -n
echo
echo "L2TokenRegistryTest relevant storage parsing/test section:"
sed -n '232,325p' contracts/contracts/test/L2TokenRegistry.t.sol | cat -nRepository: morph-l2/morph
Length of output: 15415
Align the generated balance slot encoding with the contract API.
devnet_tokens.go writes token.BalanceSlot directly at offset 1, but L2TokenRegistry storage/tests pack balanceSlot + 1 there and return balanceSlot - 1 via getTokenInfo/getTokenInfoByAddress. Either write BalanceSlot + 1 when it is non-zero and leave 0 unchanged, so setDevnetTestTokens() matches getTokenInfoByAddress. Add a storage-layout regression test covering getTokenInfo, reverse lookup, getAllTokenIDs() / getSupportedTokenList(), and the balance-slot encoding edge cases.
Suggested comment correction
- // slot+0: tokenAddress (address, 20 bytes) + first 12 bytes of balanceSlot
- // slot+1: remaining 20 bytes of balanceSlot
+ // slot+0: tokenAddress (address, 20 bytes)
+ // slot+1: balanceSlot (bytes32)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go` around lines 101 -
130, Update setTokenInfo to encode token.BalanceSlot as BalanceSlot plus one
when non-zero, while preserving zero as zero, matching L2TokenRegistry’s
getTokenInfo and getTokenInfoByAddress contract. Add a regression test covering
getTokenInfo, reverse lookup, getAllTokenIDs, getSupportedTokenList, and
zero/non-zero balance-slot storage encoding.
| // Slot+2: isActive=false (0x00) + decimals (1 byte) | ||
| // Pack as: [31 zeros][decimals][isActive=0] | ||
| slot2Key := common.BigToHash(new(big.Int).Add(baseSlot.Big(), big.NewInt(2))) | ||
| slot2Value := new(big.Int).SetUint64(uint64(token.Decimals) << 8) // decimals in second byte | ||
| db.SetState(contractAddr, slot2Key, common.BigToHash(slot2Value)) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Make the devnet activation flow contractually consistent.
Genesis only pre-registers inactive tokens and does not populate the allowlist, while the documentation and reports describe immediate successful on-chain price updates. Either add explicit, safe devnet activation/allowlist setup before oracle startup, or downgrade the documentation to registration/price-fetch-only until those steps are performed.
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go#L122-L126: retain or change inactive initialization deliberately and test the intended behavior.DEVNET_TOKENREGISTRY_SETUP.md#L5-L5: remove the unconditional “out-of-the-box” update claim or state prerequisites.DEVNET_TOKENREGISTRY_SETUP.md#L127-L141: move activation and allowlist setup before starting the oracle.FINAL_TEST_SUMMARY.md#L131-L151: mark successful transaction logs as conditional until verified.FINAL_TEST_SUMMARY.md#L274-L285: update the “complete loop” and merge-readiness claims.
📍 Affects 3 files
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go#L122-L126(this comment)DEVNET_TOKENREGISTRY_SETUP.md#L5-L5DEVNET_TOKENREGISTRY_SETUP.md#L127-L141FINAL_TEST_SUMMARY.md#L131-L151FINAL_TEST_SUMMARY.md#L274-L285
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go` around lines 122 -
126, The devnet activation flow and its documentation must agree on whether
tokens are immediately updateable. In
ops/l2-genesis/morph-chain-ops/genesis/devnet_tokens.go lines 122-126,
deliberately retain or revise the inactive token initialization and test the
intended behavior; in DEVNET_TOKENREGISTRY_SETUP.md lines 5 and 127-141, remove
the unconditional out-of-the-box update claim or state prerequisites, and place
activation/allowlist setup before oracle startup; in FINAL_TEST_SUMMARY.md lines
131-151 and 274-285, mark transaction logs and complete-loop/merge-readiness
claims as conditional until activation and allowlisting are verified.
| ## 测试覆盖 | ||
|
|
||
| ### 完整性验证 | ||
|
|
||
| | 测试项 | 状态 | 验证方式 | | ||
| |--------|------|----------| | ||
| | Genesis 生成包含 token | ✅ | 日志输出 "Pre-registered" × 3 | | ||
| | TokenRegistry.getAllTokenIDs() | ⏳ | 需要重新启动 devnet 验证 | | ||
| | TokenRegistry.getTokenInfo(1) | ⏳ | 需要重新启动 devnet 验证 | | ||
| | supportedTokenSet 长度 | ⏳ | 需要重新启动 devnet 验证 | | ||
| | token-price-oracle 启动 | ⏳ | 需要重新启动 devnet 验证 | | ||
| | 多源价格获取 | ✅ | 单元测试通过 | | ||
| | 链上价格更新 | ⏳ | 需要完整 devnet 环境 | | ||
|
|
||
| ⏳ = 等待 devnet 重新启动后验证 |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Reconcile all validation reports before treating the PR as verified.
TODAY_WORK_SUMMARY.md records several end-to-end checks as pending, while the other reports mark them passed and recommend merging. Use one authoritative status, then update every checklist, coverage matrix, conclusion, and merge recommendation.
TODAY_WORK_SUMMARY.md#L187-L201: keep the pending state until the devnet RPC checks complete.DEVNET_TOKENREGISTRY_SETUP.md#L159-L166: uncheck validations that have not been rerun.FINAL_TEST_SUMMARY.md#L171-L185: align the coverage matrix with actual evidence.TODAY_WORK_SUMMARY.md#L94-L113: label successful logs as expected output unless captured from a completed run.TODAY_WORK_SUMMARY.md#L263-L277: do not describe the full loop as complete while checks remain pending.TODAY_WORK_SUMMARY.md#L301-L309: update the final conclusion and merge recommendation after verification.
📍 Affects 3 files
TODAY_WORK_SUMMARY.md#L187-L201(this comment)DEVNET_TOKENREGISTRY_SETUP.md#L159-L166FINAL_TEST_SUMMARY.md#L171-L185TODAY_WORK_SUMMARY.md#L94-L113TODAY_WORK_SUMMARY.md#L263-L277TODAY_WORK_SUMMARY.md#L301-L309
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@TODAY_WORK_SUMMARY.md` around lines 187 - 201, Reconcile all validation
reports to use the pending devnet RPC checks as the authoritative status: in
TODAY_WORK_SUMMARY.md lines 187-201 retain pending states, lines 94-113 label
unverified logs as expected output, lines 263-277 avoid claiming the full loop
is complete, and lines 301-309 update the conclusion and merge recommendation;
uncheck unrerun validations in DEVNET_TOKENREGISTRY_SETUP.md lines 159-166 and
align the coverage matrix with actual evidence in FINAL_TEST_SUMMARY.md lines
171-185.
Summary
L2TokenRegistryunchanged; all external data sources are consumed by the off-chain updater before writing existingpriceRatiovalues.Test plan
cd token-price-oracle && go test ./...cd token-price-oracle && go vet ./...Closes #977
Summary by CodeRabbit