Skip to content

feat: stork integration#3324

Open
pp346 wants to merge 1 commit intomainfrom
feat/stork
Open

feat: stork integration#3324
pp346 wants to merge 1 commit intomainfrom
feat/stork

Conversation

@pp346
Copy link
Contributor

@pp346 pp346 commented Feb 17, 2026

Changelist

Added Stork support with 2 assets: SPXUSD and XAGUSD

Test Plan

Ran tests + built the chain locally

Author/Reviewer Checklist

  • If this PR has changes that result in a different app state given the same prior state and transaction list, manually add the state-breaking label.
  • If the PR has breaking postgres changes to the indexer add the indexer-postgres-breaking label.
  • If this PR isn't state-breaking but has changes that modify behavior in PrepareProposal or ProcessProposal, manually add the label proposal-breaking.
  • If this PR is one of many that implement a specific feature, manually label them all feature:[feature-name].
  • If you wish to for mergify-bot to automatically create a PR to backport your change to a release branch, manually add the label backport/[branch-name].
  • Manually add any of the following labels: refactor, chore, bug.

Summary by CodeRabbit

Release Notes

  • New Features
    • Integrated Stork oracle as a new price data provider for real-world asset (RWA) market feeds
    • Added support for two new market pairs: XAG-USD (Silver) and SPX-USD (S&P 500 Index)

@pp346 pp346 requested a review from a team as a code owner February 17, 2026 16:06
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

This PR adds Stork as a new price provider to the protocol's oracle system, including a complete price function implementation, configuration registration, test utilities, and framework enhancements to support authentication headers for HTTP requests.

Changes

Cohort / File(s) Summary
Stork price function module
protocol/daemons/pricefeed/client/price_function/stork/stork.go, exchange_query_details.go, exchange_query_details_test.go, stork_test.go, testdata/xag_ticker.json
New Stork price provider implementation with response parsing, price conversion (from 1e18 scale), ticker interface implementation, median price calculation, and comprehensive test coverage including edge cases (invalid JSON, price overflow, multi-market responses).
Exchange registry and configuration
protocol/daemons/pricefeed/client/constants/exchange_id.go, static_exchange_details.go, static_exchange_query_config.go, static_exchange_query_config_test.go, protocol/testutil/daemons/pricefeed/exchange_config/market_id.go, static_market_names.go, testnet_exchange_market_config.go
Added EXCHANGE_ID_STORK constant, registered Stork in StaticExchangeDetails and StaticExchangeQueryConfig (5 req/s rate limit), introduced test market IDs 34–35 (XAG-USD, SPX-USD) with corresponding names and testnet configuration entries.
Header-aware HTTP request support
protocol/daemons/types/request_handler.go, protocol/daemons/pricefeed/client/handler/exchange_query_handler.go, protocol/mocks/RequestHandler.go
Extended RequestHandler interface and implementation with GetWithHeaders method; refactored Get to delegate to GetWithHeaders(ctx, url, nil); updated ExchangeQueryHandler.Query to conditionally use GetWithHeaders when exchangeQueryDetails.Headers is populated.
Exchange query framework extension
protocol/daemons/pricefeed/client/types/exchange_query_details.go
Added optional Headers map[string]string field to ExchangeQueryDetails struct to enable custom authentication and request headers per exchange configuration.
Slinky oracle and deployment configuration
protocol/contrib/slinky/oracle.json, protocol/docker-compose.yml
Added stork_api provider entry with API endpoint, authentication (apiKey, apiKeyHeader), timeout, and interval settings; added corresponding SLINKY_CONFIG_PROVIDERS_STORK_API environment variable to slinky service.

Sequence Diagram

