@bitpay-labs/bitcore-wallet-service 11.6.0 → 11.6.3
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.
- package/package.json +2 -2
- package/src/externalservices/coingecko.ts +448 -368
- package/src/lib/common/utils.ts +37 -32
- package/src/lib/pushnotificationsservice.ts +12 -12
- package/src/lib/tss.ts +18 -10
- package/test/integration/externalservices/coinGecko.test.ts +811 -156
- package/test/integration/pushNotifications.test.ts +6 -6
- package/test/utils.test.ts +5 -0
- package/ts_build/package.json +3 -3
- package/ts_build/src/externalservices/coingecko.d.ts +24 -20
- package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
- package/ts_build/src/externalservices/coingecko.js +356 -306
- package/ts_build/src/externalservices/coingecko.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +2 -2
- package/ts_build/src/lib/common/utils.d.ts +2 -2
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +28 -29
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +2 -10
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.d.ts +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.js +710 -149
- package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
- package/ts_build/test/utils.test.js +4 -0
- package/ts_build/test/utils.test.js.map +1 -1
|
@@ -37,65 +37,207 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
const chai = __importStar(require("chai"));
|
|
40
|
-
require("
|
|
40
|
+
const sinon_1 = __importDefault(require("sinon"));
|
|
41
41
|
const util_1 = __importDefault(require("util"));
|
|
42
42
|
const server_1 = require("../../../src/lib/server");
|
|
43
43
|
const TestData = __importStar(require("../../testdata"));
|
|
44
44
|
const helpers_1 = __importDefault(require("../helpers"));
|
|
45
45
|
const config_1 = __importDefault(require("../../../src/config"));
|
|
46
|
+
const logger_1 = __importDefault(require("../../../src/lib/logger"));
|
|
46
47
|
const should = chai.should();
|
|
48
|
+
function toURL(url) {
|
|
49
|
+
try {
|
|
50
|
+
return new URL(url);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return new URL(url, 'https://example.com');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseRequestedIdsFromMarketsUrl(url) {
|
|
57
|
+
const u = toURL(url);
|
|
58
|
+
const ids = u.searchParams.get('ids');
|
|
59
|
+
return ids ? ids.split(',').filter(Boolean) : [];
|
|
60
|
+
}
|
|
61
|
+
function matchCoinIdFromMarketChartUrl(url) {
|
|
62
|
+
const u = toURL(url);
|
|
63
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
64
|
+
const coinsIdx = parts.indexOf('coins');
|
|
65
|
+
if (coinsIdx === -1)
|
|
66
|
+
return undefined;
|
|
67
|
+
const id = parts[coinsIdx + 1];
|
|
68
|
+
const next = parts[coinsIdx + 2];
|
|
69
|
+
if (id && next === 'market_chart')
|
|
70
|
+
return decodeURIComponent(id);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function matchCoinIdFromInfoUrl(url) {
|
|
74
|
+
const u = toURL(url);
|
|
75
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
76
|
+
const coinsIdx = parts.indexOf('coins');
|
|
77
|
+
if (coinsIdx === -1)
|
|
78
|
+
return undefined;
|
|
79
|
+
const id = parts[coinsIdx + 1];
|
|
80
|
+
if (!id)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (id === 'list' || id === 'markets')
|
|
83
|
+
return undefined;
|
|
84
|
+
const tail = parts.slice(coinsIdx + 2);
|
|
85
|
+
if (tail.length === 0)
|
|
86
|
+
return decodeURIComponent(id);
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function matchCoinContract(url) {
|
|
90
|
+
const u = toURL(url);
|
|
91
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
92
|
+
const coinsIdx = parts.indexOf('coins');
|
|
93
|
+
if (coinsIdx === -1)
|
|
94
|
+
return undefined;
|
|
95
|
+
const platformId = parts[coinsIdx + 1];
|
|
96
|
+
const next = parts[coinsIdx + 2];
|
|
97
|
+
const address = parts[coinsIdx + 3];
|
|
98
|
+
if (platformId && next === 'contract' && address) {
|
|
99
|
+
return { platformId: decodeURIComponent(platformId), address: decodeURIComponent(address) };
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
function matchTokenListPlatformId(url) {
|
|
104
|
+
const u = toURL(url);
|
|
105
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
106
|
+
const idx = parts.indexOf('token_lists');
|
|
107
|
+
if (idx === -1)
|
|
108
|
+
return undefined;
|
|
109
|
+
const platformId = parts[idx + 1];
|
|
110
|
+
const file = parts[idx + 2];
|
|
111
|
+
if (platformId && file === 'all.json')
|
|
112
|
+
return decodeURIComponent(platformId);
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
function createCoinGeckoRequestStub(handlers) {
|
|
116
|
+
return {
|
|
117
|
+
get: (url, _opts, cb) => {
|
|
118
|
+
try {
|
|
119
|
+
const u = toURL(url);
|
|
120
|
+
const pathname = u.pathname;
|
|
121
|
+
if (pathname.endsWith('/v3/coins/markets')) {
|
|
122
|
+
const ids = parseRequestedIdsFromMarketsUrl(url);
|
|
123
|
+
return cb(null, { body: handlers.onMarkets ? handlers.onMarkets(ids) : [] });
|
|
124
|
+
}
|
|
125
|
+
const contract = matchCoinContract(url);
|
|
126
|
+
if (contract) {
|
|
127
|
+
return cb(null, {
|
|
128
|
+
body: handlers.onCoinContract ? handlers.onCoinContract(contract.platformId, contract.address) : {}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const marketChartId = matchCoinIdFromMarketChartUrl(url);
|
|
132
|
+
if (marketChartId) {
|
|
133
|
+
return cb(null, { body: handlers.onMarketChart ? handlers.onMarketChart(marketChartId) : { prices: [] } });
|
|
134
|
+
}
|
|
135
|
+
const coinInfoId = matchCoinIdFromInfoUrl(url);
|
|
136
|
+
if (coinInfoId) {
|
|
137
|
+
return cb(null, { body: handlers.onCoinInfo ? handlers.onCoinInfo(coinInfoId) : {} });
|
|
138
|
+
}
|
|
139
|
+
const tokenListPlatformId = matchTokenListPlatformId(url);
|
|
140
|
+
if (tokenListPlatformId) {
|
|
141
|
+
return cb(null, { body: handlers.onTokenList ? handlers.onTokenList(tokenListPlatformId) : { tokens: [] } });
|
|
142
|
+
}
|
|
143
|
+
return cb(new Error('unexpected url'));
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
return cb(e);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
47
151
|
describe('CoinGecko integration', function () {
|
|
48
152
|
this.timeout(5000);
|
|
153
|
+
const sandbox = sinon_1.default.createSandbox();
|
|
49
154
|
let server;
|
|
50
155
|
let wallet;
|
|
51
156
|
let fakeRequest;
|
|
52
|
-
|
|
157
|
+
const cg = () => server.externalServices.coinGecko;
|
|
158
|
+
const getMarketStats = (query, code = 'USD') => cg().coinGeckoGetMarketStats({ params: { code }, query });
|
|
159
|
+
const getFiatRates = (query, code = 'USD') => cg().coinGeckoGetFiatRates({ params: { code }, query });
|
|
160
|
+
const getTokens = (req = {}) => cg().coinGeckoGetTokens(req);
|
|
161
|
+
function forceGlobalCacheMisses() {
|
|
162
|
+
sandbox.stub(cg().storage, 'checkAndUseGlobalCache').callsFake((_key, _duration, cb) => cb(null, null, null));
|
|
163
|
+
sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
|
|
164
|
+
}
|
|
53
165
|
before(async () => {
|
|
54
166
|
await helpers_1.default.before();
|
|
55
167
|
});
|
|
56
168
|
beforeEach(async () => {
|
|
57
169
|
config_1.default.coinGecko = {
|
|
58
|
-
api: '
|
|
59
|
-
};
|
|
60
|
-
fakeRequest = {
|
|
61
|
-
get: (_url, _opts, _cb) => {
|
|
62
|
-
return _cb(null, { body: { tokens: [{
|
|
63
|
-
chainId: 1,
|
|
64
|
-
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
65
|
-
symbol: 'USDC',
|
|
66
|
-
name: 'USD Coin',
|
|
67
|
-
decimals: 6,
|
|
68
|
-
logoURI: 'xxxxxx'
|
|
69
|
-
}] } });
|
|
70
|
-
},
|
|
170
|
+
api: 'https://pro-api.coingecko.com/api'
|
|
71
171
|
};
|
|
72
172
|
await helpers_1.default.beforeEach();
|
|
73
173
|
({ wallet } = await helpers_1.default.createAndJoinWallet(1, 1));
|
|
74
174
|
const priv = TestData.copayers[0].privKey_1H_0;
|
|
75
175
|
const sig = helpers_1.default.signMessage('hello world', priv);
|
|
76
|
-
|
|
176
|
+
server = await util_1.default.promisify(server_1.WalletService.getInstanceWithAuth).call(server_1.WalletService, {
|
|
77
177
|
copayerId: wallet.copayers[0].id,
|
|
78
178
|
message: 'hello world',
|
|
79
179
|
signature: sig,
|
|
80
180
|
clientVersion: 'bwc-2.0.0',
|
|
81
|
-
walletId: '123'
|
|
82
|
-
})
|
|
181
|
+
walletId: '123'
|
|
182
|
+
});
|
|
83
183
|
});
|
|
84
184
|
after(async () => {
|
|
85
185
|
await helpers_1.default.after();
|
|
86
186
|
});
|
|
187
|
+
afterEach(() => {
|
|
188
|
+
sandbox.restore();
|
|
189
|
+
});
|
|
87
190
|
describe('#coinGeckoGetTokenData', () => {
|
|
88
191
|
beforeEach(() => {
|
|
89
|
-
|
|
192
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
193
|
+
onTokenList: _platformId => ({
|
|
194
|
+
tokens: [
|
|
195
|
+
{
|
|
196
|
+
chainId: 1,
|
|
197
|
+
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
|
198
|
+
symbol: 'USDC',
|
|
199
|
+
name: 'USD Coin',
|
|
200
|
+
decimals: 6,
|
|
201
|
+
logoURI: 'xxxxxx'
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
cg().request = fakeRequest;
|
|
90
207
|
});
|
|
91
208
|
it('should get coinGecko list of tokens data', async () => {
|
|
92
|
-
const data = await
|
|
209
|
+
const data = await getTokens({});
|
|
93
210
|
should.exist(data);
|
|
94
211
|
});
|
|
212
|
+
it('should map chain aliases to the correct token list platform', async () => {
|
|
213
|
+
forceGlobalCacheMisses();
|
|
214
|
+
let platformSeen;
|
|
215
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
216
|
+
onTokenList: platformId => {
|
|
217
|
+
platformSeen = platformId;
|
|
218
|
+
return {
|
|
219
|
+
tokens: [
|
|
220
|
+
{
|
|
221
|
+
chainId: 137,
|
|
222
|
+
address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
|
|
223
|
+
symbol: 'USDC',
|
|
224
|
+
name: 'USD Coin',
|
|
225
|
+
decimals: 6,
|
|
226
|
+
logoURI: 'xxxxxx'
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
cg().request = fakeRequest;
|
|
233
|
+
const data = await getTokens({ params: { chain: 'pol' } });
|
|
234
|
+
should.exist(data);
|
|
235
|
+
platformSeen.should.equal('polygon-pos');
|
|
236
|
+
});
|
|
95
237
|
it('should return error if coinGecko is commented in config', async () => {
|
|
96
238
|
config_1.default.coinGecko = undefined;
|
|
97
239
|
try {
|
|
98
|
-
await
|
|
240
|
+
await getTokens({});
|
|
99
241
|
should.fail('should have thrown');
|
|
100
242
|
}
|
|
101
243
|
catch (err) {
|
|
@@ -105,102 +247,293 @@ describe('CoinGecko integration', function () {
|
|
|
105
247
|
});
|
|
106
248
|
describe('#coinGeckoGetMarketStats', () => {
|
|
107
249
|
beforeEach(() => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'bitcoin-cash',
|
|
120
|
-
'shiba-inu',
|
|
121
|
-
'polygon-ecosystem-token',
|
|
122
|
-
'apecoin',
|
|
123
|
-
'litecoin',
|
|
124
|
-
'wrapped-bitcoin',
|
|
125
|
-
'weth'
|
|
126
|
-
];
|
|
127
|
-
const marketById = {
|
|
128
|
-
bitcoin: {
|
|
129
|
-
id: 'bitcoin',
|
|
130
|
-
symbol: 'btc',
|
|
131
|
-
name: 'Bitcoin',
|
|
132
|
-
image: 'btc.png',
|
|
133
|
-
current_price: 100,
|
|
134
|
-
total_volume: 200,
|
|
135
|
-
circulating_supply: 300,
|
|
136
|
-
market_cap: 400,
|
|
137
|
-
last_updated: '2020-01-01T00:00:00.000Z'
|
|
138
|
-
},
|
|
139
|
-
ethereum: {
|
|
140
|
-
id: 'ethereum',
|
|
141
|
-
symbol: 'eth',
|
|
142
|
-
name: 'Ethereum',
|
|
143
|
-
image: 'eth.png',
|
|
144
|
-
current_price: 10,
|
|
145
|
-
total_volume: 20,
|
|
146
|
-
circulating_supply: 30,
|
|
147
|
-
market_cap: 40,
|
|
148
|
-
last_updated: '2020-01-01T00:00:00.000Z'
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
const aboutById = {
|
|
152
|
-
bitcoin: 'About BTC',
|
|
153
|
-
ethereum: 'About ETH'
|
|
154
|
-
};
|
|
155
|
-
if (url.includes('/v3/coins/markets')) {
|
|
156
|
-
return cb(null, {
|
|
157
|
-
body: [
|
|
158
|
-
...defaultIds.map(id => marketById[id]
|
|
159
|
-
? marketById[id]
|
|
160
|
-
: {
|
|
161
|
-
id,
|
|
162
|
-
symbol: id.replace(/[^a-z]/g, '').slice(0, 4),
|
|
163
|
-
name: id,
|
|
164
|
-
image: `${id}.png`,
|
|
165
|
-
current_price: 1,
|
|
166
|
-
total_volume: 1,
|
|
167
|
-
circulating_supply: 1,
|
|
168
|
-
market_cap: 1,
|
|
169
|
-
last_updated: '2020-01-01T00:00:00.000Z'
|
|
170
|
-
})
|
|
171
|
-
]
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
const mChart = url.match(/\/v3\/coins\/([^/]+)\/market_chart\?/);
|
|
175
|
-
if (mChart?.[1]) {
|
|
176
|
-
const id = decodeURIComponent(mChart[1]);
|
|
177
|
-
if (id === 'bitcoin')
|
|
178
|
-
return cb(null, { body: { prices: [[0, 90], [1, 110], [2, 95]] } });
|
|
179
|
-
if (id === 'ethereum')
|
|
180
|
-
return cb(null, { body: { prices: [[0, 9], [1, 12], [2, 8]] } });
|
|
181
|
-
return cb(null, { body: { prices: [[0, 1], [1, 1], [2, 1]] } });
|
|
182
|
-
}
|
|
183
|
-
const mInfo = url.match(/\/v3\/coins\/([^?]+)\?/);
|
|
184
|
-
if (mInfo?.[1]) {
|
|
185
|
-
const id = decodeURIComponent(mInfo[1]);
|
|
186
|
-
return cb(null, { body: { description: { en: aboutById[id] || `About ${id}` } } });
|
|
187
|
-
}
|
|
188
|
-
return cb(new Error('unexpected url'));
|
|
250
|
+
const marketById = {
|
|
251
|
+
bitcoin: {
|
|
252
|
+
id: 'bitcoin',
|
|
253
|
+
symbol: 'btc',
|
|
254
|
+
name: 'Bitcoin',
|
|
255
|
+
image: 'btc.png',
|
|
256
|
+
current_price: 100,
|
|
257
|
+
total_volume: 200,
|
|
258
|
+
circulating_supply: 300,
|
|
259
|
+
market_cap: 400,
|
|
260
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
189
261
|
}
|
|
190
262
|
};
|
|
191
|
-
|
|
263
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
264
|
+
onMarkets: ids => ids.map(id => marketById[id]
|
|
265
|
+
? marketById[id]
|
|
266
|
+
: {
|
|
267
|
+
id,
|
|
268
|
+
symbol: id.replace(/[^a-z]/g, '').slice(0, 4),
|
|
269
|
+
name: id,
|
|
270
|
+
image: `${id}.png`,
|
|
271
|
+
current_price: 1,
|
|
272
|
+
total_volume: 1,
|
|
273
|
+
circulating_supply: 1,
|
|
274
|
+
market_cap: 1,
|
|
275
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
276
|
+
}),
|
|
277
|
+
onMarketChart: id => {
|
|
278
|
+
if (id === 'bitcoin')
|
|
279
|
+
return { prices: [[0, 90], [1, 110], [2, 95]] };
|
|
280
|
+
return { prices: [[0, 1], [1, 1], [2, 1]] };
|
|
281
|
+
},
|
|
282
|
+
onCoinInfo: id => ({ description: { en: id === 'bitcoin' ? 'About BTC' : `About ${id}` } })
|
|
283
|
+
});
|
|
284
|
+
cg().request = fakeRequest;
|
|
192
285
|
});
|
|
193
286
|
it('should get market stats for a single coin', async () => {
|
|
194
|
-
const data = await
|
|
287
|
+
const data = await getMarketStats({ coin: 'btc' }, 'ARS');
|
|
195
288
|
should.exist(data);
|
|
196
289
|
data.should.have.length(1);
|
|
197
290
|
data[0].about.should.equal('About BTC');
|
|
198
291
|
data[0].low52w.should.equal(90);
|
|
199
292
|
});
|
|
293
|
+
it('should reject non-default coin symbols without tokenAddress', async () => {
|
|
294
|
+
try {
|
|
295
|
+
await getMarketStats({ coin: 'WBTC' });
|
|
296
|
+
should.fail('should have thrown');
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
err.message.should.equal('Unsupported coin. For token symbols, pass `chain` and `tokenAddress`.');
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
it('should require tokenAddress when chain is provided for market stats', async () => {
|
|
303
|
+
try {
|
|
304
|
+
await getMarketStats({ coin: ['btc'], chain: ['eth'] });
|
|
305
|
+
should.fail('should have thrown');
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
err.message.should.equal('chain is only supported for token lookups; provide tokenAddress or omit chain for native coins');
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
it('should reject invalid chain values for default marketstats coins', async () => {
|
|
312
|
+
try {
|
|
313
|
+
await getMarketStats({ coin: 'BTC', chain: '!!!' });
|
|
314
|
+
should.fail('should have thrown');
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
err.message.should.equal('Unsupported chain');
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
it('should resolve a token unambiguously when tokenAddress is provided', async () => {
|
|
321
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
322
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
323
|
+
onCoinContract: (platformId, address) => {
|
|
324
|
+
platformId.should.equal('arbitrum-one');
|
|
325
|
+
address.should.equal(tokenAddress);
|
|
326
|
+
return {
|
|
327
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
328
|
+
symbol: 'usdc.e',
|
|
329
|
+
asset_platform_id: 'arbitrum-one'
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
onMarkets: ids => ids.map(id => ({
|
|
333
|
+
id,
|
|
334
|
+
symbol: 'usdc.e',
|
|
335
|
+
name: 'Arbitrum Bridged USDC (Arbitrum)',
|
|
336
|
+
image: `${id}.png`,
|
|
337
|
+
current_price: 1,
|
|
338
|
+
total_volume: 1,
|
|
339
|
+
circulating_supply: 1,
|
|
340
|
+
market_cap: 1,
|
|
341
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
342
|
+
})),
|
|
343
|
+
onMarketChart: _id => ({ prices: [[0, 1], [1, 1]] }),
|
|
344
|
+
onCoinInfo: _id => ({ description: { en: 'About token' }, asset_platform_id: 'arbitrum-one' })
|
|
345
|
+
});
|
|
346
|
+
cg().request = fakeRequest;
|
|
347
|
+
const data = await getMarketStats({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
348
|
+
should.exist(data);
|
|
349
|
+
data.should.have.length(1);
|
|
350
|
+
data[0].name.should.equal('Arbitrum Bridged USDC (Arbitrum)');
|
|
351
|
+
});
|
|
352
|
+
it('should resolve market stats by chain + tokenAddress when coin is omitted', async () => {
|
|
353
|
+
const tokenAddress = '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8';
|
|
354
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
355
|
+
onCoinContract: (platformId, address) => {
|
|
356
|
+
platformId.should.equal('arbitrum-one');
|
|
357
|
+
address.should.equal(tokenAddress);
|
|
358
|
+
return {
|
|
359
|
+
id: 'bridged-usdc-arbitrum',
|
|
360
|
+
symbol: 'usdc',
|
|
361
|
+
asset_platform_id: 'arbitrum-one'
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
onMarkets: ids => ids.map(id => ({
|
|
365
|
+
id,
|
|
366
|
+
symbol: 'usdc',
|
|
367
|
+
name: 'Arbitrum Bridged USDC (Arbitrum)',
|
|
368
|
+
image: `${id}.png`,
|
|
369
|
+
current_price: 1,
|
|
370
|
+
total_volume: 1,
|
|
371
|
+
circulating_supply: 1,
|
|
372
|
+
market_cap: 1,
|
|
373
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
374
|
+
})),
|
|
375
|
+
onMarketChart: _id => ({ prices: [[0, 1], [1, 1]] }),
|
|
376
|
+
onCoinInfo: _id => ({ description: { en: 'About token' }, asset_platform_id: 'arbitrum-one' })
|
|
377
|
+
});
|
|
378
|
+
cg().request = fakeRequest;
|
|
379
|
+
const data = await getMarketStats({ chain: 'arb', tokenAddress });
|
|
380
|
+
should.exist(data);
|
|
381
|
+
data.should.have.length(1);
|
|
382
|
+
data[0].name.should.equal('Arbitrum Bridged USDC (Arbitrum)');
|
|
383
|
+
});
|
|
384
|
+
it('should map pol alias to polygon-pos for token marketstats lookup', async () => {
|
|
385
|
+
const tokenAddress = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
|
|
386
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
387
|
+
onCoinContract: (platformId, address) => {
|
|
388
|
+
platformId.should.equal('polygon-pos');
|
|
389
|
+
address.should.equal(tokenAddress);
|
|
390
|
+
return {
|
|
391
|
+
id: 'usd-coin',
|
|
392
|
+
symbol: 'usdc',
|
|
393
|
+
asset_platform_id: 'polygon-pos'
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
onMarkets: ids => ids.map(id => ({
|
|
397
|
+
id,
|
|
398
|
+
symbol: 'usdc',
|
|
399
|
+
name: 'USD Coin (PoS)',
|
|
400
|
+
image: `${id}.png`,
|
|
401
|
+
current_price: 1,
|
|
402
|
+
total_volume: 1,
|
|
403
|
+
circulating_supply: 1,
|
|
404
|
+
market_cap: 1,
|
|
405
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
406
|
+
})),
|
|
407
|
+
onMarketChart: _id => ({ prices: [[0, 1], [1, 1]] }),
|
|
408
|
+
onCoinInfo: _id => ({ description: { en: 'About token' }, asset_platform_id: 'polygon-pos' })
|
|
409
|
+
});
|
|
410
|
+
cg().request = fakeRequest;
|
|
411
|
+
const data = await getMarketStats({ chain: 'pol', tokenAddress });
|
|
412
|
+
should.exist(data);
|
|
413
|
+
data.should.have.length(1);
|
|
414
|
+
data[0].name.should.equal('USD Coin (PoS)');
|
|
415
|
+
});
|
|
416
|
+
it('should reject tokenAddress when returned token chain does not match chain', async () => {
|
|
417
|
+
forceGlobalCacheMisses();
|
|
418
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
419
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
420
|
+
onCoinContract: () => ({
|
|
421
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
422
|
+
symbol: 'usdc.e',
|
|
423
|
+
asset_platform_id: 'ethereum'
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
cg().request = fakeRequest;
|
|
427
|
+
try {
|
|
428
|
+
await getMarketStats({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
429
|
+
should.fail('should have thrown');
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
err.message.should.equal('tokenAddress does not match chain');
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
it('should map tokenAddress contract 404s to Unsupported tokenAddress', async () => {
|
|
436
|
+
forceGlobalCacheMisses();
|
|
437
|
+
const tokenAddress = '0x000000000000000000000000000000000000dead';
|
|
438
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
439
|
+
onCoinContract: () => ({
|
|
440
|
+
status: { error_code: 404, error_message: 'not found' }
|
|
441
|
+
})
|
|
442
|
+
});
|
|
443
|
+
cg().request = fakeRequest;
|
|
444
|
+
try {
|
|
445
|
+
await getMarketStats({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
446
|
+
should.fail('should have thrown');
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
err.message.should.equal('Unsupported tokenAddress');
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
it('should reject invalid EVM tokenAddress format before lookup', async () => {
|
|
453
|
+
const getSpy = sandbox.spy();
|
|
454
|
+
cg().request = { get: getSpy };
|
|
455
|
+
try {
|
|
456
|
+
await getMarketStats({ coin: 'USDC.e', chain: 'arb', tokenAddress: 'invalid-evm-address' });
|
|
457
|
+
should.fail('should have thrown');
|
|
458
|
+
}
|
|
459
|
+
catch (err) {
|
|
460
|
+
err.message.should.equal('Invalid tokenAddress');
|
|
461
|
+
err.statusCode.should.equal(400);
|
|
462
|
+
}
|
|
463
|
+
getSpy.callCount.should.equal(0);
|
|
464
|
+
});
|
|
465
|
+
it('should reject tokenAddress when returned token symbol does not match coin', async () => {
|
|
466
|
+
forceGlobalCacheMisses();
|
|
467
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
468
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
469
|
+
onCoinContract: () => ({
|
|
470
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
471
|
+
symbol: 'usdc.e'
|
|
472
|
+
})
|
|
473
|
+
});
|
|
474
|
+
cg().request = fakeRequest;
|
|
475
|
+
try {
|
|
476
|
+
await getMarketStats({ coin: 'WBTC', chain: 'arb', tokenAddress });
|
|
477
|
+
should.fail('should have thrown');
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
err.message.should.equal('tokenAddress does not match coin');
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
it('should use DB cache for default marketstats coin', async () => {
|
|
484
|
+
const checkCacheStub = sandbox
|
|
485
|
+
.stub(cg().storage, 'checkAndUseGlobalCache')
|
|
486
|
+
.callsFake((_key, _duration, cb) => cb(null, null, null));
|
|
487
|
+
const storeCacheStub = sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
|
|
488
|
+
const data = await getMarketStats({ coin: 'btc' });
|
|
489
|
+
should.exist(data);
|
|
490
|
+
checkCacheStub.callCount.should.equal(1);
|
|
491
|
+
checkCacheStub.getCall(0).args[0].should.equal('cgMarketStats:usd:bitcoin');
|
|
492
|
+
storeCacheStub.callCount.should.equal(1);
|
|
493
|
+
storeCacheStub.getCall(0).args[0].should.equal('cgMarketStats:usd:bitcoin');
|
|
494
|
+
});
|
|
495
|
+
it('should bypass DB cache for token marketstats requests', async () => {
|
|
496
|
+
const checkCacheStub = sandbox
|
|
497
|
+
.stub(cg().storage, 'checkAndUseGlobalCache')
|
|
498
|
+
.callsFake((_key, _duration, cb) => cb(null, null, null));
|
|
499
|
+
const storeCacheStub = sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
|
|
500
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
501
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
502
|
+
onCoinContract: (platformId, address) => {
|
|
503
|
+
platformId.should.equal('arbitrum-one');
|
|
504
|
+
address.should.equal(tokenAddress);
|
|
505
|
+
return {
|
|
506
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
507
|
+
symbol: 'usdc.e',
|
|
508
|
+
asset_platform_id: 'arbitrum-one'
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
onMarkets: ids => ids.map(id => ({
|
|
512
|
+
id,
|
|
513
|
+
symbol: 'usdc.e',
|
|
514
|
+
name: 'Arbitrum Bridged USDC (Arbitrum)',
|
|
515
|
+
image: `${id}.png`,
|
|
516
|
+
current_price: 1,
|
|
517
|
+
total_volume: 1,
|
|
518
|
+
circulating_supply: 1,
|
|
519
|
+
market_cap: 1,
|
|
520
|
+
last_updated: '2020-01-01T00:00:00.000Z'
|
|
521
|
+
})),
|
|
522
|
+
onMarketChart: _id => ({ prices: [[0, 1], [1, 1]] }),
|
|
523
|
+
onCoinInfo: _id => ({ description: { en: 'About token' } })
|
|
524
|
+
});
|
|
525
|
+
cg().request = fakeRequest;
|
|
526
|
+
const data = await getMarketStats({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
527
|
+
should.exist(data);
|
|
528
|
+
const checkedKeys = checkCacheStub.getCalls().map(c => c.args[0]);
|
|
529
|
+
const storedKeys = storeCacheStub.getCalls().map(c => c.args[0]);
|
|
530
|
+
checkedKeys.some(k => typeof k === 'string' && k.startsWith('cgMarketStats:')).should.equal(false);
|
|
531
|
+
storedKeys.some(k => typeof k === 'string' && k.startsWith('cgMarketStats:')).should.equal(false);
|
|
532
|
+
});
|
|
200
533
|
it('should return error if coinGecko is commented in config', async () => {
|
|
201
534
|
config_1.default.coinGecko = undefined;
|
|
202
535
|
try {
|
|
203
|
-
await
|
|
536
|
+
await getMarketStats({ coin: 'btc' });
|
|
204
537
|
should.fail('should have thrown');
|
|
205
538
|
}
|
|
206
539
|
catch (err) {
|
|
@@ -210,77 +543,282 @@ describe('CoinGecko integration', function () {
|
|
|
210
543
|
});
|
|
211
544
|
describe('#coinGeckoGetFiatRates', () => {
|
|
212
545
|
beforeEach(() => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
fakeRequest = {
|
|
217
|
-
get: (url, _opts, cb) => {
|
|
218
|
-
const mChart = url.match(/\/v3\/coins\/([^/]+)\/market_chart\?/);
|
|
219
|
-
if (mChart?.[1]) {
|
|
220
|
-
const id = decodeURIComponent(mChart[1]);
|
|
221
|
-
if (id === 'bitcoin')
|
|
222
|
-
return cb(null, { body: { prices: [[1, 100], [2, 110]] } });
|
|
223
|
-
if (id === 'bitcoin-cash')
|
|
224
|
-
return cb(null, { body: { prices: [[1, 200], [2, 210]] } });
|
|
225
|
-
return cb(null, { body: { prices: [[1, 1], [2, 1]] } });
|
|
226
|
-
}
|
|
227
|
-
return cb(new Error('unexpected url'));
|
|
228
|
-
}
|
|
546
|
+
const pricesById = {
|
|
547
|
+
bitcoin: [[1, 100], [2, 110]],
|
|
548
|
+
'bitcoin-cash': [[1, 200], [2, 210]]
|
|
229
549
|
};
|
|
230
|
-
|
|
550
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
551
|
+
onMarketChart: id => ({ prices: pricesById[id] || [[1, 1], [2, 1]] }),
|
|
552
|
+
});
|
|
553
|
+
cg().request = fakeRequest;
|
|
231
554
|
});
|
|
232
555
|
it('should get fiat rates with default days', async () => {
|
|
233
|
-
const data = await
|
|
234
|
-
params: { code: 'USD' },
|
|
235
|
-
query: { coin: 'BTC' }
|
|
236
|
-
});
|
|
556
|
+
const data = await getFiatRates({ coin: 'BTC' });
|
|
237
557
|
should.exist(data);
|
|
238
558
|
data.should.deep.equal([
|
|
239
559
|
{ ts: 1, rate: 100 },
|
|
240
560
|
{ ts: 2, rate: 110 }
|
|
241
561
|
]);
|
|
242
562
|
});
|
|
563
|
+
it('should reject invalid chain values for default coins', async () => {
|
|
564
|
+
try {
|
|
565
|
+
await getFiatRates({ coin: 'BTC', chain: '!!!' });
|
|
566
|
+
should.fail('should have thrown');
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
err.message.should.equal('Unsupported chain');
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
it('should require tokenAddress when chain is provided for fiat rates', async () => {
|
|
573
|
+
try {
|
|
574
|
+
await getFiatRates({ coin: 'BTC', chain: 'eth' });
|
|
575
|
+
should.fail('should have thrown');
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
err.message.should.equal('chain is only supported for token lookups; provide tokenAddress or omit chain for native coins');
|
|
579
|
+
}
|
|
580
|
+
});
|
|
243
581
|
it('should get fiat rates with explicit days', async () => {
|
|
244
|
-
const data = await
|
|
245
|
-
params: { code: 'USD' },
|
|
246
|
-
query: { coin: 'BTC', days: 365 }
|
|
247
|
-
});
|
|
582
|
+
const data = await getFiatRates({ coin: 'BTC', days: 365 });
|
|
248
583
|
should.exist(data);
|
|
249
584
|
data.should.have.length(2);
|
|
250
585
|
data[0].rate.should.equal(100);
|
|
251
586
|
});
|
|
252
587
|
it('should support BCH coin mapping', async () => {
|
|
253
|
-
const data = await
|
|
254
|
-
params: { code: 'USD' },
|
|
255
|
-
query: { coin: 'BCH' }
|
|
256
|
-
});
|
|
588
|
+
const data = await getFiatRates({ coin: 'BCH' });
|
|
257
589
|
should.exist(data);
|
|
258
590
|
data[0].rate.should.equal(200);
|
|
259
591
|
});
|
|
260
|
-
it('should
|
|
592
|
+
it('should reject non-default coin symbols without tokenAddress', async () => {
|
|
261
593
|
try {
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
594
|
+
await getFiatRates({ coin: 'WBTC' });
|
|
595
|
+
should.fail('should have thrown');
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
err.message.should.equal('Unsupported coin. For token symbols, pass `chain` and `tokenAddress`.');
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
it('should resolve fiat rates unambiguously when tokenAddress is provided', async () => {
|
|
602
|
+
forceGlobalCacheMisses();
|
|
603
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
604
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
605
|
+
onCoinContract: (platformId, address) => {
|
|
606
|
+
platformId.should.equal('arbitrum-one');
|
|
607
|
+
address.should.equal(tokenAddress);
|
|
608
|
+
return {
|
|
609
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
610
|
+
symbol: 'usdc.e',
|
|
611
|
+
asset_platform_id: 'arbitrum-one'
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
onMarketChart: id => id === 'arbitrum-bridged-usdc-arbitrum'
|
|
615
|
+
? { prices: [[1, 77], [2, 78]] }
|
|
616
|
+
: { prices: [[1, 1], [2, 1]] }
|
|
617
|
+
});
|
|
618
|
+
cg().request = fakeRequest;
|
|
619
|
+
const data = await getFiatRates({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
620
|
+
should.exist(data);
|
|
621
|
+
data.should.deep.equal([
|
|
622
|
+
{ ts: 1, rate: 77 },
|
|
623
|
+
{ ts: 2, rate: 78 }
|
|
624
|
+
]);
|
|
625
|
+
});
|
|
626
|
+
it('should resolve fiat rates by chain + tokenAddress when coin is omitted', async () => {
|
|
627
|
+
forceGlobalCacheMisses();
|
|
628
|
+
const tokenAddress = '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8';
|
|
629
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
630
|
+
onCoinContract: (platformId, address) => {
|
|
631
|
+
platformId.should.equal('arbitrum-one');
|
|
632
|
+
address.should.equal(tokenAddress);
|
|
633
|
+
return {
|
|
634
|
+
id: 'bridged-usdc-arbitrum',
|
|
635
|
+
symbol: 'usdc',
|
|
636
|
+
asset_platform_id: 'arbitrum-one'
|
|
637
|
+
};
|
|
638
|
+
},
|
|
639
|
+
onMarketChart: id => id === 'bridged-usdc-arbitrum'
|
|
640
|
+
? { prices: [[1, 99], [2, 100]] }
|
|
641
|
+
: { prices: [[1, 1], [2, 1]] }
|
|
642
|
+
});
|
|
643
|
+
cg().request = fakeRequest;
|
|
644
|
+
const data = await getFiatRates({ chain: 'arb', tokenAddress });
|
|
645
|
+
should.exist(data);
|
|
646
|
+
data.should.deep.equal([
|
|
647
|
+
{ ts: 1, rate: 99 },
|
|
648
|
+
{ ts: 2, rate: 100 }
|
|
649
|
+
]);
|
|
650
|
+
});
|
|
651
|
+
it('should map pol alias to polygon-pos for token fiatrates lookup', async () => {
|
|
652
|
+
forceGlobalCacheMisses();
|
|
653
|
+
const tokenAddress = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
|
|
654
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
655
|
+
onCoinContract: (platformId, address) => {
|
|
656
|
+
platformId.should.equal('polygon-pos');
|
|
657
|
+
address.should.equal(tokenAddress);
|
|
658
|
+
return {
|
|
659
|
+
id: 'usd-coin',
|
|
660
|
+
symbol: 'usdc',
|
|
661
|
+
asset_platform_id: 'polygon-pos'
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
onMarketChart: id => id === 'usd-coin'
|
|
665
|
+
? { prices: [[1, 11], [2, 12]] }
|
|
666
|
+
: { prices: [[1, 1], [2, 1]] }
|
|
667
|
+
});
|
|
668
|
+
cg().request = fakeRequest;
|
|
669
|
+
const data = await getFiatRates({ chain: 'pol', tokenAddress });
|
|
670
|
+
should.exist(data);
|
|
671
|
+
data.should.deep.equal([
|
|
672
|
+
{ ts: 1, rate: 11 },
|
|
673
|
+
{ ts: 2, rate: 12 }
|
|
674
|
+
]);
|
|
675
|
+
});
|
|
676
|
+
it('should require a valid chain when tokenAddress is provided', async () => {
|
|
677
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
678
|
+
try {
|
|
679
|
+
await getFiatRates({ coin: 'USDC.e', tokenAddress });
|
|
680
|
+
should.fail('should have thrown');
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
err.message.should.equal('tokenAddress requires valid chain');
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
it('should reject invalid Solana tokenAddress format before lookup', async () => {
|
|
687
|
+
const getSpy = sandbox.spy();
|
|
688
|
+
cg().request = { get: getSpy };
|
|
689
|
+
try {
|
|
690
|
+
await getFiatRates({ chain: 'sol', tokenAddress: '0xdeadbeef' });
|
|
691
|
+
should.fail('should have thrown');
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
err.message.should.equal('Invalid tokenAddress');
|
|
695
|
+
err.statusCode.should.equal(400);
|
|
696
|
+
}
|
|
697
|
+
getSpy.callCount.should.equal(0);
|
|
698
|
+
});
|
|
699
|
+
it('should rethrow non-404 tokenAddress lookup errors', async () => {
|
|
700
|
+
forceGlobalCacheMisses();
|
|
701
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
702
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
703
|
+
onCoinContract: () => ({
|
|
704
|
+
status: { error_code: 429, error_message: 'rate limited' }
|
|
705
|
+
})
|
|
706
|
+
});
|
|
707
|
+
cg().request = fakeRequest;
|
|
708
|
+
try {
|
|
709
|
+
await getFiatRates({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
710
|
+
should.fail('should have thrown');
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
err.message.should.equal('coinGecko rate limit');
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
it('should reject fiat rates tokenAddress when returned token symbol does not match coin', async () => {
|
|
717
|
+
forceGlobalCacheMisses();
|
|
718
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
719
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
720
|
+
onCoinContract: () => ({
|
|
721
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
722
|
+
symbol: 'usdc.e'
|
|
723
|
+
})
|
|
724
|
+
});
|
|
725
|
+
cg().request = fakeRequest;
|
|
726
|
+
try {
|
|
727
|
+
await getFiatRates({ coin: 'WBTC', chain: 'arb', tokenAddress });
|
|
728
|
+
should.fail('should have thrown');
|
|
729
|
+
}
|
|
730
|
+
catch (err) {
|
|
731
|
+
err.message.should.equal('tokenAddress does not match coin');
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
it('should accept array query params for days', async () => {
|
|
735
|
+
const data = await getFiatRates({ coin: 'BTC', days: ['365'] });
|
|
736
|
+
should.exist(data);
|
|
737
|
+
data.should.deep.equal([
|
|
738
|
+
{ ts: 1, rate: 100 },
|
|
739
|
+
{ ts: 2, rate: 110 }
|
|
740
|
+
]);
|
|
741
|
+
});
|
|
742
|
+
it('should treat whitespace coin as missing and return default rates', async () => {
|
|
743
|
+
const data = await getFiatRates({ coin: ' ' });
|
|
744
|
+
should.exist(data);
|
|
745
|
+
Object.keys(data)
|
|
746
|
+
.sort()
|
|
747
|
+
.should.deep.equal(['bch', 'btc', 'doge', 'eth', 'ltc', 'pol', 'sol', 'xrp']);
|
|
748
|
+
});
|
|
749
|
+
it('should reject overly long coin symbols', async () => {
|
|
750
|
+
try {
|
|
751
|
+
await getFiatRates({ coin: 'x'.repeat(100) });
|
|
752
|
+
should.fail('should have thrown');
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
err.message.should.equal('Unsupported coin');
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
it('should reject overly long days values', async () => {
|
|
759
|
+
try {
|
|
760
|
+
await getFiatRates({ coin: 'BTC', days: 'x'.repeat(100) });
|
|
266
761
|
should.fail('should have thrown');
|
|
267
762
|
}
|
|
268
763
|
catch (err) {
|
|
269
764
|
err.message.should.equal('Invalid days');
|
|
270
765
|
}
|
|
271
766
|
});
|
|
272
|
-
it('should
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
767
|
+
it('should use DB cache for default fiatrates coin', async () => {
|
|
768
|
+
const checkCacheStub = sandbox
|
|
769
|
+
.stub(cg().storage, 'checkAndUseGlobalCache')
|
|
770
|
+
.callsFake((_key, _duration, cb) => cb(null, null, null));
|
|
771
|
+
const storeCacheStub = sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
|
|
772
|
+
const data = await getFiatRates({ coin: 'BTC' });
|
|
773
|
+
should.exist(data);
|
|
774
|
+
checkCacheStub.callCount.should.equal(1);
|
|
775
|
+
checkCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:100000');
|
|
776
|
+
storeCacheStub.callCount.should.equal(1);
|
|
777
|
+
storeCacheStub.getCall(0).args[0].should.equal('cgFiatRates:bitcoin:usd:100000');
|
|
778
|
+
});
|
|
779
|
+
it('should bypass DB cache for token fiatrates requests', async () => {
|
|
780
|
+
const checkCacheStub = sandbox
|
|
781
|
+
.stub(cg().storage, 'checkAndUseGlobalCache')
|
|
782
|
+
.callsFake((_key, _duration, cb) => cb(null, null, null));
|
|
783
|
+
const storeCacheStub = sandbox.stub(cg().storage, 'storeGlobalCache').callsFake((_key, _values, cb) => cb(null));
|
|
784
|
+
const tokenAddress = '0xaf88d065e77c8cc2239327c5edb3a432268e5831';
|
|
785
|
+
fakeRequest = createCoinGeckoRequestStub({
|
|
786
|
+
onCoinContract: (platformId, address) => {
|
|
787
|
+
platformId.should.equal('arbitrum-one');
|
|
788
|
+
address.should.equal(tokenAddress);
|
|
789
|
+
return {
|
|
790
|
+
id: 'arbitrum-bridged-usdc-arbitrum',
|
|
791
|
+
symbol: 'usdc.e',
|
|
792
|
+
asset_platform_id: 'arbitrum-one'
|
|
793
|
+
};
|
|
794
|
+
},
|
|
795
|
+
onMarketChart: id => id === 'arbitrum-bridged-usdc-arbitrum'
|
|
796
|
+
? { prices: [[1, 77], [2, 78]] }
|
|
797
|
+
: { prices: [[1, 1], [2, 1]] }
|
|
276
798
|
});
|
|
799
|
+
cg().request = fakeRequest;
|
|
800
|
+
const data = await getFiatRates({ coin: 'USDC.e', chain: 'arb', tokenAddress });
|
|
277
801
|
should.exist(data);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
802
|
+
const checkedKeys = checkCacheStub.getCalls().map(c => c.args[0]);
|
|
803
|
+
const storedKeys = storeCacheStub.getCalls().map(c => c.args[0]);
|
|
804
|
+
checkedKeys.some(k => typeof k === 'string' && k.startsWith('cgFiatRates:')).should.equal(false);
|
|
805
|
+
storedKeys.some(k => typeof k === 'string' && k.startsWith('cgFiatRates:')).should.equal(false);
|
|
806
|
+
});
|
|
807
|
+
it('should return error on invalid days', async () => {
|
|
808
|
+
try {
|
|
809
|
+
await getFiatRates({ coin: 'BTC', days: 0 });
|
|
810
|
+
should.fail('should have thrown');
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
err.message.should.equal('Invalid days');
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
it('should return rates for default coins if missing coin', async () => {
|
|
817
|
+
const data = await getFiatRates({});
|
|
818
|
+
should.exist(data);
|
|
819
|
+
Object.keys(data)
|
|
820
|
+
.sort()
|
|
821
|
+
.should.deep.equal(['bch', 'btc', 'doge', 'eth', 'ltc', 'pol', 'sol', 'xrp']);
|
|
284
822
|
data.btc.should.deep.equal([
|
|
285
823
|
{ ts: 1, rate: 100 },
|
|
286
824
|
{ ts: 2, rate: 110 }
|
|
@@ -290,6 +828,29 @@ describe('CoinGecko integration', function () {
|
|
|
290
828
|
{ ts: 2, rate: 210 }
|
|
291
829
|
]);
|
|
292
830
|
});
|
|
831
|
+
it('should omit a failed default coin and log a warning', async () => {
|
|
832
|
+
const warnSpy = sandbox.spy(logger_1.default, 'warn');
|
|
833
|
+
forceGlobalCacheMisses();
|
|
834
|
+
const baseGet = fakeRequest.get;
|
|
835
|
+
fakeRequest.get = (url, opts, cb) => {
|
|
836
|
+
const mChart = url.match(/\/v3\/coins\/([^/]+)\/market_chart\?/);
|
|
837
|
+
if (mChart?.[1]) {
|
|
838
|
+
const id = decodeURIComponent(mChart[1]);
|
|
839
|
+
if (id === 'bitcoin-cash')
|
|
840
|
+
return cb(new Error('boom'));
|
|
841
|
+
}
|
|
842
|
+
return baseGet(url, opts, cb);
|
|
843
|
+
};
|
|
844
|
+
const data = await getFiatRates({});
|
|
845
|
+
should.exist(data);
|
|
846
|
+
Object.keys(data).should.not.include('bch');
|
|
847
|
+
Object.keys(data).should.include('btc');
|
|
848
|
+
const bchWarns = warnSpy
|
|
849
|
+
.getCalls()
|
|
850
|
+
.map(c => c.args)
|
|
851
|
+
.filter(args => args?.[0]?.toString().includes('CoinGecko fiat rates fetch failed') && args?.[1]?.coin === 'bch');
|
|
852
|
+
bchWarns.length.should.be.greaterThan(0);
|
|
853
|
+
});
|
|
293
854
|
});
|
|
294
855
|
});
|
|
295
856
|
//# sourceMappingURL=coinGecko.test.js.map
|