@chainlink/external-adapter-framework 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/.c8rc.json +3 -0
  2. package/.eslintignore +10 -0
  3. package/.eslintrc.js +96 -0
  4. package/.github/README.MD +42 -0
  5. package/.github/actions/setup/action.yaml +13 -0
  6. package/.github/workflows/label.yaml +39 -0
  7. package/.github/workflows/main.yaml +39 -0
  8. package/.github/workflows/publish.yaml +17 -0
  9. package/.prettierignore +13 -0
  10. package/.yarnrc +0 -0
  11. package/README.md +103 -0
  12. package/dist/examples/coingecko/test/e2e/adapter.test.ts.js +82953 -0
  13. package/dist/examples/coingecko/test/integration/adapter.test.ts.js +91672 -0
  14. package/dist/main.js +72703 -0
  15. package/docker-compose.yaml +35 -0
  16. package/env.sh +54 -0
  17. package/env2.sh +55 -0
  18. package/jest.config.js +5 -0
  19. package/package.json +14 -3
  20. package/publish.sh +0 -0
  21. package/src/adapter.ts +263 -0
  22. package/src/background-executor.ts +52 -0
  23. package/src/cache/factory.ts +26 -0
  24. package/src/cache/index.ts +258 -0
  25. package/src/cache/local.ts +73 -0
  26. package/src/cache/metrics.ts +112 -0
  27. package/src/cache/redis.ts +93 -0
  28. package/src/config/index.ts +517 -0
  29. package/src/config/provider-limits.ts +127 -0
  30. package/src/examples/bank-frick/README.MD +10 -0
  31. package/src/examples/bank-frick/accounts.ts +246 -0
  32. package/src/examples/bank-frick/config/index.ts +53 -0
  33. package/src/examples/bank-frick/index.ts +13 -0
  34. package/src/examples/bank-frick/types.d.ts +38 -0
  35. package/src/examples/bank-frick/util.ts +55 -0
  36. package/src/examples/coingecko/src/config/index.ts +12 -0
  37. package/src/examples/coingecko/src/config/overrides.json +10826 -0
  38. package/src/examples/coingecko/src/cryptoUtils.ts +88 -0
  39. package/src/examples/coingecko/src/endpoint/coins.ts +54 -0
  40. package/src/examples/coingecko/src/endpoint/crypto-marketcap.ts +66 -0
  41. package/src/examples/coingecko/src/endpoint/crypto-volume.ts +66 -0
  42. package/src/examples/coingecko/src/endpoint/crypto.ts +63 -0
  43. package/src/examples/coingecko/src/endpoint/dominance.ts +40 -0
  44. package/src/examples/coingecko/src/endpoint/global-marketcap.ts +40 -0
  45. package/src/examples/coingecko/src/endpoint/index.ts +6 -0
  46. package/src/examples/coingecko/src/globalUtils.ts +78 -0
  47. package/src/examples/coingecko/src/index.ts +17 -0
  48. package/src/examples/coingecko/test/e2e/adapter.test.ts +278 -0
  49. package/src/examples/coingecko/test/integration/__snapshots__/adapter.test.ts.snap +15 -0
  50. package/src/examples/coingecko/test/integration/adapter.test.ts +281 -0
  51. package/src/examples/coingecko/test/integration/capturedRequests.json +1 -0
  52. package/src/examples/coingecko/test/integration/fixtures.ts +42 -0
  53. package/src/examples/coingecko-old/batch-warming.ts +79 -0
  54. package/src/examples/coingecko-old/index.ts +9 -0
  55. package/src/examples/coingecko-old/rest.ts +77 -0
  56. package/src/examples/ncfx/config/index.ts +12 -0
  57. package/src/examples/ncfx/index.ts +9 -0
  58. package/src/examples/ncfx/websocket.ts +99 -0
  59. package/src/index.ts +149 -0
  60. package/src/metrics/constants.ts +23 -0
  61. package/src/metrics/index.ts +115 -0
  62. package/src/metrics/util.ts +18 -0
  63. package/src/rate-limiting/background/fixed-frequency.ts +45 -0
  64. package/src/rate-limiting/index.ts +100 -0
  65. package/src/rate-limiting/metrics.ts +18 -0
  66. package/src/rate-limiting/request/simple-counting.ts +76 -0
  67. package/src/transports/batch-warming.ts +127 -0
  68. package/src/transports/index.ts +152 -0
  69. package/src/transports/metrics.ts +95 -0
  70. package/src/transports/rest.ts +168 -0
  71. package/src/transports/util.ts +63 -0
  72. package/src/transports/websocket.ts +245 -0
  73. package/src/util/index.ts +23 -0
  74. package/src/util/logger.ts +69 -0
  75. package/src/util/recordRequests.ts +47 -0
  76. package/src/util/request.ts +117 -0
  77. package/src/util/subscription-set/expiring-sorted-set.ts +54 -0
  78. package/src/util/subscription-set/subscription-set.ts +35 -0
  79. package/src/util/test-payload-loader.ts +87 -0
  80. package/src/validation/error.ts +116 -0
  81. package/src/validation/index.ts +110 -0
  82. package/src/validation/input-params.ts +45 -0
  83. package/src/validation/override-functions.ts +44 -0
  84. package/src/validation/overrideFunctions.ts +44 -0
  85. package/src/validation/preset-tokens.json +23 -0
  86. package/src/validation/validator.ts +384 -0
  87. package/test/adapter.test.ts +27 -0
  88. package/test/background-executor.test.ts +108 -0
  89. package/test/cache/cache-key.test.ts +114 -0
  90. package/test/cache/helper.ts +100 -0
  91. package/test/cache/local.test.ts +54 -0
  92. package/test/cache/redis.test.ts +89 -0
  93. package/test/correlation.test.ts +114 -0
  94. package/test/index.test.ts +37 -0
  95. package/test/metrics/feed-id.test.ts +38 -0
  96. package/test/metrics/helper.ts +14 -0
  97. package/test/metrics/labels.test.ts +36 -0
  98. package/test/metrics/metrics.test.ts +267 -0
  99. package/test/metrics/redis-metrics.test.ts +113 -0
  100. package/test/metrics/warmer-metrics.test.ts +193 -0
  101. package/test/metrics/ws-metrics.test.ts +225 -0
  102. package/test/rate-limit-config.test.ts +242 -0
  103. package/test/smoke/smoke.test.ts +166 -0
  104. package/test/smoke/test-payload-fail.json +3 -0
  105. package/test/smoke/test-payload.js +22 -0
  106. package/test/smoke/test-payload.json +7 -0
  107. package/test/transports/batch.test.ts +466 -0
  108. package/test/transports/rest.test.ts +242 -0
  109. package/test/transports/websocket.test.ts +183 -0
  110. package/test/tsconfig.json +5 -0
  111. package/test/util.ts +77 -0
  112. package/test/validation.test.ts +178 -0
  113. package/test.sh +20 -0
  114. package/test2.sh +2 -0
  115. package/tsconfig.json +28 -0
  116. package/typedoc.json +6 -0
  117. package/webpack.config.js +57 -0
  118. package/yarn-error.log +3778 -0
  119. package/adapter.d.ts +0 -107
  120. package/adapter.js +0 -115
  121. package/background-executor.d.ts +0 -11
  122. package/background-executor.js +0 -45
  123. package/cache/factory.d.ts +0 -6
  124. package/cache/factory.js +0 -55
  125. package/cache/index.d.ts +0 -94
  126. package/cache/index.js +0 -173
  127. package/cache/local.d.ts +0 -23
  128. package/cache/local.js +0 -83
  129. package/cache/metrics.d.ts +0 -27
  130. package/cache/metrics.js +0 -120
  131. package/cache/redis.d.ts +0 -16
  132. package/cache/redis.js +0 -100
  133. package/chainlink-external-adapter-framework-0.0.6.tgz +0 -0
  134. package/config/index.d.ts +0 -209
  135. package/config/index.js +0 -380
  136. package/config/provider-limits.d.ts +0 -31
  137. package/config/provider-limits.js +0 -79
  138. package/examples/bank-frick/accounts.d.ts +0 -39
  139. package/examples/bank-frick/accounts.js +0 -191
  140. package/examples/bank-frick/config/index.d.ts +0 -4
  141. package/examples/bank-frick/config/index.js +0 -54
  142. package/examples/bank-frick/index.d.ts +0 -2
  143. package/examples/bank-frick/index.js +0 -14
  144. package/examples/bank-frick/util.d.ts +0 -4
  145. package/examples/bank-frick/util.js +0 -39
  146. package/examples/coingecko/batch-warming.d.ts +0 -2
  147. package/examples/coingecko/batch-warming.js +0 -52
  148. package/examples/coingecko/index.d.ts +0 -2
  149. package/examples/coingecko/index.js +0 -10
  150. package/examples/coingecko/rest.d.ts +0 -2
  151. package/examples/coingecko/rest.js +0 -50
  152. package/examples/ncfx/config/index.d.ts +0 -12
  153. package/examples/ncfx/config/index.js +0 -15
  154. package/examples/ncfx/index.d.ts +0 -2
  155. package/examples/ncfx/index.js +0 -10
  156. package/examples/ncfx/websocket.d.ts +0 -36
  157. package/examples/ncfx/websocket.js +0 -72
  158. package/index.d.ts +0 -11
  159. package/index.js +0 -133
  160. package/metrics/constants.d.ts +0 -16
  161. package/metrics/constants.js +0 -25
  162. package/metrics/index.d.ts +0 -15
  163. package/metrics/index.js +0 -122
  164. package/metrics/util.d.ts +0 -7
  165. package/metrics/util.js +0 -9
  166. package/package/adapter.d.ts +0 -88
  167. package/package/adapter.js +0 -112
  168. package/package/background-executor.d.ts +0 -11
  169. package/package/background-executor.js +0 -45
  170. package/package/cache/factory.d.ts +0 -6
  171. package/package/cache/factory.js +0 -57
  172. package/package/cache/index.d.ts +0 -90
  173. package/package/cache/index.js +0 -169
  174. package/package/cache/local.d.ts +0 -23
  175. package/package/cache/local.js +0 -83
  176. package/package/cache/metrics.d.ts +0 -27
  177. package/package/cache/metrics.js +0 -120
  178. package/package/cache/redis.d.ts +0 -16
  179. package/package/cache/redis.js +0 -100
  180. package/package/config/index.d.ts +0 -195
  181. package/package/config/index.js +0 -365
  182. package/package/config/provider-limits.d.ts +0 -31
  183. package/package/config/provider-limits.js +0 -76
  184. package/package/examples/coingecko/batch-warming.d.ts +0 -2
  185. package/package/examples/coingecko/batch-warming.js +0 -52
  186. package/package/examples/coingecko/index.d.ts +0 -2
  187. package/package/examples/coingecko/index.js +0 -10
  188. package/package/examples/coingecko/rest.d.ts +0 -2
  189. package/package/examples/coingecko/rest.js +0 -50
  190. package/package/examples/ncfx/config/index.d.ts +0 -12
  191. package/package/examples/ncfx/config/index.js +0 -15
  192. package/package/examples/ncfx/index.d.ts +0 -2
  193. package/package/examples/ncfx/index.js +0 -10
  194. package/package/examples/ncfx/websocket.d.ts +0 -36
  195. package/package/examples/ncfx/websocket.js +0 -72
  196. package/package/index.d.ts +0 -12
  197. package/package/index.js +0 -92
  198. package/package/metrics/constants.d.ts +0 -16
  199. package/package/metrics/constants.js +0 -25
  200. package/package/metrics/index.d.ts +0 -15
  201. package/package/metrics/index.js +0 -123
  202. package/package/metrics/util.d.ts +0 -3
  203. package/package/metrics/util.js +0 -9
  204. package/package/package.json +0 -72
  205. package/package/rate-limiting/background/fixed-frequency.d.ts +0 -10
  206. package/package/rate-limiting/background/fixed-frequency.js +0 -37
  207. package/package/rate-limiting/index.d.ts +0 -54
  208. package/package/rate-limiting/index.js +0 -63
  209. package/package/rate-limiting/metrics.d.ts +0 -3
  210. package/package/rate-limiting/metrics.js +0 -44
  211. package/package/rate-limiting/request/simple-counting.d.ts +0 -20
  212. package/package/rate-limiting/request/simple-counting.js +0 -62
  213. package/package/test.d.ts +0 -1
  214. package/package/test.js +0 -6
  215. package/package/transports/batch-warming.d.ts +0 -34
  216. package/package/transports/batch-warming.js +0 -101
  217. package/package/transports/index.d.ts +0 -87
  218. package/package/transports/index.js +0 -87
  219. package/package/transports/metrics.d.ts +0 -21
  220. package/package/transports/metrics.js +0 -105
  221. package/package/transports/rest.d.ts +0 -43
  222. package/package/transports/rest.js +0 -129
  223. package/package/transports/util.d.ts +0 -8
  224. package/package/transports/util.js +0 -85
  225. package/package/transports/websocket.d.ts +0 -80
  226. package/package/transports/websocket.js +0 -169
  227. package/package/util/expiring-sorted-set.d.ts +0 -21
  228. package/package/util/expiring-sorted-set.js +0 -47
  229. package/package/util/index.d.ts +0 -11
  230. package/package/util/index.js +0 -35
  231. package/package/util/logger.d.ts +0 -42
  232. package/package/util/logger.js +0 -62
  233. package/package/util/request.d.ts +0 -55
  234. package/package/util/request.js +0 -2
  235. package/package/validation/error.d.ts +0 -50
  236. package/package/validation/error.js +0 -79
  237. package/package/validation/index.d.ts +0 -5
  238. package/package/validation/index.js +0 -86
  239. package/package/validation/input-params.d.ts +0 -15
  240. package/package/validation/input-params.js +0 -30
  241. package/package/validation/override-functions.d.ts +0 -3
  242. package/package/validation/override-functions.js +0 -40
  243. package/package/validation/preset-tokens.json +0 -23
  244. package/package/validation/validator.d.ts +0 -47
  245. package/package/validation/validator.js +0 -303
  246. package/rate-limiting/background/fixed-frequency.d.ts +0 -10
  247. package/rate-limiting/background/fixed-frequency.js +0 -35
  248. package/rate-limiting/index.d.ts +0 -54
  249. package/rate-limiting/index.js +0 -63
  250. package/rate-limiting/metrics.d.ts +0 -3
  251. package/rate-limiting/metrics.js +0 -44
  252. package/rate-limiting/request/simple-counting.d.ts +0 -20
  253. package/rate-limiting/request/simple-counting.js +0 -62
  254. package/test.d.ts +0 -1
  255. package/test.js +0 -6
  256. package/transports/batch-warming.d.ts +0 -35
  257. package/transports/batch-warming.js +0 -101
  258. package/transports/index.d.ts +0 -70
  259. package/transports/index.js +0 -87
  260. package/transports/metrics.d.ts +0 -21
  261. package/transports/metrics.js +0 -105
  262. package/transports/rest.d.ts +0 -44
  263. package/transports/rest.js +0 -131
  264. package/transports/util.d.ts +0 -8
  265. package/transports/util.js +0 -85
  266. package/transports/websocket.d.ts +0 -81
  267. package/transports/websocket.js +0 -168
  268. package/util/expiring-sorted-set.d.ts +0 -21
  269. package/util/expiring-sorted-set.js +0 -47
  270. package/util/index.d.ts +0 -12
  271. package/util/index.js +0 -35
  272. package/util/logger.d.ts +0 -42
  273. package/util/logger.js +0 -62
  274. package/util/request.d.ts +0 -57
  275. package/util/request.js +0 -2
  276. package/util/subscription-set/expiring-sorted-set.d.ts +0 -22
  277. package/util/subscription-set/expiring-sorted-set.js +0 -47
  278. package/util/subscription-set/subscription-set.d.ts +0 -18
  279. package/util/subscription-set/subscription-set.js +0 -19
  280. package/util/test-payload-loader.d.ts +0 -25
  281. package/util/test-payload-loader.js +0 -83
  282. package/validation/error.d.ts +0 -50
  283. package/validation/error.js +0 -79
  284. package/validation/index.d.ts +0 -5
  285. package/validation/index.js +0 -91
  286. package/validation/input-params.d.ts +0 -15
  287. package/validation/input-params.js +0 -30
  288. package/validation/override-functions.d.ts +0 -3
  289. package/validation/override-functions.js +0 -40
  290. package/validation/preset-tokens.json +0 -23
  291. package/validation/validator.d.ts +0 -47
  292. package/validation/validator.js +0 -303
