@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.
- package/.env.example +2 -2
- package/CHANGELOG.md +18 -0
- package/dist/cjs/factories.d.ts.map +1 -1
- package/dist/cjs/factories.js +14 -8
- package/dist/cjs/factories.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/factories.d.ts.map +1 -1
- package/dist/esm/factories.js +14 -8
- package/dist/esm/factories.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -19
- package/src/factories.ts +36 -10
- package/test/k6/scenarios/wallets.test.js +1 -1
- package/test/k6/scenarios/web-socket.test.js +84 -20
- package/test/long-running/webSocket.test.ts +4 -2
- package/test/wallet_epoch_3/PersonalWallet/conwayTransactions.test.ts +6 -6
- package/test/ws-server/webSocket.test.ts +197 -28
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
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
|
|
63
|
-
|
|
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 =
|
|
98
|
-
|
|
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
|
|
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
|
|
114
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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
|
-
|
|
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
|
|
164
|
-
openServer(
|
|
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
|
|
194
|
+
const health = await listenToClientHealthFor5Seconds();
|
|
171
195
|
|
|
172
|
-
// Considering the server performs timeouts check every
|
|
173
|
-
// We expect the heath state of the client goes up and down more
|
|
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
|
|
183
|
-
openServer(
|
|
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(
|
|
211
|
+
openClient({ heartbeatInterval: 1 });
|
|
188
212
|
await wsProviderReady(client);
|
|
189
213
|
|
|
190
|
-
const health = await
|
|
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
|
});
|