sequenceDiagram
    participant ExchangeQueryHandler
    participant RequestHandler
    participant StorkAPI as Stork API<br/>(REST Endpoint)
    participant StorkPriceFunction
    participant PriceConverter

    ExchangeQueryHandler->>ExchangeQueryHandler: Load ExchangeQueryDetails<br/>(includes Headers with auth)
    ExchangeQueryHandler->>RequestHandler: GetWithHeaders(ctx, url, headers)
    RequestHandler->>RequestHandler: Apply headers to request<br/>(Authorization, etc.)
    RequestHandler->>StorkAPI: HTTP GET /v1/prices/latest
    StorkAPI-->>RequestHandler: JSON Response<br/>(asset prices scaled 1e18)
    RequestHandler-->>ExchangeQueryHandler: *http.Response
    ExchangeQueryHandler->>StorkPriceFunction: Parse response
    StorkPriceFunction->>PriceConverter: convertStorkPrice(rawPrice)
    PriceConverter-->>StorkPriceFunction: Decimal price string
    StorkPriceFunction->>StorkPriceFunction: Create StorkTicker instances
    StorkPriceFunction->>StorkPriceFunction: Calculate median price<br/>via GetMedianPricesFromTickers
    StorkPriceFunction-->>ExchangeQueryHandler: tickerToPrice map
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

non-breaking

Suggested reviewers

  • teddyding
  • vincentwschau
  • chenyaoy

Poem

🐰 Hop, hop! The Stork flies in with prices so fine,
Headers and scales, each number divine,
From 1e18 down to decimals neat,
Medians calculated, the oracle's complete!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: stork integration' clearly and concisely summarizes the main change—adding Stork oracle support to the codebase.
Description check ✅ Passed The description includes the required Changelist and Test Plan sections with sufficient detail, and all Author/Reviewer Checklist items are marked complete.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/stork

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

Comment @coderabbitai help to get the list of available commands and usage tips.

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

🧹 Nitpick comments (6)
protocol/daemons/pricefeed/client/constants/static_exchange_query_config_test.go (1)

166-168: Missing a test case for STORK in TestStaticExchangeQueryConfigCache.

The length assertion was updated to 16, but the table-driven test TestStaticExchangeQueryConfigCache (lines 12–163) doesn't include a "Get STORK exchangeDetails" case to validate the STORK entry's values. Every other exchange (except test exchanges) has a corresponding case.

Proposed addition to the test table

Add a test case alongside the existing ones (e.g., after the CoinbasePro case at line 150):

