OPDATA-7578 Add strcUSX tranche exchange rate endpoint#5097
OPDATA-7578 Add strcUSX tranche exchange rate endpoint#5097magiodev-cll wants to merge 18 commits into
Conversation
🦋 Changeset detectedLatest commit: a60669c 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 junior Testing senior |
2b9c47c to
e87f379
Compare
Automated EA Review Harness Output + Live API EvidencePR: #5097 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: 87ms 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/strcusx-exchange-rate.ts:420`Severity: critical (assets * 10n ** BigInt(RESULT_DECIMALS + trancheMintDecimals)) /
(shares * 10n ** BigInt(assetMintDecimals))🟠 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/strcusx-exchange-rate.ts:238`Severity: high export const deriveControllerAddress = (programAddress: string) => {
const [address] = PublicKey.findProgramAddressSync(
[Buffer.from('CONTROLLER')],🟠 SEC-007 Prototype Pollution `src/transport/strcusx-exchange-rate.ts:246`Severity: high export const deriveStrategyAddress = (programAddress: string, strategyName: string) => {
const [address] = PublicKey.findProgramAddressSync(
[Buffer.from('STRATEGY'), Buffer.from(strategyName)],🟠 SEC-007 Prototype Pollution `src/transport/strcusx-exchange-rate.ts:254`Severity: high export const deriveAccountingAddress = (programAddress: string, strategyName: string) => {
const [address] = PublicKey.findProgramAddressSync(
[Buffer.from('ACCOUNTING'), Buffer.from(strategyName)],🟠 SEC-007 Prototype Pollution `src/transport/strcusx-exchange-rate.ts:262`Severity: high export const deriveJuniorMintAddress = (programAddress: string, strategyName: string) => {
const [address] = PublicKey.findProgramAddressSync(
[Buffer.from('JUNIOR_MINT'), Buffer.from(strategyName)],🟠 SEC-007 Prototype Pollution `src/transport/strcusx-exchange-rate.ts:270`Severity: high export const deriveSeniorMintAddress = (programAddress: string, strategyName: string) => {
const [address] = PublicKey.findProgramAddressSync(
[Buffer.from('SENIOR_MINT'), Buffer.from(strategyName)],🟠 SEC-007 Prototype Pollution `src/transport/strcusx-exchange-rate.ts:536`Severity: high const [assetMintAccount] = await this.fetchAccounts([controller.assetMintAddress])🟠 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/strcusx-exchange-rate.ts:439`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/strcusx-exchange-rate.ts:493`Severity: high clockAccount,
] = await this.fetchAccounts([
controllerAddress,🟠 DATA-001 Missing Error Response `src/transport/strcusx-exchange-rate.ts:536`Severity: high const [assetMintAccount] = await this.fetchAccounts([controller.assetMintAddress])🟠 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/strcusx-exchange-rate.ts:456`Severity: high providerIndicatedTimeUnixMs: undefined🟠 CHAIN-001 RPC Timeout `src/transport/strcusx-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 18297 \
--dynamic-env RPC_URL=https://api.devnet.solana.com \
-f markdownDynamic Harness OutputEA Review ResultsProfile: dynamic-only | Adapters: solana-functions | Duration: 5.2s 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=18207 \
RPC_URL=https://api.devnet.solana.com \
METRICS_ENABLED=false \
yarn node -e "require('./dist/index.js').server()"Live Endpoint Request/Response{
"rpcUrl": "https://api.devnet.solana.com",
"responses": [
{
"label": "junior",
"request": {
"data": {
"endpoint": "strcusx-exchange-rate",
"programAddress": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe",
"strategyName": "STRC-USX-1",
"tranche": "junior",
"minRate": "950000000000000000",
"maxRate": "1050000000000000000"
}
},
"response": {
"statusCode": 200,
"elapsedMs": 1409,
"body": {
"data": {
"result": "1000000002222222222",
"computedResult": "1000000002222222222",
"tranche": "junior",
"decimals": 18,
"minRate": "950000000000000000",
"maxRate": "1050000000000000000",
"boundsApplied": false,
"vestedTotalAssets": "650000001",
"vestedSeniorAssets": "200000000",
"vestedJuniorAssets": "450000001",
"seniorShares": "200000000",
"juniorShares": "450000000",
"unvestedTotalAssets": "0",
"unvestedSeniorAssets": "0"
},
"statusCode": 200,
"result": "1000000002222222222",
"timestamps": {
"providerDataRequestedUnixMs": 1782131031969,
"providerDataReceivedUnixMs": 1782131032284,
"providerIndicatedTimeUnixMs": 1782131030000
}
}
}
},
{
"label": "senior",
"request": {
"data": {
"endpoint": "strcusx-exchange-rate",
"programAddress": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe",
"strategyName": "STRC-USX-1",
"tranche": "senior",
"minRate": "950000000000000000",
"maxRate": "1050000000000000000"
}
},
"response": {
"statusCode": 200,
"elapsedMs": 1005,
"body": {
"data": {
"result": "1000000000000000000",
"computedResult": "1000000000000000000",
"tranche": "senior",
"decimals": 18,
"minRate": "950000000000000000",
"maxRate": "1050000000000000000",
"boundsApplied": false,
"vestedTotalAssets": "650000001",
"vestedSeniorAssets": "200000000",
"vestedJuniorAssets": "450000001",
"seniorShares": "200000000",
"juniorShares": "450000000",
"unvestedTotalAssets": "0",
"unvestedSeniorAssets": "0"
},
"statusCode": 200,
"result": "1000000000000000000",
"timestamps": {
"providerDataRequestedUnixMs": 1782131033297,
"providerDataReceivedUnixMs": 1782131033412,
"providerIndicatedTimeUnixMs": 1782131032000
}
}
}
}
]
} |
Follow-up: EA Review Finding ScopeThe automated Static Harness ScopeStatic harness failures reported:
Findings directly in new strcUSX 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 strcUSX 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 this and I went through the logic and I believe it maybe functional and matches the methodology in OPDATA-7578.
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.That said, the state of the code makes it very hard to review and maintain, and it reads as largely LLM-generated. That's acceptable as a draft, but to meet Chainlink standards it needs to follow our best practices, reuse existing abstractions, and be reliable enough for someone else to own. The 658-line transport is roughly 6× a comparable endpoint here, mostly from duplicated scaffolding and an inline hand-rolled decoder.
Please refactor, and when you do, explicitly instruct the model to follow best practices, prefer existing abstractions over reinventing them, and avoid code smells. For large PRs like this, please use a stronger model (ideally Opus 4.8 or equivalent). The final version should have no magic numbers or arbitrary hardcoded values, every offset, discriminator, and decimal must be named and traceable to the program IDL.
Main issue: brittle manual decoding (highest priority)
- Account fields are read with bare byte offsets and hardcoded discriminators (
readPublicKey(data, 47),readPublicKey(data, 79),readU128LE(data, 73),data.readUInt8(239), etc.). Any program redeploy that reorders or pads a field silently returns a garbage rate. This is the kind of magic-number code we can't accept.- Decode via the program IDL using the adapter's existing
SolanaAccountReader(BorshCoder). - If these are zero-copy/bytemuck accounts that Borsh can't decode reliably (likely why it was hand-rolled), use a typed
@solana/buffer-layoutstructlikeshared/buffer-layout-accounts.tsdoes for Sanctum, with named fields, not scattered offsets.
- Decode via the program IDL using the adapter's existing
- Commit the program IDL to
src/idl/. The IDL attached to the ticket was never added to the repo, so none of the offsets/discriminators are reviewable. Please add it.
Code quality / structure
- De-duplicate shared helpers.
parseRateBound,getAccountDataBuffer,assertOwnerProgram/assertTokenProgramOwner, the bounds-clamp, and the shared types are copy-pasted verbatim from the stSLX PR. Extract them intosrc/shared/. - Reuse existing abstractions instead of re-implementing raw
getMultipleAccounts+ buffer slicing with hand-rolled RPC response types (MultipleAccountsRpcResponse,ascasts). - Stick to one Solana stack: new code mixes legacy
@solana/web3.jsinto the adapter's modern@solana/addresses/@solana/rpcidiom.
Specific issues
trancheis a freetype: 'string'validated by a hand-writtenparseTranche. Use the framework'sInputParametersoptions: ['junior', 'senior']enum, it validates at the framework layer and self-documents in the README.ASSET_MINT_DECIMALS = 6is asserted against the decoded mint, then the constant (not the decodedassetMint.decimals) is passed intocalculateRate. Use the decoded value for consistency, and document why 6 is expected.- Bounds-clamp is an inline nested ternary duplicated from the other PR, extract to a single named helper.
boundsApplieduses string comparison; compare bigints instead.- General: every constant (PDA seeds, discriminators, offsets, decimals) must be named and traceable, no inline literals in the decode path.
Happy to review iteratively
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:
|
|
To make the previous two points concrete on how to structure the refactor: Keep this as its own endpoint, but move the common logic into a shared library under
With those in place, this transport reduces to just: derive PDAs -> atomic read -> Ideally land the shared utils + IDL as a small base PR first, then rebase this one on |
0f15faa to
e0e4f94
Compare
93ec3a7 to
9cabe24
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new strcusx-exchange-rate endpoint to the solana-functions adapter to compute Solstice strcUSX junior/senior tranche exchange rates from on-chain accounts (PDAs + SPL mints), including vesting-aware “book value” adjustments and min/max bounds clamping.
Changes:
- Introduces
strcusx-exchange-rateendpoint + transport that derives PDAs, fetches/decodes Solana accounts, computes tranche rates, and applies required bounds. - Adds unit + integration tests covering PDA derivation, vesting behavior, bounds clamping, and validation/error cases.
- Updates adapter wiring, README docs, and adds a changeset for release.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/sources/solana-functions/src/transport/strcusx-exchange-rate.ts | Implements PDA derivation, account decoding, vesting adjustment, rate computation, and bounds clamping. |
| packages/sources/solana-functions/src/endpoint/strcusx-exchange-rate.ts | Defines request params and response typing for the new endpoint. |
| packages/sources/solana-functions/src/endpoint/index.ts | Exports the new endpoint. |
| packages/sources/solana-functions/src/index.ts | Registers the new endpoint with the adapter. |
| packages/sources/solana-functions/test/unit/strcusx-exchange-rate.test.ts | Unit tests for derivation, calculations, clamping, and error paths. |
| packages/sources/solana-functions/test/integration/strcusx-exchange-rate.test.ts | Integration test validating end-to-end adapter response shape with mocked RPC. |
| packages/sources/solana-functions/README.md | Documents the new endpoint and adds it to the endpoint list. |
| .changeset/fresh-tigers-pay.md | Declares a minor bump for the adapter package due to the new endpoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return { | ||
| data: { | ||
| result, | ||
| computedResult, | ||
| tranche, | ||
| decimals: RESULT_DECIMALS, | ||
| minRate: minRate.toString(), | ||
| maxRate: maxRate.toString(), | ||
| boundsApplied, | ||
| vestedTotalAssets: bookValueAssets.totalAssets.toString(), | ||
| vestedSeniorAssets: bookValueAssets.seniorAssets.toString(), | ||
| vestedJuniorAssets: juniorAssets.toString(), | ||
| seniorShares: accounting.seniorShares.toString(), | ||
| juniorShares: accounting.juniorShares.toString(), | ||
| unvestedTotalAssets: bookValueAssets.unvestedTotalAssets.toString(), | ||
| unvestedSeniorAssets: bookValueAssets.unvestedSeniorAssets.toString(), | ||
| }, |
9cabe24 to
88fdc62
Compare
|
Verdict: APPROVE — offset-to-layout migration is mechanically correct; layout spans match old magic constants exactly, no logic changes. strcusx-exchange-rate.ts:L73: yagni: ControllerStateLayoutFields/StrategyStateLayoutFields/AccountingStateLayoutFields used only as BufferLayout.struct generic params, never referenced directly. Drop the generic type params; cast decoded result inline where needed. ~40 lines deleted. strcusx-exchange-rate.ts:L224-241: shrink: five derive*Address exports each repeat .then((pda) => pda.toString()). A one-line local const toAddr = (p) => p.then(a => a.toString()) removes 5 identical chains. −5 lines. |
| "tranche": "senior", | ||
| "minRate": "950000000000000000", | ||
| "maxRate": "1050000000000000000" | ||
| } |
There was a problem hiding this comment.
consider:
- The ability to enable/disable rateBound. For ie: applyBounds(boolean)
- The ability to switch between Solana Mainnet or Devnet. For ie: network(mainnet or devnet)
|
@magiodev-cll The decoders are hand-maintained constants ( It's important because:
What I'd like to see:
Also:
|
|
@magiodev-cll I ran These are This is exactly the pattern the EA already uses in
Adopting it lets us remove the entire hand-built layer ( const coder = new BorshAccountsCoder(strcusxYieldStrategyIdl as unknown as Idl)
const accounting = coder.decode('AccountingState', accountingBuf)
const totalAssets = BigInt(accounting.total_assets.toString()) // anchor returns BN for u64/u128
const name = Buffer.from(accounting.name).toString('utf8').replace(/\0+$/, '') // [u8;32] -> stringOnly the BN->bigint and [u8;32]->string conversions remain, no offsets, no discriminator constants, no layout builder. Suggest also routing it through a programToIdlMap like anchor-data does, and keeping the fixture decode test as the regression guard. This is the change that makes the transport meaningfully shorter and aligns it with the existing EA convention. |
|
On a separate point, the response
Only By that bar I'd suggest:
That roughly halves the field set, tightens the response contract, and reduces snapshot churn. Please let me know if there's a plan to use some of those fields though. |
|
On the validation in the transport, I think a lot of the Keep (genuine safety):
Drop (re-checking what's already guaranteed):
The reasoning: One inconsistency the other way: we decode Net: keep the decode-safety plus underflow/div-by-zero plus owner/existence checks, drop the name/address/decimals/supply re-validations, and decide whether |
|
Following on from the validation point, the equality/ Name matches ( Supply equals shares ( Decimals equals 6 ( Address matches ( One simplification these point at: the address-match checks only exist because we front-derive the mints to batch them into the first atomic read. But the mints are only used for (a) Keep: the Net: these |
| const providerError = (message: string) => | ||
| new AdapterDataProviderError( | ||
| { | ||
| message, | ||
| statusCode: 502, | ||
| }, | ||
| { | ||
| providerDataRequestedUnixMs: 0, | ||
| providerDataReceivedUnixMs: 0, | ||
| providerIndicatedTimeUnixMs: undefined, | ||
| }, | ||
| ) | ||
|
|
There was a problem hiding this comment.
897d94a to
5adee8f
Compare
d9e1aec to
eb5c92e
Compare
| const decodeAnchorAccount = <T>(accountName: string, data: Buffer) => | ||
| strcusxAccountsCoder.decode(accountName, data) as T | ||
|
|
||
| const assertProgramOwner = ( |
There was a problem hiding this comment.
assertProgramOwner is a thin wrapper over the shared assertOwnerProgram (fixed message + single-element array). Either inline the three call sites or keep it, minor, but it's one more function for little gain.
| type DecodedControllerState = { | ||
| asset_mint: Stringable | ||
| } | ||
|
|
||
| type DecodedStrategyState = { | ||
| junior_mint: Stringable | ||
| senior_mint: Stringable | ||
| } |
There was a problem hiding this comment.
The trivial Decoded*State types: DecodedControllerState (1 field) and DecodedStrategyState (2 fields), can be inlined into their decode functions rather than declared as named types. DecodedAccountingState (8 fields, line 57) is the one worth keeping for field-name safety, since it mirrors the bigint domain type.
| return derivePda(programAddress, [PDA_SEEDS.ACCOUNTING, strategyName]).then((pda) => | ||
| pda.toString(), | ||
| ) | ||
| } |
There was a problem hiding this comment.
Collapse the redundant helpers
- The three derive*Address functions (lines 108, 112, 116) are near-identical, they differ only by seed. Collapse to one:
const deriveAccountAddress = (programAddress: string, seeds: PdaSeed[]) =>
derivePda(programAddress, seeds).then((pda) => pda.toString())
- and call
deriveAccountAddress(programAddress, [PDA_SEEDS.STRATEGY, strategyName])etc. Removes two functions. (They're currently exported only for tests, the test can call the one helper with different seeds.)
| type RequestParams = typeof inputParameters.validated | ||
| type Tranche = 'junior' | 'senior' | ||
|
|
||
| type Stringable = { toString(): string } |
There was a problem hiding this comment.
- Stringable (line 46), toBigint (line 91), and decodeAnchorAccount (line 93) are generic Anchor-decoding utilities. decodeAnchorAccount just needs to take the coder as a param to be reusable:
// shared/anchor-utils.ts (or fold into account-reader.ts, which already wraps BorshCoder)
export type Stringable = { toString(): string }
export const toBigint = (v: Stringable) => BigInt(v.toString())
export const decodeAnchorAccount = <T>(coder: BorshAccountsCoder, name: string, data: Buffer) => coder.decode(name, data) as T
- Note account-reader.ts already constructs a BorshCoder and decodes, so there's a real case to consolidate the "decode an Anchor account" concern there instead of having two BorshCoder code paths in the EA.
| const PDA_SEEDS = { | ||
| CONTROLLER: 'CONTROLLER', | ||
| STRATEGY: 'STRATEGY', | ||
| ACCOUNTING: 'ACCOUNTING', | ||
| } as const | ||
|
|
There was a problem hiding this comment.
Everything domain-specific PDA_SEEDS, strcusxAccountsCoder, parseStrategyName, the Decoded*/AccountingState types, decodeControllerState/decodeStrategyState/decodeAccountingState, the collapsed deriveAccountAddress, and calculateBookValueAssets moves to a co-located file, e.g. src/transport/strcusx-accounts.ts (or strcusx-strategy.ts). The transport then imports from it and contains only the class (initialize/backgroundHandler/handleRequest/_handleRequest/getSubscriptionTtl).
This is consistent with the EA's existing pattern of putting decode logic in dedicated modules (account-reader.ts, buffer-layout-accounts.ts), just co-located since it's program-specific rather than shared.
There was a problem hiding this comment.
-
Please Add a test that mocks
getMultipleAccounts().send()rejecting and assertshandleRequestproduces the 502 error response. The catch/responseCache.writepath is untested. -
Please add a Token-2022-with-extensions decode test for the asset/tranche mints, since
decodeMintInfotolerates trailing extension bytes in prod but the tests only feed base-span buffers.
eb5c92e to
a60669c
Compare
Summary
strcusx-exchange-rateendpoint tosolana-functionsfor Solstice strcUSX junior/senior tranche exchange rates.programAddressandstrategyName.bigintmath.tranche: juniorandtranche: seniorusing required jobspec boundsminRate/maxRate.data.result,data.computedResult, per-tranche computed rates, decoded accounting values, and derived addresses for observability.OPDATA
Adapter Checklist
main..changeset/fresh-tigers-pay.md.ea_context/files are included in this PR.Request Shape
{ "data": { "endpoint": "strcusx-exchange-rate", "programAddress": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", "strategyName": "STRC-USX-1", "tranche": "junior", "minRate": "950000000000000000", "maxRate": "1050000000000000000" } }Devnet Smoke Test
7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXeSTRC-USX-1https://api.devnet.solana.com1000000002222222222,statusCode: 200,boundsApplied: false1000000000000000000,statusCode: 200,boundsApplied: falseValidation
yarn install --immutableyarn workspace @chainlink/solana-functions-adapter buildyarn tsc -p packages/sources/solana-functions/tsconfig.test.jsonyarn eslint packages/sources/solana-functions/src packages/sources/solana-functions/test(warnings only, existing repo warnings)yarn prettier --check ...yarn test packages/sources/solana-functions/test/unit/strcusx-exchange-rate.test.ts packages/sources/solana-functions/test/integration/strcusx-exchange-rate.test.ts --runInBand(18 passed, 3 snapshots)Notes
main.Automated EA Review and Live Evidence
ea-reviewstatic + dynamic harness was posted in this PR comment: OPDATA-7578 Add strcUSX tranche exchange rate endpoint #5097 (comment)https://api.devnet.solana.com.juniorresponse returnedstatusCode: 200,result: 1000000002222222222,computedResult: 1000000002222222222, andboundsApplied: false.seniorresponse returnedstatusCode: 200,result: 1000000000000000000,computedResult: 1000000000000000000, andboundsApplied: false.