Skip to content

Commit 3bf9f56

Browse files
authored
OPDATA-3674: Add totalBalance endpoint to avalanche-platform EA (#4038)
* New totalBalance endpoint boilerplate * Add totalBalance endpoint to avalanche-platform EA * changeset * Remove getBalance and getStake * TODO to delete balance endpoint
1 parent e4ff873 commit 3bf9f56

File tree

14 files changed

+803
-12
lines changed

14 files changed

+803
-12
lines changed

.changeset/nine-wolves-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/avalanche-platform-adapter': minor
3+
---
4+
5+
Add totalBalance endpoint

.pnp.cjs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sources/avalanche-platform/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"devDependencies": {
3131
"@types/jest": "^29.5.14",
3232
"@types/node": "22.14.1",
33+
"axios": "1.9.0",
3334
"nock": "13.5.6",
3435
"typescript": "5.8.3"
3536
},

packages/sources/avalanche-platform/src/config/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ export const config = new AdapterConfig(
88
type: 'string',
99
required: true,
1010
},
11+
GROUP_SIZE: {
12+
description:
13+
'Number of requests to execute asynchronously before the adapter waits to execute the next group of requests.',
14+
type: 'number',
15+
default: 10,
16+
},
17+
BACKGROUND_EXECUTE_MS: {
18+
description:
19+
'The amount of time the background execute should sleep before performing the next request',
20+
type: 'number',
21+
default: 10_000,
22+
},
1123
},
1224
{
1325
envDefaultOverrides: {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { endpoint as balance } from './balance'
2+
export { endpoint as totalBalance } from './totalBalance'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
PoRBalanceEndpoint,
3+
PoRBalanceResponse,
4+
} from '@chainlink/external-adapter-framework/adapter/por'
5+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
6+
import { config } from '../config'
7+
import { totalBalanceTransport } from '../transport/totalBalance'
8+
9+
export const inputParameters = new InputParameters(
10+
{
11+
addresses: {
12+
aliases: ['result'],
13+
array: true,
14+
type: {
15+
address: {
16+
type: 'string',
17+
description: 'an address to get the balance of',
18+
required: true,
19+
},
20+
},
21+
description:
22+
'An array of addresses to get the balances of (as an object with string `address` as an attribute)',
23+
required: true,
24+
},
25+
assetId: {
26+
type: 'string',
27+
description: 'The ID of the asset to get the balance for',
28+
default: 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z', // AVAX asset ID
29+
},
30+
},
31+
[
32+
{
33+
addresses: [
34+
{
35+
address: 'P-avax1tnuesf6cqwnjw7fxjyk7lhch0vhf0v95wj5jvy',
36+
},
37+
],
38+
assetId: 'FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z',
39+
},
40+
],
41+
)
42+
43+
export type BaseEndpointTypes = {
44+
Parameters: typeof inputParameters.definition
45+
Response: PoRBalanceResponse & {
46+
Data: {
47+
decimals: number
48+
}
49+
}
50+
Settings: typeof config.settings
51+
}
52+
53+
export const endpoint = new PoRBalanceEndpoint({
54+
name: 'totalBalance',
55+
aliases: [],
56+
transport: totalBalanceTransport,
57+
inputParameters,
58+
})
Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { PoRAdapter } from '@chainlink/external-adapter-framework/adapter/por'
33
import { config } from './config'
4-
import { balance } from './endpoint'
4+
import { balance, totalBalance } from './endpoint'
55

66
export const adapter = new PoRAdapter({
77
defaultEndpoint: balance.name,
88
name: 'AVALANCHE_PLATFORM',
99
config,
10-
endpoints: [balance],
11-
rateLimiting: {
12-
tiers: {
13-
default: {
14-
rateLimit1m: 6,
15-
note: 'Considered unlimited tier, but setting reasonable limits',
16-
},
17-
},
18-
},
10+
// TODO: The 'balance' endpoint seems to be unused. Delete it after
11+
// confirming that it's not needed anymore.
12+
endpoints: [balance, totalBalance],
1913
})
2014

2115
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import { calculateHttpRequestKey } from '@chainlink/external-adapter-framework/cache'
3+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
4+
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
5+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
6+
import { GroupRunner } from '@chainlink/external-adapter-framework/util/group-runner'
7+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
8+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
9+
import { BaseEndpointTypes, inputParameters } from '../endpoint/totalBalance'
10+
11+
const logger = makeLogger('TotalBalanceTransport')
12+
13+
type RequestParams = typeof inputParameters.validated
14+
15+
export type GetBalanceResult = {
16+
balance: string
17+
unlocked: string
18+
lockedStakeable: string
19+
lockedNotStakeable: string
20+
balances: Record<string, string>
21+
unlockeds: Record<string, string>
22+
lockedStakeables: Record<string, string>
23+
lockedNotStakeables: Record<string, string>
24+
utxoIDs: {
25+
txID: string
26+
outputIndex: number
27+
}[]
28+
}
29+
30+
export type GetStakeResult = {
31+
staked: string
32+
stakeds: Record<string, string>
33+
stakedOutputs: string[]
34+
encoding: string
35+
}
36+
37+
type PlatformResponse<T> = {
38+
result: T
39+
}
40+
41+
type BalanceResult = {
42+
address: string
43+
balance: string
44+
unlocked: string
45+
lockedStakeable: string
46+
lockedNotStakeable: string
47+
staked: string
48+
}
49+
50+
const RESULT_DECIMALS = 18
51+
const P_CHAIN_DECIMALS = 9
52+
53+
const scaleFactor = 10n ** BigInt(RESULT_DECIMALS - P_CHAIN_DECIMALS)
54+
const scale = (n: string) => (BigInt(n) * scaleFactor).toString()
55+
56+
export class TotalBalanceTransport extends SubscriptionTransport<BaseEndpointTypes> {
57+
config!: BaseEndpointTypes['Settings']
58+
endpointName!: string
59+
requester!: Requester
60+
61+
async initialize(
62+
dependencies: TransportDependencies<BaseEndpointTypes>,
63+
adapterSettings: BaseEndpointTypes['Settings'],
64+
endpointName: string,
65+
transportName: string,
66+
): Promise<void> {
67+
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
68+
this.config = adapterSettings
69+
this.endpointName = endpointName
70+
this.requester = dependencies.requester
71+
}
72+
73+
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
74+
await Promise.all(entries.map(async (param) => this.handleRequest(param)))
75+
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
76+
}
77+
78+
async handleRequest(param: RequestParams) {
79+
let response: AdapterResponse<BaseEndpointTypes['Response']>
80+
try {
81+
response = await this._handleRequest(param)
82+
} catch (e) {
83+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
84+
logger.error(e, errorMessage)
85+
response = {
86+
statusCode: (e as AdapterError)?.statusCode || 502,
87+
errorMessage,
88+
timestamps: {
89+
providerDataRequestedUnixMs: 0,
90+
providerDataReceivedUnixMs: 0,
91+
providerIndicatedTimeUnixMs: undefined,
92+
},
93+
}
94+
}
95+
await this.responseCache.write(this.name, [{ params: param, response }])
96+
}
97+
98+
async _handleRequest(
99+
params: RequestParams,
100+
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
101+
const providerDataRequestedUnixMs = Date.now()
102+
103+
const result = await this.getTotalBalances({
104+
addresses: params.addresses,
105+
assetId: params.assetId,
106+
})
107+
108+
return {
109+
data: {
110+
result,
111+
decimals: RESULT_DECIMALS,
112+
},
113+
statusCode: 200,
114+
result: null,
115+
timestamps: {
116+
providerDataRequestedUnixMs,
117+
providerDataReceivedUnixMs: Date.now(),
118+
providerIndicatedTimeUnixMs: undefined,
119+
},
120+
}
121+
}
122+
123+
async getTotalBalances({
124+
addresses,
125+
assetId,
126+
}: {
127+
addresses: { address: string }[]
128+
assetId: string
129+
}): Promise<BalanceResult[]> {
130+
const runner = new GroupRunner(this.config.GROUP_SIZE)
131+
132+
const getBalance: (address: string) => Promise<GetBalanceResult> = runner.wrapFunction(
133+
(address: string) =>
134+
this.callPlatformMethod({
135+
method: 'getBalance',
136+
address,
137+
}),
138+
)
139+
140+
const getStake: (address: string) => Promise<GetStakeResult> = runner.wrapFunction(
141+
(address: string) =>
142+
this.callPlatformMethod({
143+
method: 'getStake',
144+
address,
145+
}),
146+
)
147+
148+
return await Promise.all(
149+
addresses.map(async ({ address }) => {
150+
const [balanceResult, stakedResult] = await Promise.all([
151+
getBalance(address),
152+
getStake(address),
153+
])
154+
const unlocked = scale(balanceResult.unlockeds[assetId] ?? '0')
155+
const lockedStakeable = scale(balanceResult.lockedStakeables[assetId] ?? '0')
156+
const lockedNotStakeable = scale(balanceResult.lockedNotStakeables[assetId] ?? '0')
157+
const staked = scale(stakedResult.stakeds[assetId] ?? '0')
158+
const balance = [unlocked, lockedStakeable, lockedNotStakeable, staked]
159+
.reduce((a, b) => a + BigInt(b), 0n)
160+
.toString()
161+
return {
162+
address,
163+
balance,
164+
unlocked,
165+
lockedStakeable,
166+
lockedNotStakeable,
167+
staked,
168+
}
169+
}),
170+
)
171+
}
172+
173+
async callPlatformMethod<T>({
174+
method,
175+
address,
176+
}: {
177+
method: string
178+
address: string
179+
}): Promise<T> {
180+
const requestConfig = {
181+
method: 'POST',
182+
baseURL: this.config.P_CHAIN_RPC_URL,
183+
data: {
184+
jsonrpc: '2.0',
185+
method: `platform.${method}`,
186+
params: { addresses: [address] },
187+
id: '1',
188+
},
189+
}
190+
191+
const result = await this.requester.request<PlatformResponse<T>>(
192+
calculateHttpRequestKey<BaseEndpointTypes>({
193+
context: {
194+
adapterSettings: this.config,
195+
inputParameters,
196+
endpointName: this.endpointName,
197+
},
198+
data: requestConfig.data,
199+
transportName: this.name,
200+
}),
201+
requestConfig,
202+
)
203+
204+
return result.response.data.result
205+
}
206+
207+
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
208+
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
209+
}
210+
}
211+
212+
export const totalBalanceTransport = new TotalBalanceTransport()
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute totalBalance endpoint should return success 1`] = `
4+
{
5+
"data": {
6+
"decimals": 18,
7+
"result": [
8+
{
9+
"address": "P-fuji1vd9sddlllrlk9fvj9lhntpw8t00lmvtnqkl2jt",
10+
"balance": "5000000000000000000000",
11+
"lockedNotStakeable": "0",
12+
"lockedStakeable": "0",
13+
"staked": "2000000000000000000000",
14+
"unlocked": "3000000000000000000000",
15+
},
16+
],
17+
},
18+
"result": null,
19+
"statusCode": 200,
20+
"timestamps": {
21+
"providerDataReceivedUnixMs": 978347471111,
22+
"providerDataRequestedUnixMs": 978347471111,
23+
},
24+
}
25+
`;

packages/sources/avalanche-platform/test/integration/adapter.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
setEnvVariables,
44
} from '@chainlink/external-adapter-framework/util/testing-utils'
55
import * as nock from 'nock'
6-
import { mockBalanceSuccess } from './fixtures'
6+
import { mockStakeSuccess } from './fixtures'
77

88
describe('execute', () => {
99
let spy: jest.SpyInstance
@@ -41,7 +41,7 @@ describe('execute', () => {
4141
},
4242
],
4343
}
44-
mockBalanceSuccess()
44+
mockStakeSuccess()
4545
const response = await testAdapter.request(data)
4646
expect(response.statusCode).toBe(200)
4747
expect(response.json()).toMatchSnapshot()

0 commit comments

Comments
 (0)