Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sharp-lions-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/solana-functions-adapter': minor
---

Add stSLX exchange rate endpoint
41 changes: 38 additions & 3 deletions packages/sources/solana-functions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ There are no rate limits for this adapter.

## Input Parameters

| Required? | Name | Description | Type | Options | Default |
| :-------: | :------: | :-----------------: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------: |
| | endpoint | The endpoint to use | string | [anchor-data](#anchor-data-endpoint), [buffer-layout](#buffer-layout-endpoint), [eusx-price](#eusx-price-endpoint), [extension](#extension-endpoint), [pool-token-rate](#pool-token-rate-endpoint), [sanctum-infinity](#sanctum-infinity-endpoint) | `eusx-price` |
| Required? | Name | Description | Type | Options | Default |
| :-------: | :------: | :-----------------: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------: |
| | endpoint | The endpoint to use | string | [anchor-data](#anchor-data-endpoint), [buffer-layout](#buffer-layout-endpoint), [eusx-price](#eusx-price-endpoint), [extension](#extension-endpoint), [pool-token-rate](#pool-token-rate-endpoint), [sanctum-infinity](#sanctum-infinity-endpoint), [stslx-exchange-rate](#stslx-exchange-rate-endpoint) | `eusx-price` |

## Eusx-price Endpoint

Expand Down Expand Up @@ -239,4 +239,39 @@ Request:

---

## Stslx-exchange-rate Endpoint

`stslx-exchange-rate` is the only supported name for this endpoint.

### Input Params

| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With |
| :-------: | :------------------------: | :-----: | :--------------------------------------------------------------------------: | :----: | :-----: | :--------------------------------------------: | :--------: | :------------: |
| | slxMintAddress | | SLX mint address | string | | `SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq` | | |
| | stslxMintAddress | | stSLX mint address | string | | `GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq` | | |
| | glamStateAddress | | GLAM state address used to derive the vault PDA | string | | `5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB` | | |
| | glamProtocolProgramAddress | | GLAM protocol program address used to derive the vault PDA | string | | `GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz` | | |
| | minRate | | Minimum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer | string | | | | |
| | maxRate | | Maximum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer | string | | | | |

### Example

Request:

```json
{
"data": {
"endpoint": "stslx-exchange-rate",
"slxMintAddress": "SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq",
"stslxMintAddress": "GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq",
"glamStateAddress": "5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB",
"glamProtocolProgramAddress": "GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz",
"minRate": "950000000000000000",
"maxRate": "1050000000000000000"
}

@jcly99 jcly99 Jun 24, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

consider:

  1. The ability to enable/disable rateBound. For ie: applyBounds(boolean)

}
```

---

MIT License
1 change: 1 addition & 0 deletions packages/sources/solana-functions/src/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { endpoint as eusxPrice } from './eusx-price'
export { endpoint as extension } from './extension'
export { endpoint as poolTokenRate } from './pool-token-rate'
export { endpoint as sanctumInfinity } from './sanctum-infinity'
export { endpoint as stslxExchangeRate } from './stslx-exchange-rate'
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { type AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
import { config } from '../config'
import { parseRateBounds } from '../shared/exchange-rate-utils'
import { stslxExchangeRateTransport } from '../transport/stslx-exchange-rate'

// Defaults come from Solstice's OPDATA-7578 production stSLX feed config. They
// are request/job-spec fallbacks; if Solstice migrates, override these params
// first and update defaults only when the production feed defaults change. The
// GLAM vault and SLX ATA are derived from these source addresses in transport.
// Explorer checks:
// - https://explorer.solana.com/address/SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq
// - https://explorer.solana.com/address/GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq
// - https://explorer.solana.com/address/5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB
// - https://explorer.solana.com/address/GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz
export const DEFAULT_SLX_MINT_ADDRESS = 'SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq'
export const DEFAULT_STSLX_MINT_ADDRESS = 'GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq'
export const DEFAULT_GLAM_STATE_ADDRESS = '5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB'
export const DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS = 'GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also for any hardcoded address/constant, please document how we set this and where did we source it from and what happens if it changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also addressed in a81dc7e


Comment on lines +17 to +21

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I might be confused, but why did we revert to hardcode all addresses as opposed to deriving them? Didn't we already address this in this comment: #5095 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah i see where the confusion comes from. these are default input parameter fallbacks, not hardcoded transport addresses. The GLAM vault and SLX ATA are still derived at runtime in stslx-exchange-rate.ts from the source addresses

btw I added source/change documentation for the default address params, and also replaced the associated token program hardcode with the @solana/spl-token exported constant.

please check the new commit: a81dc7e

export const inputParameters = new InputParameters(
{
slxMintAddress: {
description: 'SLX mint address',
type: 'string',
required: false,
default: DEFAULT_SLX_MINT_ADDRESS,
},
stslxMintAddress: {
description: 'stSLX mint address',
type: 'string',
required: false,
default: DEFAULT_STSLX_MINT_ADDRESS,
},
glamStateAddress: {
description: 'GLAM state address used to derive the vault PDA',
type: 'string',
required: false,
default: DEFAULT_GLAM_STATE_ADDRESS,
},
glamProtocolProgramAddress: {
description: 'GLAM protocol program address used to derive the vault PDA',
type: 'string',
required: false,
default: DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS,
},
minRate: {
description: 'Minimum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer',
type: 'string',
required: false,
},
maxRate: {
description: 'Maximum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer',
type: 'string',
required: false,
},
},
[
{
slxMintAddress: DEFAULT_SLX_MINT_ADDRESS,
stslxMintAddress: DEFAULT_STSLX_MINT_ADDRESS,
glamStateAddress: DEFAULT_GLAM_STATE_ADDRESS,
glamProtocolProgramAddress: DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS,
minRate: '950000000000000000',
maxRate: '1050000000000000000',
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: {
Result: string
Data: {
Comment thread
magiodev-cll marked this conversation as resolved.
result: string
computedResult: string
decimals: number
boundsApplied: boolean
slxBalance: string
stslxSupply: string
}
}
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'stslx-exchange-rate',
aliases: [],
transport: stslxExchangeRateTransport,
inputParameters,
customInputValidation: (req): AdapterInputError | undefined => {
parseRateBounds(req.requestContext.data.minRate, req.requestContext.data.maxRate)

return
},
})
11 changes: 10 additions & 1 deletion packages/sources/solana-functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ import {
extension,
poolTokenRate,
sanctumInfinity,
stslxExchangeRate,
} from './endpoint'

export const adapter = new Adapter({
defaultEndpoint: eusxPrice.name,
name: 'SOLANA_FUNCTIONS',
config,
endpoints: [eusxPrice, anchorData, sanctumInfinity, bufferLayout, extension, poolTokenRate],
endpoints: [
eusxPrice,
anchorData,
sanctumInfinity,
bufferLayout,
extension,
poolTokenRate,
stslxExchangeRate,
],
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
181 changes: 181 additions & 0 deletions packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
import { address, getAddressEncoder, type Address } from '@solana/addresses'
import { type Rpc, type SolanaRpcApi } from '@solana/rpc'
import { ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token'
import { BaseEndpointTypes, inputParameters } from '../endpoint/stslx-exchange-rate'
import {
assertTokenProgramOwner,
decodeMintInfo,
decodeTokenAccountInfo,
LEGACY_TOKEN_PROGRAM_ADDRESS,
} from '../shared/buffer-layout-accounts'
import {
applyRateBounds,
calculateNormalizedRate,
parseRateBounds,
RESULT_DECIMALS,
} from '../shared/exchange-rate-utils'
import {
assertOwnerProgram,
derivePda,
fetchMultipleAccounts,
getAccountDataBuffer,
parseSolanaAddress,
providerError,
} from '../shared/solana-account-utils'
import { SolanaRpcFactory } from '../shared/solana-rpc-factory'

const logger = makeLogger('StslxExchangeRateTransport')

const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = ASSOCIATED_TOKEN_PROGRAM_ID.toBase58()
const GLAM_VAULT_SEED = 'vault'
const addressEncoder = getAddressEncoder()

type RequestParams = typeof inputParameters.validated

const deriveVaultAddress = (glamStateAddress: Address, glamProtocolProgramAddress: Address) =>
derivePda(glamProtocolProgramAddress, [GLAM_VAULT_SEED, addressEncoder.encode(glamStateAddress)])

const deriveSlxTokenAccountAddress = (vaultAddress: Address, slxMintAddress: Address) =>
derivePda(ASSOCIATED_TOKEN_PROGRAM_ADDRESS, [
addressEncoder.encode(vaultAddress),
addressEncoder.encode(address(LEGACY_TOKEN_PROGRAM_ADDRESS)),
addressEncoder.encode(slxMintAddress),
])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

export class StslxExchangeRateTransport extends SubscriptionTransport<BaseEndpointTypes> {
rpc!: Rpc<SolanaRpcApi>

async initialize(
dependencies: TransportDependencies<BaseEndpointTypes>,
adapterSettings: BaseEndpointTypes['Settings'],
endpointName: string,
transportName: string,
): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
this.rpc = new SolanaRpcFactory().create(adapterSettings.RPC_URL)
}

async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
}

async handleRequest(param: RequestParams) {
let response: AdapterResponse<BaseEndpointTypes['Response']>
try {
response = await this._handleRequest(param)
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
logger.error(e, errorMessage)
response = {
statusCode: (e as AdapterInputError)?.statusCode || 502,
errorMessage,
timestamps: {
providerDataRequestedUnixMs: 0,
providerDataReceivedUnixMs: 0,
providerIndicatedTimeUnixMs: undefined,
},
}
}

await this.responseCache.write(this.name, [{ params: param, response }])
}

async _handleRequest(
params: RequestParams,
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
const providerDataRequestedUnixMs = Date.now()
const slxMintAddress = parseSolanaAddress(params.slxMintAddress, 'slxMintAddress')
const stslxMintAddress = parseSolanaAddress(params.stslxMintAddress, 'stslxMintAddress')
const glamStateAddress = parseSolanaAddress(params.glamStateAddress, 'glamStateAddress')
const glamProtocolProgramAddress = parseSolanaAddress(
params.glamProtocolProgramAddress,
'glamProtocolProgramAddress',
)
const { minRate, maxRate } = parseRateBounds(params.minRate, params.maxRate)
const vaultAddress = await deriveVaultAddress(glamStateAddress, glamProtocolProgramAddress)
const slxTokenAccountAddress = await deriveSlxTokenAccountAddress(vaultAddress, slxMintAddress)

// The stSLX feed reads GLAM vault's canonical SLX ATA as its SLX balance source.
const [slxMintAccount, stslxMintAccount, slxTokenAccount] = await fetchMultipleAccounts(
this.rpc,
[slxMintAddress, stslxMintAddress, slxTokenAccountAddress],
)

assertTokenProgramOwner(slxMintAccount, `SLX mint '${slxMintAddress}'`)
assertTokenProgramOwner(stslxMintAccount, `stSLX mint '${stslxMintAddress}'`)
assertOwnerProgram(
slxTokenAccount,
`SLX token account '${slxTokenAccountAddress}'`,
[LEGACY_TOKEN_PROGRAM_ADDRESS],
'the legacy SPL Token program',
)

const slxMint = decodeMintInfo(
getAccountDataBuffer(slxMintAccount, `SLX mint '${slxMintAddress}'`),
`SLX mint '${slxMintAddress}'`,
)
const stslxMint = decodeMintInfo(
getAccountDataBuffer(stslxMintAccount, `stSLX mint '${stslxMintAddress}'`),
`stSLX mint '${stslxMintAddress}'`,
)
const slxToken = decodeTokenAccountInfo(
getAccountDataBuffer(slxTokenAccount, `SLX token account '${slxTokenAccountAddress}'`),
`SLX token account '${slxTokenAccountAddress}'`,
)

const computedRate = calculateNormalizedRate(
slxToken.amount,
stslxMint.supply,
slxMint.decimals,
stslxMint.decimals,
)
if (computedRate === null) {
throw providerError(`stSLX mint '${stslxMintAddress}' has zero supply`)
}

const { rate, boundsApplied } = applyRateBounds(computedRate, minRate, maxRate)
const result = rate.toString()
const computedResult = computedRate.toString()
if (boundsApplied) {
logger.warn(
{
computedResult,
result,
minRate: minRate?.toString(),
maxRate: maxRate?.toString(),
},
'stSLX exchange rate bounds applied',
)
}

return {
data: {
result,
computedResult,
decimals: RESULT_DECIMALS,
boundsApplied,
slxBalance: slxToken.amount.toString(),
stslxSupply: stslxMint.supply.toString(),
},
statusCode: 200,
result,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
Comment thread
mohamed-mehany marked this conversation as resolved.
}

getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
}
}

export const stslxExchangeRateTransport = new StslxExchangeRateTransport()
Loading