"Get Stork exchangeDetails": {
    exchangeId: exchange_common.EXCHANGE_ID_STORK,
    expectedValue: &types.ExchangeQueryConfig{
        ExchangeId: exchange_common.EXCHANGE_ID_STORK,
        IntervalMs: 2_000,
        TimeoutMs:  3_000,
        MaxQueries: 1,
    },
    expectedFound: true,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@protocol/daemons/pricefeed/client/constants/static_exchange_query_config_test.go`
around lines 166 - 168, TestStaticExchangeQueryConfigCache is missing a case for
STORK; add a table entry in the existing table-driven test
(TestStaticExchangeQueryConfigCache) alongside the other exchange cases (e.g.,
after the CoinbasePro case) that uses exchange_common.EXCHANGE_ID_STORK and
asserts found==true and the expected types.ExchangeQueryConfig values
(ExchangeId: exchange_common.EXCHANGE_ID_STORK, IntervalMs: 2_000, TimeoutMs:
3_000, MaxQueries: 1) to validate constants.StaticExchangeQueryConfig contains
the STORK entry.
protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details_test.go (1)

10-19: Consider adding a test for the Headers field.

The tests cover Url and IsMultiMarket but skip the Headers map, which carries the Authorization header. Since Headers is the security-sensitive piece of this integration (authentication), a test asserting its structure (e.g., that the key "Authorization" exists and starts with "Basic ") would improve coverage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details_test.go`
around lines 10 - 19, Add a unit test asserting the Authorization header on
stork.StorkDetails by creating a new test (e.g., TestStorkHeaders or extend
TestStorkIsMultiMarket) that checks stork.StorkDetails.Headers contains the
"Authorization" key, that the value is non-empty, and that it starts with the
"Basic " prefix; use require.NotEmpty/require.Contains and
require.True(strings.HasPrefix(...)) (import strings) to validate structure so
the security-sensitive header is covered.
protocol/daemons/pricefeed/client/constants/static_exchange_query_config.go (1)

124-130: Minor: Add a documentation link for the Stork rate limit.

Other exchange entries include a link to the rate-limit documentation. Consider adding the Stork API docs link for the "5 requests per second" claim for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/daemons/pricefeed/client/constants/static_exchange_query_config.go`
around lines 124 - 130, The Stork exchange entry
(exchange_common.EXCHANGE_ID_STORK) currently states "rate limit of 5 requests
per second" without a reference; add the same style documentation URL comment
used for other exchanges above this block that cites Stork's rate-limit docs
(e.g., the Stork API or rate-limit page) so the comment for the map entry
includes the source link next to the rate-limit claim for consistency with other
exchange entries.
protocol/daemons/pricefeed/client/types/exchange_query_details.go (1)

26-27: Headers field is well-implemented; API key validation is optional, not critical.

The Headers field addition is clean and properly handled in the query handler. The handler correctly checks len(exchangeQueryDetails.Headers) > 0 before using headers (line 118 of exchange_query_handler.go).

However, if STORK_API_KEY is unset, the Authorization header becomes "Basic ", which will trigger a 401/403 response from the server. This is not a silent failure—the HTTP error is caught and properly reported through the unavailableMarkets map (lines 159–161). Since docker-compose.yml shows this env var is expected to be configured in deployment, adding explicit validation or logging is optional rather than critical.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/daemons/pricefeed/client/types/exchange_query_details.go` around
lines 26 - 27, The Authorization header can become "Basic " when STORK_API_KEY
is unset which causes needless 401/403 responses; update the header-building
logic where exchangeQueryDetails.Headers is applied so you only set the
"Authorization" header if the env var / header value is non-empty (i.e., skip
adding Authorization when its value is ""), and optionally add a startup check
that logs a warning if STORK_API_KEY is unset; look for the Headers field on
ExchangeQueryDetails, the code path in the exchange query handler that checks
len(exchangeQueryDetails.Headers) and inserts headers, and the
unavailableMarkets reporting logic to ensure you still report unavailable
markets but avoid inserting empty "Authorization" values.
protocol/daemons/pricefeed/client/price_function/stork/stork.go (2)

54-63: Minor: doc example doesn't match actual output.

result.Text('f', 18) always produces 18 decimal places, so "29500000000000000000" would yield "29.500000000000000000", not "29.5" as stated in the comment. Not a bug (downstream parsing handles it), but the doc is misleading.

📝 Fix the doc comment
-// For example, "29500000000000000000" becomes "29.5".
+// For example, "29500000000000000000" becomes "29.500000000000000000".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/daemons/pricefeed/client/price_function/stork/stork.go` around lines
54 - 63, The doc comment for convertStorkPrice is misleading: result.Text('f',
18) always returns 18 decimal places, so inputs like "29500000000000000000"
produce "29.500000000000000000" not "29.5"; update the comment above
convertStorkPrice to state that the function returns a decimal string with 18
fractional digits (e.g., "29.500000000000000000") or note that it uses 18 fixed
decimal places, referencing convertStorkPrice and the use of result.Text('f',
18) / storkScale so readers know the exact output format.

14-14: Package-level *big.Float is mutable — consider protecting it.

storkScale is a shared mutable *big.Float. While current usage as a Quo divisor is safe (it won't be mutated), a future caller could accidentally use it as a receiver (e.g., storkScale.Quo(...)) and corrupt it for all subsequent calls. A function-scoped value or a getter function would be more defensive.

♻️ Option: use a function to return a fresh copy each time
-var storkScale = new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
+func storkScale() *big.Float {
+	return new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil))
+}

Then update convertStorkPrice accordingly:

