Skip to content

Commit 78100bd

Browse files
authored
Support Virtune API in por-address-list (#3990)
* Copy transport/zeusBTC.ts * Support Virtune API in por-address-list * changeset * lint
1 parent 49ddf73 commit 78100bd

File tree

10 files changed

+611
-2
lines changed

10 files changed

+611
-2
lines changed

.changeset/cold-apricots-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/por-address-list-adapter': minor
3+
---
4+
5+
Support Virtune API to get addresses

packages/sources/por-address-list/src/config/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,15 @@ export const config = new AdapterConfig({
6969
type: 'string',
7070
default: 'https://indexer.zeuslayer.io/api/v2/chainlink/proof-of-reserves',
7171
},
72+
VIRTUNE_API_URL: {
73+
description: 'An API endpoint for Virtune address lists',
74+
type: 'string',
75+
default:
76+
'https://proof-of-reserves-chainlink-283003lt.nw.gateway.dev/api/external/proof-of-reserves',
77+
},
78+
VIRTUNE_API_KEY: {
79+
description: 'The API key for Virtune address list API',
80+
type: 'string',
81+
default: '',
82+
},
7283
})
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
export { endpoint as address } from './address'
2-
export { endpoint as solvBTC } from './solvBTC'
32
export { endpoint as bedrockBTC } from './bedrockBTC'
43
export { endpoint as coinbaseBTC } from './coinbaseCBBTC'
54
export { endpoint as multichainAddress } from './multichainAddress'
65
export { endpoint as openedenAddress } from './openEdenUSDOAddress'
6+
export { endpoint as solvBTC } from './solvBTC'
7+
export { endpoint as virtune } from './virtune'
78
export { endpoint as zeusBtcAddress } from './zeusBTC'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { PoRAddress } from '@chainlink/external-adapter-framework/adapter/por'
3+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
4+
import { config } from '../config'
5+
import { virtuneTransport } from '../transport/virtune'
6+
7+
export const inputParameters = new InputParameters(
8+
{
9+
accountId: {
10+
description: 'The account ID to fetch addresses for',
11+
type: 'string',
12+
required: true,
13+
},
14+
network: {
15+
description:
16+
'The network the addresses are on. This is only used to include in the response.',
17+
type: 'string',
18+
required: true,
19+
},
20+
chainId: {
21+
description:
22+
'The chainId of the network the addresses are on. This is only used to include in the response.',
23+
type: 'string',
24+
required: true,
25+
},
26+
},
27+
[
28+
{
29+
accountId: 'VIRBTC',
30+
network: 'bitcoin',
31+
chainId: 'mainnet',
32+
},
33+
],
34+
)
35+
36+
export type BaseEndpointTypes = {
37+
Parameters: typeof inputParameters.definition
38+
Response: {
39+
Result: null
40+
Data: {
41+
result: PoRAddress[]
42+
}
43+
}
44+
Settings: typeof config.settings
45+
}
46+
47+
export const endpoint = new AdapterEndpoint({
48+
name: 'virtune',
49+
transport: virtuneTransport,
50+
inputParameters,
51+
})

packages/sources/por-address-list/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { PoRAdapter } from '@chainlink/external-adapter-framework/adapter/por'
33
import { config } from './config'
44
import {
55
address,
6-
solvBTC,
76
bedrockBTC,
87
coinbaseBTC,
98
multichainAddress,
109
openedenAddress,
10+
solvBTC,
11+
virtune,
1112
zeusBtcAddress,
1213
} from './endpoint'
1314

