hive-stream 3.0.2 → 3.0.4

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 (202) hide show
  1. package/DOCUMENTATION.md +50 -2
  2. package/README.md +282 -4
  3. package/dist/adapters/base.adapter.d.ts +5 -0
  4. package/dist/adapters/base.adapter.js +9 -0
  5. package/dist/adapters/base.adapter.js.map +1 -1
  6. package/dist/adapters/mongodb.adapter.d.ts +6 -6
  7. package/dist/adapters/mongodb.adapter.js +36 -21
  8. package/dist/adapters/mongodb.adapter.js.map +1 -1
  9. package/dist/adapters/postgresql.adapter.d.ts +7 -0
  10. package/dist/adapters/postgresql.adapter.js +46 -19
  11. package/dist/adapters/postgresql.adapter.js.map +1 -1
  12. package/dist/adapters/sqlite.adapter.d.ts +4 -0
  13. package/dist/adapters/sqlite.adapter.js +10 -0
  14. package/dist/adapters/sqlite.adapter.js.map +1 -1
  15. package/dist/api.d.ts +13 -3
  16. package/dist/api.js +96 -62
  17. package/dist/api.js.map +1 -1
  18. package/dist/builders.d.ts +176 -0
  19. package/dist/builders.js +727 -0
  20. package/dist/builders.js.map +1 -0
  21. package/dist/config.d.ts +16 -1
  22. package/dist/config.js +95 -3
  23. package/dist/config.js.map +1 -1
  24. package/dist/contracts/auctionhouse.contract.d.ts +4 -0
  25. package/dist/contracts/auctionhouse.contract.js +234 -0
  26. package/dist/contracts/auctionhouse.contract.js.map +1 -0
  27. package/dist/contracts/booking.contract.d.ts +4 -0
  28. package/dist/contracts/booking.contract.js +225 -0
  29. package/dist/contracts/booking.contract.js.map +1 -0
  30. package/dist/contracts/bountyboard.contract.d.ts +4 -0
  31. package/dist/contracts/bountyboard.contract.js +233 -0
  32. package/dist/contracts/bountyboard.contract.js.map +1 -0
  33. package/dist/contracts/bundlemarketplace.contract.d.ts +4 -0
  34. package/dist/contracts/bundlemarketplace.contract.js +195 -0
  35. package/dist/contracts/bundlemarketplace.contract.js.map +1 -0
  36. package/dist/contracts/charitymatch.contract.d.ts +4 -0
  37. package/dist/contracts/charitymatch.contract.js +172 -0
  38. package/dist/contracts/charitymatch.contract.js.map +1 -0
  39. package/dist/contracts/coinflip.contract.js +25 -22
  40. package/dist/contracts/coinflip.contract.js.map +1 -1
  41. package/dist/contracts/crowdfund.contract.d.ts +4 -0
  42. package/dist/contracts/crowdfund.contract.js +290 -0
  43. package/dist/contracts/crowdfund.contract.js.map +1 -0
  44. package/dist/contracts/dcabot.contract.d.ts +4 -0
  45. package/dist/contracts/dcabot.contract.js +217 -0
  46. package/dist/contracts/dcabot.contract.js.map +1 -0
  47. package/dist/contracts/dice.contract.js +25 -22
  48. package/dist/contracts/dice.contract.js.map +1 -1
  49. package/dist/contracts/domainregistry.contract.d.ts +4 -0
  50. package/dist/contracts/domainregistry.contract.js +232 -0
  51. package/dist/contracts/domainregistry.contract.js.map +1 -0
  52. package/dist/contracts/exchange.contract.js +209 -168
  53. package/dist/contracts/exchange.contract.js.map +1 -1
  54. package/dist/contracts/fanclub.contract.d.ts +4 -0
  55. package/dist/contracts/fanclub.contract.js +193 -0
  56. package/dist/contracts/fanclub.contract.js.map +1 -0
  57. package/dist/contracts/giftcard.contract.d.ts +4 -0
  58. package/dist/contracts/giftcard.contract.js +158 -0
  59. package/dist/contracts/giftcard.contract.js.map +1 -0
  60. package/dist/contracts/grantrounds.contract.d.ts +4 -0
  61. package/dist/contracts/grantrounds.contract.js +265 -0
  62. package/dist/contracts/grantrounds.contract.js.map +1 -0
  63. package/dist/contracts/groupbuy.contract.d.ts +4 -0
  64. package/dist/contracts/groupbuy.contract.js +198 -0
  65. package/dist/contracts/groupbuy.contract.js.map +1 -0
  66. package/dist/contracts/helpers.d.ts +66 -0
  67. package/dist/contracts/helpers.js +166 -0
  68. package/dist/contracts/helpers.js.map +1 -0
  69. package/dist/contracts/insurancepool.contract.d.ts +4 -0
  70. package/dist/contracts/insurancepool.contract.js +281 -0
  71. package/dist/contracts/insurancepool.contract.js.map +1 -0
  72. package/dist/contracts/invoice.contract.d.ts +4 -0
  73. package/dist/contracts/invoice.contract.js +193 -0
  74. package/dist/contracts/invoice.contract.js.map +1 -0
  75. package/dist/contracts/launchpad.contract.d.ts +4 -0
  76. package/dist/contracts/launchpad.contract.js +225 -0
  77. package/dist/contracts/launchpad.contract.js.map +1 -0
  78. package/dist/contracts/lotto.contract.js +53 -37
  79. package/dist/contracts/lotto.contract.js.map +1 -1
  80. package/dist/contracts/multisigtreasury.contract.d.ts +4 -0
  81. package/dist/contracts/multisigtreasury.contract.js +245 -0
  82. package/dist/contracts/multisigtreasury.contract.js.map +1 -0
  83. package/dist/contracts/nft.contract.d.ts +1 -0
  84. package/dist/contracts/nft.contract.js +234 -195
  85. package/dist/contracts/nft.contract.js.map +1 -1
  86. package/dist/contracts/oraclebounty.contract.d.ts +4 -0
  87. package/dist/contracts/oraclebounty.contract.js +250 -0
  88. package/dist/contracts/oraclebounty.contract.js.map +1 -0
  89. package/dist/contracts/payroll.contract.d.ts +4 -0
  90. package/dist/contracts/payroll.contract.js +232 -0
  91. package/dist/contracts/payroll.contract.js.map +1 -0
  92. package/dist/contracts/paywall.contract.d.ts +4 -0
  93. package/dist/contracts/paywall.contract.js +185 -0
  94. package/dist/contracts/paywall.contract.js.map +1 -0
  95. package/dist/contracts/poll.contract.js +2 -0
  96. package/dist/contracts/poll.contract.js.map +1 -1
  97. package/dist/contracts/predictionmarket.contract.d.ts +4 -0
  98. package/dist/contracts/predictionmarket.contract.js +213 -0
  99. package/dist/contracts/predictionmarket.contract.js.map +1 -0
  100. package/dist/contracts/proposaltimelock.contract.d.ts +4 -0
  101. package/dist/contracts/proposaltimelock.contract.js +250 -0
  102. package/dist/contracts/proposaltimelock.contract.js.map +1 -0
  103. package/dist/contracts/questpass.contract.d.ts +4 -0
  104. package/dist/contracts/questpass.contract.js +214 -0
  105. package/dist/contracts/questpass.contract.js.map +1 -0
  106. package/dist/contracts/referral.contract.d.ts +4 -0
  107. package/dist/contracts/referral.contract.js +238 -0
  108. package/dist/contracts/referral.contract.js.map +1 -0
  109. package/dist/contracts/rental.contract.d.ts +4 -0
  110. package/dist/contracts/rental.contract.js +221 -0
  111. package/dist/contracts/rental.contract.js.map +1 -0
  112. package/dist/contracts/revenuesplit.contract.d.ts +4 -0
  113. package/dist/contracts/revenuesplit.contract.js +211 -0
  114. package/dist/contracts/revenuesplit.contract.js.map +1 -0
  115. package/dist/contracts/rps.contract.js +48 -20
  116. package/dist/contracts/rps.contract.js.map +1 -1
  117. package/dist/contracts/savings.contract.d.ts +4 -0
  118. package/dist/contracts/savings.contract.js +208 -0
  119. package/dist/contracts/savings.contract.js.map +1 -0
  120. package/dist/contracts/subscription.contract.d.ts +4 -0
  121. package/dist/contracts/subscription.contract.js +241 -0
  122. package/dist/contracts/subscription.contract.js.map +1 -0
  123. package/dist/contracts/sweepstakes.contract.d.ts +4 -0
  124. package/dist/contracts/sweepstakes.contract.js +209 -0
  125. package/dist/contracts/sweepstakes.contract.js.map +1 -0
  126. package/dist/contracts/ticketing.contract.d.ts +4 -0
  127. package/dist/contracts/ticketing.contract.js +185 -0
  128. package/dist/contracts/ticketing.contract.js.map +1 -0
  129. package/dist/contracts/tipjar.contract.js +2 -0
  130. package/dist/contracts/tipjar.contract.js.map +1 -1
  131. package/dist/contracts/token.contract.js +135 -125
  132. package/dist/contracts/token.contract.js.map +1 -1
  133. package/dist/index.d.ts +40 -0
  134. package/dist/index.js +72 -1
  135. package/dist/index.js.map +1 -1
  136. package/dist/metadata.d.ts +20 -0
  137. package/dist/metadata.js +320 -1
  138. package/dist/metadata.js.map +1 -1
  139. package/dist/providers/block-provider.d.ts +22 -0
  140. package/dist/providers/block-provider.js +3 -0
  141. package/dist/providers/block-provider.js.map +1 -0
  142. package/dist/providers/haf-client.d.ts +30 -0
  143. package/dist/providers/haf-client.js +119 -0
  144. package/dist/providers/haf-client.js.map +1 -0
  145. package/dist/providers/haf-provider.d.ts +49 -0
  146. package/dist/providers/haf-provider.js +256 -0
  147. package/dist/providers/haf-provider.js.map +1 -0
  148. package/dist/providers/hive-provider.d.ts +13 -0
  149. package/dist/providers/hive-provider.js +25 -0
  150. package/dist/providers/hive-provider.js.map +1 -0
  151. package/dist/providers/index.d.ts +4 -0
  152. package/dist/providers/index.js +21 -0
  153. package/dist/providers/index.js.map +1 -0
  154. package/dist/streamer.d.ts +65 -4
  155. package/dist/streamer.js +768 -72
  156. package/dist/streamer.js.map +1 -1
  157. package/dist/types/hive-stream.d.ts +317 -0
  158. package/dist/utils.d.ts +33 -0
  159. package/dist/utils.js +198 -2
  160. package/dist/utils.js.map +1 -1
  161. package/package.json +16 -1
  162. package/.claude/settings.local.json +0 -12
  163. package/.env.example +0 -3
  164. package/.travis.yml +0 -11
  165. package/AGENTS.md +0 -35
  166. package/CLAUDE.md +0 -75
  167. package/ecosystem.config.js +0 -17
  168. package/examples/contracts/README.md +0 -8
  169. package/examples/contracts/exchange.ts +0 -38
  170. package/examples/contracts/poll.ts +0 -21
  171. package/examples/contracts/rps.ts +0 -19
  172. package/examples/contracts/tipjar.ts +0 -19
  173. package/jest.config.js +0 -9
  174. package/test-contract-block.md +0 -19
  175. package/tests/actions.spec.ts +0 -252
  176. package/tests/adapters/actions-persistence.spec.ts +0 -144
  177. package/tests/adapters/postgresql.adapter.spec.ts +0 -127
  178. package/tests/adapters/sqlite.adapter.spec.ts +0 -181
  179. package/tests/config-input.spec.ts +0 -90
  180. package/tests/contracts/coinflip.contract.spec.ts +0 -94
  181. package/tests/contracts/dice.contract.spec.ts +0 -87
  182. package/tests/contracts/entrants.json +0 -729
  183. package/tests/contracts/exchange.contract.spec.ts +0 -84
  184. package/tests/contracts/lotto.contract.spec.ts +0 -59
  185. package/tests/contracts/nft.contract.spec.ts +0 -948
  186. package/tests/contracts/token.contract.spec.ts +0 -90
  187. package/tests/exchanges/coingecko.exchange.spec.ts +0 -169
  188. package/tests/exchanges/exchange.base.spec.ts +0 -246
  189. package/tests/helpers/mock-adapter.ts +0 -214
  190. package/tests/helpers/mock-fetch.ts +0 -165
  191. package/tests/hive-chain-features.spec.ts +0 -319
  192. package/tests/hive-rates.spec.ts +0 -443
  193. package/tests/integration/hive-rates.integration.spec.ts +0 -35
  194. package/tests/metadata.spec.ts +0 -63
  195. package/tests/setup.ts +0 -30
  196. package/tests/streamer-actions.spec.ts +0 -274
  197. package/tests/streamer.spec.ts +0 -342
  198. package/tests/types/rates.spec.ts +0 -216
  199. package/tests/utils.spec.ts +0 -113
  200. package/tsconfig.build.json +0 -4
  201. package/tslint.json +0 -21
  202. package/wallaby.js +0 -26