-	result := new(big.Float).SetPrec(256).Quo(priceBigFloat, storkScale)
+	result := new(big.Float).SetPrec(256).Quo(priceBigFloat, storkScale())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@protocol/daemons/pricefeed/client/price_function/stork/stork.go` at line 14,
storkScale is a package-level mutable *big.Float which is unsafe; change it so
each use gets a fresh big.Float instead of sharing the pointer. Replace the
package var storkScale with either a small helper function (e.g., storkScale()
or getStorkScale()) that constructs and returns a new big.Float with 10^18, or
instantiate the scale inside convertStorkPrice before using Quo, and update
convertStorkPrice to call the helper or use the local value so no shared
*big.Float is mutated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details.go`:
- Around line 10-19: StorkDetails is being created at package init and reads
os.Getenv("STORK_API_KEY") immediately, which can produce a blank "Basic "
Authorization header and prevents runtime key rotation; change this by not
evaluating the env var at package-level: either add a startup validation that
reads os.Getenv("STORK_API_KEY") and fails or logs a warning before constructing
StorkDetails, or replace the package var with a constructor function (e.g.,
GetStorkDetails or NewStorkDetails) that builds the Headers map using the
current env value at startup, or defer injecting the Authorization header inside
the request code that uses StorkPriceFunction; ensure you reference and update
StorkDetails (and any code that depends on it) and add a log warning/error when
the API key is missing.

---

Nitpick comments:
In
`@protocol/daemons/pricefeed/client/constants/static_exchange_query_config_test.go`:
- Around line 166-168: TestStaticExchangeQueryConfigCache is missing a case for
STORK; add a table entry in the existing table-driven test
(TestStaticExchangeQueryConfigCache) alongside the other exchange cases (e.g.,
after the CoinbasePro case) that uses exchange_common.EXCHANGE_ID_STORK and
asserts found==true and the expected types.ExchangeQueryConfig values
(ExchangeId: exchange_common.EXCHANGE_ID_STORK, IntervalMs: 2_000, TimeoutMs:
3_000, MaxQueries: 1) to validate constants.StaticExchangeQueryConfig contains
the STORK entry.

In `@protocol/daemons/pricefeed/client/constants/static_exchange_query_config.go`:
- Around line 124-130: The Stork exchange entry
(exchange_common.EXCHANGE_ID_STORK) currently states "rate limit of 5 requests
per second" without a reference; add the same style documentation URL comment
used for other exchanges above this block that cites Stork's rate-limit docs
(e.g., the Stork API or rate-limit page) so the comment for the map entry
includes the source link next to the rate-limit claim for consistency with other
exchange entries.

In
`@protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details_test.go`:
- Around line 10-19: Add a unit test asserting the Authorization header on
stork.StorkDetails by creating a new test (e.g., TestStorkHeaders or extend
TestStorkIsMultiMarket) that checks stork.StorkDetails.Headers contains the
"Authorization" key, that the value is non-empty, and that it starts with the
"Basic " prefix; use require.NotEmpty/require.Contains and
require.True(strings.HasPrefix(...)) (import strings) to validate structure so
the security-sensitive header is covered.

In `@protocol/daemons/pricefeed/client/price_function/stork/stork.go`:
- Around line 54-63: The doc comment for convertStorkPrice is misleading:
result.Text('f', 18) always returns 18 decimal places, so inputs like
"29500000000000000000" produce "29.500000000000000000" not "29.5"; update the
comment above convertStorkPrice to state that the function returns a decimal
string with 18 fractional digits (e.g., "29.500000000000000000") or note that it
uses 18 fixed decimal places, referencing convertStorkPrice and the use of
result.Text('f', 18) / storkScale so readers know the exact output format.
- Line 14: storkScale is a package-level mutable *big.Float which is unsafe;
change it so each use gets a fresh big.Float instead of sharing the pointer.
Replace the package var storkScale with either a small helper function (e.g.,
storkScale() or getStorkScale()) that constructs and returns a new big.Float
with 10^18, or instantiate the scale inside convertStorkPrice before using Quo,
and update convertStorkPrice to call the helper or use the local value so no
shared *big.Float is mutated.

