-
Notifications
You must be signed in to change notification settings - Fork 261
OPDATA-7578 Add stSLX exchange rate endpoint #5095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: refactor/solana-exchange-rate-shared
Are you sure you want to change the base?
Changes from all commits
ece552e
aa855c9
eafeaa5
dd79be1
0404717
e704fc5
097f922
fcfeb7a
b4ed8d5
f4f2a15
fb2c78c
42241a6
44c9088
1d62233
1bddedf
b3ad68e
ad46925
f1aba31
1ff029a
894d234
8c6e243
8f1f6bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@chainlink/solana-functions-adapter': minor | ||
| --- | ||
|
|
||
| Add stSLX exchange rate endpoint |
| 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' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also addressed in a81dc7e |
||
|
|
||
|
Comment on lines
+17
to
+21
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
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 | ||
| }, | ||
| }) | ||
| 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), | ||
| ]) | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant address re-parsing. |
||
| 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, | ||
| }, | ||
| } | ||
|
mohamed-mehany marked this conversation as resolved.
|
||
| } | ||
|
|
||
| getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { | ||
| return adapterSettings.WARMUP_SUBSCRIPTION_TTL | ||
| } | ||
| } | ||
|
|
||
| export const stslxExchangeRateTransport = new StslxExchangeRateTransport() | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider: