Skip to content

Commit 509be25

Browse files
authored
OPDATA-5255: Add cardano endpoint to token-balance (#4748)
* Copy XRP * Add cardano endpoint to token-balance * Fix snapshot * Fix type in fixture
1 parent bacc33a commit 509be25

11 files changed

Lines changed: 792 additions & 0 deletions

File tree

.changeset/tough-meals-own.md

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

packages/sources/token-balance/src/config/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ export const config = new AdapterConfig({
5656
default: '',
5757
sensitive: false,
5858
},
59+
CARDANO_RPC_URL: {
60+
description: 'RPC url of Cardano Yaci Store Indexer RPC node',
61+
type: 'string',
62+
default: '',
63+
sensitive: false,
64+
},
5965
BACKGROUND_EXECUTE_MS: {
6066
description:
6167
'The amount of time the background execute should sleep before performing the next request',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
4+
import { config } from '../config'
5+
import { cardanoTransport } from '../transport/cardano'
6+
import { getCardanoRpcUrl } from '../transport/cardano-utils'
7+
8+
export const inputParameters = new InputParameters(
9+
{
10+
addresses: {
11+
required: true,
12+
type: {
13+
address: {
14+
required: true,
15+
type: 'string',
16+
description: 'Address of the account to fetch the balance of',
17+
},
18+
},
19+
array: true,
20+
description: 'List of addresses to read',
21+
},
22+
},
23+
[
24+
{
25+
addresses: [
26+
{
27+
address: 'addr1w8z0xlftcx54tn7uxdvhk0qgj9u7hmlaccjthnc9kvu4pmcyemglm',
28+
},
29+
],
30+
},
31+
],
32+
)
33+
34+
export type AddressWithBalance = {
35+
address: string
36+
balance: string
37+
}
38+
39+
export type BaseEndpointTypes = {
40+
Parameters: typeof inputParameters.definition
41+
Response: {
42+
Result: null
43+
Data: {
44+
result: AddressWithBalance[]
45+
decimals: number
46+
}
47+
}
48+
Settings: typeof config.settings
49+
}
50+
51+
export const endpoint = new AdapterEndpoint({
52+
name: 'cardano',
53+
transport: cardanoTransport,
54+
inputParameters,
55+
customInputValidation: (_request, settings): AdapterError | undefined => {
56+
getCardanoRpcUrl(settings)
57+
return
58+
},
59+
})

packages/sources/token-balance/src/endpoint/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { endpoint as cardano } from './cardano'
12
export { endpoint as etherFi } from './etherFi'
23
export { endpoint as evm } from './evm'
34
export { endpoint as litecoin } from './litecoin'

packages/sources/token-balance/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
22
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
33
import { config } from './config'
44
import {
5+
cardano,
56
etherFi,
67
evm,
78
litecoin,
@@ -26,6 +27,7 @@ export const adapter = new Adapter({
2627
tbill,
2728
xrp,
2829
xrpl,
30+
cardano,
2931
solana,
3032
solanaMulti,
3133
solanaBalance,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
2+
import { config } from '../config'
3+
4+
export const getCardanoRpcUrl = (settings: typeof config.settings) => {
5+
if (!settings.CARDANO_RPC_URL) {
6+
throw new AdapterInputError({
7+
statusCode: 400,
8+
message: 'Environment variable CARDANO_RPC_URL is missing',
9+
})
10+
}
11+
return settings.CARDANO_RPC_URL
12+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
9+
import { AddressWithBalance, BaseEndpointTypes, inputParameters } from '../endpoint/cardano'
10+
import { getCardanoRpcUrl } from './cardano-utils'
11+
12+
const logger = makeLogger('Token Balance - Cardano')
13+
14+
type RequestParams = typeof inputParameters.validated
15+
16+
const RESULT_DECIMALS = 6
17+
18+
type AmountEntry = {
19+
unit: string
20+
quantity: number
21+
}
22+
23+
export class CardanoTransport extends SubscriptionTransport<BaseEndpointTypes> {
24+
config!: BaseEndpointTypes['Settings']
25+
endpointName!: string
26+
requester!: Requester
27+
28+
async initialize(
29+
dependencies: TransportDependencies<BaseEndpointTypes>,
30+
adapterSettings: BaseEndpointTypes['Settings'],
31+
endpointName: string,
32+
transportName: string,
33+
): Promise<void> {
34+
await super.initialize(dependencies, adapterSettings, endpointName, transportName)
35+
this.config = adapterSettings
36+
this.endpointName = endpointName
37+
this.requester = dependencies.requester
38+
}
39+
40+
async backgroundHandler(context: EndpointContext<BaseEndpointTypes>, entries: RequestParams[]) {
41+
await Promise.all(entries.map(async (param) => this.handleRequest(context, param)))
42+
await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS)
43+
}
44+
45+
async handleRequest(_context: EndpointContext<BaseEndpointTypes>, param: RequestParams) {
46+
let response: AdapterResponse<BaseEndpointTypes['Response']>
47+
try {
48+
response = await this._handleRequest(param)
49+
} catch (e: unknown) {
50+
const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred'
51+
logger.error(e, errorMessage)
52+
response = {
53+
statusCode: (e as AdapterInputError)?.statusCode || 502,
54+
errorMessage,
55+
timestamps: {
56+
providerDataRequestedUnixMs: 0,
57+
providerDataReceivedUnixMs: 0,
58+
providerIndicatedTimeUnixMs: undefined,
59+
},
60+
}
61+
}
62+
await this.responseCache.write(this.name, [{ params: param, response }])
63+
}
64+
65+
async _handleRequest(
66+
param: RequestParams,
67+
): Promise<AdapterResponse<BaseEndpointTypes['Response']>> {
68+
const providerDataRequestedUnixMs = Date.now()
69+
const result = await this.getTokenBalances(param.addresses)
70+
71+
return {
72+
data: {
73+
result,
74+
decimals: RESULT_DECIMALS,
75+
},
76+
statusCode: 200,
77+
result: null,
78+
timestamps: {
79+
providerDataRequestedUnixMs,
80+
providerDataReceivedUnixMs: Date.now(),
81+
providerIndicatedTimeUnixMs: undefined,
82+
},
83+
}
84+
}
85+
86+
async getTokenBalances(
87+
addresses: {
88+
address: string
89+
}[],
90+
): Promise<AddressWithBalance[]> {
91+
const runner = new GroupRunner(this.config.GROUP_SIZE)
92+
const getBalance = runner.wrapFunction(
93+
async ({ address }: { address: string }): Promise<AddressWithBalance> => {
94+
const balance = await this.getTokenBalance(address)
95+
return {
96+
address,
97+
balance,
98+
}
99+
},
100+
)
101+
return await Promise.all(addresses.map(getBalance))
102+
}
103+
104+
async getTokenBalance(address: string): Promise<string> {
105+
const url = `/api/v1/addresses/${encodeURIComponent(address)}/amounts`
106+
const requestConfig = {
107+
method: 'GET' as const,
108+
baseURL: getCardanoRpcUrl(this.config),
109+
url,
110+
}
111+
112+
const result = await this.requester.request<AmountEntry[]>(
113+
calculateHttpRequestKey<BaseEndpointTypes>({
114+
context: {
115+
adapterSettings: this.config,
116+
inputParameters,
117+
endpointName: this.endpointName,
118+
},
119+
data: { url },
120+
transportName: this.name,
121+
}),
122+
requestConfig,
123+
)
124+
125+
const balance = result.response.data
126+
.filter((entry) => entry.unit === 'lovelace')
127+
.reduce((acc, entry) => acc + BigInt(entry.quantity), 0n)
128+
return balance.toString()
129+
}
130+
131+
getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number {
132+
return adapterSettings.WARMUP_SUBSCRIPTION_TTL
133+
}
134+
}
135+
136+
export const cardanoTransport = new CardanoTransport()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute cardano endpoint should return success 1`] = `
4+
{
5+
"data": {
6+
"decimals": 6,
7+
"result": [
8+
{
9+
"address": "addr1w8z0xlftcx54tn7uxdvhk0qgj9u7hmlaccjthnc9kvu4pmcyemglm",
10+
"balance": "19999926",
11+
},
12+
],
13+
},
14+
"result": null,
15+
"statusCode": 200,
16+
"timestamps": {
17+
"providerDataReceivedUnixMs": 978347471111,
18+
"providerDataRequestedUnixMs": 978347471111,
19+
},
20+
}
21+
`;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
TestAdapter,
3+
setEnvVariables,
4+
} from '@chainlink/external-adapter-framework/util/testing-utils'
5+
import * as nock from 'nock'
6+
import { mockCardanoResponseSuccess } from './fixtures'
7+
8+
describe('execute', () => {
9+
let spy: jest.SpyInstance
10+
let testAdapter: TestAdapter
11+
let oldEnv: NodeJS.ProcessEnv
12+
13+
beforeAll(async () => {
14+
oldEnv = JSON.parse(JSON.stringify(process.env))
15+
process.env.CARDANO_RPC_URL = 'http://localhost-cardano:8080'
16+
process.env.BACKGROUND_EXECUTE_MS = '0'
17+
18+
const mockDate = new Date('2001-01-01T11:11:11.111Z')
19+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
20+
21+
const adapter = (await import('./../../src')).adapter
22+
adapter.rateLimiting = undefined
23+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
24+
testAdapter: {} as TestAdapter<never>,
25+
})
26+
})
27+
28+
afterAll(async () => {
29+
setEnvVariables(oldEnv)
30+
await testAdapter.api.close()
31+
nock.restore()
32+
nock.cleanAll()
33+
spy.mockRestore()
34+
})
35+
36+
describe('cardano endpoint', () => {
37+
it('should return success', async () => {
38+
const data = {
39+
endpoint: 'cardano',
40+
addresses: [
41+
{
42+
address: 'addr1w8z0xlftcx54tn7uxdvhk0qgj9u7hmlaccjthnc9kvu4pmcyemglm',
43+
},
44+
],
45+
}
46+
mockCardanoResponseSuccess()
47+
48+
const response = await testAdapter.request(data)
49+
expect(response.statusCode).toBe(200)
50+
expect(response.json()).toMatchSnapshot()
51+
})
52+
})
53+
})

packages/sources/token-balance/test/integration/fixtures.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,12 @@ export const mockXrpResponseSuccess = (): nock.Scope =>
558558
)
559559
.persist()
560560

561+
export const mockCardanoResponseSuccess = (): nock.Scope =>
562+
nock('http://localhost-cardano:8080', { encodedQueryParams: true })
563+
.persist()
564+
.get('/api/v1/addresses/addr1w8z0xlftcx54tn7uxdvhk0qgj9u7hmlaccjthnc9kvu4pmcyemglm/amounts')
565+
.reply(200, [{ unit: 'lovelace', quantity: 19999926 }])
566+
561567
export const mockStellarResponseSuccess = (): nock.Scope =>
562568
nock('http://localhost-stellar:8080', { encodedQueryParams: true })
563569
.persist()

0 commit comments

Comments
 (0)