In `@protocol/daemons/pricefeed/client/types/exchange_query_details.go`:
- Around line 26-27: The Authorization header can become "Basic " when
STORK_API_KEY is unset which causes needless 401/403 responses; update the
header-building logic where exchangeQueryDetails.Headers is applied so you only
set the "Authorization" header if the env var / header value is non-empty (i.e.,
skip adding Authorization when its value is ""), and optionally add a startup
check that logs a warning if STORK_API_KEY is unset; look for the Headers field
on ExchangeQueryDetails, the code path in the exchange query handler that checks
len(exchangeQueryDetails.Headers) and inserts headers, and the
unavailableMarkets reporting logic to ensure you still report unavailable
markets but avoid inserting empty "Authorization" values.

Comment on lines +10 to +19
var (
StorkDetails = types.ExchangeQueryDetails{
Exchange: exchange_common.EXCHANGE_ID_STORK,
Url: "https://rest.jp.stork-oracle.network/v1/prices/latest?assets=$",
PriceFunction: StorkPriceFunction,
IsMultiMarket: true,
Headers: map[string]string{
"Authorization": "Basic " + os.Getenv("STORK_API_KEY"),
},
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for os.Getenv usage in exchange details files
find . -path "*/price_function/*/exchange_query_details.go" -type f 2>/dev/null | head -20

Repository: dydxprotocol/v4-chain

Length of output: 1454


🏁 Script executed:

#!/bin/bash
# Check for os.Getenv or os.Environ usage across price_function directory
rg -n 'os\.(Getenv|Environ)' --type go -g '*/price_function/*' -B 2 -A 2

Repository: dydxprotocol/v4-chain

Length of output: 213


🏁 Script executed:

#!/bin/bash
# Examine the stork exchange_query_details.go file to understand the structure
cat -n "protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details.go"

Repository: dydxprotocol/v4-chain

Length of output: 754


🏁 Script executed:

#!/bin/bash
# Search for os.Getenv across all exchange_query_details.go files more directly
rg -n 'os\.Getenv' protocol/daemons/pricefeed/client/price_function/ -B 2 -A 2

Repository: dydxprotocol/v4-chain

Length of output: 604


🏁 Script executed:

#!/bin/bash
# Check a few other exchange files to see if they use Headers with auth
for file in protocol/daemons/pricefeed/client/price_function/coinbase_pro/exchange_query_details.go \
            protocol/daemons/pricefeed/client/price_function/binance/exchange_query_details.go \
            protocol/daemons/pricefeed/client/price_function/kraken/exchange_query_details.go; do
  echo "=== $file ==="
  cat -n "$file" | head -30
done

Repository: dydxprotocol/v4-chain

Length of output: 2241


🏁 Script executed:

#!/bin/bash
# Look for any startup validation or key initialization logic in stork package
find protocol/daemons/pricefeed/client/price_function/stork -type f -name "*.go" | xargs cat | head -100

Repository: dydxprotocol/v4-chain

Length of output: 2915


os.Getenv("STORK_API_KEY") is evaluated once at package initialization time.

StorkDetails is a package-level var, so the environment variable is read when the package is first imported. If the env var is unset, the Authorization header becomes "Basic " (empty credential), and requests will be sent with this malformed header. Consider adding startup-time validation to ensure the key is present and log a warning if it's missing.

Additionally, the API key cannot be rotated without restarting the process — acceptable if that matches your operational model, but worth documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@protocol/daemons/pricefeed/client/price_function/stork/exchange_query_details.go`
around lines 10 - 19, StorkDetails is being created at package init and reads
os.Getenv("STORK_API_KEY") immediately, which can produce a blank "Basic "
Authorization header and prevents runtime key rotation; change this by not
evaluating the env var at package-level: either add a startup validation that
reads os.Getenv("STORK_API_KEY") and fails or logs a warning before constructing
StorkDetails, or replace the package var with a constructor function (e.g.,
GetStorkDetails or NewStorkDetails) that builds the Headers map using the
current env value at startup, or defer injecting the Authorization header inside
the request code that uses StorkPriceFunction; ensure you reference and update
StorkDetails (and any code that depends on it) and add a log warning/error when
the API key is missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant