Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3635dc1
Release v1.72.0
boxhock May 30, 2023
ebd0bb6
USDO - Support Solana reserves
Subarna-Singh Aug 18, 2025
f3ba40f
USDO - Solana reserves
Subarna-Singh Aug 18, 2025
fc0cbaf
Add: Changeset
Subarna-Singh Aug 18, 2025
40daac3
Add: unit and integration testing
Subarna-Singh Aug 19, 2025
775554c
change addresses structure
Subarna-Singh Aug 19, 2025
9f85a31
FIX: unit test
Subarna-Singh Aug 19, 2025
78b4a7b
FIX: failing compilation
Subarna-Singh Aug 19, 2025
16b5834
REDO: work post review
Subarna-Singh Aug 19, 2025
696db5b
Revert change to getToken - not related to this PR
Subarna-Singh Aug 19, 2025
7100d1c
UPDATE: unit and integration test
Subarna-Singh Aug 20, 2025
23560d5
FIX: unit test compilation
Subarna-Singh Aug 20, 2025
bad454c
FIX: Lint error
Subarna-Singh Aug 20, 2025
5c535b3
Merge branch 'main' of github.com:smartcontractkit/external-adapters-…
Subarna-Singh Aug 21, 2025
f298771
REWORK: work post review
Subarna-Singh Aug 21, 2025
1743943
FIX: unit and integration tests
Subarna-Singh Aug 21, 2025
c3716eb
FIX: compilation error
Subarna-Singh Aug 21, 2025
d67d3df
FIX: compilation error
Subarna-Singh Aug 21, 2025
731b957
REWORK: post review work
Subarna-Singh Aug 21, 2025
80a78b5
Merge branch 'master' of github.com:smartcontractkit/external-adapter…
Subarna-Singh Aug 21, 2025
d22fa29
merge main
Subarna-Singh Aug 21, 2025
ef95b78
REWORK: post review
Subarna-Singh Aug 22, 2025
0a784ca
FIX: compiling error
Subarna-Singh Aug 22, 2025
febb4ae
FIX: compiling error
Subarna-Singh Aug 22, 2025
d9d346c
UPDATE: test cases
Subarna-Singh Aug 22, 2025
63547a6
Merge branch 'main' into OPDATA-3708-USDO-Add-Solana-Reserves-TOKEN_B…
Subarna-Singh Aug 22, 2025
113d875
REWORK: work post review
Subarna-Singh Aug 23, 2025
1d8633d
Merge branch 'OPDATA-3708-USDO-Add-Solana-Reserves-TOKEN_BALANCE' of …
Subarna-Singh Aug 23, 2025
c17472a
FIX: compiling issue
Subarna-Singh Aug 23, 2025
04ee629
FIX: compiling issue
Subarna-Singh Aug 23, 2025
b88ec9e
Error Handling: If account does not exist
Subarna-Singh Aug 24, 2025
649f827
REWORK
Subarna-Singh Aug 25, 2025
c906525
FIX: test
Subarna-Singh Aug 25, 2025
9c7c5b9
Merge branch 'main' into OPDATA-3708-USDO-Add-Solana-Reserves-TOKEN_B…
app-token-issuer-data-feeds[bot] Aug 27, 2025
0b3fb3c
Merge branch 'main' into OPDATA-3708-USDO-Add-Solana-Reserves-TOKEN_B…
app-token-issuer-data-feeds[bot] Aug 28, 2025
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/mighty-tools-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/token-balance-adapter': major
---

USDO - Add Solana Reserves
1 change: 1 addition & 0 deletions packages/sources/token-balance/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { endpoint as etherFi } from './etherFi'
export { endpoint as evm } from './evm'
export { endpoint as solana } from './solana'
export { endpoint as solvJlp } from './solvJlp'
export { endpoint as tbill } from './tbill'
export { endpoint as xrpl } from './xrpl'
92 changes: 92 additions & 0 deletions packages/sources/token-balance/src/endpoint/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { solanaTransport } from '../transport/solana'

export const inputParameters = new InputParameters(
{
addresses: {
required: true,
description:
'List of wallet addresses to query. The balances of all provided wallets will be retrieved and summed together.',
type: {
address: {
required: true,
type: 'string',
description: 'Public wallet address whose token balance will be queried.',
},
},
array: true,
},
tokenMint: {
required: true,
description:
'A token mint is the canonical on-chain account that defines the token’s metadata (name, symbol, supply rules).',
type: {
token: {
required: true,
type: 'string',
description: 'token symbol of token mint.',
},
contractAddress: {
required: true,
type: 'string',
description: 'On-chain contract address of the token mint.',
},
},
},
priceOracle: {
required: true,
description:
'Configuration of the on-chain price oracle that provides real-time token valuations.',
type: {
contractAddress: {
required: true,
type: 'string',
description: 'Contract address of the price oracle used to fetch token price data.',
},
network: {
required: true,
type: 'string',
description:
'Blockchain network of the price oracle contract (e.g., ETHEREUM, ARBITRUM).',
},
},
},
},
[
{
addresses: [
{
address: 'G7v3P9yPtBj1e3JN7B6dq4zbkrrW3e2ovdwAkSTKuUFG',
},
],
tokenMint: {
token: 'tbill',
contractAddress: '4MmJVdwYN8LwvbGeCowYjSx7KoEi6BJWg8XXnW4fDDp6 ',
},
priceOracle: {
contractAddress: '0xCe9a6626Eb99eaeA829D7fA613d5D0A2eaE45F40',
network: 'ETHEREUM',
},
},
],
)

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