@@ -22,6 +23,7 @@ export const adapter = new PoRAdapter({
2223
coinbaseBTC,
2324
multichainAddress,
2425
openedenAddress,
26+
virtune,
2527
zeusBtcAddress,
2628
],
2729
rateLimiting: {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
HttpTransport,
3+
HttpTransportConfig,
4+
} from '@chainlink/external-adapter-framework/transports'
5+
import { BaseEndpointTypes } from '../endpoint/virtune'
6+
7+
interface ResponseSchema {
8+
accountName: string
9+
result: {
10+
symbol: string
11+
totalBalance: string
12+
totalBalanceUsd: string
13+
wallets: {
14+
address: string
15+
symbol: string
16+
custody: string
17+
name: string
18+
isStakingWallet: boolean
19+
}[]
20+
}[]
21+
count: number
22+
lastUpdatedAt: string
23+
}
24+
25+
export type HttpTransportTypes = BaseEndpointTypes & {
26+
Provider: {
27+
RequestBody: never
28+
ResponseBody: ResponseSchema
29+
}
30+
}
31+
32+
const getAddresses = ({
33+
data,
34+
network,
35+
chainId,
36+
}: {
37+
data: ResponseSchema
38+
network: string
39+
chainId: string
40+
}) => {
41+
return data.result.flatMap((r) =>
42+
r.wallets.map((wallet) => ({
43+
address: wallet.address,
44+
network,
45+
chainId,
46+
})),
47+
)
48+
}
49+
50+
const transportConfig: HttpTransportConfig<HttpTransportTypes> = {
51+
prepareRequests: (params, config) => {
52+
return params.map((param) => ({
53+
params: [param],
54+
request: {
55+
baseURL: config.VIRTUNE_API_URL,
56+
url: param.accountId,
57+
params: {
58+
key: config.VIRTUNE_API_KEY,
59+
},
60+
},
61+
}))
62+
},
63+
parseResponse: (params, response) => {
64+
const [param] = params
65+
if (!response.data) {
66+
return [
67+
{
68+
params: param,
69+
response: {
70+
errorMessage: `The data provider didn't return any data for virtune`,
71+
statusCode: 502,
72+
},
73+
},
74+
]
75+
}
76+
77+
const addresses = getAddresses({
78+
data: response.data,
79+
network: param.network,
80+
chainId: param.chainId,
81+
})
82+
83+
if (addresses.length == 0) {
84+
return [
85+
{
86+
params: param,
87+
response: {
88+
errorMessage: `The data provider didn't return any address for virtune`,
89+
statusCode: 502,
90+
},
91+
},
92+
]
93+
}
94+
95+
return [
96+
{
97+
params: param,
98+
response: {
99+
result: null,
100+
data: {
101+
result: addresses,
102+
},
103+
},
104+
},
105+
]
106+
},
107+
}
108+
109+
// Exported for testing
110+
export class VirtuneTransport extends HttpTransport<HttpTransportTypes> {
111+
constructor() {
112+
super(transportConfig)
113+
}
114+
}
115+
116+
export const virtuneTransport = new VirtuneTransport()
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`execute virtune should return success 1`] = `
4+
{
5+
"data": {
6+
"result": [
7+
{
8+
"address": "1JSYkxvBJy4wXDskdXfadfTj6Hg9n5r3br",
9+
"chainId": "mainnet",
10+
"network": "bitcoin",
11+
},
12+
{
13+
"address": "17ABiL5ToFwYdjGtVngEXo2Bw4EKN5myTT",
14+
"chainId": "mainnet",
15+
"network": "bitcoin",
16+
},
17+
],
18+
},
19+
"result": null,
20+
"statusCode": 200,
21+
"timestamps": {
22+
"providerDataReceivedUnixMs": 978347471111,
23+
"providerDataRequestedUnixMs": 978347471111,
24+
},
25+
}
26+
`;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {
2+
TestAdapter,
3+
setEnvVariables,
4+
} from '@chainlink/external-adapter-framework/util/testing-utils'
5+
import nock from 'nock'
6+
import { mockVirtuneResponseSuccess } from './fixtures-api'
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.RPC_URL = process.env.RPC_URL ?? 'http://localhost:8080'
16+
process.env.VIRTUNE_API_URL = 'http://virtune'
17+
process.env.VIRTUNE_API_KEY = 'virtuneApiKey'
18+
process.env.BACKGROUND_EXECUTE_MS = '0'
19+
20+
const mockDate = new Date('2001-01-01T11:11:11.111Z')
21+
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
22+
23+
const adapter = (await import('./../../src')).adapter
24+
adapter.rateLimiting = undefined
25+
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
26+
testAdapter: {} as TestAdapter<never>,
27+
})
28+
})
29+
30+
afterAll(async () => {
31+
setEnvVariables(oldEnv)
32+
await testAdapter.api.close()
33+
nock.restore()
34+
nock.cleanAll()
35+
spy.mockRestore()
36+
})
37+
38+
describe('virtune', () => {
39+
it('should return success', async () => {
40+
const data = {
41+
endpoint: 'virtune',
42+
accountId: 'VIRBTC',
43+
network: 'bitcoin',
44+
chainId: 'mainnet',
45+
}
46+
47+
mockVirtuneResponseSuccess()
48+
49+
const response = await testAdapter.request(data)
50+
51+
expect(response.statusCode).toBe(200)
52+
expect(response.json()).toMatchSnapshot()
53+
})
54+
})
55+
})

packages/sources/por-address-list/test/integration/fixtures-api.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,51 @@ export const mockBaseContractCallResponseSuccess = (): nock.Scope =>
393393
}
394394
}
395395
})
396+
397+
export const mockVirtuneResponseSuccess = (): nock.Scope =>
398+
nock('http://virtune', {
399+
encodedQueryParams: true,
400+
})
401+
.get('/VIRBTC?key=virtuneApiKey')
402+
.reply(
403+
200,
404+
() => ({
405+
accountName: 'Virtune Bitcoin ETP',
406+
result: [
407+
{
408+
symbol: 'BTC',
409+
totalBalance: '123.45678990',
410+
totalBalanceUsd: '14567890.12',
411+
wallets: [
412+
{
413+
address: '1JSYkxvBJy4wXDskdXfadfTj6Hg9n5r3br',
414+
symbol: 'BTC',
415+
custody: 'coinbase',
416+
name: 'Collateral',
417+
isStakingWallet: false,
418+
},
419+
{
420+
address: '17ABiL5ToFwYdjGtVngEXo2Bw4EKN5myTT',
421+
symbol: 'BTC',
422+
custody: 'coinbase',
423+
name: 'Trading',
424+
isStakingWallet: false,
425+
},
426+
],
427+
},
428+
],
429+
count: 2,
430+
lastUpdatedAt: '2025-09-03T07:01:44.562+00:00',
431+
}),
432+
[
433+
'Content-Type',
434+
'application/json',
435+
'Connection',
436+
'close',
437+
'Vary',
438+
'Accept-Encoding',
439+
'Vary',
440+
'Origin',
441+
],
442+
)
443+
.persist()

0 commit comments

Comments
 (0)