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
|
@@ -1,334 +1,90 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
+
});
|
|
3
15
|
|
|
4
16
|
describe('TokenContract', () => {
|
|
5
|
-
let
|
|
6
|
-
let
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
tokenContract = new TokenContract();
|
|
16
|
-
tokenContract._instance = mockStreamer;
|
|
17
|
-
tokenContract.updateBlockInfo(12345, 'block123', 'prevblock123', 'txn123');
|
|
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);
|
|
18
27
|
});
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
mockAdapter.reset();
|
|
23
|
-
tokenContract.create();
|
|
24
|
-
mockAdapter.setQueryResult([]); // No existing token with same symbol
|
|
25
|
-
|
|
26
|
-
const payload = {
|
|
27
|
-
symbol: 'TEST',
|
|
28
|
-
name: 'Test Token',
|
|
29
|
-
precision: 3,
|
|
30
|
-
maxSupply: '1000000'
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
await (tokenContract as any).createToken(payload, { sender: 'alice' });
|
|
34
|
-
|
|
35
|
-
const insertQuery = mockAdapter.queries.find(q => q.includes('INSERT INTO tokens'));
|
|
36
|
-
expect(insertQuery).toBeDefined();
|
|
37
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
38
|
-
expect(mockAdapter.events[0].action).toBe('createToken');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should reject invalid token symbol', async () => {
|
|
42
|
-
const payload = {
|
|
43
|
-
symbol: 'invalid-symbol',
|
|
44
|
-
name: 'Test Token',
|
|
45
|
-
maxSupply: '1000000'
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await expect((tokenContract as any).createToken(payload, { sender: 'alice' }))
|
|
49
|
-
.rejects.toThrow('Symbol must be 1-10 uppercase alphanumeric characters');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should reject invalid max supply', async () => {
|
|
53
|
-
const payload = {
|
|
54
|
-
symbol: 'TEST',
|
|
55
|
-
name: 'Test Token',
|
|
56
|
-
maxSupply: '0'
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
await expect((tokenContract as any).createToken(payload, { sender: 'alice' }))
|
|
60
|
-
.rejects.toThrow('Maximum supply must be between 1 and 9007199254740991');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should reject max supply too large', async () => {
|
|
64
|
-
const payload = {
|
|
65
|
-
symbol: 'TEST',
|
|
66
|
-
name: 'Test Token',
|
|
67
|
-
maxSupply: '9007199254740992'
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
await expect((tokenContract as any).createToken(payload, { sender: 'alice' }))
|
|
71
|
-
.rejects.toThrow('Maximum supply must be between 1 and 9007199254740991');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should reject invalid precision', async () => {
|
|
75
|
-
const payload = {
|
|
76
|
-
symbol: 'TEST',
|
|
77
|
-
name: 'Test Token',
|
|
78
|
-
precision: 9,
|
|
79
|
-
maxSupply: '1000000'
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
await expect((tokenContract as any).createToken(payload, { sender: 'alice' }))
|
|
83
|
-
.rejects.toThrow('Precision must be between 0 and 8');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should create token with URL', async () => {
|
|
87
|
-
mockAdapter.reset();
|
|
88
|
-
tokenContract.create();
|
|
89
|
-
mockAdapter.setQueryResult([]); // No existing token with same symbol
|
|
90
|
-
|
|
91
|
-
const payload = {
|
|
92
|
-
symbol: 'TEST',
|
|
93
|
-
name: 'Test Token',
|
|
94
|
-
url: 'https://example.com/token',
|
|
95
|
-
precision: 3,
|
|
96
|
-
maxSupply: '1000000'
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
await (tokenContract as any).createToken(payload, { sender: 'alice' });
|
|
100
|
-
|
|
101
|
-
const insertQuery = mockAdapter.queries.find(q => q.includes('INSERT INTO tokens'));
|
|
102
|
-
expect(insertQuery).toBeDefined();
|
|
103
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
104
|
-
expect(mockAdapter.events[0].action).toBe('createToken');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should reject URL too long', async () => {
|
|
108
|
-
const longUrl = 'https://example.com/' + 'a'.repeat(250);
|
|
109
|
-
const payload = {
|
|
110
|
-
symbol: 'TEST',
|
|
111
|
-
name: 'Test Token',
|
|
112
|
-
url: longUrl,
|
|
113
|
-
maxSupply: '1000000'
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
await expect((tokenContract as any).createToken(payload, { sender: 'alice' }))
|
|
117
|
-
.rejects.toThrow('URL must be 256 characters or less');
|
|
118
|
-
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await streamer.stop();
|
|
119
31
|
});
|
|
120
32
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
creator: 'alice',
|
|
131
|
-
precision: 3,
|
|
132
|
-
current_supply: '0',
|
|
133
|
-
max_supply: '1000000'
|
|
134
|
-
}]);
|
|
135
|
-
|
|
136
|
-
const payload = {
|
|
137
|
-
symbol: 'TEST',
|
|
138
|
-
to: 'bob',
|
|
139
|
-
amount: '100',
|
|
140
|
-
memo: 'Initial distribution'
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
await (tokenContract as any).issueTokens(payload, { sender: 'alice' });
|
|
144
|
-
|
|
145
|
-
const updateQuery = mockAdapter.queries.find(q => q.includes('UPDATE tokens SET current_supply'));
|
|
146
|
-
const insertQuery = mockAdapter.queries.find(q => q.includes('INSERT INTO token_balances'));
|
|
147
|
-
expect(updateQuery).toBeDefined();
|
|
148
|
-
expect(insertQuery).toBeDefined();
|
|
149
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
150
|
-
expect(mockAdapter.events[0].action).toBe('issueTokens');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should reject issuance by non-creator', async () => {
|
|
154
|
-
mockAdapter.reset();
|
|
155
|
-
tokenContract.create();
|
|
156
|
-
|
|
157
|
-
mockAdapter.setQueryResult([{
|
|
158
|
-
symbol: 'TEST',
|
|
159
|
-
name: 'Test Token',
|
|
160
|
-
creator: 'alice',
|
|
161
|
-
precision: 3,
|
|
162
|
-
current_supply: '0',
|
|
163
|
-
max_supply: '1000000'
|
|
164
|
-
}]);
|
|
165
|
-
|
|
166
|
-
const payload = {
|
|
167
|
-
symbol: 'TEST',
|
|
168
|
-
to: 'bob',
|
|
169
|
-
amount: '100'
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
await expect((tokenContract as any).issueTokens(payload, { sender: 'charlie' }))
|
|
173
|
-
.rejects.toThrow('Only the token creator can issue new tokens');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should reject issuance exceeding max supply', async () => {
|
|
177
|
-
mockAdapter.reset();
|
|
178
|
-
mockAdapter.setTestContext({ maxSupplyExceeded: true });
|
|
179
|
-
tokenContract.create();
|
|
180
|
-
|
|
181
|
-
mockAdapter.setQueryResult([{
|
|
182
|
-
symbol: 'TEST',
|
|
183
|
-
name: 'Test Token',
|
|
184
|
-
creator: 'alice',
|
|
185
|
-
precision: 3,
|
|
186
|
-
current_supply: '999999',
|
|
187
|
-
max_supply: '1000000'
|
|
188
|
-
}]);
|
|
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
|
+
};
|
|
189
42
|
|
|
190
|
-
|
|
191
|
-
symbol: 'TEST',
|
|
192
|
-
to: 'bob',
|
|
193
|
-
amount: '100'
|
|
194
|
-
};
|
|
43
|
+
await contract.actions.createToken.handler(payload, ctx);
|
|
195
44
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
45
|
+
expect(adapter.queries.join(' ')).toContain('INSERT INTO tokens');
|
|
46
|
+
expect(adapter.events.length).toBeGreaterThan(0);
|
|
47
|
+
expect(adapter.events[0].action).toBe('createToken');
|
|
199
48
|
});
|
|
200
49
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
amount: '100',
|
|
210
|
-
memo: 'Payment'
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
await (tokenContract as any).transferTokens(payload, { sender: 'alice' });
|
|
214
|
-
|
|
215
|
-
const updateQuery = mockAdapter.queries.find(q => q.includes('UPDATE token_balances SET balance'));
|
|
216
|
-
const insertQuery = mockAdapter.queries.find(q => q.includes('INSERT INTO token_transfers'));
|
|
217
|
-
expect(updateQuery).toBeDefined();
|
|
218
|
-
expect(insertQuery).toBeDefined();
|
|
219
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
220
|
-
expect(mockAdapter.events[0].action).toBe('transferTokens');
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('should reject transfer with insufficient balance', async () => {
|
|
224
|
-
mockAdapter.reset();
|
|
225
|
-
mockAdapter.setTestContext({ insufficientBalance: true });
|
|
226
|
-
tokenContract.create();
|
|
227
|
-
|
|
228
|
-
const payload = {
|
|
229
|
-
symbol: 'TEST',
|
|
230
|
-
to: 'bob',
|
|
231
|
-
amount: '100'
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
await expect((tokenContract as any).transferTokens(payload, { sender: 'alice' }))
|
|
235
|
-
.rejects.toThrow('Insufficient balance');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should reject transfer of non-existent token', async () => {
|
|
239
|
-
mockAdapter.reset();
|
|
240
|
-
mockAdapter.setTestContext({ nonExistentToken: 'NONEXISTENT' });
|
|
241
|
-
tokenContract.create();
|
|
242
|
-
|
|
243
|
-
const payload = {
|
|
244
|
-
symbol: 'NONEXISTENT',
|
|
245
|
-
to: 'bob',
|
|
246
|
-
amount: '100'
|
|
247
|
-
};
|
|
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
|
+
};
|
|
248
58
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
59
|
+
await expect(contract.actions.createToken.handler(payload, ctx))
|
|
60
|
+
.rejects
|
|
61
|
+
.toThrow('Token with symbol TEST already exists');
|
|
252
62
|
});
|
|
253
63
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const payload = {
|
|
262
|
-
account: 'alice',
|
|
263
|
-
symbol: 'TEST'
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
await (tokenContract as any).getBalance(payload, { sender: 'bob' });
|
|
267
|
-
|
|
268
|
-
const selectQuery = mockAdapter.queries.find(q => q.includes('SELECT balance FROM token_balances'));
|
|
269
|
-
expect(selectQuery).toBeDefined();
|
|
270
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
271
|
-
expect(mockAdapter.events[0].action).toBe('getBalance');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('should return zero balance for non-existent account', async () => {
|
|
275
|
-
mockAdapter.reset();
|
|
276
|
-
mockAdapter.setTestContext({ zeroBalance: true });
|
|
277
|
-
tokenContract.create();
|
|
278
|
-
|
|
279
|
-
const payload = {
|
|
280
|
-
account: 'alice',
|
|
281
|
-
symbol: 'TEST'
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
await (tokenContract as any).getBalance(payload, { sender: 'bob' });
|
|
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
|
+
};
|
|
285
71
|
|
|
286
|
-
|
|
287
|
-
|
|
72
|
+
await expect(contract.actions.issueTokens.handler(payload, ctx))
|
|
73
|
+
.rejects
|
|
74
|
+
.toThrow('Only the token creator can issue new tokens');
|
|
288
75
|
});
|
|
289
76
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
url: 'https://example.com/token',
|
|
299
|
-
precision: 3,
|
|
300
|
-
max_supply: '1000000',
|
|
301
|
-
current_supply: '500000',
|
|
302
|
-
creator: 'alice',
|
|
303
|
-
created_at: new Date()
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
mockAdapter.setQueryResult([tokenData]);
|
|
307
|
-
|
|
308
|
-
const payload = {
|
|
309
|
-
symbol: 'TEST'
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
await (tokenContract as any).getTokenInfo(payload, { sender: 'bob' });
|
|
313
|
-
|
|
314
|
-
const selectQuery = mockAdapter.queries.find(q => q.includes('SELECT * FROM tokens'));
|
|
315
|
-
expect(selectQuery).toBeDefined();
|
|
316
|
-
expect(mockAdapter.events.length).toBe(1);
|
|
317
|
-
expect(mockAdapter.events[0].action).toBe('getTokenInfo');
|
|
318
|
-
expect(mockAdapter.events[0].data.data.token_info).toEqual(tokenData);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('should reject request for non-existent token', async () => {
|
|
322
|
-
mockAdapter.reset();
|
|
323
|
-
mockAdapter.setTestContext({ nonExistentToken: 'NONEXISTENT' });
|
|
324
|
-
tokenContract.create();
|
|
325
|
-
|
|
326
|
-
const payload = {
|
|
327
|
-
symbol: 'NONEXISTENT'
|
|
328
|
-
};
|
|
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
|
+
};
|
|
329
85
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
86
|
+
await expect(contract.actions.transferTokens.handler(payload, ctx))
|
|
87
|
+
.rejects
|
|
88
|
+
.toThrow('Insufficient balance');
|
|
333
89
|
});
|
|
334
|
-
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
});
|