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/nine-wolves-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/avalanche-platform-adapter': minor
---

Add totalBalance endpoint
1 change: 1 addition & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/sources/avalanche-platform/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "22.14.1",
"axios": "1.9.0",
"nock": "13.5.6",
"typescript": "5.8.3"
},
Expand Down
12 changes: 12 additions & 0 deletions packages/sources/avalanche-platform/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ export const config = new AdapterConfig(
type: 'string',
required: true,
},
GROUP_SIZE: {
description:
'Number of requests to execute asynchronously before the adapter waits to execute the next group of requests.',
type: 'number',
default: 10,
},
BACKGROUND_EXECUTE_MS: {
description:
'The amount of time the background execute should sleep before performing the next request',
type: 'number',
default: 10_000,
},
},
{
envDefaultOverrides: {
Expand Down
1 change: 1 addition & 0 deletions packages/sources/avalanche-platform/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { endpoint as balance } from './balance'
export { endpoint as totalBalance } from './totalBalance'
58 changes: 58 additions & 0 deletions packages/sources/avalanche-platform/src/endpoint/totalBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
PoRBalanceEndpoint,
PoRBalanceResponse,
} from '@chainlink/external-adapter-framework/adapter/por'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
import { config } from '../config'
import { totalBalanceTransport } from '../transport/totalBalance'

export const inputParameters = new InputParameters(
{
addresses: {
aliases: ['result'],
array: true,
type: {
address: {
type: 'string',
description: 'an address to get the balance of',
required: true,
},
},
description:
'An array of addresses to get the balances of (as an object with string `address` as an attribute)',
required: true,
},
assetId: {
type: 'string',
description: 'The ID of the asset to get the balance for',
default: 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z', // AVAX asset ID
},
},
[
{
addresses: [
{
address: 'P-avax1tnuesf6cqwnjw7fxjyk7lhch0vhf0v95wj5jvy',
},
],
assetId: 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z',
},
],
)

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: PoRBalanceResponse & {
Data: {
decimals: number
}
}
Settings: typeof config.settings
}

export const endpoint = new PoRBalanceEndpoint({
name: 'totalBalance',
aliases: [],
transport: totalBalanceTransport,
inputParameters,
})
14 changes: 4 additions & 10 deletions packages/sources/avalanche-platform/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { PoRAdapter } from '@chainlink/external-adapter-framework/adapter/por'
import { config } from './config'
import { balance } from './endpoint'
import { balance, totalBalance } from './endpoint'

export const adapter = new PoRAdapter({
defaultEndpoint: balance.name,
name: 'AVALANCHE_PLATFORM',
config,
endpoints: [balance],
rateLimiting: {
tiers: {
default: {
rateLimit1m: 6,
note: 'Considered unlimited tier, but setting reasonable limits',
},
},
},
// TODO: The 'balance' endpoint seems to be unused. Delete it after
// confirming that it's not needed anymore.
endpoints: [balance, totalBalance],
})

export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
212 changes: 212 additions & 0 deletions packages/sources/avalanche-platform/src/transport/totalBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
import { calculateHttpRequestKey } from '@chainlink/external-adapter-framework/cache'
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 { GroupRunner } from '@chainlink/external-adapter-framework/util/group-runner'
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
import { BaseEndpointTypes, inputParameters } from '../endpoint/totalBalance'

const logger = makeLogger('TotalBalanceTransport')

type RequestParams = typeof inputParameters.validated

export type GetBalanceResult = {
balance: string
unlocked: string
lockedStakeable: string
lockedNotStakeable: string
balances: Record<string, string>
unlockeds: Record<string, string>
lockedStakeables: Record<string, string>
lockedNotStakeables: Record<string, string>
utxoIDs: {
txID: string
outputIndex: number
}[]
}

export type GetStakeResult = {
staked: string
stakeds: Record<string, string>
stakedOutputs: string[]
encoding: string
}

type PlatformResponse<T> = {
result: T
}

