Skip to content

Commit 8d4db8a

Browse files
authored
OPDATA-4861: Add buffer-layout endpoint to solana-functions (#4156)
* Copy sanctum-infinity * Add buffer-layout endpoint to solana-functions * changeset
1 parent b39bd6a commit 8d4db8a

File tree

9 files changed

+447
-2
lines changed

9 files changed

+447
-2
lines changed

.changeset/tiny-ligers-brake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/solana-functions-adapter': minor
3+
---
4+
5+
Add buffer-layout endpoint
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { config } from '../config'
4+
import { bufferLayoutTransport } from '../transport/buffer-layout'
5+
6+
export const inputParameters = new InputParameters(
7+
{
8+
stateAccountAddress: {
9+
description: 'The state account address for the program',
10+
type: 'string',
11+
required: true,
12+
},
13+
field: {
14+
description: 'The name of the field to retrieve from the state account',
15+
type: 'string',
16+
required: true,
17+
},
18+
},
19+
[
20+
{
21+
stateAccountAddress: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
22+
field: 'supply',
23+
},
24+
],
25+
)
26+
27+
export type BaseEndpointTypes = {
28+
Parameters: typeof inputParameters.definition
29+
Response: {
30+
Data: {
31+
result: string
32+
}
33+
Result: string
34+
}
35+
Settings: typeof config.settings
36+
}
37+
38+
export const endpoint = new AdapterEndpoint({
39+
name: 'buffer-layout',
40+
aliases: [],
41+
transport: bufferLayoutTransport,
42+
inputParameters,
43+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { endpoint as anchorData } from './anchor-data'
2+
export { endpoint as bufferLayout } from './buffer-layout'
23
export { endpoint as eusxPrice } from './eusx-price'
34
export { endpoint as sanctumInfinity } from './sanctum-infinity'
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
33
import { config } from './config'
4-
import { anchorData, eusxPrice, sanctumInfinity } from './endpoint'
4+
import { anchorData, bufferLayout, eusxPrice, sanctumInfinity } from './endpoint'
55

66
export const adapter = new Adapter({
77
defaultEndpoint: eusxPrice.name,
88
name: 'SOLANA_FUNCTIONS',
99
config,
10-
endpoints: [eusxPrice, anchorData, sanctumInfinity],
10+
endpoints: [eusxPrice, anchorData, sanctumInfinity, bufferLayout],
1111
})
1212

1313
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
3+
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
4+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
5+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
6+
import { type Rpc, type SolanaRpcApi } from '@solana/rpc'
7+
import { BaseEndpointTypes, inputParameters } from '../endpoint/buffer-layout'
8+
import { fetchFieldFromBufferLayoutStateAccount } from '../shared/buffer-layout-accounts'
9+
import { SolanaRpcFactory } from '../shared/solana-rpc-factory'
10+
11+
const logger = makeLogger('BufferLayoutTransport')
12+
13+
type RequestParams = typeof inputParameters.validated
14+
15+
export class BufferLayoutTransport extends SubscriptionTransport<BaseEndpointTypes> {
16+
rpc!: Rpc<SolanaRpcApi>
17+
18+
async initialize(
19+
dependencies: TransportDependencies<BaseEndpointTypes>,
20+
adapterSettings: BaseEndpointTypes['Settings'],
21+
endpointName: string,
22+
transportName: string,
23+
): Promise<void> {
24+
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
25+
this.rpc = new SolanaRpcFactory().create(adapterSettings.RPC_URL)
26+
}
27+
28+
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
29+
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
30+
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
31+
}
32+
33+
async handleRequest(param: RequestParams) {
34+
let response: AdapterResponse<BaseEndpointTypes['Response']>
35+
try {
36+
response = await this._handleRequest(param)
37+
} catch (e: unknown) {
38+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
39+
logger.error(e, errorMessage)
40+
response = {
41+
statusCode: (e as AdapterInputError)?.statusCode || 502,
42+
errorMessage,
43+
timestamps: {
44+
providerDataRequestedUnixMs: 0,
45+
providerDataReceivedUnixMs: 0,
46+
providerIndicatedTimeUnixMs: undefined,
47+
},
48+
}
49+
}
50+
51+
await this.responseCache.write(this.name, [{ params: param, response }])
52+
}
53+
54+
async _handleRequest(
55+
params: RequestParams,
56+
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
57+
const providerDataRequestedUnixMs = Date.now()
58+
59+
const result = await fetchFieldFromBufferLayoutStateAccount({
60+
stateAccountAddress: params.stateAccountAddress,
61+
field: params.field,
62+
rpc: this.rpc,
63+
})
64+
65+
return {
66+
data: {
67+
result,
68+
},
69+
statusCode: 200,
70+
result,
71+
timestamps: {
72+
providerDataRequestedUnixMs,
73+
providerDataReceivedUnixMs: Date.now(),
74+
providerIndicatedTimeUnixMs: undefined,
75+
},
76+
}
77+
}
78+
79+
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
80+
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
81+
}
82+
}
83+
84+
export const bufferLayoutTransport = new BufferLayoutTransport()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"jsonrpc": "2.0",
3+
"result": {
4+
"context": {
5+
"apiVersion": "3.0.7",
6+
"slot": 376122381
7+
},
8+
"value": {
9+
"data": [
10+
"AQAAAJj+huiNm+Lqi8HMpIeLKYjCQPUrhCS/tA7Rot3LXhmbBNbXhrT9IwAGAQEAAABicKqKWcWUBbRShshncubNEm6bil06OFNtN/e0FOi2Zw==",
11+
"base64"
12+
],
13+
"executable": false,
14+
"lamports": 419286183851,
15+
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
16+
"rentEpoch": 18446744073709551615,
17+
"space": 82
18+
}
19+
},
20+
"id": 1
21+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute buffer-layout should return success USDC supply 1`] = `
4+
{
5+
"data": {
6+
"result": "10130575983105540",
7+
},
8+
"result": "10130575983105540",
9+
"statusCode": 200,
10+
"timestamps": {
11+
"providerDataReceivedUnixMs": 978347471111,
12+
"providerDataRequestedUnixMs": 978347471111,
13+
},
14+
}
15+
`;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
TestAdapter,
3+
makeStub,
4+
setEnvVariables,
5+
} from '@chainlink/external-adapter-framework/util/testing-utils'
6+
import * as usdcMinterAccountData from '../fixtures/usdc-minter-account-data-2025-10-27.json'
7+
8+
const usdcMinterAddress = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
9+
10+
const solanaRpc = makeStub('solanaRpc', {
11+
getAccountInfo: (address: string) => ({
12+
async send() {
13+
switch (address) {
14+
case usdcMinterAddress:
15+
return usdcMinterAccountData.result
16+
}
17+
throw new Error(`Unexpected account address: ${address}`)
18+
},
19+
}),
20+
})
21+
22+
const createSolanaRpc = () => solanaRpc
23+
24+
jest.mock('@solana/rpc', () => ({
25+
createSolanaRpc() {
26+
return createSolanaRpc()
27+
},
28+
}))
29+
30+
describe('execute', () => {
31+
let spy: jest.SpyInstance
32+
let testAdapter: TestAdapter
33+
let oldEnv: NodeJS.ProcessEnv
34+
35+
beforeAll(async () => {
36+
oldEnv = JSON.parse(JSON.stringify(process.env))
37+
process.env.RPC_URL = 'solana.rpc.url'
38+
process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0'
39+
const mockDate = new Date('2001-01-01T11:11:11.111Z')
40+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
41+
42+
const adapter = (await import('./../../src')).adapter
43+
adapter.rateLimiting = undefined
44+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
45+
testAdapter: {} as TestAdapter<never>,
46+
})
47+
})
48+
49+
afterAll(async () => {
50+
setEnvVariables(oldEnv)
51+
await testAdapter.api.close()
52+
spy.mockRestore()
53+
})
54+
55+
describe('buffer-layout', () => {
56+
it('should return success USDC supply', async () => {
57+
const data = {
58+
endpoint: 'buffer-layout',
59+
stateAccountAddress: usdcMinterAddress,
60+
field: 'supply',
61+
}
62+
const response = await testAdapter.request(data)
63+
expect(response.json()).toMatchSnapshot()
64+
expect(response.statusCode).toBe(200)
65+
})
66+
})
67+
})

0 commit comments

Comments
 (0)