export const endpoint = new AdapterEndpoint({
name: 'solana',
transport: solanaTransport,
inputParameters,
})
4 changes: 2 additions & 2 deletions packages/sources/token-balance/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 { etherFi, evm, solvJlp, tbill, xrpl } from './endpoint'
import { etherFi, evm, solana, solvJlp, tbill, xrpl } from './endpoint'

export const adapter = new Adapter({
defaultEndpoint: evm.name,
name: 'TOKEN_BALANCE',
config,
endpoints: [evm, solvJlp, etherFi, tbill, xrpl],
endpoints: [evm, solvJlp, etherFi, tbill, xrpl, solana],
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
157 changes: 157 additions & 0 deletions packages/sources/token-balance/src/transport/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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 {
AdapterError,
AdapterInputError,
} from '@chainlink/external-adapter-framework/validation/error'
import { Commitment, Connection } from '@solana/web3.js'
import { BaseEndpointTypes, inputParameters } from '../endpoint/solana'
import { getTokenPrice } from './priceFeed'
import { getToken } from './solana-utils'

const logger = makeLogger('Token Balance - Solana')

type RequestParams = typeof inputParameters.validated

const RESULT_DECIMALS = 18

export class SolanaTransport extends SubscriptionTransport<BaseEndpointTypes> {
connection!: Connection

async initialize(
dependencies: TransportDependencies<BaseEndpointTypes>,
adapterSettings: BaseEndpointTypes['Settings'],
endpointName: string,
transportName: string,
): Promise<void> {
await super.initialize(dependencies, adapterSettings, endpointName, transportName)

if (!adapterSettings.SOLANA_RPC_URL) {
logger.warn('SOLANA_RPC_URL is missing')
} else {
this.connection = new Connection(
adapterSettings.SOLANA_RPC_URL,
adapterSettings.SOLANA_COMMITMENT as Commitment,
)
}
}

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(
param: RequestParams,
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
const { addresses, tokenMint } = param
const providerDataRequestedUnixMs = Date.now()

// 1. Fetch token price ONCE from oracle contract
const tokenPrice = await getTokenPrice({
priceOracleAddress: param.priceOracle.contractAddress,
priceOracleNetwork: param.priceOracle.network,
})

// 2. Fetch balances for each Solana wallet and calculate their USD value using the SINGLE tokenPrice
const totalTokenUSD = await this.calculateTokenAumUSD(addresses, tokenMint, tokenPrice)

// 3. Build adapter response object
return {
data: {
result: String(totalTokenUSD), // formatted as string for API
decimals: RESULT_DECIMALS,
},
statusCode: 200,
result: String(totalTokenUSD),
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}

async calculateTokenAumUSD(
addresses: typeof inputParameters.validated.addresses,
tokenMint: typeof inputParameters.validated.tokenMint,
tokenPrice: { value: bigint; decimal: number },
): Promise<bigint> {
// 1. Transform new schema → getToken schema
const addressesForGetToken = [
{
token: tokenMint.token,
contractAddress: tokenMint.contractAddress,
wallets: addresses.map((a) => a.address),
},
]

// 2. Fetch token balances for the given address on Solana
const { result: balances } = await getToken(
addressesForGetToken,
tokenMint.token,
this.connection,
)

// 3. Sum raw balances (all balances are for the same mint, so same decimals)
let totalRaw = 0n

let tokenDecimals = undefined
for (const bal of balances) {
totalRaw += bal.value
if (!bal.decimals) {
throw new AdapterError({
statusCode: 400,
message: 'Missing decimals on balance response',
})
}
if (tokenDecimals !== undefined && bal.decimals !== tokenDecimals) {
throw new AdapterError({
statusCode: 400,
message: `Inconsistent balance decimals: ${tokenDecimals} != ${bal.decimals}`,
})
}
tokenDecimals = bal.decimals
}
tokenDecimals ??= RESULT_DECIMALS

// 4. Calculate AUM
const totalAumUSD =
(totalRaw * tokenPrice.value * 10n ** BigInt(RESULT_DECIMALS)) /
10n ** BigInt(tokenDecimals) /
10n ** BigInt(tokenPrice.decimal)

// 5. Return total USD value for this address
return totalAumUSD
}

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

export const solanaTransport = new SolanaTransport()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`execute SolanaTransport endpoint returns success 1`] = `
{
"data": {
"decimals": 18,
"result": "1500000000000000000000",
},
"result": "1500000000000000000000",
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;
Loading
Loading