OPDATA-7578 Add stSLX exchange rate endpoint#5095
Conversation
🦋 Changeset detectedLatest commit: 5113652 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Spawning the local server: Testing it: |
d416f82 to
9d0ad80
Compare
Automated EA Review Harness Output + Live API EvidencePR: #5095 Static Harness CommandEA_REVIEW_NO_TELEMETRY=1 node <ea-review>/dist/cli.js scan \
-a solana-functions \
-r <external-adapters-js> \
-p ci-fast \
--severity critical,high \
-f markdownStatic Harness OutputEA Review ResultsProfile: ci-fast | Adapters: solana-functions | Duration: 89ms Summary
solana-functions🔴 DATA-002 NaN/Infinity Result `src/transport/eusx-price.ts:90`Severity: critical numerator / denominator🔴 DATA-002 NaN/Infinity Result `src/transport/sanctum-infinity.ts:76`Severity: critical Number(totalPoolValueString.result) / Number(totalPoolTokenSupplyString.result)🔴 DATA-002 NaN/Infinity Result `src/transport/sanctum-infinity.ts:76`Severity: critical Number(totalPoolValueString.result)🔴 DATA-002 NaN/Infinity Result `src/transport/sanctum-infinity.ts:76`Severity: critical Number(totalPoolTokenSupplyString.result)🔴 DATA-002 NaN/Infinity Result `src/transport/stslx-exchange-rate.ts:218`Severity: critical (slxBalance * 10n ** BigInt(RESULT_DECIMALS + stslxMint.decimals)) /
(stslxMint.supply * 10n ** BigInt(slxMint.decimals))🟠 SEC-007 Prototype Pollution `src/shared/account-reader.ts:16`Severity: high ): Promise<T> {
const [pda] = await getProgramDerivedAddress({
programAddress,🟠 SEC-007 Prototype Pollution `src/transport/eusx-price.ts:99`Severity: high const programAddress = params.address as Address
const [vestingSchedule, yieldPool] = await Promise.all([
accountReader.fetchAccountInformationByAddressAndSeeds<VestingSchedule>(🟠 SEC-007 Prototype Pollution `src/transport/extension.ts:54`Severity: high const extensionData = buffer.slice(offset, offset + extensionLength)
extensionDataByType[extensionType] = extensionData
offset += extensionLength🟠 SEC-007 Prototype Pollution `src/transport/extension.ts:119`Severity: high for (const baseField of params.baseFields) {
resultData[baseField.name] = getFieldFromBuffer(baseField, data)
}🟠 SEC-007 Prototype Pollution `src/transport/extension.ts:127`Severity: high for (const extensionField of params.extensionFields) {
resultData[extensionField.name] = getExtensionField(extensionField, extensionDataByType)
}🟠 SEC-007 Prototype Pollution `src/transport/extension.ts:132`Severity: high if (params.resultDecimals !== undefined) {
resultData['decimals'] = params.resultDecimals
}🟠 SEC-007 Prototype Pollution `src/transport/sanctum-infinity.ts:62`Severity: high const [totalPoolValueString, totalPoolTokenSupplyString] = await Promise.all([
fetchFieldFromBufferLayoutStateAccount({🟠 SEC-007 Prototype Pollution `src/transport/stslx-exchange-rate.ts:128`Severity: high // GLAM stores token assets in a vault PDA derived from the state account and protocol program.
const [vaultAddress] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), new PublicKey(glamStateAddress).toBuffer()],🟠 SEC-007 Prototype Pollution `src/transport/stslx-exchange-rate.ts:202`Severity: high // divided by stSLX mint supply, normalized by each mint's native decimals.
const [slxMint, stslxMint, slxBalance] = await Promise.all([
this.fetchMintInfo(SLX_MINT_ADDRESS, 'SLX mint'),🟠 CFG-001 Missing Rate Limiting `src/index.ts:1`Severity: high 🟠 DATA-001 Missing Error Response `src/transport/anchor-data.ts:39`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/buffer-layout.ts:29`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/buffer-layout.ts:59`Severity: high const result = await fetchFieldFromBufferLayoutStateAccount({
stateAccountAddress: params.stateAccountAddress,🟠 DATA-001 Missing Error Response `src/transport/eusx-price.ts:61`Severity: high ) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/eusx-price.ts:99`Severity: high const programAddress = params.address as Address
const [vestingSchedule, yieldPool] = await Promise.all([
accountReader.fetchAccountInformationByAddressAndSeeds<VestingSchedule>(🟠 DATA-001 Missing Error Response `src/transport/extension.ts:80`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/pool-token-rate.ts:31`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/pool-token-rate.ts:61`Severity: high const { programAddress, data } = await fetchDataFromBufferLayoutStateAccount({
stateAccountAddress: params.stakePoolAccountAddress,🟠 DATA-001 Missing Error Response `src/transport/sanctum-infinity.ts:32`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/sanctum-infinity.ts:62`Severity: high const [totalPoolValueString, totalPoolTokenSupplyString] = await Promise.all([
fetchFieldFromBufferLayoutStateAccount({🟠 DATA-001 Missing Error Response `src/transport/stslx-exchange-rate.ts:158`Severity: high async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)🟠 DATA-001 Missing Error Response `src/transport/stslx-exchange-rate.ts:202`Severity: high // divided by stSLX mint supply, normalized by each mint's native decimals.
const [slxMint, stslxMint, slxBalance] = await Promise.all([
this.fetchMintInfo(SLX_MINT_ADDRESS, 'SLX mint'),🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/anchor-data.ts:56`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/anchor-data.ts:117`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/buffer-layout.ts:46`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/buffer-layout.ts:76`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/eusx-price.ts:78`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/eusx-price.ts:165`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/extension.ts:97`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/extension.ts:142`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/pool-token-rate.ts:48`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/pool-token-rate.ts:92`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/sanctum-infinity.ts:49`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/sanctum-infinity.ts:87`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/stslx-exchange-rate.ts:175`Severity: high providerIndicatedTimeUnixMs: undefined🟠 ERR-002 Zero/Null Timestamp in Error Response `src/transport/stslx-exchange-rate.ts:241`Severity: high providerIndicatedTimeUnixMs: undefined🟠 CHAIN-001 RPC Timeout `src/transport/stslx-exchange-rate.ts:1`Severity: high Dynamic Harness CommandEA_REVIEW_NO_TELEMETRY=1 node <ea-review>/dist/cli.js scan \
-a solana-functions \
-r <external-adapters-js> \
-p dynamic-only \
--dynamic \
--dynamic-port 18295 \
--dynamic-env RPC_URL=https://api.mainnet-beta.solana.com \
-f markdownDynamic Harness OutputEA Review ResultsProfile: dynamic-only | Adapters: solana-functions | Duration: 5.5s Summary
solana-functions🟠 DYN-002 Runtime Price ValidationSeverity: high 🟠 DYN-006 Data Quality BoundsSeverity: high 🟠 DYN-012 Error RecoverySeverity: high 🟡 DYN-009 Response ConsistencySeverity: medium Live Endpoint Runtime CommandEA_PORT=18205 \
RPC_URL=https://api.mainnet-beta.solana.com \
METRICS_ENABLED=false \
yarn node -e "require('./dist/index.js').server()"Live Endpoint Request/Response{
"rpcUrl": "https://api.mainnet-beta.solana.com",
"request": {
"data": {
"endpoint": "stslx-exchange-rate",
"glamStateAddress": "5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB",
"minRate": "950000000000000000",
"maxRate": "1050000000000000000"
}
},
"response": {
"statusCode": 200,
"elapsedMs": 1008,
"body": {
"data": {
"result": "1009079081993244827",
"computedResult": "1009079081993244827",
"decimals": 18,
"minRate": "950000000000000000",
"maxRate": "1050000000000000000",
"boundsApplied": false
},
"statusCode": 200,
"result": "1009079081993244827",
"timestamps": {
"providerDataRequestedUnixMs": 1782130917209,
"providerDataReceivedUnixMs": 1782130917432
}
}
}
} |
Follow-up: EA Review Finding ScopeThe automated Static Harness ScopeStatic harness failures reported:
Findings directly in new stSLX code:
Touched but not specific to the new endpoint:
Pre-existing files with findings:
Dynamic Harness ScopeDynamic harness failures reported:
These are not clearly caused by the new stSLX endpoint. The experimental dynamic harness sends generic/default adapter requests and received SummaryMost harness findings are from pre-existing |
Follow-up: Why the PR-local EA review findings are no-fixThis is a concise rationale for the findings that landed in files introduced or touched by this PR. I am not treating all findings as generic false positives; some are real-ish but adapter-wide or cross-cutting. I do not see a required endpoint code change for this PR.
Dynamic harness note: the Suggested follow-up outside this PR: if the team wants to address the most legitimate scanner concern, create a separate shared task for explicit Solana RPC timeout support in |
mohamed-mehany
left a comment
There was a problem hiding this comment.
Review summary
Thanks @magiodev-cll for putting this together.
To be upfront: I went through the logic and I believe while it maybe functional and matches the methodology in OPDATA-7578 (vault SLX balance / stSLX supply, bigint fixed-point, min/max clamp).
The problem is the state of the code, it's quite hard to read and review, and it reads as largely LLM-generated. That's fine as a starting point, but for this to meet Chainlink standards it needs to follow our best practices, reuse the abstractions we already have, and be reliable/maintainable enough for the next person to review and own. Right now it forks its own parallel idiom instead of building on the adapter.
Could you please refactor this and, when you do, explicitly instruct the model to follow best practices, prefer existing abstractions, and not reinvent what the adapter already provides? Also, for large PRs like this one, please use a stronger model (ideally Opus 4.8 or equivalent) to avoid the code smells below. There should be no magic numbers or arbitrary hardcoded values left in the final version; every constant should be named and traceable to a source (IDL, on-chain address, or config).
Correctness issues
- Non-atomic reads cause the exact read-skew the ticket warns about. The SLX balance and stSLX supply are fetched as three independent
getAccountInfocalls viaPromise.all, so they can resolve at different slots and produce a transiently wrong rate. Use a singlegetMultipleAccountscall to read them atomically (same slot). Note the strcUSX PR already does this correctly, please mirror that. - Confirm the ATA assumption with Solstice/GLAM. We derive the canonical associated token account for the vault's SLX, whereas the ticket's reference code enumerates
getTokenAccountsByOwner. If GLAM ever holds SLX in a non-ATA account, this endpoint breaks. It fails safe today (mint/owner are validated), but the assumption should be confirmed and documented.
Code quality / structure
- Reuse existing abstractions. The adapter already has
SolanaAccountReaderandshared/buffer-layout-accounts.ts(typedMintLayout/AccountLayoutdecode + owner/length checks). This PR re-implements raw RPC reads and buffer slicing with hand-rolled response types (AccountInfoRpcResponse,ascasts) instead of extending those. Please build on the shared readers. - De-duplicate shared helpers.
parseRateBound,getAccountDataBuffer,assertOwnerProgram/assertTokenProgramOwner, the bounds-clamp, and theAccountInfo/MintInfotypes are duplicated verbatim with the strcUSX PR. Extract them intosrc/shared/so both endpoints share one implementation. - Stick to one Solana stack. New code pulls in legacy
@solana/web3.jsPublicKeyand@solana/spl-tokenhelpers, while the rest of the adapter uses the modern@solana/addresses/@solana/rpcstack (getProgramDerivedAddress). Please align with the existing stack rather than mixing both. - No hardcoded values / magic numbers. Pull the bounds-clamp out into a single named helper instead of an inline nested ternary, and ensure any constant (decimals, addresses) is a named, documented constant.
boundsAppliedis computed by string comparison (result !== computedResult); compare the bigint values instead.
Suggestion: extract a shared-components PR and stack these on topStepping back from the per-PR comments, both PRs (#5095 (stSLX) and #5097 (strcUSX)) and endpoints duplicate a substantial amount of the same code ( Rather than refactoring each PR independently and reconciling the two copies later, it
Benefits:
|
|
Two follow-ups:
|
9d0ad80 to
76fd8f3
Compare
| export const SLX_MINT_ADDRESS = 'SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq' | ||
| export const STSLX_MINT_ADDRESS = 'GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq' | ||
| export const GLAM_VAULT_ADDRESS = 'GMwdh2jTdTrrhA7dMR7Cc2zC6gV38UePzAXeoFHrXnfH' | ||
| export const SLX_TOKEN_ACCOUNT_ADDRESS = '7CssRFNePpnDiCzjRC5kPRDpEJn87JMeDG7s6Gww9CTf' | ||
|
|
36d1ca6 to
781cafa
Compare
|
Code Review: PR #5095 — stSLX exchange rate: use new shared helpers Verdict: APPROVE — tight, focused refactoring. Three minor points, none blocking. What Changed (+ lines only) Adopts assertTokenProgramOwner, derivePda, and parseSolanaAddress from PR #5128. Adds deriveVaultAddress and deriveSlxTokenAccountAddress as exported helpers. Exports intermediate addresses (GLAM_STATE_ADDRESS, GLAM_PROTOCOL_PROGRAM_ADDRESS, ASSOCIATED_TOKEN_PROGRAM_ADDRESS) that were previously implicit. Findings stslx-exchange-rate.ts:L59: [LOW] correctness: parseSolanaAddress(LEGACY_TOKEN_PROGRAM_ADDRESS, 'tokenProgramAddress') validates a known-good constant (TOKEN_PROGRAM_ID.toBase58()). This call can never throw — redundant guard. Pass it directly or cast with address(). stslx-exchange-rate.ts:L51: [LOW] shrink: vaultAddress.toString() — derivePda returns Address which already extends string. No-op. Return vaultAddress directly. stslx-exchange-rate.ts:L44-61: [LOW] yagni: deriveVaultAddress and deriveSlxTokenAccountAddress are exported but _handleRequest still uses the hardcoded GLAM_VAULT_ADDRESS and SLX_TOKEN_ACCOUNT_ADDRESS constants. If these only exist to verify the hardcoded values in tests, a single test assertion is enough — no need to export them from the transport module. |
781cafa to
5d8f0d3
Compare
| "endpoint": "stslx-exchange-rate", | ||
| "minRate": "950000000000000000", | ||
| "maxRate": "1050000000000000000" | ||
| } |
There was a problem hiding this comment.
consider:
- The ability to enable/disable rateBound. For ie: applyBounds(boolean)
|
| export const SLX_MINT_ADDRESS = 'SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq' | ||
| export const STSLX_MINT_ADDRESS = 'GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq' | ||
| export const GLAM_VAULT_ADDRESS = 'GMwdh2jTdTrrhA7dMR7Cc2zC6gV38UePzAXeoFHrXnfH' | ||
| export const SLX_TOKEN_ACCOUNT_ADDRESS = '7CssRFNePpnDiCzjRC5kPRDpEJn87JMeDG7s6Gww9CTf' |
There was a problem hiding this comment.
On the hardcoded addresses in the transport (SLX_MINT_ADDRESS, STSLX_MINT_ADDRESS, GLAM_VAULT_ADDRESS, SLX_TOKEN_ACCOUNT_ADDRESS): it works and the derivation test is a good mitigation, but I'd reconsider the approach. Three points:
1. It diverges from this EA's convention. The other endpoints parameterize their addresses via inputParameters with a default rather than hardcoding them in the transport, e.g. eusx-price takes address as a required param with a default, and pool-token-rate/anchor-data take their account addresses as params. Parameterizing with defaults gives the same out-of-the-box behavior plus: the addresses get documented in the generated README, they're visible at the job-spec layer, and they're overridable without a code change. And there's no determinism cost, since all NOPs share the same defaults so the computed rate is identical across operators.
2. Hardcoding makes the feed un-reconfigurable without a release. If Solstice migrates the GLAM vault, redeploys, or the mint changes, this needs a code change + EA release + redeploy instead of a config/job-spec update. That's a slow path for a live price feed.
3. Two of the four constants are derived values and shouldn't be hardcoded at all. GLAM_VAULT_ADDRESS and SLX_TOKEN_ACCOUNT_ADDRESS are deterministic functions of the primary addresses:
- vault = PDA of
["vault", glamState]under the GLAM program - SLX ATA = ATA of
(vault, SLX_MINT)under the legacy token program
Hardcoding a derived value adds stale-risk for no benefit, and the derivation test is already computing them to prove the constants, so the code derives them twice. Cleaner to derive vault + ATA once from the source addresses via the shared derivePda (@solana/addresses, no web3.js, no I/O), which removes the constants-to-keep-in-sync and the need for the derivation test.
One caveat on that test: it only proves the constants are internally consistent (given the test's glamState/program/mint, the vault/ATA derive correctly). It does not prove the primary addresses are the correct real-world ones, so real-world correctness still rests on the mainnet smoke test.
Suggested shape: expose the primary addresses (or just glamState) as input params with defaults, and derive vault + ATA at runtime from them via derivePda. If you'd rather keep it locked/hardcoded for a single feed (also fine), then at least only hardcode the source-of-truth addresses (program, mints, glamState) and derive vault + ATA from them rather than hardcoding derived values.
| const providerError = (message: string) => | ||
| new AdapterDataProviderError( | ||
| { | ||
| message, | ||
| statusCode: 502, | ||
| }, | ||
| { | ||
| providerDataRequestedUnixMs: 0, | ||
| providerDataReceivedUnixMs: 0, | ||
| providerIndicatedTimeUnixMs: undefined, | ||
| }, | ||
| ) | ||
|
|
||
| const asProviderError = <T>(callback: () => T) => { | ||
| try { | ||
| return callback() | ||
| } catch (e: unknown) { | ||
| throw providerError(e instanceof Error ? e.message : 'Unknown provider error') | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
providerError is duplicated with #5097. Extract it to src/shared/ rather than defining it in both transports.
| return { | ||
| data: { | ||
| result, | ||
| computedResult, | ||
| decimals: RESULT_DECIMALS, | ||
| boundsApplied, | ||
| }, | ||
| statusCode: 200, | ||
| result, | ||
| timestamps: { | ||
| providerDataRequestedUnixMs, | ||
| providerDataReceivedUnixMs: Date.now(), | ||
| providerIndicatedTimeUnixMs: undefined, | ||
| }, | ||
| } |
There was a problem hiding this comment.
Please add slxBalance + stslxSupply so a wrong rate is debuggable from the response and the two endpoints are consistent.
| addressEncoder.encode(address(LEGACY_TOKEN_PROGRAM_ADDRESS)), | ||
| addressEncoder.encode(parseSolanaAddress(slxMintAddress, 'slxMintAddress')), | ||
| ]) | ||
|
|
There was a problem hiding this comment.
Redundant address re-parsing. parseSolanaAddress runs in _handleRequest, then again inside deriveVaultAddress/deriveSlxTokenAccountAddress, then again inside derivePda. Idempotent but wasteful/confusing; consider threading the parsed Address through.
610e127 to
92f3cfd
Compare
02913c8 to
fbc893c
Compare
| const asProviderError = <T>(callback: () => T) => { | ||
| try { | ||
| return callback() | ||
| } catch (e: unknown) { | ||
| throw providerError(e instanceof Error ? e.message : 'Unknown provider error') | ||
| } | ||
| } |
There was a problem hiding this comment.
Delete asProviderError once #5128 fixes the error taxonomy. It exists only to convert the shared helpers' AdapterInputError 500 into a 502. Once the shared account/decode helpers throw AdapterDataProviderError directly (see #5128), the wrapper and all its call sites (lines 117, 118, 121, 130, 136, 142) collapse to plain calls. This also removes the current inconsistency where strcUSX does not wrap and emits 500 for the same failures.
fbc893c to
5113652
Compare
Closes
OPDATA-7578
Description
Adds a
stslx-exchange-rateendpoint to the existingsolana-functionsadapter for the Solstice stSLX-SLX exchange-rate feed.Changes
stslx-exchange-rateendpoint insolana-functions.Steps to Test
yarn prettier --check "packages/sources/solana-functions/src/endpoint/stslx-exchange-rate.ts" "packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts" "packages/sources/solana-functions/test/unit/stslx-exchange-rate.test.ts" "packages/sources/solana-functions/test/integration/stslx-exchange-rate.test.ts"yarn workspace @chainlink/solana-functions-adapter buildyarn eslint "packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts" "packages/sources/solana-functions/src/endpoint/stslx-exchange-rate.ts" "packages/sources/solana-functions/test/unit/stslx-exchange-rate.test.ts" "packages/sources/solana-functions/test/integration/stslx-exchange-rate.test.ts"yarn tsc -p "packages/sources/solana-functions/tsconfig.test.json"yarn test "packages/sources/solana-functions/test" --runInBandQuality Assurance
infra-k8supdate needed.adapter-secretsupdate needed.test-payload.jsonexists underpackages/sources/solana-functions; unit and integration tests were added instead.feat/OPDATA-7578-stslx-exchange-rate.any/disable patterns.Automated EA Review and Live Evidence
ea-reviewstatic + dynamic harness was posted in this PR comment: OPDATA-7578 Add stSLX exchange rate endpoint #5095 (comment)https://api.mainnet-beta.solana.com.stslx-exchange-rateresponse returnedstatusCode: 200,result: 1009061601987286399,computedResult: 1009061601987286399, andboundsApplied: false.