@@ -1,90 +0,0 @@
1
- import { createTokenContract } from '../../src/contracts/token.contract';
2
- import { Streamer } from '../../src/streamer';
3
- import { createMockAdapter } from '../helpers/mock-adapter';
4
-
5
- const createContext = (streamer: Streamer, adapter: any, sender: string) => ({
6
- trigger: 'custom_json' as const,
7
- streamer,
8
- adapter,
9
- config: streamer['config'],
10
- block: { number: 123, id: 'block123', previousId: 'prevblock123', time: new Date() },
11
- transaction: { id: 'txn123' },
12
- sender,
13
- customJson: { id: 'hivestream', json: {}, isSignedWithActiveKey: true }
14
- });
15
-
16
- describe('TokenContract', () => {
17
- let streamer: Streamer;
18
- let adapter: any;
19
- let contract: ReturnType<typeof createTokenContract>;
20
-
21
- beforeEach(async () => {
22
- streamer = new Streamer();
23
- adapter = createMockAdapter();
24
- await streamer.registerAdapter(adapter);
25
- contract = createTokenContract();
26
- await streamer.registerContract(contract);
27
- });
28
-
29
- afterEach(async () => {
30
- await streamer.stop();
31
- });
32
-
33
- test('Creates a token', async () => {
34
- const ctx = createContext(streamer, adapter, 'alice');
35
- const payload = {
36
- symbol: 'TEST',
37
- name: 'Test Token',
38
- url: 'https://example.com',
39
- precision: 3,
40
- maxSupply: '1000000'
41
- };
42
-
43
- await contract.actions.createToken.handler(payload, ctx);
44
-
45
- expect(adapter.queries.join(' ')).toContain('INSERT INTO tokens');
46
- expect(adapter.events.length).toBeGreaterThan(0);
47
- expect(adapter.events[0].action).toBe('createToken');
48
- });
49
-
50
- test('Rejects duplicate token symbols', async () => {
51
- adapter.setTestContext({ existingToken: 'TEST' });
52
- const ctx = createContext(streamer, adapter, 'alice');
53
- const payload = {
54
- symbol: 'TEST',
55
- name: 'Test Token',
56
- maxSupply: '1000000'
57
- };
58
-
59
- await expect(contract.actions.createToken.handler(payload, ctx))
60
- .rejects
61
- .toThrow('Token with symbol TEST already exists');
62
- });
63
-
64
- test('Prevents non-creators from issuing tokens', async () => {
65
- const ctx = createContext(streamer, adapter, 'bob');
66
- const payload = {
67
- symbol: 'TEST',
68
- to: 'carol',
69
- amount: '100'
70
- };
71
-
72
- await expect(contract.actions.issueTokens.handler(payload, ctx))
73
- .rejects
74
- .toThrow('Only the token creator can issue new tokens');
75
- });
76
-
77
- test('Prevents transfers with insufficient balance', async () => {
78
- adapter.setTestContext({ insufficientBalance: true });
79
- const ctx = createContext(streamer, adapter, 'alice');
80
- const payload = {
81
- symbol: 'TEST',
82
- to: 'bob',
83
- amount: '9999'
84
- };
85
-
86
- await expect(contract.actions.transferTokens.handler(payload, ctx))
87
- .rejects
88
- .toThrow('Insufficient balance');
89
- });
90
- });
@@ -1,169 +0,0 @@
1
- import { CoinGeckoExchange } from '../../src/exchanges/coingecko';
2
- import { NetworkError, ValidationError } from '../../src/types/rates';
3
- import { mockSuccessfulApis, mockNetworkErrors, mockHttpErrors, mockInvalidResponses, cleanupMocks } from '../helpers/mock-fetch';
4
-
5
- describe('CoinGeckoExchange', () => {
6
- let exchange: CoinGeckoExchange;
7
-
8
- beforeEach(() => {
9
- exchange = new CoinGeckoExchange();
10
- });
11
-
12
- afterEach(() => {
13
- cleanupMocks();
14
- });
15
-
16
- describe('fetchRates', () => {
17
- it('should fetch rates successfully', async () => {
18
- mockSuccessfulApis();
19
-
20
- const success = await exchange.fetchRates();
21
-
22
- expect(success).toBe(true);
23
- expect(exchange.rateUsdHive).toBe(0.25);
24
- expect(exchange.rateUsdHbd).toBe(1.00);
25
- });
26
-
27
- it('should handle network errors', async () => {
28
- mockNetworkErrors();
29
-
30
- await expect(exchange.fetchRates()).rejects.toThrow(NetworkError);
31
- });
32
-
33
- it('should handle HTTP errors', async () => {
34
- mockHttpErrors(500);
35
-
36
- await expect(exchange.fetchRates()).rejects.toThrow(NetworkError);
37
- });
38
-
39
- it('should handle invalid JSON responses', async () => {
40
- mockInvalidResponses();
41
-
42
- await expect(exchange.fetchRates()).rejects.toThrow(ValidationError);
43
- });
44
-
45
- it('should retry on failure', async () => {
46
- const retryExchange = new CoinGeckoExchange({ maxRetries: 3, retryDelay: 100 });
47
-
48
- // Mock first two calls to fail, third to succeed
49
- let callCount = 0;
50
- global.fetch = jest.fn().mockImplementation(() => {
51
- callCount++;
52
- if (callCount <= 2) {
53
- return Promise.reject(new Error('Network error'));
54
- }
55
- return Promise.resolve({
56
- ok: true,
57
- json: () => Promise.resolve({
58
- hive: { usd: 0.25 },
59
- 'hive_dollar': { usd: 1.00 }
60
- })
61
- });
62
- });
63
-
64
- const success = await retryExchange.updateRates();
65
-
66
- expect(success).toBe(true);
67
- expect(retryExchange.rateUsdHive).toBe(0.25);
68
- expect(callCount).toBe(3);
69
- }, 10000); // Increase timeout for retry test
70
-
71
- it('should respect timeout', async () => {
72
- const timeoutExchange = new CoinGeckoExchange({ timeout: 100 });
73
-
74
- // Mock a delayed response
75
- global.fetch = jest.fn().mockImplementation(() =>
76
- new Promise((resolve) => {
77
- setTimeout(() => resolve({
78
- ok: true,
79
- json: () => Promise.resolve({ hive: { usd: 0.25 } })
80
- }), 200); // Longer than timeout
81
- })
82
- );
83
-
84
- await expect(timeoutExchange.fetchRates()).rejects.toThrow();
85
- });
86
-
87
- it('should validate rate values', async () => {
88
- // Mock response with invalid rates
89
- global.fetch = jest.fn().mockResolvedValue({
90
- ok: true,
91
- json: () => Promise.resolve({
92
- hive: { usd: -1 }, // Invalid negative rate
93
- 'hive_dollar': { usd: 1.00 }
94
- })
95
- });
96
-
97
- await expect(exchange.fetchRates()).rejects.toThrow(ValidationError);
98
- });
99
-
100
- it('should handle missing rate data', async () => {
101
- // Mock response with missing hive data
102
- global.fetch = jest.fn().mockResolvedValue({
103
- ok: true,
104
- json: () => Promise.resolve({
105
- 'hive_dollar': { usd: 1.00 }
106
- // Missing hive data
107
- })
108
- });
109
-
110
- await expect(exchange.fetchRates()).rejects.toThrow(ValidationError);
111
- });
112
- });
113
-
114
- describe('caching', () => {
115
- it('should use cached rates when valid', async () => {
116
- mockSuccessfulApis();
117
-
118
- // First fetch
119
- const rates1 = await exchange.updateRates();
120
-
121
- // Second fetch should use cache
122
- const rates2 = await exchange.updateRates();
123
-
124
- expect(rates1).toBe(true);
125
- expect(rates2).toBe(false);
126
- expect(global.fetch).toHaveBeenCalledTimes(1);
127
- });
128
-
129
- it('should refresh cache when expired', async () => {
130
- const shortCacheExchange = new CoinGeckoExchange({ cacheDuration: 100 });
131
- mockSuccessfulApis();
132
-
133
- // First fetch
134
- await shortCacheExchange.updateRates();
135
-
136
- // Wait for cache to expire
137
- await new Promise(resolve => setTimeout(resolve, 150));
138
-
139
- // Second fetch should hit API again
140
- await shortCacheExchange.updateRates();
141
-
142
- expect(global.fetch).toHaveBeenCalledTimes(2);
143
- });
144
-
145
- it('should report cache validity correctly', async () => {
146
- mockSuccessfulApis();
147
-
148
- expect(exchange.isCacheValid()).toBe(false);
149
-
150
- await exchange.updateRates();
151
-
152
- expect(exchange.isCacheValid()).toBe(true);
153
- });
154
-
155
- it('should report last fetch time', async () => {
156
- mockSuccessfulApis();
157
-
158
- expect(exchange.getLastFetchTime()).toBeUndefined();
159
-
160
- const beforeFetch = Date.now();
161
- await exchange.updateRates();
162
- const afterFetch = Date.now();
163
-
164
- const lastFetchTime = exchange.getLastFetchTime();
165
- expect(lastFetchTime).toBeGreaterThanOrEqual(beforeFetch);
166
- expect(lastFetchTime).toBeLessThanOrEqual(afterFetch);
167
- });
168
- });
169
- });
@@ -1,246 +0,0 @@
1
- import { Exchange } from '../../src/exchanges/exchange';
2
- import { NetworkError, RateConfig } from '../../src/types/rates';
3
-
4
- // Mock fetch for tests
5
- const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
6
- global.fetch = mockFetch;
7
-
8
- class TestExchange extends Exchange {
9
- public readonly exchangeId = 'test-exchange';
10
- public shouldSucceed = true;
11
- public fetchCallCount = 0;
12
-
13
- public async fetchRates(): Promise<boolean> {
14
- this.fetchCallCount++;
15
-
16
- if (!this.shouldSucceed) {
17
- throw new Error('Test error');
18
- }
19
-
20
- this.rateUsdHive = 0.5;
21
- this.rateUsdHbd = 1.0;
22
- return true;
23
- }
24
- }
25
-
26
- describe('Exchange', () => {
27
- let exchange: TestExchange;
28
-
29
- beforeEach(() => {
30
- exchange = new TestExchange({ cacheDuration: 100, maxRetries: 2, retryDelay: 10 });
31
- mockFetch.mockClear();
32
- jest.clearAllTimers();
33
- jest.useFakeTimers();
34
- });
35
-
36
- afterEach(() => {
37
- jest.useRealTimers();
38
- });
39
-
40
- describe('constructor', () => {
41
- it('should use default config when none provided', () => {
42
- const exchange = new TestExchange();
43
- expect(exchange.isCacheValid()).toBe(false);
44
- });
45
-
46
- it('should accept custom config', () => {
47
- const config: RateConfig = {
48
- cacheDuration: 5000,
49
- maxRetries: 5,
50
- retryDelay: 2000,
51
- timeout: 15000
52
- };
53
-
54
- const exchange = new TestExchange(config);
55
- expect(exchange).toBeDefined();
56
- });
57
- });
58
-
59
- describe('updateRates', () => {
60
- it('should fetch rates successfully', async () => {
61
- const result = await exchange.updateRates();
62
-
63
- expect(result).toBe(true);
64
- expect(exchange.fetchCallCount).toBe(1);
65
- expect(exchange.rateUsdHive).toBe(0.5);
66
- expect(exchange.rateUsdHbd).toBe(1.0);
67
- });
68
-
69
- it('should return false when cache is valid', async () => {
70
- // First fetch
71
- await exchange.updateRates();
72
- expect(exchange.fetchCallCount).toBe(1);
73
-
74
- // Second fetch should use cache
75
- const result = await exchange.updateRates();
76
- expect(result).toBe(false);
77
- expect(exchange.fetchCallCount).toBe(1);
78
- });
79
-
80
- it('should update when cache expires', async () => {
81
- // First fetch
82
- await exchange.updateRates();
83
- expect(exchange.fetchCallCount).toBe(1);
84
-
85
- // Advance time beyond cache duration
86
- jest.advanceTimersByTime(150);
87
-
88
- // Second fetch should happen
89
- const result = await exchange.updateRates();
90
- expect(result).toBe(true);
91
- expect(exchange.fetchCallCount).toBe(2);
92
- });
93
-
94
- it('should retry on failure', async () => {
95
- let callCount = 0;
96
- exchange.fetchRates = jest.fn().mockImplementation(() => {
97
- callCount++;
98
- if (callCount === 1) {
99
- throw new Error('Test error');
100
- }
101
- exchange.rateUsdHive = 0.5;
102
- exchange.rateUsdHbd = 1.0;
103
- return Promise.resolve(true);
104
- });
105
-
106
- const resultPromise = exchange.updateRates();
107
- await jest.runAllTimersAsync();
108
- const result = await resultPromise;
109
- expect(result).toBe(true);
110
- expect(exchange.fetchRates).toHaveBeenCalledTimes(2);
111
- }, 10000);
112
-
113
- it('should throw NetworkError after max retries', async () => {
114
- exchange.shouldSucceed = false;
115
-
116
- const resultPromise = exchange.updateRates();
117
- const expectation = expect(resultPromise).rejects.toThrow(NetworkError);
118
- const messageExpectation = expect(resultPromise).rejects.toThrow('Failed to fetch rates after 2 attempts');
119
- await jest.runAllTimersAsync();
120
- await expectation;
121
- await messageExpectation;
122
- expect(exchange.fetchCallCount).toBe(2);
123
- }, 10000);
124
- });
125
-
126
- describe('fetchWithTimeout', () => {
127
- beforeEach(() => {
128
- // Reset to use real fetch for this test
129
- (global.fetch as any) = mockFetch;
130
- });
131
-
132
- it('should fetch successfully within timeout', async () => {
133
- mockFetch.mockResolvedValueOnce({
134
- ok: true,
135
- json: async () => ({ test: 'data' }),
136
- } as Response);
137
-
138
- const response = await exchange['fetchWithTimeout']('https://example.com');
139
- expect(response.ok).toBe(true);
140
- });
141
-
142
- it('should throw NetworkError for HTTP errors', async () => {
143
- mockFetch.mockResolvedValueOnce({
144
- ok: false,
145
- status: 404,
146
- statusText: 'Not Found',
147
- } as Response);
148
-
149
- await expect(exchange['fetchWithTimeout']('https://example.com'))
150
- .rejects.toThrow(NetworkError);
151
- });
152
-
153
- it('should include proper headers', async () => {
154
- mockFetch.mockResolvedValueOnce({
155
- ok: true,
156
- } as Response);
157
-
158
- await exchange['fetchWithTimeout']('https://example.com');
159
-
160
- expect(mockFetch).toHaveBeenCalledWith(
161
- 'https://example.com',
162
- expect.objectContaining({
163
- headers: {
164
- 'User-Agent': 'hive-stream/3.0.0',
165
- 'Accept': 'application/json',
166
- },
167
- })
168
- );
169
- });
170
-
171
- it('should handle timeout', async () => {
172
- const exchange = new TestExchange({ timeout: 50 });
173
-
174
- // Mock fetch to never resolve (timeout scenario)
175
- mockFetch.mockImplementationOnce((_, options) =>
176
- new Promise((_, reject) => {
177
- const signal = options?.signal as AbortSignal | undefined;
178
- if (signal) {
179
- signal.addEventListener('abort', () => {
180
- reject(new Error('Aborted'));
181
- });
182
- }
183
- })
184
- );
185
-
186
- const timeoutPromise = exchange['fetchWithTimeout']('https://example.com');
187
- const timeoutExpectation = expect(timeoutPromise).rejects.toThrow();
188
- await jest.advanceTimersByTimeAsync(60);
189
- await timeoutExpectation;
190
- }, 1000);
191
- });
192
-
193
- describe('utility methods', () => {
194
- it('should track last fetch time correctly', async () => {
195
- expect(exchange.getLastFetchTime()).toBeUndefined();
196
-
197
- await exchange.updateRates();
198
-
199
- const fetchTime = exchange.getLastFetchTime();
200
- expect(fetchTime).toBeDefined();
201
- expect(typeof fetchTime).toBe('number');
202
- });
203
-
204
- it('should report cache validity correctly', async () => {
205
- expect(exchange.isCacheValid()).toBe(false);
206
-
207
- await exchange.updateRates();
208
- expect(exchange.isCacheValid()).toBe(true);
209
-
210
- jest.advanceTimersByTime(150);
211
- expect(exchange.isCacheValid()).toBe(false);
212
- });
213
-
214
- it('should handle cache check with no previous fetch', () => {
215
- expect(exchange.isCacheValid()).toBe(false);
216
- });
217
- });
218
-
219
- describe('error handling', () => {
220
- it('should preserve original error types in retries', async () => {
221
- let callCount = 0;
222
- exchange.fetchRates = jest.fn().mockImplementation(() => {
223
- callCount++;
224
- if (callCount === 1) {
225
- throw new NetworkError('First failure', 'test');
226
- }
227
- return Promise.resolve(true);
228
- });
229
-
230
- const resultPromise = exchange.updateRates();
231
- await jest.runAllTimersAsync();
232
- const result = await resultPromise;
233
- expect(result).toBe(true);
234
- expect(exchange.fetchRates).toHaveBeenCalledTimes(2);
235
- }, 10000);
236
-
237
- it('should handle non-Error rejections', async () => {
238
- exchange.fetchRates = jest.fn().mockRejectedValue('string error');
239
-
240
- const resultPromise = exchange.updateRates();
241
- const expectation = expect(resultPromise).rejects.toThrow(NetworkError);
242
- await jest.runAllTimersAsync();
243
- await expectation;
244
- }, 10000);
245
- });
246
- });