@@ -0,0 +1,466 @@
1
+ import FakeTimers, { InstalledClock } from '@sinonjs/fake-timers'
2
+ import untypedTest, { TestFn } from 'ava'
3
+ import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
4
+ import { AddressInfo } from 'net'
5
+ import nock from 'nock'
6
+ import { expose } from '../../src'
7
+ import { Adapter, AdapterContext } from '../../src/adapter'
8
+ import { SettingsMap } from '../../src/config'
9
+ import { DEFAULT_SHARED_MS_BETWEEN_REQUESTS } from '../../src/rate-limiting'
10
+ import { BatchWarmingTransport } from '../../src/transports'
11
+ import { ProviderResult } from '../../src/util'
12
+ import { MockCache, runAllUntilTime } from '../util'
13
+
14
+ const test = untypedTest as TestFn<{
15
+ clock: InstalledClock
16
+ }>
17
+
18
+ const URL = 'http://test-url.com'
19
+ const endpoint = '/price'
20
+
21
+ interface AdapterRequestParams {
22
+ from: string
23
+ to: string
24
+ }
25
+
26
+ interface ProviderRequestBody {
27
+ pairs: Array<{
28
+ base: string
29
+ quote: string
30
+ }>
31
+ }
32
+
33
+ interface ProviderResponseBody {
34
+ prices: Array<{
35
+ pair: string
36
+ price: number
37
+ }>
38
+ }
39
+
40
+ test.before(() => {
41
+ nock.disableNetConnect()
42
+ nock.enableNetConnect('localhost')
43
+ })
44
+
45
+ test.after(() => {
46
+ nock.restore()
47
+ })
48
+
49
+ test.beforeEach((t) => {
50
+ t.context.clock = FakeTimers.install({ shouldAdvanceTime: true, advanceTimeDelta: 100 })
51
+ })
52
+
53
+ test.afterEach((t) => {
54
+ t.context.clock.uninstall()
55
+ })
56
+
57
+ class MockBatchWarmingTransport extends BatchWarmingTransport<
58
+ AdapterRequestParams,
59
+ ProviderRequestBody,
60
+ ProviderResponseBody,
61
+ SettingsMap
62
+ > {
63
+ backgroundExecuteCalls = 0
64
+
65
+ constructor(private callSuper = false) {
66
+ super({
67
+ prepareRequest: (params: AdapterRequestParams[]): AxiosRequestConfig<ProviderRequestBody> => {
68
+ return {
69
+ baseURL: URL,
70
+ url: '/price',
71
+ method: 'POST',
72
+ data: {
73
+ pairs: params.map((p) => ({ base: p.from, quote: p.to })),
74
+ },
75
+ }
76
+ },
77
+ parseResponse: (
78
+ params: AdapterRequestParams[],
79
+ res: AxiosResponse<ProviderResponseBody>,
80
+ ): ProviderResult<AdapterRequestParams>[] => {
81
+ return res.data.prices.map((p) => {
82
+ const [from, to] = p.pair.split('/')
83
+ return {
84
+ params: { from, to },
85
+ value: p.price,
86
+ }
87
+ })
88
+ },
89
+ })
90
+ }
91
+
92
+ override async backgroundExecute(context: AdapterContext<SettingsMap>): Promise<number> {
93
+ this.backgroundExecuteCalls++
94
+ if (this.callSuper) {
95
+ super.backgroundExecute(context)
96
+ }
97
+ return this.rateLimiter.msUntilNextExecution(context.adapterEndpoint.name)
98
+ }
99
+ }
100
+
101
+ // Disable retries to make the testing flow easier
102
+ process.env['CACHE_POLLING_MAX_RETRIES'] = '0'
103
+
104
+ const from = 'ETH'
105
+ const to = 'USD'
106
+ const price = 1234
107
+
108
+ nock(URL)
109
+ .post(endpoint, {
110
+ pairs: [
111
+ {
112
+ base: from,
113
+ quote: to,
114
+ },
115
+ ],
116
+ })
117
+ .reply(200, {
118
+ prices: [
119
+ {
120
+ pair: `${from}/${to}`,
121
+ price,
122
+ },
123
+ ],
124
+ })
125
+ .persist()
126
+
127
+ const inputParameters = {
128
+ from: {
129
+ type: 'string',
130
+ required: true,
131
+ },
132
+ to: {
133
+ type: 'string',
134
+ required: true,
135
+ },
136
+ } as const
137
+
138
+ test.serial('sends request to DP and returns response', async (t) => {
139
+ const adapter: Adapter = {
140
+ name: 'test',
141
+ defaultEndpoint: 'test',
142
+ endpoints: [
143
+ {
144
+ name: 'test',
145
+ inputParameters,
146
+ transport: new MockBatchWarmingTransport(true),
147
+ },
148
+ ],
149
+ }
150
+
151
+ // Create mocked cache so we can listen when values are set
152
+ // This is a more reliable method than expecting precise clock timings
153
+ const mockCache = new MockCache()
154
+
155
+ // Start the adapter
156
+ const server = await expose(adapter, {
157
+ cache: mockCache,
158
+ })
159
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
160
+
161
+ const makeRequest = () =>
162
+ axios.post(address, {
163
+ data: {
164
+ from,
165
+ to,
166
+ },
167
+ })
168
+
169
+ // Expect the first response to time out
170
+ // The polling behavior is tested in the cache tests, so this is easier here.
171
+ // Start the request:
172
+ const errorPromise: Promise<AxiosError | undefined> = t.throwsAsync(makeRequest)
173
+ // Advance enough time for the initial request async flow
174
+ t.context.clock.tickAsync(10)
175
+ // Wait for the failed cache get -> instant 504
176
+ const error = await errorPromise
177
+ t.is(error?.response?.status, 504)
178
+
179
+ // Advance clock so that the batch warmer executes once again and wait for the cache to be set
180
+ const cacheValueSetPromise = mockCache.waitForNextSet()
181
+ await t.context.clock.tickAsync(DEFAULT_SHARED_MS_BETWEEN_REQUESTS + 10)
182
+ await cacheValueSetPromise
183
+
184
+ // Second request should find the response in the cache
185
+ const response = await makeRequest()
186
+
187
+ t.is(response.status, 200)
188
+ t.deepEqual(response.data, {
189
+ data: null,
190
+ result: price,
191
+ statusCode: 200,
192
+ })
193
+ })
194
+
195
+ test.serial(
196
+ 'per minute rate limit of 4 with one batch transport results in a call every 15s',
197
+ async (t) => {
198
+ const rateLimit1m = 4
199
+ const transport = new MockBatchWarmingTransport()
200
+
201
+ const adapter: Adapter = {
202
+ name: 'test',
203
+ defaultEndpoint: 'test',
204
+ endpoints: [
205
+ {
206
+ name: 'test',
207
+ inputParameters,
208
+ transport: transport,
209
+ },
210
+ ],
211
+ rateLimiting: {
212
+ tiers: {
213
+ default: {
214
+ rateLimit1m,
215
+ },
216
+ },
217
+ },
218
+ }
219
+
220
+ // Start the adapter
221
+ const server = await expose(adapter)
222
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
223
+
224
+ const makeRequest = () =>
225
+ axios.post(address, {
226
+ data: {
227
+ from,
228
+ to,
229
+ },
230
+ })
231
+
232
+ // Expect the first response to time out
233
+ // The polling behavior is tested in the cache tests, so this is easier here.
234
+ // Start the request:
235
+ const errorPromise: Promise<AxiosError | undefined> = t.throwsAsync(makeRequest)
236
+ // Advance enough time for the initial request async flow
237
+ t.context.clock.tickAsync(10)
238
+ // Wait for the failed cache get -> instant 504
239
+ const error = await errorPromise
240
+ t.is(error?.response?.status, 504)
241
+
242
+ // Wait for the first background execute and check that it's been called
243
+ await t.context.clock.tickAsync(10)
244
+ t.is(transport.backgroundExecuteCalls, 1)
245
+
246
+ // Advance the clock a few minutes and check that the amount of calls is as expected
247
+ // +1 because of the previous first
248
+ await t.context.clock.tickAsync(5 * 60 * 1000) // 5m
249
+ t.is(transport.backgroundExecuteCalls, 5 * rateLimit1m + 1)
250
+ },
251
+ )
252
+
253
+ test.serial(
254
+ 'per second limit of 1 with one batch transport results in a call every 1000ms',
255
+ async (t) => {
256
+ const rateLimit1s = 1
257
+ const transport = new MockBatchWarmingTransport()
258
+
259
+ const adapter: Adapter = {
260
+ name: 'test',
261
+ defaultEndpoint: 'test',
262
+ endpoints: [
263
+ {
264
+ name: 'test',
265
+ inputParameters,
266
+ transport: transport,
267
+ },
268
+ ],
269
+ rateLimiting: {
270
+ tiers: {
271
+ default: {
272
+ rateLimit1s,
273
+ },
274
+ },
275
+ },
276
+ envDefaultOverrides: {
277
+ WARMUP_SUBSCRIPTION_TTL: 100000,
278
+ },
279
+ }
280
+
281
+ // Start the adapter
282
+ const server = await expose(adapter)
283
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
284
+
285
+ const makeRequest = () =>
286
+ axios.post(address, {
287
+ data: {
288
+ from,
289
+ to,
290
+ },
291
+ })
292
+
293
+ // Expect the first response to time out
294
+ // The polling behavior is tested in the cache tests, so this is easier here.
295
+ // Start the request:
296
+ const errorPromise: Promise<AxiosError | undefined> = t.throwsAsync(makeRequest)
297
+ // Advance enough time for the initial request async flow
298
+ t.context.clock.tickAsync(10)
299
+ // Wait for the failed cache get -> instant 504
300
+ const error = await errorPromise
301
+ t.is(error?.response?.status, 504)
302
+
303
+ // Wait for the first background execute and check that it's been called
304
+ await t.context.clock.tickAsync(10)
305
+ t.is(transport.backgroundExecuteCalls, 1)
306
+
307
+ // Run for an entire minute and check that the values are as expected
308
+ await runAllUntilTime(t.context.clock, 59 * 1000)
309
+
310
+ t.is(transport.backgroundExecuteCalls, 60 * rateLimit1s + 1)
311
+ },
312
+ )
313
+
314
+ test.serial(
315
+ 'per second limit of 1 with two batch transports results in a call every 2000ms for each',
316
+ async (t) => {
317
+ const rateLimit1s = 1
318
+ const transportA = new MockBatchWarmingTransport()
319
+ const transportB = new MockBatchWarmingTransport()
320
+
321
+ const adapter: Adapter = {
322
+ name: 'test',
323
+ endpoints: [
324
+ {
325
+ name: 'A',
326
+ inputParameters,
327
+ transport: transportA,
328
+ },
329
+ {
330
+ name: 'B',
331
+ inputParameters,
332
+ transport: transportB,
333
+ },
334
+ ],
335
+ rateLimiting: {
336
+ tiers: {
337
+ default: {
338
+ rateLimit1s,
339
+ },
340
+ },
341
+ },
342
+ envDefaultOverrides: {
343
+ WARMUP_SUBSCRIPTION_TTL: 100000,
344
+ },
345
+ }
346
+
347
+ // Start the adapter
348
+ const server = await expose(adapter)
349
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
350
+
351
+ const makeRequest = (endpointParam: string) =>
352
+ axios.post(address, {
353
+ data: {
354
+ from,
355
+ to,
356
+ },
357
+ endpoint: endpointParam,
358
+ })
359
+
360
+ // Expect the first response to time out
361
+ // The polling behavior is tested in the cache tests, so this is easier here.
362
+ // Start the request:
363
+ const errorPromiseA: Promise<AxiosError | undefined> = t.throwsAsync(() => makeRequest('A'))
364
+ // Advance enough time for the initial request async flow
365
+ t.context.clock.tickAsync(10)
366
+ // Wait for the failed cache get -> instant 504
367
+ const errorA = await errorPromiseA
368
+ t.is(errorA?.response?.status, 504)
369
+
370
+ // Do the same thing for transport B
371
+ const errorPromiseB: Promise<AxiosError | undefined> = t.throwsAsync(() => makeRequest('B'))
372
+ t.context.clock.tickAsync(10)
373
+ const errorB = await errorPromiseB
374
+ t.is(errorB?.response?.status, 504)
375
+
376
+ // Wait for the first background executes and check that they've been called
377
+ await t.context.clock.tickAsync(10)
378
+ t.is(transportA.backgroundExecuteCalls, 1)
379
+ t.is(transportB.backgroundExecuteCalls, 1)
380
+
381
+ // Run for a minute (59s actually, it'll start at 0 and go on regular intervals)
382
+ await runAllUntilTime(t.context.clock, 59 * 1000 + 10)
383
+
384
+ t.is(transportA.backgroundExecuteCalls, 30 * rateLimit1s + 1) // +1 for the first call
385
+ t.is(transportB.backgroundExecuteCalls, 30 * rateLimit1s)
386
+ },
387
+ )
388
+
389
+ test.serial(
390
+ 'per second limit of 1 with two batch transports with different allocations results in correct time distribution',
391
+ async (t) => {
392
+ const rateLimit1s = 1
393
+ const transportA = new MockBatchWarmingTransport()
394
+ const transportB = new MockBatchWarmingTransport()
395
+
396
+ const adapter: Adapter = {
397
+ name: 'test',
398
+ endpoints: [
399
+ {
400
+ name: 'A',
401
+ inputParameters,
402
+ transport: transportA,
403
+ rateLimiting: {
404
+ allocationPercentage: 75,
405
+ },
406
+ },
407
+ {
408
+ // This one should be dynamically allocated
409
+ name: 'B',
410
+ inputParameters,
411
+ transport: transportB,
412
+ },
413
+ ],
414
+ rateLimiting: {
415
+ tiers: {
416
+ default: {
417
+ rateLimit1s,
418
+ },
419
+ },
420
+ },
421
+ envDefaultOverrides: {
422
+ WARMUP_SUBSCRIPTION_TTL: 100000,
423
+ },
424
+ }
425
+
426
+ // Start the adapter
427
+ const server = await expose(adapter)
428
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
429
+
430
+ const makeRequest = (endpointParam: string) =>
431
+ axios.post(address, {
432
+ data: {
433
+ from,
434
+ to,
435
+ },
436
+ endpoint: endpointParam,
437
+ })
438
+
439
+ // Expect the first response to time out
440
+ // The polling behavior is tested in the cache tests, so this is easier here.
441
+ // Start the request:
442
+ const errorPromiseA: Promise<AxiosError | undefined> = t.throwsAsync(() => makeRequest('A'))
443
+ // Advance enough time for the initial request async flow
444
+ t.context.clock.tickAsync(10)
445
+ // Wait for the failed cache get -> instant 504
446
+ const errorA = await errorPromiseA
447
+ t.is(errorA?.response?.status, 504)
448
+
449
+ // Do the same thing for transport B
450
+ const errorPromiseB: Promise<AxiosError | undefined> = t.throwsAsync(() => makeRequest('B'))
451
+ t.context.clock.tickAsync(10)
452
+ const errorB = await errorPromiseB
453
+ t.is(errorB?.response?.status, 504)
454
+
455
+ // Wait for the first background executes and check that they've been called
456
+ await t.context.clock.tickAsync(10)
457
+ t.is(transportA.backgroundExecuteCalls, 1)
458
+ t.is(transportB.backgroundExecuteCalls, 1)
459
+
460
+ // Run for a minute (59s actually, it'll start at 0 and go on regular intervals)
461
+ await runAllUntilTime(t.context.clock, 59 * 1000 + 10)
462
+
463
+ t.is(transportA.backgroundExecuteCalls, 45 * rateLimit1s + 1) // +1 for the first call
464
+ t.is(transportB.backgroundExecuteCalls, 15 * rateLimit1s)
465
+ },
466
+ )
@@ -0,0 +1,242 @@
1
+ import untypedTest, { TestFn } from 'ava'
2
+ import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
3
+ import { AddressInfo } from 'net'
4
+ import nock from 'nock'
5
+ import { expose } from '../../src'
6
+ import { Adapter, AdapterEndpoint } from '../../src/adapter'
7
+ import { EmptySettings } from '../../src/config'
8
+ import { RestTransport } from '../../src/transports'
9
+ import { AdapterRequest, AdapterResponse, sleep } from '../../src/util'
10
+ import { deferredPromise } from '../util'
11
+
12
+ const test = untypedTest as TestFn<{
13
+ serverAddress: string
14
+ }>
15
+
16
+ const URL = 'http://test-url.com'
17
+
18
+ test.before(() => {
19
+ nock.disableNetConnect()
20
+ nock.enableNetConnect('localhost')
21
+ })
22
+
23
+ test.after(() => {
24
+ nock.restore()
25
+ })
26
+
27
+ interface AdapterRequestParams {
28
+ from: string
29
+ to: string
30
+ }
31
+
32
+ interface ProviderRequestBody {
33
+ base: string
34
+ quote: string
35
+ }
36
+
37
+ interface ProviderResponseBody {
38
+ price: number
39
+ }
40
+
41
+ const endpoint = '/price'
42
+
43
+ const createAdapterEndpoint = (): AdapterEndpoint => {
44
+ const restEndpointTransport = new RestTransport<
45
+ AdapterRequestParams,
46
+ ProviderRequestBody,
47
+ ProviderResponseBody,
48
+ EmptySettings
49
+ >({
50
+ prepareRequest: (
51
+ req: AdapterRequest<AdapterRequestParams>,
52
+ ): AxiosRequestConfig<ProviderRequestBody> => {
53
+ return {
54
+ baseURL: URL,
55
+ url: endpoint,
56
+ method: 'GET',
57
+ params: {
58
+ base: req.requestContext.data.from,
59
+ quote: req.requestContext.data.to,
60
+ },
61
+ }
62
+ },
63
+ parseResponse: (
64
+ req: AdapterRequest<AdapterRequestParams>,
65
+ res: AxiosResponse<ProviderResponseBody>,
66
+ ): AdapterResponse<ProviderResponseBody> => {
67
+ return {
68
+ data: res.data,
69
+ statusCode: 200,
70
+ result: res.data.price,
71
+ }
72
+ },
73
+ options: {
74
+ coalescing: true,
75
+ },
76
+ })
77
+
78
+ return {
79
+ name: 'test',
80
+ inputParameters: {
81
+ from: {
82
+ type: 'string',
83
+ required: true,
84
+ },
85
+ to: {
86
+ type: 'string',
87
+ required: true,
88
+ },
89
+ },
90
+ transport: restEndpointTransport,
91
+ }
92
+ }
93
+
94
+ const from = 'ETH'
95
+ const to = 'USD'
96
+ const price = 1234
97
+
98
+ test('sends request to DP and returns response', async (t) => {
99
+ const adapter: Adapter = {
100
+ name: 'test',
101
+ defaultEndpoint: 'test',
102
+ endpoints: [createAdapterEndpoint()],
103
+ }
104
+
105
+ const server = await expose(adapter)
106
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
107
+
108
+ nock(URL)
109
+ .get(endpoint)
110
+ .query({
111
+ base: from,
112
+ quote: to,
113
+ })
114
+ .reply(200, {
115
+ price,
116
+ })
117
+
118
+ const response = await axios.post(address, {
119
+ data: {
120
+ from,
121
+ to,
122
+ },
123
+ })
124
+
125
+ t.is(response.status, 200)
126
+ t.deepEqual(response.data, {
127
+ data: { price },
128
+ result: price,
129
+ statusCode: 200,
130
+ })
131
+ })
132
+
133
+ test('identical request to EA is coalesced and returned from cache', async (t) => {
134
+ const adapter: Adapter = {
135
+ name: 'test',
136
+ defaultEndpoint: 'test',
137
+ endpoints: [createAdapterEndpoint()],
138
+ }
139
+
140
+ const server = await expose(adapter)
141
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
142
+
143
+ const [promise, resolve] = deferredPromise<ProviderResponseBody>()
144
+ nock(URL)
145
+ .get(endpoint)
146
+ .query({
147
+ base: from,
148
+ quote: to,
149
+ })
150
+ .reply(200, () => promise)
151
+
152
+ // Send first request, that will be blocked until we resolve the promise
153
+ const request1 = axios.post(address, {
154
+ data: {
155
+ from,
156
+ to,
157
+ },
158
+ })
159
+ await sleep(1) // To ensure all inner ops move along
160
+
161
+ // Send second request, and wait for response.
162
+ // This one will be coalesced with the first one, and made to wait until the cache is filled.
163
+ // We know it's not going to the DP twice, because Nock would block it.
164
+ const request2 = axios.post(address, {
165
+ data: {
166
+ from,
167
+ to,
168
+ },
169
+ })
170
+ await sleep(1) // To ensure all inner ops move along
171
+
172
+ // Resolve the promise
173
+ resolve({
174
+ price,
175
+ })
176
+
177
+ const responses = await Promise.all([request1, request2])
178
+
179
+ for (const response of responses) {
180
+ t.is(response.status, 200)
181
+ t.deepEqual(response.data, {
182
+ data: { price },
183
+ result: price,
184
+ statusCode: 200,
185
+ })
186
+ }
187
+ })
188
+
189
+ test('rate limits (per second) incoming request (without retries)', async (t) => {
190
+ const adapter: Adapter = {
191
+ name: 'test',
192
+ defaultEndpoint: 'test',
193
+ endpoints: [createAdapterEndpoint()],
194
+ rateLimiting: {
195
+ tiers: {
196
+ base: {
197
+ rateLimit1m: 1,
198
+ },
199
+ },
200
+ },
201
+ envDefaultOverrides: {
202
+ REST_TRANSPORT_MAX_RATE_LIMIT_RETRIES: 0,
203
+ },
204
+ }
205
+
206
+ const server = await expose(adapter)
207
+ const address = `http://localhost:${(server?.address() as AddressInfo)?.port}`
208
+
209
+ const payloads = [
210
+ {
211
+ base: 'ETH',
212
+ quote: 'BTC',
213
+ },
214
+ {
215
+ base: 'ETH',
216
+ quote: 'USD',
217
+ },
218
+ ]
219
+
220
+ payloads.forEach((p) => {
221
+ nock(URL)
222
+ .get(endpoint)
223
+ .query(p)
224
+ .reply(200, {
225
+ price,
226
+ })
227
+ .persist()
228
+ })
229
+
230
+ const makeRequest = ({ base, quote }: { base: string; quote: string }) =>
231
+ axios.post(address, {
232
+ data: {
233
+ from: base,
234
+ to: quote,
235
+ },
236
+ })
237
+
238
+ const error: AxiosError | undefined = await t.throwsAsync(() =>
239
+ Promise.all(payloads.map((p) => makeRequest(p))),
240
+ )
241
+ t.is(error?.response?.status, 504)
242
+ })