Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/tiny-ligers-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/solana-functions-adapter': minor
---

Add buffer-layout endpoint
43 changes: 43 additions & 0 deletions packages/sources/solana-functions/src/endpoint/buffer-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { bufferLayoutTransport } from '../transport/buffer-layout'

export const inputParameters = new InputParameters(
{
stateAccountAddress: {
description: 'The state account address for the program',
type: 'string',
required: true,
},
field: {
description: 'The name of the field to retrieve from the state account',
type: 'string',
required: true,
},
},
[
{
stateAccountAddress: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
field: 'supply',
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: {
Data: {
result: string
}
Result: string
}
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'buffer-layout',
aliases: [],
transport: bufferLayoutTransport,
inputParameters,
})
1 change: 1 addition & 0 deletions packages/sources/solana-functions/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { endpoint as anchorData } from './anchor-data'
export { endpoint as bufferLayout } from './buffer-layout'
export { endpoint as eusxPrice } from './eusx-price'
export { endpoint as sanctumInfinity } from './sanctum-infinity'
4 changes: 2 additions & 2 deletions packages/sources/solana-functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { anchorData, eusxPrice, sanctumInfinity } from './endpoint'
import { anchorData, bufferLayout, eusxPrice, sanctumInfinity } from './endpoint'

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

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
84 changes: 84 additions & 0 deletions packages/sources/solana-functions/src/transport/buffer-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 { type Rpc, type SolanaRpcApi } from '@solana/rpc'
import { BaseEndpointTypes, inputParameters } from '../endpoint/buffer-layout'
import { fetchFieldFromBufferLayoutStateAccount } from '../shared/buffer-layout-accounts'
import { SolanaRpcFactory } from '../shared/solana-rpc-factory'

const logger = makeLogger('BufferLayoutTransport')

type RequestParams = typeof inputParameters.validated

export class BufferLayoutTransport 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 result = await fetchFieldFromBufferLayoutStateAccount({
stateAccountAddress: params.stateAccountAddress,
field: params.field,
rpc: this.rpc,
})

return {
data: {
result,
},
statusCode: 200,
result,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}

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

export const bufferLayoutTransport = new BufferLayoutTransport()
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"jsonrpc": "2.0",
"result": {
"context": {
"apiVersion": "3.0.7",
"slot": 376122381
},
"value": {
"data": [
"AQAAAJj+huiNm+Lqi8HMpIeLKYjCQPUrhCS/tA7Rot3LXhmbBNbXhrT9IwAGAQEAAABicKqKWcWUBbRShshncubNEm6bil06OFNtN/e0FOi2Zw==",
"base64"
],
"executable": false,
"lamports": 419286183851,
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"rentEpoch": 18446744073709551615,
"space": 82
}
},
"id": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`execute buffer-layout should return success USDC supply 1`] = `
{
"data": {
"result": "10130575983105540",
},
"result": "10130575983105540",
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
TestAdapter,
makeStub,
setEnvVariables,
} from '@chainlink/external-adapter-framework/util/testing-utils'
import * as usdcMinterAccountData from '../fixtures/usdc-minter-account-data-2025-10-27.json'

const usdcMinterAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'

const solanaRpc = makeStub('solanaRpc', {
getAccountInfo: (address: string) => ({
async send() {
switch (address) {
case usdcMinterAddress:
return usdcMinterAccountData.result
}
throw new Error(`Unexpected account address: ${address}`)
},
}),
})

const createSolanaRpc = () => solanaRpc

jest.mock('@solana/rpc', () => ({
createSolanaRpc() {
return createSolanaRpc()
},
}))

describe('execute', () => {
let spy: jest.SpyInstance
let testAdapter: TestAdapter
let oldEnv: NodeJS.ProcessEnv

beforeAll(async () => {
oldEnv = JSON.parse(JSON.stringify(process.env))
process.env.RPC_URL = 'solana.rpc.url'
process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0'
const mockDate = new Date('2001-01-01T11:11:11.111Z')
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())

const adapter = (await import('./../../src')).adapter
adapter.rateLimiting = undefined
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
testAdapter: {} as TestAdapter<never>,
})
})

afterAll(async () => {
setEnvVariables(oldEnv)
await testAdapter.api.close()
spy.mockRestore()
})

describe('buffer-layout', () => {
it('should return success USDC supply', async () => {
const data = {
endpoint: 'buffer-layout',
stateAccountAddress: usdcMinterAddress,
field: 'supply',
}
const response = await testAdapter.request(data)
expect(response.json()).toMatchSnapshot()
expect(response.statusCode).toBe(200)
})
})
})
Loading
Loading