@chainlink/external-adapter-framework 0.0.10 → 0.0.12

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