@cardano-sdk/e2e 0.43.0 → 0.44.0

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.
@@ -1,4 +1,12 @@
1
- import { CardanoWsClient } from '@cardano-sdk/cardano-services-client';
1
+ // cSpell:ignore cardano utxos
2
+
3
+ import { Cardano, HealthCheckResponse } from '@cardano-sdk/core';
4
+ import {
5
+ CardanoWsClient,
6
+ WsProvider,
7
+ chainHistoryHttpProvider,
8
+ utxoHttpProvider
9
+ } from '@cardano-sdk/cardano-services-client';
2
10
  import {
3
11
  CardanoWsServer,
4
12
  GenesisData,
@@ -6,16 +14,18 @@ import {
6
14
  getOgmiosCardanoNode,
7
15
  util
8
16
  } from '@cardano-sdk/cardano-services';
9
- import { HealthCheckResponse, WsProvider } from '@cardano-sdk/core';
10
17
  import { OgmiosCardanoNode } from '@cardano-sdk/ogmios';
11
18
  import { Pool } from 'pg';
12
19
  import { filter, firstValueFrom } from 'rxjs';
13
20
  import { getEnv, walletVariables } from '../../src';
14
21
  import { getPort } from 'get-port-please';
15
22
  import { logger } from '@cardano-sdk/util-dev';
23
+ import { toSerializableObject } from '@cardano-sdk/util';
16
24
 
17
25
  const env = getEnv([...walletVariables, 'DB_SYNC_CONNECTION_STRING', 'OGMIOS_URL']);
18
26
 
27
+ const pagination = { limit: 25, startAt: 0 };
28
+
19
29
  const wsProviderReady = (provider: WsProvider) =>
20
30
  new Promise<void>((resolve, reject) => {
21
31
  // eslint-disable-next-line prefer-const
@@ -30,6 +40,7 @@ const wsProviderReady = (provider: WsProvider) =>
30
40
  });
31
41
 
32
42
  timeout = setTimeout(() => {
43
+ timeout = undefined;
33
44
  subscription.unsubscribe();
34
45
  reject(new Error('WsProvider timeout'));
35
46
  }, 10_000);
@@ -52,15 +63,13 @@ const wsProviderReadyAgain = (provider: WsProvider, close: () => Promise<unknown
52
63
  subscription.unsubscribe();
53
64
 
54
65
  try {
55
- const [, ...last] = oks;
56
-
57
66
  // The first emitted event is the ok buffered one
58
- // next we expect at least one not ok event i.e. the close function had the desired effect
59
- // last we expect one more ok event when provider is operational once again
60
- expect(oks.length).toBeGreaterThanOrEqual(3);
61
67
  expect(oks[0]).toBeTruthy();
62
- expect(oks[1]).toBeFalsy();
63
- expect(last.some((result) => result));
68
+ // Next we expect at least one not ok event i.e. the close function had the desired effect
69
+ const firstFalsy = oks.findIndex((element) => !element);
70
+ expect(firstFalsy).toBeGreaterThan(0);
71
+ // Last we expect one more ok event when provider is operational once again
72
+ expect(oks.findIndex((element, index) => element && index > firstFalsy)).toBeGreaterThan(firstFalsy);
64
73
 
65
74
  resolve();
66
75
  } catch (error) {
@@ -72,12 +81,14 @@ const wsProviderReadyAgain = (provider: WsProvider, close: () => Promise<unknown
72
81
  try {
73
82
  await close();
74
83
  } catch (error) {
75
- reject(error);
84
+ return reject(error);
76
85
  }
77
86
 
78
87
  closed = true;
79
88
 
89
+ // eslint-disable-next-line sonarjs/no-identical-functions
80
90
  timeout = setTimeout(() => {
91
+ timeout = undefined;
81
92
  subscription.unsubscribe();
82
93
  reject(new Error('WsProvider timeout'));
83
94
  }, 10_000);
@@ -86,6 +97,9 @@ const wsProviderReadyAgain = (provider: WsProvider, close: () => Promise<unknown
86
97
  });
87
98
 
88
99
  describe('Web Socket', () => {
100
+ const chainHistoryProvider = chainHistoryHttpProvider({ logger, ...env.TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS });
101
+ const utxoProvider = utxoHttpProvider({ logger, ...env.TEST_CLIENT_CHAIN_HISTORY_PROVIDER_PARAMS });
102
+
89
103
  let db: Pool;
90
104
  let cardanoNode: OgmiosCardanoNode;
91
105
  let genesisData: GenesisData;
@@ -94,35 +108,43 @@ describe('Web Socket', () => {
94
108
  let client: CardanoWsClient;
95
109
  let server: CardanoWsServer;
96
110
 
97
- const openClient = (heartbeatInterval = 55) =>
98
- (client = new CardanoWsClient({ logger }, { heartbeatInterval, url: new URL(`ws://localhost:${port}/ws`) }));
111
+ const openClient = (options: { heartbeatInterval?: number; url?: string } = {}) => {
112
+ const { heartbeatInterval, url } = { heartbeatInterval: 55, url: `ws://localhost:${port}/ws`, ...options };
113
+
114
+ return (client = new CardanoWsClient({ chainHistoryProvider, logger }, { heartbeatInterval, url: new URL(url) }));
115
+ };
99
116
 
100
117
  const openServer = (heartbeatTimeout = 60) =>
101
118
  (server = new CardanoWsServer(
102
119
  { cardanoNode, db, genesisData, logger },
103
- { dbCacheTtl: 120, heartbeatTimeout, port }
120
+ { dbCacheTtl: 120, heartbeatInterval: 1, heartbeatTimeout, port }
104
121
  ));
105
122
 
106
123
  const closeClient = () => (client ? client.close() : Promise.resolve());
107
124
  const closeServer = () => (server ? new Promise<void>((resolve) => server.close(resolve)) : Promise.resolve());
108
125
 
109
- const listenToClientHealthFor15Seconds = async () => {
126
+ const listenToClientHealthFor5Seconds = async () => {
110
127
  const health: HealthCheckResponse[] = [];
111
128
  const subscription = client.health$.subscribe((value) => health.push(value));
112
129
 
113
- // Listen on client.health$ for 15"
114
- await new Promise((resolve) => setTimeout(resolve, 15_000));
130
+ // Listen on client.health$ for 5"
131
+ await new Promise((resolve) => setTimeout(resolve, 5000));
115
132
 
116
133
  subscription.unsubscribe();
117
134
 
118
135
  return health;
119
136
  };
120
137
 
138
+ const transactionsByAddresses = () =>
139
+ client.chainHistoryProvider.transactionsByAddresses({
140
+ addresses: ['fake_address' as Cardano.PaymentAddress],
141
+ pagination
142
+ });
143
+
121
144
  beforeAll(async () => {
122
145
  const dnsResolver = createDnsResolver({ factor: 1.1, maxRetryTime: 1000 }, logger);
123
146
 
124
147
  cardanoNode = await getOgmiosCardanoNode(dnsResolver, logger, { ogmiosUrl: new URL(env.OGMIOS_URL) });
125
- db = new Pool({ connectionString: env.DB_SYNC_CONNECTION_STRING });
126
148
  genesisData = await util.loadGenesisData('local-network/config/network/cardano-node/config.json');
127
149
  port = await getPort();
128
150
 
@@ -130,9 +152,11 @@ describe('Web Socket', () => {
130
152
  await cardanoNode.start();
131
153
  });
132
154
 
133
- afterAll(() => Promise.all([cardanoNode.shutdown(), db.end()]));
155
+ beforeEach(() => (db = new Pool({ connectionString: env.DB_SYNC_CONNECTION_STRING })));
156
+
157
+ afterAll(() => cardanoNode.shutdown());
134
158
 
135
- afterEach(() => Promise.all([closeClient(), closeServer()]));
159
+ afterEach(() => Promise.all([db.end(), closeClient(), closeServer()]));
136
160
 
137
161
  it('Server can re-connect to DB if NOTIFY connection drops', async () => {
138
162
  // Close server db connection from DB server side
@@ -160,17 +184,17 @@ describe('Web Socket', () => {
160
184
  });
161
185
 
162
186
  it('Server disconnects clients on heartbeat timeout', async () => {
163
- // Open a server with 3" heartbeat timeout
164
- openServer(3);
187
+ // Open a server with 2" heartbeat timeout
188
+ openServer(2);
165
189
  await wsProviderReady(server);
166
190
 
167
191
  openClient();
168
192
  await wsProviderReady(client);
169
193
 
170
- const health = await listenToClientHealthFor15Seconds();
194
+ const health = await listenToClientHealthFor5Seconds();
171
195
 
172
- // Considering the server performs timeouts check every 10"
173
- // We expect the heath state of the client goes up and down more time
196
+ // Considering the server performs timeouts check every second
197
+ // We expect the heath state of the client goes up and down more times
174
198
  expect(health.length).toBeGreaterThanOrEqual(3);
175
199
  // We expect the heath state of the client goes up at least twice
176
200
  expect(health.filter(({ ok }) => ok).length).toBeGreaterThanOrEqual(2);
@@ -179,15 +203,15 @@ describe('Web Socket', () => {
179
203
  });
180
204
 
181
205
  it("Server doesn't disconnects clients without heartbeat timeouts", async () => {
182
- // Open a server with 3" heartbeat timeout
183
- openServer(3);
206
+ // Open a server with 2" heartbeat timeout
207
+ openServer(2);
184
208
  await wsProviderReady(server);
185
209
 
186
210
  // Open a client with 2" heartbeat interval
187
- openClient(2);
211
+ openClient({ heartbeatInterval: 1 });
188
212
  await wsProviderReady(client);
189
213
 
190
- const health = await listenToClientHealthFor15Seconds();
214
+ const health = await listenToClientHealthFor5Seconds();
191
215
 
192
216
  // We expect only the buffered ok heath state
193
217
  expect(health.length).toBe(1);
@@ -217,4 +241,149 @@ describe('Web Socket', () => {
217
241
  await expect(client.networkInfoProvider.ledgerTip()).rejects.toThrowError('CONNECTION_FAILURE');
218
242
  });
219
243
  });
244
+
245
+ describe('CardanoWsClient.chainHistoryProvider.transactionsByAddresses', () => {
246
+ // The first two tests are identical to CardanoWsClient.networkInfoProvider ones,
247
+ // they are anyway required because the code behind the two providers is completely different
248
+ it('It throws when disconnected but when starting', async () => {
249
+ openServer();
250
+ await wsProviderReady(server);
251
+
252
+ openClient();
253
+
254
+ await expect(transactionsByAddresses()).resolves.toHaveProperty('pageResults');
255
+
256
+ await closeServer();
257
+ await firstValueFrom(client.health$.pipe(filter(({ ok }) => !ok)));
258
+
259
+ await expect(transactionsByAddresses()).rejects.toThrowError('CONNECTION_FAILURE');
260
+ });
261
+
262
+ it('If called when still starting, it throws on connect error', async () => {
263
+ openClient();
264
+
265
+ await expect(transactionsByAddresses()).rejects.toThrowError('CONNECTION_FAILURE');
266
+ });
267
+
268
+ it('More calls while syncing address throw', async () => {
269
+ openServer();
270
+ await wsProviderReady(server);
271
+
272
+ openClient();
273
+ await wsProviderReady(client);
274
+
275
+ const deferred = async () => {
276
+ await new Promise((resolve) => setTimeout(resolve, 1));
277
+ await expect(transactionsByAddresses()).rejects.toThrowError('CONFLICT');
278
+ };
279
+
280
+ await Promise.all([
281
+ expect(transactionsByAddresses()).resolves.toHaveProperty('pageResults'),
282
+ deferred(),
283
+ deferred()
284
+ ]);
285
+ });
286
+
287
+ it('More calls after address is synced, never throw', async () => {
288
+ openServer();
289
+ await wsProviderReady(server);
290
+
291
+ openClient();
292
+ await wsProviderReady(client);
293
+
294
+ await expect(transactionsByAddresses()).resolves.toHaveProperty('pageResults');
295
+
296
+ await Promise.all([
297
+ expect(transactionsByAddresses()).resolves.toHaveProperty('pageResults'),
298
+ expect(transactionsByAddresses()).resolves.toHaveProperty('pageResults')
299
+ ]);
300
+ });
301
+ });
302
+
303
+ describe('transactions & utxos', () => {
304
+ const tests: string[][] = [
305
+ ['collaterals', 'SELECT tx_in_id AS tx_id FROM collateral_tx_in'],
306
+ ['collateralReturn', 'SELECT tx_id FROM collateral_tx_out'],
307
+ [
308
+ 'datum',
309
+ 'SELECT tx_id FROM tx_out LEFT JOIN tx_in ON tx_out_id = tx_id AND tx_out_index = index WHERE inline_datum_id IS NOT NULL AND tx_out_id IS NULL AND stake_address_id IS NOT NULL'
310
+ ],
311
+ ['failed phase 2 validation', 'SELECT id AS tx_id FROM tx WHERE valid_contract = false'],
312
+ ['mint', 'SELECT tx_id FROM ma_tx_mint'],
313
+ ['metadata', 'SELECT tx_id FROM tx_metadata'],
314
+ ['withdrawals', 'SELECT tx_id FROM withdrawal'],
315
+ ['redeemers', 'SELECT tx_id FROM redeemer'],
316
+ ['governance action proposals', 'SELECT tx_id FROM gov_action_proposal'],
317
+ ['voting procedures', 'SELECT tx_id FROM voting_procedure'],
318
+ ['certificate: stake pool registration', 'SELECT registered_tx_id AS tx_id FROM pool_update ORDER BY id DESC'],
319
+ ['certificate: stake pool retire', 'SELECT announced_tx_id AS tx_id FROM pool_retire'],
320
+ ['certificate: stake credential registration', 'SELECT tx_id FROM stake_registration ORDER BY id DESC'],
321
+ ['certificate: stake credential deregistration', 'SELECT tx_id FROM stake_deregistration'],
322
+ ['certificate: stake delegation', 'SELECT tx_id FROM delegation ORDER BY id DESC'],
323
+ ['certificate: vote delegation', 'SELECT tx_id FROM delegation_vote'],
324
+ ['certificate: delegation representative registration', 'SELECT tx_id FROM drep_registration WHERE deposit > 0'],
325
+ ['certificate: delegation representative update', 'SELECT tx_id FROM drep_registration WHERE deposit IS NULL'],
326
+ [
327
+ 'certificate: delegation representative deregistration',
328
+ 'SELECT tx_id FROM drep_registration WHERE deposit < 0'
329
+ ],
330
+ ['certificate: committee registration', 'SELECT tx_id FROM committee_registration'],
331
+ ['certificate: committee deregistration', 'SELECT tx_id FROM committee_de_registration']
332
+ ];
333
+
334
+ test.each(tests)('transactions with %s', async (name, subQuery) => {
335
+ // cSpell:disable
336
+ const query = `\
337
+ SELECT address, block_no::INTEGER AS "lowerBound" FROM (${subQuery} LIMIT 1) t, tx, tx_out o, block
338
+ WHERE tx.id = o.tx_id AND t.tx_id = o.tx_id AND block_id = block.id AND address NOT IN (
339
+ 'addr_test1wz3937ykmlcaqxkf4z7stxpsfwfn4re7ncy48yu8vutcpxgnj28k0', 'addr_test1wqmpwrh2mlqa04e2mf3vr8w9rjt9du0dpnync8dzc85spgsya8emz')`;
340
+ // cSpell:enable
341
+ const result = await db.query<{ address: Cardano.PaymentAddress; lowerBound: Cardano.BlockNo }>(query);
342
+ let step = '';
343
+
344
+ if (!result.rowCount) return logger.fatal(`Test 'transactions with ${name}': not valid transactions found`);
345
+
346
+ const { address, lowerBound } = result.rows[0];
347
+ const request = { addresses: [address], blockRange: { lowerBound }, pagination };
348
+
349
+ openClient({ url: env.WS_PROVIDER_URL });
350
+
351
+ try {
352
+ step = 'txs ws';
353
+ const wsTxs = await client.chainHistoryProvider.transactionsByAddresses(request);
354
+ step = 'txs http';
355
+ const httpTxs = await chainHistoryProvider.transactionsByAddresses(request);
356
+ step = 'txs test';
357
+ expect(toSerializableObject(wsTxs)).toEqual(toSerializableObject(httpTxs));
358
+
359
+ step = 'utxos ws';
360
+ const wsUtxos = await client.utxoProvider.utxoByAddresses(request);
361
+ step = 'utxos http';
362
+ const httpUtxos = await utxoProvider.utxoByAddresses(request);
363
+ step = 'utxos test';
364
+ expect(toSerializableObject(wsUtxos)).toEqual(toSerializableObject(httpUtxos));
365
+ } catch (error) {
366
+ logger.fatal(name, step, JSON.stringify(request));
367
+ throw error;
368
+ }
369
+ });
370
+
371
+ test('utxos from more addresses', async () => {
372
+ openClient({ url: env.WS_PROVIDER_URL });
373
+
374
+ const { rows } = await db.query<{ address: Cardano.PaymentAddress }>(`\
375
+ SELECT COUNT(DISTINCT tx_id), address FROM tx_out LEFT JOIN tx_in ON tx_out_id = tx_id AND tx_out_index = index
376
+ WHERE tx_out_id IS NULL GROUP BY address HAVING COUNT(DISTINCT tx_id) < 1000 ORDER BY COUNT(DISTINCT tx_id) DESC LIMIT 5`);
377
+
378
+ const ledgerTip = await firstValueFrom(client.networkInfo.ledgerTip$);
379
+ const lowerBound = Math.floor(ledgerTip.blockNo * 0.8) as Cardano.BlockNo;
380
+ const request = { addresses: rows.flatMap(({ address }) => address), blockRange: { lowerBound }, pagination };
381
+
382
+ await client.chainHistoryProvider.transactionsByAddresses(request);
383
+
384
+ const wsUtxos = await client.utxoProvider.utxoByAddresses(request);
385
+ const httpUtxos = await utxoProvider.utxoByAddresses(request);
386
+ expect(toSerializableObject(wsUtxos)).toEqual(toSerializableObject(httpUtxos));
387
+ });
388
+ });
220
389
  });