type BalanceResult = {
address: string
balance: string
unlocked: string
lockedStakeable: string
lockedNotStakeable: string
staked: string
}

const RESULT_DECIMALS = 18
const P_CHAIN_DECIMALS = 9

const scaleFactor = 10n ** BigInt(RESULT_DECIMALS - P_CHAIN_DECIMALS)
const scale = (n: string) => (BigInt(n) * scaleFactor).toString()

export class TotalBalanceTransport extends SubscriptionTransport<BaseEndpointTypes> {
config!: BaseEndpointTypes['Settings']
endpointName!: string
requester!: Requester

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

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) {
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
logger.error(e, errorMessage)
response = {
statusCode: (e as AdapterError)?.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 this.getTotalBalances({
addresses: params.addresses,
assetId: params.assetId,
})

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

async getTotalBalances({
addresses,
assetId,
}: {
addresses: { address: string }[]
assetId: string
}): Promise<BalanceResult[]> {
const runner = new GroupRunner(this.config.GROUP_SIZE)

const getBalance: (address: string) => Promise<GetBalanceResult> = runner.wrapFunction(
(address: string) =>
this.callPlatformMethod({
method: 'getBalance',
address,
}),
)

const getStake: (address: string) => Promise<GetStakeResult> = runner.wrapFunction(
(address: string) =>
this.callPlatformMethod({
method: 'getStake',
address,
}),
)

return await Promise.all(
addresses.map(async ({ address }) => {
const [balanceResult, stakedResult] = await Promise.all([
getBalance(address),
getStake(address),
])
const unlocked = scale(balanceResult.unlockeds[assetId] ?? '0')
const lockedStakeable = scale(balanceResult.lockedStakeables[assetId] ?? '0')
const lockedNotStakeable = scale(balanceResult.lockedNotStakeables[assetId] ?? '0')
const staked = scale(stakedResult.stakeds[assetId] ?? '0')
const balance = [unlocked, lockedStakeable, lockedNotStakeable, staked]
.reduce((a, b) => a + BigInt(b), 0n)
.toString()
return {
address,
balance,
unlocked,
lockedStakeable,
lockedNotStakeable,
staked,
}
}),
)
}

async callPlatformMethod<T>({
method,
address,
}: {
method: string
address: string
}): Promise<T> {
const requestConfig = {
method: 'POST',
baseURL: this.config.P_CHAIN_RPC_URL,
data: {
jsonrpc: '2.0',
method: `platform.${method}`,
params: { addresses: [address] },
id: '1',
},
}

const result = await this.requester.request<PlatformResponse<T>>(
calculateHttpRequestKey<BaseEndpointTypes>({
context: {
adapterSettings: this.config,
inputParameters,
endpointName: this.endpointName,
},
data: requestConfig.data,
transportName: this.name,
}),
requestConfig,
)

return result.response.data.result
}

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

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

exports[`execute totalBalance endpoint should return success 1`] = `
{
"data": {
"decimals": 18,
"result": [
{
"address": "P-fuji1vd9sddlllrlk9fvj9lhntpw8t00lmvtnqkl2jt",
"balance": "5000000000000000000000",
"lockedNotStakeable": "0",
"lockedStakeable": "0",
"staked": "2000000000000000000000",
"unlocked": "3000000000000000000000",
},
],
},
"result": null,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 978347471111,
"providerDataRequestedUnixMs": 978347471111,
},
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
setEnvVariables,
} from '@chainlink/external-adapter-framework/util/testing-utils'
import * as nock from 'nock'
import { mockBalanceSuccess } from './fixtures'
import { mockStakeSuccess } from './fixtures'

describe('execute', () => {
let spy: jest.SpyInstance
Expand Down Expand Up @@ -41,7 +41,7 @@ describe('execute', () => {
},
],
}
mockBalanceSuccess()
mockStakeSuccess()
const response = await testAdapter.request(data)
expect(response.statusCode).toBe(200)
expect(response.json()).toMatchSnapshot()
Expand Down
Loading
Loading