Skip to content

Commit 2a5bf50

Browse files
authored
Refactor anchorage (#4184)
1 parent 1aa3734 commit 2a5bf50

File tree

5 files changed

+209
-61
lines changed

5 files changed

+209
-61
lines changed

.changeset/giant-ligers-itch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/anchorage-adapter': major
3+
---
4+
5+
The maximum API_LIMIT is now reduced from 500 to 100

packages/sources/anchorage/src/config/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const config = new AdapterConfig({
1111
description: 'The maximum number of results to request from the API',
1212
type: 'number',
1313
default: 50,
14-
validate: validator.integer({ min: 1, max: 500 }),
14+
validate: validator.integer({ min: 1, max: 100 }),
1515
},
1616
BACKGROUND_EXECUTE_MS: {
1717
description:
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
2+
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
3+
4+
interface GeneralResponse<T> {
5+
data: T[]
6+
page: {
7+
next: string
8+
}
9+
}
10+
11+
export const request = async <T>(
12+
requester: Requester,
13+
endpoint: string,
14+
path: string,
15+
apiKey: string,
16+
apiLimit: number,
17+
) => {
18+
const results: T[] = []
19+
20+
const requestConfig = {
21+
baseURL: endpoint,
22+
url: path + `?limit=${apiLimit}`,
23+
headers: {
24+
'Api-Access-Key': apiKey,
25+
},
26+
}
27+
28+
let hasNext = true
29+
while (hasNext) {
30+
const response = await requester.request<GeneralResponse<T>>(
31+
JSON.stringify(requestConfig),
32+
requestConfig,
33+
)
34+
35+
if (!response || !response.response || !response.response.data) {
36+
throw new AdapterError({
37+
statusCode: 500,
38+
message: `API ${requestConfig.baseURL + requestConfig.url} does not return data`,
39+
})
40+
}
41+
42+
results.push(...response.response.data.data)
43+
44+
requestConfig.url = response.response.data.page.next
45+
hasNext = response.response.data.page.next !== null
46+
}
47+
48+
return results
49+
}

packages/sources/anchorage/src/transport/wallet.ts

Lines changed: 41 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
22
import { ResponseCache } from '@chainlink/external-adapter-framework/cache/response'
3-
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
4-
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
3+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
54
import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription'
6-
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
7-
import { BaseEndpointTypes, inputParameters } from '../endpoint/wallet'
5+
import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util'
6+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
87
import { AdapterError } from '@chainlink/external-adapter-framework/validation/error'
8+
import { BaseEndpointTypes, inputParameters } from '../endpoint/wallet'
9+
import { request } from './requester'
910
import { getApiInfo } from './utils'
1011

1112
const logger = makeLogger('WalletTransport')
@@ -15,41 +16,36 @@ type RequestParams = typeof inputParameters.validated
1516
export type WalletTransportTypes = BaseEndpointTypes
1617

1718
interface WalletResponse {
18-
data: {
19-
assets: {
19+
assets: {
20+
assetType: string
21+
availableBalance: {
2022
assetType: string
21-
availableBalance: {
22-
assetType: string
23-
currentPrice: string
24-
currentUSDValue: string
25-
quantity: string
26-
}
27-
totalBalance: {
28-
assetType: string
29-
currentPrice: string
30-
currentUSDValue: string
31-
quantity: string
32-
}
33-
}[]
34-
depositAddress: {
35-
address: string
36-
addressId: string
37-
addressSignaturePayload: string
38-
addressID: string
39-
signature: string
23+
currentPrice: string
24+
currentUSDValue: string
25+
quantity: string
26+
}
27+
totalBalance: {
28+
assetType: string
29+
currentPrice: string
30+
currentUSDValue: string
31+
quantity: string
4032
}
41-
isDefault: boolean
42-
networkId: string
43-
subaccountId: string
44-
type: string
45-
vaultId: string
46-
vaultName: string
47-
walletId: string
48-
walletName: string
4933
}[]
50-
page: {
51-
next: string | null
34+
depositAddress: {
35+
address: string
36+
addressId: string
37+
addressSignaturePayload: string
38+
addressID: string
39+
signature: string
5240
}
41+
isDefault: boolean
42+
networkId: string
43+
subaccountId: string
44+
type: string
45+
vaultId: string
46+
vaultName: string
47+
walletId: string
48+
walletName: string
5349
}
5450

5551
export class WalletTransport extends SubscriptionTransport<WalletTransportTypes> {
@@ -131,30 +127,15 @@ export class WalletTransport extends SubscriptionTransport<WalletTransportTypes>
131127
}
132128

133129
async fetchWallets(vaultId: string, coin: string, apiKey: string) {
134-
const wallets = []
135-
let hasNext = true
136-
const requestConfig = {
137-
baseURL: this.settings.API_ENDPOINT,
138-
url: `/v2/vaults/${vaultId}/wallets?limit=${this.settings.API_LIMIT}`,
139-
headers: {
140-
'Api-Access-Key': apiKey,
141-
},
142-
}
143-
144-
while (hasNext) {
145-
const reqKey = `${requestConfig.baseURL}${requestConfig.url}`
146-
const response = await this.requester.request<WalletResponse>(reqKey, requestConfig)
147-
wallets.push(
148-
...response.response.data.data.filter(
149-
(w) => w.networkId.toUpperCase() === coin.toUpperCase(),
150-
),
151-
)
152-
hasNext = response.response.data.page.next !== null
153-
if (response.response.data.page.next) {
154-
requestConfig.url = response.response.data.page.next
155-
}
156-
}
157-
return wallets
130+
const response = await request<WalletResponse>(
131+
this.requester,
132+
this.settings.API_ENDPOINT,
133+
`/v2/vaults/${vaultId}/wallets`,
134+
apiKey,
135+
this.settings.API_LIMIT,
136+
)
137+
138+
return response.filter((w) => w.networkId.toUpperCase() == coin.toUpperCase())
158139
}
159140

160141
getSubscriptionTtlFromConfig(adapterSettings: WalletTransportTypes['Settings']): number {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Requester } from '@chainlink/external-adapter-framework/util/requester'
2+
import { request } from '../../src/transport/requester'
3+
4+
describe('requester.ts', () => {
5+
let mockRequester: jest.Mocked<Requester>
6+
7+
beforeEach(() => {
8+
mockRequester = {
9+
request: jest.fn(),
10+
} as any
11+
})
12+
13+
describe('request function', () => {
14+
it('should return data from a single page', async () => {
15+
mockRequester.request.mockResolvedValueOnce({
16+
response: {
17+
data: {
18+
data: [
19+
{ id: 1, value: 'test1' },
20+
{ id: 2, value: 'test2' },
21+
],
22+
page: {
23+
next: null,
24+
},
25+
},
26+
},
27+
} as any)
28+
29+
const result = await request(mockRequester, 'url', 'path', 'key', 100)
30+
31+
expect(result).toEqual([
32+
{ id: 1, value: 'test1' },
33+
{ id: 2, value: 'test2' },
34+
])
35+
})
36+
37+
it('should handle pagination across multiple pages', async () => {
38+
mockRequester.request
39+
.mockResolvedValueOnce({
40+
response: {
41+
data: {
42+
data: [{ id: 1, value: 'page1' }],
43+
page: {
44+
next: 'next1',
45+
},
46+
},
47+
},
48+
} as any)
49+
.mockResolvedValueOnce({
50+
response: {
51+
data: {
52+
data: [{ id: 2, value: 'page2' }],
53+
page: {
54+
next: 'next2',
55+
},
56+
},
57+
},
58+
} as any)
59+
.mockResolvedValueOnce({
60+
response: {
61+
data: {
62+
data: [{ id: 3, value: 'page3' }],
63+
page: {
64+
next: null,
65+
},
66+
},
67+
},
68+
} as any)
69+
70+
const result = await request(mockRequester, 'url', 'path', 'key', 100)
71+
72+
expect(result).toEqual([
73+
{ id: 1, value: 'page1' },
74+
{ id: 2, value: 'page2' },
75+
{ id: 3, value: 'page3' },
76+
])
77+
})
78+
79+
it('should handle empty data array', async () => {
80+
mockRequester.request.mockResolvedValueOnce({
81+
response: {
82+
data: {
83+
data: [],
84+
page: {
85+
next: null,
86+
},
87+
},
88+
},
89+
} as any)
90+
91+
const result = await request(mockRequester, 'url', 'path', 'key', 100)
92+
93+
expect(result).toEqual([])
94+
})
95+
96+
it('should throw AdapterError when response is invalid', async () => {
97+
mockRequester.request.mockResolvedValueOnce(null as any)
98+
await expect(request(mockRequester, 'url', 'path', 'key', 100)).rejects.toThrow(
99+
'API urlpath?limit=100 does not return data',
100+
)
101+
102+
mockRequester.request.mockResolvedValueOnce({ response: null } as any)
103+
await expect(request(mockRequester, 'url', 'path', 'key', 100)).rejects.toThrow(
104+
'API urlpath?limit=100 does not return data',
105+
)
106+
107+
mockRequester.request.mockResolvedValueOnce({ response: { data: null } } as any)
108+
await expect(request(mockRequester, 'url', 'path', 'key', 100)).rejects.toThrow(
109+
'API urlpath?limit=100 does not return data',
110+
)
111+
})
112+
})
113+
})

0 commit comments

Comments
 (0)