hive-stream 3.0.0 → 3.0.1
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/AGENTS.md +35 -0
- package/DOCUMENTATION.md +380 -0
- package/README.md +113 -22
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +7 -7
- package/dist/actions.js.map +1 -1
- package/dist/adapters/base.adapter.d.ts +19 -1
- package/dist/adapters/base.adapter.js +16 -0
- package/dist/adapters/base.adapter.js.map +1 -1
- package/dist/adapters/mongodb.adapter.d.ts +5 -11
- package/dist/adapters/mongodb.adapter.js +10 -10
- package/dist/adapters/mongodb.adapter.js.map +1 -1
- package/dist/adapters/postgresql.adapter.d.ts +17 -0
- package/dist/adapters/postgresql.adapter.js +99 -8
- package/dist/adapters/postgresql.adapter.js.map +1 -1
- package/dist/adapters/sqlite.adapter.d.ts +17 -0
- package/dist/adapters/sqlite.adapter.js +99 -8
- package/dist/adapters/sqlite.adapter.js.map +1 -1
- package/dist/api.js +86 -0
- package/dist/api.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +6 -3
- package/dist/config.js.map +1 -1
- package/dist/contracts/coinflip.contract.d.ts +8 -26
- package/dist/contracts/coinflip.contract.js +123 -144
- package/dist/contracts/coinflip.contract.js.map +1 -1
- package/dist/contracts/contract.d.ts +3 -0
- package/dist/contracts/contract.js +26 -0
- package/dist/contracts/contract.js.map +1 -0
- package/dist/contracts/dice.contract.d.ts +9 -36
- package/dist/contracts/dice.contract.js +135 -200
- package/dist/contracts/dice.contract.js.map +1 -1
- package/dist/contracts/exchange.contract.d.ts +11 -0
- package/dist/contracts/exchange.contract.js +492 -0
- package/dist/contracts/exchange.contract.js.map +1 -0
- package/dist/contracts/lotto.contract.d.ts +15 -19
- package/dist/contracts/lotto.contract.js +154 -162
- package/dist/contracts/lotto.contract.js.map +1 -1
- package/dist/contracts/nft.contract.d.ts +4 -0
- package/dist/contracts/nft.contract.js +65 -0
- package/dist/contracts/nft.contract.js.map +1 -1
- package/dist/contracts/poll.contract.d.ts +4 -0
- package/dist/contracts/poll.contract.js +105 -0
- package/dist/contracts/poll.contract.js.map +1 -0
- package/dist/contracts/rps.contract.d.ts +9 -0
- package/dist/contracts/rps.contract.js +217 -0
- package/dist/contracts/rps.contract.js.map +1 -0
- package/dist/contracts/tipjar.contract.d.ts +4 -0
- package/dist/contracts/tipjar.contract.js +60 -0
- package/dist/contracts/tipjar.contract.js.map +1 -0
- package/dist/contracts/token.contract.d.ts +3 -17
- package/dist/contracts/token.contract.js +128 -80
- package/dist/contracts/token.contract.js.map +1 -1
- package/dist/exchanges/coingecko.d.ts +7 -1
- package/dist/exchanges/coingecko.js +38 -21
- package/dist/exchanges/coingecko.js.map +1 -1
- package/dist/exchanges/exchange.d.ts +15 -8
- package/dist/exchanges/exchange.js +65 -11
- package/dist/exchanges/exchange.js.map +1 -1
- package/dist/hive-rates.d.ts +29 -4
- package/dist/hive-rates.js +179 -92
- package/dist/hive-rates.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/streamer.d.ts +101 -8
- package/dist/streamer.js +410 -140
- package/dist/streamer.js.map +1 -1
- package/dist/test.js +11 -12
- package/dist/test.js.map +1 -1
- package/dist/types/hive-stream.d.ts +85 -14
- package/dist/types/rates.d.ts +47 -0
- package/dist/types/rates.js +29 -0
- package/dist/types/rates.js.map +1 -0
- package/dist/utils.d.ts +318 -11
- package/dist/utils.js +804 -115
- package/dist/utils.js.map +1 -1
- package/examples/contracts/README.md +8 -0
- package/examples/contracts/exchange.ts +38 -0
- package/examples/contracts/poll.ts +21 -0
- package/examples/contracts/rps.ts +19 -0
- package/examples/contracts/tipjar.ts +19 -0
- package/package.json +20 -19
- package/tests/actions.spec.ts +7 -7
- package/tests/adapters/actions-persistence.spec.ts +4 -4
- package/tests/adapters/sqlite.adapter.spec.ts +2 -2
- package/tests/contracts/coinflip.contract.spec.ts +26 -154
- package/tests/contracts/dice.contract.spec.ts +24 -140
- package/tests/contracts/exchange.contract.spec.ts +84 -0
- package/tests/contracts/lotto.contract.spec.ts +30 -295
- package/tests/contracts/token.contract.spec.ts +72 -316
- package/tests/exchanges/coingecko.exchange.spec.ts +169 -0
- package/tests/exchanges/exchange.base.spec.ts +246 -0
- package/tests/helpers/mock-fetch.ts +165 -0
- package/tests/hive-chain-features.spec.ts +238 -0
- package/tests/hive-rates.spec.ts +443 -0
- package/tests/integration/hive-rates.integration.spec.ts +35 -0
- package/tests/streamer-actions.spec.ts +29 -18
- package/tests/streamer.spec.ts +142 -49
- package/tests/types/rates.spec.ts +216 -0
- package/tests/utils.spec.ts +27 -6
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { HiveRates } from '../src/hive-rates';
|
|
2
|
+
import { CoinGeckoExchange } from '../src/exchanges/coingecko';
|
|
3
|
+
import { Exchange } from '../src/exchanges/exchange';
|
|
4
|
+
import { NetworkError, ValidationError, RatesError } from '../src/types/rates';
|
|
5
|
+
|
|
6
|
+
// Mock fetch for tests
|
|
7
|
+
const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
|
|
8
|
+
global.fetch = mockFetch;
|
|
9
|
+
|
|
10
|
+
class MockExchange extends Exchange {
|
|
11
|
+
public readonly exchangeId = 'mock-exchange';
|
|
12
|
+
public shouldSucceed = true;
|
|
13
|
+
public mockHiveRate = 0.5;
|
|
14
|
+
public mockHbdRate = 1.0;
|
|
15
|
+
|
|
16
|
+
constructor(config: Partial<{ cacheDuration: number; maxRetries: number; retryDelay: number }> = {}) {
|
|
17
|
+
// Use shorter timeouts for tests
|
|
18
|
+
super({
|
|
19
|
+
cacheDuration: config.cacheDuration ?? 100,
|
|
20
|
+
maxRetries: config.maxRetries ?? 1,
|
|
21
|
+
retryDelay: config.retryDelay ?? 10
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async fetchRates(): Promise<boolean> {
|
|
26
|
+
if (!this.shouldSucceed) {
|
|
27
|
+
throw new Error('Mock exchange error');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.rateUsdHive = this.mockHiveRate;
|
|
31
|
+
this.rateUsdHbd = this.mockHbdRate;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('HiveRates', () => {
|
|
37
|
+
let hiveRates: HiveRates;
|
|
38
|
+
let mockExchange: MockExchange;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
mockExchange = new MockExchange({ cacheDuration: 100 });
|
|
42
|
+
hiveRates = new HiveRates({ cacheDuration: 100 }, [mockExchange]);
|
|
43
|
+
mockFetch.mockClear();
|
|
44
|
+
jest.clearAllTimers();
|
|
45
|
+
jest.useFakeTimers();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('constructor', () => {
|
|
53
|
+
it('should use default CoinGecko exchange when no custom exchanges provided', () => {
|
|
54
|
+
const rates = new HiveRates();
|
|
55
|
+
expect(rates).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should use custom exchanges when provided', () => {
|
|
59
|
+
const customExchange = new MockExchange();
|
|
60
|
+
const rates = new HiveRates({}, [customExchange]);
|
|
61
|
+
expect(rates).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should use default config when none provided', () => {
|
|
65
|
+
const rates = new HiveRates();
|
|
66
|
+
expect(rates).toBeDefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('fetchRates', () => {
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
// Mock successful fiat API response
|
|
73
|
+
mockFetch.mockResolvedValue({
|
|
74
|
+
ok: true,
|
|
75
|
+
json: async () => ({
|
|
76
|
+
usd: {
|
|
77
|
+
eur: 0.85,
|
|
78
|
+
gbp: 0.73,
|
|
79
|
+
jpy: 110,
|
|
80
|
+
cad: 1.25,
|
|
81
|
+
aud: 1.35
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
} as Response);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should fetch rates successfully', async () => {
|
|
88
|
+
const result = await hiveRates.fetchRates();
|
|
89
|
+
|
|
90
|
+
expect(result).toBe(true);
|
|
91
|
+
|
|
92
|
+
// Check crypto rates
|
|
93
|
+
const cryptoRates = hiveRates.getCryptoRates();
|
|
94
|
+
expect(cryptoRates.hive).toBe(0.5);
|
|
95
|
+
expect(cryptoRates.hbd).toBe(1.0);
|
|
96
|
+
|
|
97
|
+
// Check some cross rates
|
|
98
|
+
expect(hiveRates.getRate('EUR', 'HIVE')).toBeCloseTo(0.5 * 0.85);
|
|
99
|
+
expect(hiveRates.getRate('GBP', 'HBD')).toBeCloseTo(1.0 * 0.73);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle crypto fetch failure gracefully', async () => {
|
|
103
|
+
mockExchange.shouldSucceed = false;
|
|
104
|
+
|
|
105
|
+
const result = await hiveRates.fetchRates();
|
|
106
|
+
|
|
107
|
+
// Should still succeed if fiat rates succeed
|
|
108
|
+
expect(result).toBe(true);
|
|
109
|
+
}, 10000);
|
|
110
|
+
|
|
111
|
+
it('should handle fiat fetch failure gracefully', async () => {
|
|
112
|
+
mockFetch.mockRejectedValue(new Error('Fiat API error'));
|
|
113
|
+
|
|
114
|
+
// Should still succeed if crypto rates succeed
|
|
115
|
+
const result = await hiveRates.fetchRates();
|
|
116
|
+
expect(result).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return false when both crypto and fiat fail', async () => {
|
|
120
|
+
mockExchange.shouldSucceed = false;
|
|
121
|
+
mockFetch.mockRejectedValue(new Error('All APIs failed'));
|
|
122
|
+
|
|
123
|
+
const result = await hiveRates.fetchRates();
|
|
124
|
+
expect(result).toBe(false);
|
|
125
|
+
}, 10000);
|
|
126
|
+
|
|
127
|
+
it('should calculate average rates from multiple exchanges', async () => {
|
|
128
|
+
const exchange1 = new MockExchange();
|
|
129
|
+
exchange1.mockHiveRate = 0.4;
|
|
130
|
+
exchange1.mockHbdRate = 0.9;
|
|
131
|
+
|
|
132
|
+
const exchange2 = new MockExchange();
|
|
133
|
+
exchange2.mockHiveRate = 0.6;
|
|
134
|
+
exchange2.mockHbdRate = 1.1;
|
|
135
|
+
|
|
136
|
+
const rates = new HiveRates({ cacheDuration: 100 }, [exchange1, exchange2]);
|
|
137
|
+
|
|
138
|
+
await rates.fetchRates();
|
|
139
|
+
|
|
140
|
+
const cryptoRates = rates.getCryptoRates();
|
|
141
|
+
expect(cryptoRates.hive).toBe(0.5); // Average of 0.4 and 0.6
|
|
142
|
+
expect(cryptoRates.hbd).toBe(1.0); // Average of 0.9 and 1.1
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should ignore invalid crypto rates in average calculation', async () => {
|
|
146
|
+
const exchange1 = new MockExchange();
|
|
147
|
+
exchange1.mockHiveRate = 0.5;
|
|
148
|
+
exchange1.mockHbdRate = 1.0;
|
|
149
|
+
|
|
150
|
+
const exchange2 = new MockExchange();
|
|
151
|
+
exchange2.mockHiveRate = 0; // Invalid
|
|
152
|
+
exchange2.mockHbdRate = -1; // Invalid
|
|
153
|
+
|
|
154
|
+
const rates = new HiveRates({ cacheDuration: 100 }, [exchange1, exchange2]);
|
|
155
|
+
|
|
156
|
+
await rates.fetchRates();
|
|
157
|
+
|
|
158
|
+
const cryptoRates = rates.getCryptoRates();
|
|
159
|
+
expect(cryptoRates.hive).toBe(0.5); // Only valid rate used
|
|
160
|
+
expect(cryptoRates.hbd).toBe(1.0); // Only valid rate used
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('rate retrieval methods', () => {
|
|
165
|
+
beforeEach(async () => {
|
|
166
|
+
mockFetch.mockResolvedValue({
|
|
167
|
+
ok: true,
|
|
168
|
+
json: async () => ({
|
|
169
|
+
usd: {
|
|
170
|
+
eur: 0.85,
|
|
171
|
+
gbp: 0.73,
|
|
172
|
+
jpy: 110
|
|
173
|
+
}
|
|
174
|
+
}),
|
|
175
|
+
} as Response);
|
|
176
|
+
|
|
177
|
+
await hiveRates.fetchRates();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should get specific rate correctly', () => {
|
|
181
|
+
const rate = hiveRates.getRate('EUR', 'HIVE');
|
|
182
|
+
expect(rate).toBeCloseTo(0.5 * 0.85);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should return null for non-existent rate', () => {
|
|
186
|
+
const rate = hiveRates.getRate('XYZ' as any, 'HIVE');
|
|
187
|
+
expect(rate).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should get all rates', () => {
|
|
191
|
+
const allRates = hiveRates.getAllRates();
|
|
192
|
+
expect(typeof allRates).toBe('object');
|
|
193
|
+
expect(allRates['EUR_HIVE']).toBeCloseTo(0.5 * 0.85);
|
|
194
|
+
expect(allRates['GBP_HBD']).toBeCloseTo(1.0 * 0.73);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should get crypto rates only', () => {
|
|
198
|
+
const cryptoRates = hiveRates.getCryptoRates();
|
|
199
|
+
expect(cryptoRates.hive).toBe(0.5);
|
|
200
|
+
expect(cryptoRates.hbd).toBe(1.0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should get fiat rates only', () => {
|
|
204
|
+
const fiatRates = hiveRates.getFiatRates();
|
|
205
|
+
expect(fiatRates.EUR).toBe(0.85);
|
|
206
|
+
expect(fiatRates.GBP).toBe(0.73);
|
|
207
|
+
expect(fiatRates.JPY).toBe(110);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should support legacy method', () => {
|
|
211
|
+
const rate = hiveRates.fiatToHiveRate('EUR', 'HIVE');
|
|
212
|
+
expect(rate).toBeCloseTo(0.5 * 0.85);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('caching behavior', () => {
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
mockFetch.mockResolvedValue({
|
|
219
|
+
ok: true,
|
|
220
|
+
json: async () => ({
|
|
221
|
+
usd: { eur: 0.85 }
|
|
222
|
+
}),
|
|
223
|
+
} as Response);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should respect cache duration', async () => {
|
|
227
|
+
// First fetch
|
|
228
|
+
await hiveRates.fetchRates();
|
|
229
|
+
|
|
230
|
+
// Second fetch within cache duration should not call API
|
|
231
|
+
mockFetch.mockClear();
|
|
232
|
+
await hiveRates.fetchRates();
|
|
233
|
+
|
|
234
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should fetch when cache expires', async () => {
|
|
238
|
+
// First fetch
|
|
239
|
+
await hiveRates.fetchRates();
|
|
240
|
+
|
|
241
|
+
// Advance time beyond cache duration
|
|
242
|
+
jest.advanceTimersByTime(150);
|
|
243
|
+
|
|
244
|
+
// Second fetch should call API
|
|
245
|
+
mockFetch.mockClear();
|
|
246
|
+
await hiveRates.fetchRates();
|
|
247
|
+
|
|
248
|
+
expect(mockFetch).toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('should track last fetch times', async () => {
|
|
252
|
+
const timesBefore = hiveRates.getLastFetchTimes();
|
|
253
|
+
expect(timesBefore.crypto).toBeUndefined();
|
|
254
|
+
expect(timesBefore.fiat).toBeUndefined();
|
|
255
|
+
|
|
256
|
+
await hiveRates.fetchRates();
|
|
257
|
+
|
|
258
|
+
const timesAfter = hiveRates.getLastFetchTimes();
|
|
259
|
+
expect(timesAfter.crypto).toBeDefined();
|
|
260
|
+
expect(timesAfter.fiat).toBeDefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should report cache validity', async () => {
|
|
264
|
+
const validityBefore = hiveRates.isCacheValid();
|
|
265
|
+
expect(validityBefore.crypto).toBe(false);
|
|
266
|
+
expect(validityBefore.fiat).toBe(false);
|
|
267
|
+
|
|
268
|
+
await hiveRates.fetchRates();
|
|
269
|
+
|
|
270
|
+
const validityAfter = hiveRates.isCacheValid();
|
|
271
|
+
expect(validityAfter.crypto).toBe(true);
|
|
272
|
+
expect(validityAfter.fiat).toBe(true);
|
|
273
|
+
|
|
274
|
+
jest.advanceTimersByTime(150);
|
|
275
|
+
|
|
276
|
+
const validityExpired = hiveRates.isCacheValid();
|
|
277
|
+
expect(validityExpired.crypto).toBe(false);
|
|
278
|
+
expect(validityExpired.fiat).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('fiat API handling', () => {
|
|
283
|
+
it('should try multiple fiat endpoints', async () => {
|
|
284
|
+
// First endpoint fails, second succeeds
|
|
285
|
+
mockFetch
|
|
286
|
+
.mockRejectedValueOnce(new Error('First endpoint failed'))
|
|
287
|
+
.mockResolvedValueOnce({
|
|
288
|
+
ok: true,
|
|
289
|
+
json: async () => ({
|
|
290
|
+
usd: { eur: 0.85 }
|
|
291
|
+
}),
|
|
292
|
+
} as Response);
|
|
293
|
+
|
|
294
|
+
const result = await hiveRates.fetchRates();
|
|
295
|
+
expect(result).toBe(true);
|
|
296
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should handle all fiat endpoints failing', async () => {
|
|
300
|
+
mockFetch.mockRejectedValue(new Error('All endpoints failed'));
|
|
301
|
+
|
|
302
|
+
// This should still succeed because crypto fetch succeeds
|
|
303
|
+
const result = await hiveRates.fetchRates();
|
|
304
|
+
expect(result).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should validate fiat response format', async () => {
|
|
308
|
+
mockFetch.mockResolvedValue({
|
|
309
|
+
ok: true,
|
|
310
|
+
json: async () => ({ invalid: 'format' }),
|
|
311
|
+
} as Response);
|
|
312
|
+
|
|
313
|
+
// Should still succeed because crypto fetch succeeds, fiat just fails silently
|
|
314
|
+
const result = await hiveRates.fetchRates();
|
|
315
|
+
expect(result).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should filter invalid fiat rates', async () => {
|
|
319
|
+
mockFetch.mockResolvedValue({
|
|
320
|
+
ok: true,
|
|
321
|
+
json: async () => ({
|
|
322
|
+
usd: {
|
|
323
|
+
eur: 0.85,
|
|
324
|
+
invalid: 'string',
|
|
325
|
+
nan: NaN,
|
|
326
|
+
negative: -1,
|
|
327
|
+
zero: 0
|
|
328
|
+
}
|
|
329
|
+
}),
|
|
330
|
+
} as Response);
|
|
331
|
+
|
|
332
|
+
await hiveRates.fetchRates();
|
|
333
|
+
|
|
334
|
+
const fiatRates = hiveRates.getFiatRates();
|
|
335
|
+
expect(fiatRates.EUR).toBe(0.85);
|
|
336
|
+
expect(fiatRates.INVALID).toBeUndefined();
|
|
337
|
+
expect(fiatRates.NAN).toBeUndefined();
|
|
338
|
+
expect(fiatRates.NEGATIVE).toBeUndefined();
|
|
339
|
+
expect(fiatRates.ZERO).toBeUndefined();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should convert currency codes to uppercase', async () => {
|
|
343
|
+
mockFetch.mockResolvedValue({
|
|
344
|
+
ok: true,
|
|
345
|
+
json: async () => ({
|
|
346
|
+
usd: {
|
|
347
|
+
eur: 0.85,
|
|
348
|
+
gbp: 0.73
|
|
349
|
+
}
|
|
350
|
+
}),
|
|
351
|
+
} as Response);
|
|
352
|
+
|
|
353
|
+
await hiveRates.fetchRates();
|
|
354
|
+
|
|
355
|
+
const fiatRates = hiveRates.getFiatRates();
|
|
356
|
+
expect(fiatRates.EUR).toBe(0.85);
|
|
357
|
+
expect(fiatRates.GBP).toBe(0.73);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('error handling', () => {
|
|
362
|
+
it('should throw RatesError for unexpected errors', async () => {
|
|
363
|
+
// Mock an unexpected error in fetchCryptoRates
|
|
364
|
+
const originalMethod = hiveRates['fetchCryptoRates'];
|
|
365
|
+
hiveRates['fetchCryptoRates'] = jest.fn().mockImplementation(() => {
|
|
366
|
+
throw new TypeError('Unexpected error');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await expect(hiveRates.fetchRates()).rejects.toThrow(RatesError);
|
|
370
|
+
await expect(hiveRates.fetchRates()).rejects.toThrow('Unexpected error');
|
|
371
|
+
|
|
372
|
+
// Restore original method
|
|
373
|
+
hiveRates['fetchCryptoRates'] = originalMethod;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should preserve known error types', async () => {
|
|
377
|
+
mockFetch.mockResolvedValue({
|
|
378
|
+
ok: true,
|
|
379
|
+
json: async () => ({ invalid: 'format' }),
|
|
380
|
+
} as Response);
|
|
381
|
+
|
|
382
|
+
// Should still succeed because crypto fetch succeeds, fiat just fails silently
|
|
383
|
+
const result = await hiveRates.fetchRates();
|
|
384
|
+
expect(result).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('edge cases', () => {
|
|
389
|
+
it('should handle empty fiat rates response', async () => {
|
|
390
|
+
mockFetch.mockResolvedValue({
|
|
391
|
+
ok: true,
|
|
392
|
+
json: async () => ({
|
|
393
|
+
usd: {}
|
|
394
|
+
}),
|
|
395
|
+
} as Response);
|
|
396
|
+
|
|
397
|
+
// Should still succeed because crypto fetch succeeds, fiat just fails silently
|
|
398
|
+
const result = await hiveRates.fetchRates();
|
|
399
|
+
expect(result).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('should handle no valid crypto rates', async () => {
|
|
403
|
+
mockExchange.mockHiveRate = 0;
|
|
404
|
+
mockExchange.mockHbdRate = 0;
|
|
405
|
+
|
|
406
|
+
mockFetch.mockResolvedValue({
|
|
407
|
+
ok: true,
|
|
408
|
+
json: async () => ({
|
|
409
|
+
usd: { eur: 0.85 }
|
|
410
|
+
}),
|
|
411
|
+
} as Response);
|
|
412
|
+
|
|
413
|
+
const result = await hiveRates.fetchRates();
|
|
414
|
+
expect(result).toBe(true);
|
|
415
|
+
|
|
416
|
+
const cryptoRates = hiveRates.getCryptoRates();
|
|
417
|
+
expect(cryptoRates.hive).toBeUndefined();
|
|
418
|
+
expect(cryptoRates.hbd).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should handle partial crypto rates', async () => {
|
|
422
|
+
mockExchange.mockHiveRate = 0.5;
|
|
423
|
+
mockExchange.mockHbdRate = 0; // Invalid
|
|
424
|
+
|
|
425
|
+
mockFetch.mockResolvedValue({
|
|
426
|
+
ok: true,
|
|
427
|
+
json: async () => ({
|
|
428
|
+
usd: { eur: 0.85 }
|
|
429
|
+
}),
|
|
430
|
+
} as Response);
|
|
431
|
+
|
|
432
|
+
await hiveRates.fetchRates();
|
|
433
|
+
|
|
434
|
+
const cryptoRates = hiveRates.getCryptoRates();
|
|
435
|
+
expect(cryptoRates.hive).toBe(0.5);
|
|
436
|
+
expect(cryptoRates.hbd).toBeUndefined();
|
|
437
|
+
|
|
438
|
+
// Should still calculate HIVE rates but not HBD rates
|
|
439
|
+
expect(hiveRates.getRate('EUR', 'HIVE')).toBeCloseTo(0.5 * 0.85);
|
|
440
|
+
expect(hiveRates.getRate('EUR', 'HBD')).toBeNull();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { HiveRates } from '../../src/hive-rates';
|
|
2
|
+
|
|
3
|
+
const shouldRunIntegration = process.env.RUN_INTEGRATION === 'true';
|
|
4
|
+
|
|
5
|
+
describe('HiveRates Integration Test', () => {
|
|
6
|
+
(shouldRunIntegration ? it : it.skip)('should fetch real rates successfully', async () => {
|
|
7
|
+
const hiveRates = new HiveRates();
|
|
8
|
+
|
|
9
|
+
const result = await hiveRates.fetchRates();
|
|
10
|
+
expect(result).toBe(true);
|
|
11
|
+
|
|
12
|
+
// Check that we have some crypto rates
|
|
13
|
+
const cryptoRates = hiveRates.getCryptoRates();
|
|
14
|
+
expect(cryptoRates.hive).toBeGreaterThan(0);
|
|
15
|
+
expect(cryptoRates.hbd).toBeGreaterThan(0);
|
|
16
|
+
|
|
17
|
+
// Check that we have some fiat rates
|
|
18
|
+
const fiatRates = hiveRates.getFiatRates();
|
|
19
|
+
expect(Object.keys(fiatRates).length).toBeGreaterThan(0);
|
|
20
|
+
expect(fiatRates.EUR).toBeGreaterThan(0);
|
|
21
|
+
|
|
22
|
+
// Check that cross rates are calculated
|
|
23
|
+
const eurHiveRate = hiveRates.getRate('EUR', 'HIVE');
|
|
24
|
+
expect(eurHiveRate).toBeGreaterThan(0);
|
|
25
|
+
|
|
26
|
+
const gbpHbdRate = hiveRates.getRate('GBP', 'HBD');
|
|
27
|
+
expect(gbpHbdRate).toBeGreaterThan(0);
|
|
28
|
+
|
|
29
|
+
console.log('Sample rates:');
|
|
30
|
+
console.log(`HIVE: $${cryptoRates.hive?.toFixed(6)}`);
|
|
31
|
+
console.log(`HBD: $${cryptoRates.hbd?.toFixed(6)}`);
|
|
32
|
+
console.log(`EUR/HIVE: ${eurHiveRate?.toFixed(6)}`);
|
|
33
|
+
console.log(`GBP/HBD: ${gbpHbdRate?.toFixed(6)}`);
|
|
34
|
+
}, 30000); // 30 second timeout for real API calls
|
|
35
|
+
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { TimeAction } from '../src/actions';
|
|
2
2
|
import { Streamer } from '../src/streamer';
|
|
3
|
+
import { action, defineContract } from '../src/contracts/contract';
|
|
3
4
|
|
|
4
5
|
describe('Streamer Time-based Actions', () => {
|
|
5
6
|
let streamer: Streamer;
|
|
6
7
|
let mockAdapter: any;
|
|
7
8
|
let mockContract: any;
|
|
9
|
+
let testHandler: jest.Mock;
|
|
10
|
+
let asyncTestHandler: jest.Mock;
|
|
8
11
|
|
|
9
12
|
beforeEach(async () => {
|
|
10
13
|
mockAdapter = {
|
|
@@ -23,12 +26,20 @@ describe('Streamer Time-based Actions', () => {
|
|
|
23
26
|
replace: jest.fn().mockResolvedValue(true)
|
|
24
27
|
};
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
testHandler = jest.fn();
|
|
30
|
+
asyncTestHandler = jest.fn().mockResolvedValue(true);
|
|
31
|
+
|
|
32
|
+
mockContract = defineContract({
|
|
33
|
+
name: 'testcontract',
|
|
34
|
+
hooks: {
|
|
35
|
+
create: jest.fn(),
|
|
36
|
+
destroy: jest.fn()
|
|
37
|
+
},
|
|
38
|
+
actions: {
|
|
39
|
+
testMethod: action(testHandler, { trigger: 'time' }),
|
|
40
|
+
asyncTestMethod: action(asyncTestHandler, { trigger: 'time' })
|
|
41
|
+
}
|
|
42
|
+
});
|
|
32
43
|
|
|
33
44
|
streamer = new Streamer({
|
|
34
45
|
JSON_ID: 'testing',
|
|
@@ -36,7 +47,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
36
47
|
});
|
|
37
48
|
|
|
38
49
|
await streamer.registerAdapter(mockAdapter);
|
|
39
|
-
streamer.registerContract(
|
|
50
|
+
await streamer.registerContract(mockContract);
|
|
40
51
|
});
|
|
41
52
|
|
|
42
53
|
afterEach(async () => {
|
|
@@ -75,11 +86,11 @@ describe('Streamer Time-based Actions', () => {
|
|
|
75
86
|
);
|
|
76
87
|
});
|
|
77
88
|
|
|
78
|
-
test('Should throw error when registering action for non-existent
|
|
89
|
+
test('Should throw error when registering action for non-existent action', async () => {
|
|
79
90
|
const action = new TimeAction('1m', 'test-action', 'testcontract', 'nonexistentMethod');
|
|
80
91
|
|
|
81
92
|
await expect(streamer.registerAction(action)).rejects.toThrow(
|
|
82
|
-
'
|
|
93
|
+
'Action \'nonexistentMethod\' not found in contract \'testcontract\' for action \'test-action\''
|
|
83
94
|
);
|
|
84
95
|
});
|
|
85
96
|
});
|
|
@@ -166,7 +177,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
166
177
|
|
|
167
178
|
await streamer['processActions']();
|
|
168
179
|
|
|
169
|
-
expect(
|
|
180
|
+
expect(testHandler).toHaveBeenCalledWith({ testData: 'value' }, expect.any(Object));
|
|
170
181
|
expect(testAction.executionCount).toBe(1);
|
|
171
182
|
expect(testAction.lastExecution).toBeInstanceOf(Date);
|
|
172
183
|
});
|
|
@@ -180,7 +191,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
180
191
|
|
|
181
192
|
await streamer['processActions']();
|
|
182
193
|
|
|
183
|
-
expect(
|
|
194
|
+
expect(testHandler).not.toHaveBeenCalled();
|
|
184
195
|
expect(testAction.executionCount).toBe(0);
|
|
185
196
|
});
|
|
186
197
|
|
|
@@ -190,7 +201,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
190
201
|
|
|
191
202
|
await streamer['processActions']();
|
|
192
203
|
|
|
193
|
-
expect(
|
|
204
|
+
expect(testHandler).not.toHaveBeenCalled();
|
|
194
205
|
});
|
|
195
206
|
|
|
196
207
|
test('Should not execute actions that have reached max executions', async () => {
|
|
@@ -200,11 +211,11 @@ describe('Streamer Time-based Actions', () => {
|
|
|
200
211
|
|
|
201
212
|
await streamer['processActions']();
|
|
202
213
|
|
|
203
|
-
expect(
|
|
214
|
+
expect(testHandler).not.toHaveBeenCalled();
|
|
204
215
|
});
|
|
205
216
|
|
|
206
217
|
test('Should handle contract method errors gracefully', async () => {
|
|
207
|
-
|
|
218
|
+
testHandler.mockImplementation(() => {
|
|
208
219
|
throw new Error('Contract method error');
|
|
209
220
|
});
|
|
210
221
|
|
|
@@ -213,7 +224,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
213
224
|
// Should not throw, but log error
|
|
214
225
|
await expect(streamer['processActions']()).resolves.toBeUndefined();
|
|
215
226
|
|
|
216
|
-
expect(
|
|
227
|
+
expect(testHandler).toHaveBeenCalled();
|
|
217
228
|
// Action should not increment execution count on error
|
|
218
229
|
expect(testAction.executionCount).toBe(0);
|
|
219
230
|
});
|
|
@@ -224,7 +235,7 @@ describe('Streamer Time-based Actions', () => {
|
|
|
224
235
|
|
|
225
236
|
await streamer['processActions']();
|
|
226
237
|
|
|
227
|
-
expect(
|
|
238
|
+
expect(testHandler).toHaveBeenCalled();
|
|
228
239
|
expect(streamer.getActions()).toHaveLength(0); // Action should be removed
|
|
229
240
|
});
|
|
230
241
|
});
|
|
@@ -255,9 +266,9 @@ describe('Streamer Time-based Actions', () => {
|
|
|
255
266
|
|
|
256
267
|
await streamer['processActions']();
|
|
257
268
|
|
|
258
|
-
expect(
|
|
269
|
+
expect(testHandler).toHaveBeenCalled();
|
|
259
270
|
expect(action.executionCount).toBe(1);
|
|
260
271
|
});
|
|
261
272
|
});
|
|
262
273
|
});
|
|
263
|
-
});
|
|
274
|
+
});
|