Skip to content
Open
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/silly-pumpkins-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/token-balance-adapter': minor
---

Add solanaMulti endpoint
1 change: 1 addition & 0 deletions packages/sources/token-balance/src/endpoint/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { endpoint as etherFi } from './etherFi'
export { endpoint as evm } from './evm'
export { endpoint as solana } from './solana'
export { endpoint as solanaBalance } from './solana-balance'
export { endpoint as solanaMulti } from './solanaMulti'
export { endpoint as solvJlp } from './solvJlp'
export { endpoint as tbill } from './tbill'
export { endpoint as xrp } from './xrp'
Expand Down
106 changes: 106 additions & 0 deletions packages/sources/token-balance/src/endpoint/solanaMulti.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { solanaMultiTransport } from '../transport/solanaMulti'

export const inputParameters = new InputParameters(
{
addresses: {
required: true,
description:
'List of addresses in the format returned by the multichainAddress endpoint por-address-list.',
type: {
token: {
required: true,
type: 'string',
description: 'Token the address is associated with to filter addresses by token',
},
network: {
required: true,
type: 'string',
description: 'Addresses with a network other than SOLANA will be ignored',
},
contractAddress: {
required: true,
type: 'string',
description: 'Address of token contract',
},
wallets: {
required: true,
type: 'string',
array: true,
description: 'Array of wallets to sum balances',
},
},
array: true,
},
token: {
required: true,
description: 'Token symbol used to filter addresses',
type: 'string',
},
priceOracle: {
required: false,
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: [
{
token: 'WBTC',
network: 'SOLANA',
contractAddress: '3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh',
wallets: ['EXrqY7jLTLp83H38L8Zw3GvGkk1KoQbYTckPGBghwD8X'],
},
],
token: 'WBTC',
priceOracle: {
contractAddress: '0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23',
network: 'ETHEREUM',
},
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: {
Result: string
Data: {
result: string
decimals: number
wallets: {
token: string
wallet: string
value: string
decimals: number
}[]
tokenPrice: {
value: string
decimals: number
}
}
}
Settings: typeof config.settings
}

export const endpoint = new AdapterEndpoint({
name: 'solanaMulti',
transport: solanaMultiTransport,
inputParameters,
})
14 changes: 12 additions & 2 deletions packages/sources/token-balance/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import { etherFi, evm, solana, solanaBalance, solvJlp, tbill, xrp, xrpl } from './endpoint'
import {
etherFi,
evm,
solana,
solanaBalance,
solanaMulti,
solvJlp,
tbill,
xrp,
xrpl,
} from './endpoint'

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

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

const logger = makeLogger('Token Balances - SolanaMulti')

type RequestParams = typeof inputParameters.validated

const RESULT_DECIMALS = 18

export class SolanaMultiTransport extends SubscriptionTransport<BaseEndpointTypes> {
connection!: Connection
provider!: ethers.JsonRpcProvider

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, token, priceOracle } = param
const providerDataRequestedUnixMs = Date.now()

const [tokenResponse, tokenPrice] = await Promise.all([
getToken(addresses, token, this.connection),
this.getTokenPrice(priceOracle),
])

const maxTokenDecimals = tokenResponse.result.reduce(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can just scale everything to 18

(max, elem) => Math.max(elem.decimals, max),
0,
)
const tokenAmount = tokenResponse.result.reduce(
(sum, elem) => sum + elem.value * 10n ** BigInt(maxTokenDecimals - elem.decimals),
0n,
)

const result = (
(tokenAmount * tokenPrice.value * 10n ** BigInt(RESULT_DECIMALS)) /
10n ** BigInt(maxTokenDecimals + tokenPrice.decimal)
).toString()

return {
data: {
result,
decimals: RESULT_DECIMALS,
wallets: tokenResponse.formattedResponse,
tokenPrice: {
value: String(tokenPrice.value),
decimals: tokenPrice.decimal,
},
},
statusCode: 200,
result,
timestamps: {
providerDataRequestedUnixMs,
providerDataReceivedUnixMs: Date.now(),
providerIndicatedTimeUnixMs: undefined,
},
}
}

async getTokenPrice(
priceOracle:
| {
contractAddress: string
network: string
}
| undefined,
): Promise<{ value: bigint; decimal: number }> {
if (priceOracle === undefined) {
return {
value: 10n ** BigInt(RESULT_DECIMALS),
decimal: RESULT_DECIMALS,
}
}
return getTokenPrice({
priceOracleAddress: priceOracle.contractAddress,
priceOracleNetwork: priceOracle.network,
})
}

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

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

exports[`execute SolanaMultiTransport endpoint returns success 1`] = `
{
"data": {
"decimals": 18,
"result": "1500000000000000000000",
"tokenPrice": {
"decimals": 8,
"value": "150000000",
},
"wallets": [
{
"decimals": 6,
"token": "4MmJVdwYN8LwvbGeCowYjSx7KoEi6BJWg8XXnW4fDDp6",
"value": "1000000000",
"wallet": "G7v3P9yPtBj1e3JN7B6dq4zbkrrW3e2ovdwAkSTKuUFG",
},
],
},
"result": "1500000000000000000000",
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;
Loading
Loading