Skip to content

Commit a63ad03

Browse files
authored
Bitgo Reserve multi client (#3772)
* Bitgo Reserve multi client * comments * comment * Comment * comments
1 parent acaae12 commit a63ad03

File tree

7 files changed

+150
-14
lines changed

7 files changed

+150
-14
lines changed

.changeset/chatty-laws-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/bitgo-reserves-adapter': minor
3+
---
4+
5+
Allow multi client

packages/sources/bitgo-reserves/src/config/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
22

33
export const config = new AdapterConfig({
44
API_ENDPOINT: {
5-
description: 'An API endpoint for Data Provider',
5+
description: 'An API endpoint for Go USD',
66
type: 'string',
77
default: 'https://reserves.gousd.com/por.json',
88
},
99
VERIFICATION_PUBKEY: {
1010
description:
11-
'Public RSA key used for verifying data signature. Expected to be formatted as a single line eg: "-----BEGIN PUBLIC KEY-----\\n...contents...\\n-----END PUBLIC KEY-----"',
11+
'Public RSA key used for verifying data signature for Go USD. Expected to be formatted as a single line eg: "-----BEGIN PUBLIC KEY-----\\n...contents...\\n-----END PUBLIC KEY-----"',
1212
type: 'string',
13-
required: true,
13+
default: '',
1414
},
15+
// You can additionally add ${client}_API_ENDPOINT ${client}_VERIFICATION_PUBKEY to match ${client} in EA input param
1516
})

packages/sources/bitgo-reserves/src/endpoint/reserves.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@ import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
22
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
33
import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util'
44
import { config } from '../config'
5-
import { httpTransport } from '../transport/reserves'
5+
import { httpTransport, getCreds } from '../transport/reserves'
6+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
67

7-
export const inputParameters = new InputParameters({})
8+
export const inputParameters = new InputParameters(
9+
{
10+
client: {
11+
description:
12+
'Used to match ${client}_API_ENDPOINT ${client}_VERIFICATION_PUBKEY environment variables',
13+
type: 'string',
14+
default: 'gousd',
15+
},
16+
},
17+
[
18+
{
19+
client: 'gousd',
20+
},
21+
],
22+
)
823

924
export type BaseEndpointTypes = {
1025
Parameters: typeof inputParameters.definition
@@ -16,4 +31,12 @@ export const endpoint = new AdapterEndpoint({
1631
name: 'reserves',
1732
transport: httpTransport,
1833
inputParameters,
34+
customInputValidation: (request, adapterSettings): AdapterInputError | undefined => {
35+
getCreds(
36+
request.requestContext.data.client,
37+
adapterSettings.API_ENDPOINT,
38+
adapterSettings.VERIFICATION_PUBKEY,
39+
)
40+
return
41+
},
1942
})

packages/sources/bitgo-reserves/src/transport/reserves.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from '@chainlink/external-adapter-framework/transports'
66
import { BaseEndpointTypes } from '../endpoint/reserves'
77
import * as crypto from 'crypto'
8-
import { AdapterSettings } from '@chainlink/external-adapter-framework/config'
8+
import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error'
99

1010
export interface DataSchema {
1111
totalReserve: string
@@ -29,32 +29,57 @@ export type HttpTransportTypes = BaseEndpointTypes & {
2929

3030
class ReservesHttpTransport extends HttpTransport<HttpTransportTypes> {
3131
pubkey!: string
32+
endpoint!: string
3233

3334
constructor(config: HttpTransportConfig<HttpTransportTypes>) {
3435
super(config)
3536
}
3637

3738
override async initialize(
3839
dependencies: TransportDependencies<HttpTransportTypes>,
39-
adapterSettings: AdapterSettings<{
40-
API_ENDPOINT: { description: string; type: 'string'; default: string }
41-
VERIFICATION_PUBKEY: { description: string; type: 'string'; required: true }
42-
}>,
40+
adapterSettings: HttpTransportTypes['Settings'],
4341
endpointName: string,
4442
transportName: string,
4543
): Promise<void> {
4644
super.initialize(dependencies, adapterSettings, endpointName, transportName)
4745
this.pubkey = adapterSettings.VERIFICATION_PUBKEY.replace(/\\n/g, '\n')
46+
this.endpoint = adapterSettings.API_ENDPOINT
47+
}
48+
}
49+
50+
export const getCreds = (client: string, defaultEndpoint: string, defaultKey: string) => {
51+
if (client == 'gousd' && defaultKey.length != 0) {
52+
return {
53+
endpoint: defaultEndpoint,
54+
key: defaultKey,
55+
}
56+
} else {
57+
const apiEndpointName = `${client.toUpperCase()}_API_ENDPOINT`
58+
const pubKeyName = `${client.toUpperCase()}_VERIFICATION_PUBKEY`
59+
const endpoint = process.env[apiEndpointName]
60+
const pubKey = process.env[pubKeyName]
61+
if (!endpoint || !pubKey) {
62+
throw new AdapterInputError({
63+
statusCode: 400,
64+
message: `Missing '${apiEndpointName}' or '${pubKeyName}' environment variables.`,
65+
})
66+
}
67+
return {
68+
endpoint: endpoint,
69+
key: pubKey.replace(/\\n/g, '\n'),
70+
}
4871
}
4972
}
5073

5174
export const httpTransport = new ReservesHttpTransport({
5275
prepareRequests: (params, config) => {
5376
return params.map((param) => {
77+
const endpoint = getCreds(param.client, config.API_ENDPOINT, httpTransport.pubkey)
78+
.endpoint as string
5479
return {
5580
params: [param],
5681
request: {
57-
baseURL: config.API_ENDPOINT,
82+
baseURL: endpoint,
5883
},
5984
}
6085
})
@@ -89,7 +114,12 @@ export const httpTransport = new ReservesHttpTransport({
89114

90115
const verifier = crypto.createVerify('sha256')
91116
verifier.update(payload.data)
92-
if (!verifier.verify(httpTransport.pubkey, payload.dataSignature, 'base64')) {
117+
const verified = verifier.verify(
118+
getCreds(params[0].client, httpTransport.endpoint, httpTransport.pubkey).key,
119+
payload.dataSignature,
120+
'base64',
121+
)
122+
if (!verified) {
93123
return params.map((param) => {
94124
return {
95125
params: param,

packages/sources/bitgo-reserves/test/integration/__snapshots__/adapter.test.ts.snap

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`execute reserves endpoint should return failure for non exist client 1`] = `
4+
{
5+
"error": {
6+
"message": "Missing 'C2_API_ENDPOINT' or 'C2_VERIFICATION_PUBKEY' environment variables.",
7+
"name": "AdapterError",
8+
},
9+
"status": "errored",
10+
"statusCode": 400,
11+
}
12+
`;
13+
314
exports[`execute reserves endpoint should return success 1`] = `
415
{
516
"data": {
@@ -13,4 +24,19 @@ exports[`execute reserves endpoint should return success 1`] = `
1324
"providerIndicatedTimeUnixMs": 1733793825000,
1425
},
1526
}
16-
`;
27+
`;
28+
29+
exports[`execute reserves endpoint should return success for multi client 1`] = `
30+
{
31+
"data": {
32+
"result": 12345678.91,
33+
},
34+
"result": 12345678.91,
35+
"statusCode": 200,
36+
"timestamps": {
37+
"providerDataReceivedUnixMs": 978347471111,
38+
"providerDataRequestedUnixMs": 978347471111,
39+
"providerIndicatedTimeUnixMs": 1733793825000,
40+
},
41+
}
42+
`;

packages/sources/bitgo-reserves/test/integration/adapter.test.ts

Lines changed: 27 additions & 1 deletion
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 { mockResponseSuccess } from './fixtures'
6+
import { mockResponseSuccess, mockResponseSuccessC1 } from './fixtures'
77

88
describe('execute', () => {
99
let spy: jest.SpyInstance
@@ -15,6 +15,9 @@ describe('execute', () => {
1515
process.env.API_ENDPOINT = 'http://test-endpoint.com'
1616
process.env.VERIFICATION_PUBKEY = 'test'
1717

18+
process.env.C1_API_ENDPOINT = 'http://test-endpoint-c1.com'
19+
process.env.C1_VERIFICATION_PUBKEY = 'test-c1'
20+
1821
const mockDate = new Date('2001-01-01T11:11:11.111Z')
1922
spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime())
2023

@@ -49,6 +52,29 @@ describe('execute', () => {
4952
expect(response.statusCode).toBe(200)
5053
expect(response.json()).toMatchSnapshot()
5154
})
55+
56+
it('should return success for multi client', async () => {
57+
const data = {
58+
endpoint: 'reserves',
59+
client: 'c1',
60+
}
61+
mockResponseSuccessC1()
62+
const response = await testAdapter.request(data)
63+
expect(response.statusCode).toBe(200)
64+
expect(response.json()).toMatchSnapshot()
65+
})
66+
67+
it('should return failure for non exist client', async () => {
68+
const data = {
69+
endpoint: 'reserves',
70+
client: 'c2',
71+
}
72+
mockResponseSuccess()
73+
mockResponseSuccessC1()
74+
const response = await testAdapter.request(data)
75+
expect(response.statusCode).toBe(400)
76+
expect(response.json()).toMatchSnapshot()
77+
})
5278
})
5379
// Note: issues with mock verifier prevent further tests
5480
})

packages/sources/bitgo-reserves/test/integration/fixtures.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,28 @@ export const mockResponseSuccess = (): nock.Scope =>
2424
],
2525
)
2626
.persist()
27+
28+
export const mockResponseSuccessC1 = (): nock.Scope =>
29+
nock('http://test-endpoint-c1.com', {
30+
encodedQueryParams: true,
31+
})
32+
.get('/')
33+
.reply(
34+
200,
35+
() => ({
36+
data: '{"totalReserve":"12345678.91","cashReserve":"2345678.91","investedReserve":"10000000.01","lastUpdated":"2024-12-10T01:23:45Z"}',
37+
dataSignature: 'testsig',
38+
lastUpdated: '2024-10-01T01:23:45Z',
39+
}),
40+
[
41+
'Content-Type',
42+
'application/json',
43+
'Connection',
44+
'close',
45+
'Vary',
46+
'Accept-Encoding',
47+
'Vary',
48+
'Origin',
49+
],
50+
)
51+
.persist()

0 commit comments

Comments
 (0)