@cardano-sdk/e2e 0.24.0 → 0.26.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.
Files changed (78) hide show
  1. package/.env.example +1 -7
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +0 -29
  4. package/dist/cjs/environment.d.ts.map +1 -1
  5. package/dist/cjs/environment.js.map +1 -1
  6. package/dist/cjs/factories.d.ts.map +1 -1
  7. package/dist/cjs/factories.js +7 -1
  8. package/dist/cjs/factories.js.map +1 -1
  9. package/dist/cjs/measurement-util.d.ts.map +1 -1
  10. package/dist/cjs/measurement-util.js.map +1 -1
  11. package/dist/cjs/scripts/is-local-network-ready.js.map +1 -1
  12. package/dist/cjs/scripts/mnemonic.js.map +1 -1
  13. package/dist/cjs/tools/multi-delegation-data-gen/utils/files.d.ts.map +1 -1
  14. package/dist/cjs/tools/multi-delegation-data-gen/utils/files.js.map +1 -1
  15. package/dist/cjs/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.d.ts.map +1 -1
  16. package/dist/cjs/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.js.map +1 -1
  17. package/dist/cjs/tools/multi-delegation-data-gen/utils/utils.d.ts.map +1 -1
  18. package/dist/cjs/tools/multi-delegation-data-gen/utils/utils.js.map +1 -1
  19. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  20. package/dist/esm/environment.d.ts.map +1 -1
  21. package/dist/esm/environment.js.map +1 -1
  22. package/dist/esm/factories.d.ts.map +1 -1
  23. package/dist/esm/factories.js +7 -1
  24. package/dist/esm/factories.js.map +1 -1
  25. package/dist/esm/measurement-util.d.ts.map +1 -1
  26. package/dist/esm/measurement-util.js.map +1 -1
  27. package/dist/esm/scripts/is-local-network-ready.js.map +1 -1
  28. package/dist/esm/scripts/mnemonic.js.map +1 -1
  29. package/dist/esm/tools/multi-delegation-data-gen/utils/files.d.ts.map +1 -1
  30. package/dist/esm/tools/multi-delegation-data-gen/utils/files.js.map +1 -1
  31. package/dist/esm/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.d.ts.map +1 -1
  32. package/dist/esm/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.js.map +1 -1
  33. package/dist/esm/tools/multi-delegation-data-gen/utils/utils.d.ts.map +1 -1
  34. package/dist/esm/tools/multi-delegation-data-gen/utils/utils.js.map +1 -1
  35. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  36. package/docker-compose.yml +4 -0
  37. package/jest.config.js +0 -1
  38. package/local-network/scripts/common.sh +19 -0
  39. package/local-network/scripts/make-babbage.sh +2 -1
  40. package/local-network/scripts/mint-handles.sh +2 -18
  41. package/local-network/scripts/mint-tokens.sh +2 -18
  42. package/local-network/scripts/mnemonic_keys.sh +0 -0
  43. package/local-network/scripts/setup-wallets.sh +2 -17
  44. package/local-network/templates/babbage/submit-api-config.json +115 -0
  45. package/package.json +27 -29
  46. package/src/environment.ts +2 -6
  47. package/src/factories.ts +11 -5
  48. package/src/measurement-util.ts +1 -4
  49. package/src/scripts/is-local-network-ready.ts +1 -3
  50. package/src/scripts/mnemonic.ts +1 -3
  51. package/src/tools/multi-delegation-data-gen/utils/files.ts +1 -3
  52. package/src/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.ts +2 -6
  53. package/src/tools/multi-delegation-data-gen/utils/utils.ts +1 -3
  54. package/test/artillery/StakePoolSearch.ts +5 -16
  55. package/test/artillery/artillery.ts +20 -61
  56. package/test/artillery/wallet-restoration/WalletRestoration.ts +1 -3
  57. package/test/artillery/wallet-restoration/queries.ts +1 -3
  58. package/test/artillery/wallet-restoration/types.ts +1 -3
  59. package/test/load-test-custom/stake-pool-search/stake-pool-search.test.ts +1 -3
  60. package/test/load-test-custom/wallet-init/wallet-init.test.ts +10 -1
  61. package/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts +1 -4
  62. package/test/local-network/register-pool.test.ts +24 -14
  63. package/test/long-running/cache-invalidation.test.ts +7 -4
  64. package/test/long-running/multisig-wallet/MultiSigTx.ts +117 -0
  65. package/test/long-running/multisig-wallet/MultiSigWallet.ts +491 -0
  66. package/test/long-running/multisig-wallet/multisig-delegation-rewards.test.ts +318 -0
  67. package/test/projection/offline-fork.test.ts +47 -20
  68. package/test/projection/single-tenant-utxo.test.ts +40 -27
  69. package/test/providers/StakePoolProvider.test.ts +22 -17
  70. package/test/tsconfig.json +3 -0
  71. package/test/wallet/PersonalWallet/delegation.test.ts +6 -3
  72. package/test/wallet/PersonalWallet/handle.test.ts +2 -1
  73. package/test/wallet/PersonalWallet/mint.test.ts +2 -1
  74. package/test/wallet/PersonalWallet/multiAddress.test.ts +2 -1
  75. package/test/wallet/PersonalWallet/multisignature.test.ts +4 -2
  76. package/test/wallet/PersonalWallet/nft.test.ts +2 -1
  77. package/test/web-extension/extension/ui.ts +2 -8
  78. package/test/load-testing/tx-submit-load.test.ts +0 -341
@@ -0,0 +1,318 @@
1
+ import * as Crypto from '@cardano-sdk/crypto';
2
+ import { Cardano, EraSummary, StakePoolProvider, createSlotEpochCalc } from '@cardano-sdk/core';
3
+ import { InMemoryKeyAgent, KeyRole } from '@cardano-sdk/key-management';
4
+ import { MultiSigTx } from './MultiSigTx';
5
+ import { MultiSigWallet } from './MultiSigWallet';
6
+ import { Observable, filter, firstValueFrom, map, take } from 'rxjs';
7
+ import { PersonalWallet } from '@cardano-sdk/wallet';
8
+ import { TrackerSubject } from '@cardano-sdk/util-rxjs';
9
+ import {
10
+ bip32Ed25519Factory,
11
+ createStandaloneKeyAgent,
12
+ getEnv,
13
+ getWallet,
14
+ waitForEpoch,
15
+ walletReady,
16
+ walletVariables
17
+ } from '../../../src';
18
+ import { isNotNil } from '@cardano-sdk/util';
19
+ import { logger } from '@cardano-sdk/util-dev';
20
+
21
+ const env = getEnv(walletVariables);
22
+
23
+ // eslint-disable-next-line max-len
24
+ const aliceMnemonics =
25
+ 'decorate survey empower stairs pledge humble social leisure baby wrap grief exact monster rug dash kiss perfect select science light frame play swallow day';
26
+
27
+ // eslint-disable-next-line max-len
28
+ const bobMnemonics =
29
+ 'salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice';
30
+
31
+ // eslint-disable-next-line max-len
32
+ const charlotteMnemonics =
33
+ 'phrase raw learn suspect inmate powder combine apology regular hero gain chronic fruit ritual short screen goddess odor keen creek brand today kit machine';
34
+
35
+ const DERIVATION_PATH = {
36
+ index: 0,
37
+ role: KeyRole.External
38
+ };
39
+
40
+ const getPoolIds = async (stakePoolProvider: StakePoolProvider, count: number) => {
41
+ const activePools = await stakePoolProvider.queryStakePools({
42
+ filters: { pledgeMet: true, status: [Cardano.StakePoolStatus.Active] },
43
+ pagination: { limit: count, startAt: 0 }
44
+ });
45
+ expect(activePools.totalResultCount).toBeGreaterThanOrEqual(count);
46
+ const poolIds = activePools.pageResults.map(({ id }) => id);
47
+ expect(poolIds.every((poolId) => poolId !== undefined)).toBeTruthy();
48
+ logger.info('Wallet funds will be staked to pools:', poolIds);
49
+ return poolIds;
50
+ };
51
+
52
+ const fundMultiSigWallet = async (sendingWallet: PersonalWallet, address: Cardano.PaymentAddress) => {
53
+ logger.info(`Funding multisig wallet with address: ${address}`);
54
+
55
+ const tAdaToSend = 5_000_000n;
56
+
57
+ const txBuilder = sendingWallet.createTxBuilder();
58
+ const txOut = await txBuilder.buildOutput().address(address).coin(tAdaToSend).build();
59
+ const { tx: signedTx } = await txBuilder.addOutput(txOut).build().sign();
60
+ await sendingWallet.submitTx(signedTx);
61
+ };
62
+
63
+ const getKeyAgent = async (mnemonics: string, faucetWallet: PersonalWallet, bip32Ed25519: Crypto.Bip32Ed25519) => {
64
+ const genesis = await firstValueFrom(faucetWallet.genesisParameters$);
65
+
66
+ const keyAgent = await createStandaloneKeyAgent(mnemonics.split(' '), genesis, bip32Ed25519);
67
+
68
+ const pubKey = await keyAgent.derivePublicKey(DERIVATION_PATH);
69
+
70
+ return { keyAgent, pubKey };
71
+ };
72
+
73
+ const generateTxs = async (sendingWallet: PersonalWallet, receivingWallet: PersonalWallet) => {
74
+ logger.info('Sending 100 txs to generate reward fees');
75
+
76
+ const tAdaToSend = 5_000_000n;
77
+ const [{ address: receivingAddress }] = await firstValueFrom(receivingWallet.addresses$);
78
+
79
+ for (let i = 0; i < 100; i++) {
80
+ const txBuilder = sendingWallet.createTxBuilder();
81
+ const txOut = await txBuilder.buildOutput().address(receivingAddress).coin(tAdaToSend).build();
82
+ const { tx: signedTx } = await txBuilder.addOutput(txOut).build().sign();
83
+ await sendingWallet.submitTx(signedTx);
84
+ }
85
+ };
86
+
87
+ const createMultiSignWallet = async (
88
+ keyAgent: InMemoryKeyAgent,
89
+ faucetWallet: PersonalWallet,
90
+ participants: Array<Crypto.Ed25519PublicKeyHex>
91
+ ) => {
92
+ const props = {
93
+ chainHistoryProvider: faucetWallet.chainHistoryProvider,
94
+ expectedSigners: participants,
95
+ inMemoryKeyAgent: keyAgent,
96
+ networkId: env.KEY_MANAGEMENT_PARAMS.chainId.networkId,
97
+ networkInfoProvider: faucetWallet.networkInfoProvider,
98
+ pollingInterval: 50,
99
+ rewardsProvider: faucetWallet.rewardsProvider,
100
+ txSubmitProvider: faucetWallet.txSubmitProvider,
101
+ utxoProvider: faucetWallet.utxoProvider
102
+ };
103
+
104
+ return await MultiSigWallet.createMultiSigWallet(props);
105
+ };
106
+
107
+ const getTxConfirmationEpoch = async (
108
+ history$: Observable<Cardano.HydratedTx[]>,
109
+ tx: Cardano.Tx<Cardano.TxBody>,
110
+ eraSummaries$: TrackerSubject<EraSummary[]>
111
+ ) => {
112
+ const txs = await firstValueFrom(history$.pipe(filter((_) => _.some(({ id }) => id === tx.id))));
113
+ const observedTx = txs.find(({ id }) => id === tx.id);
114
+ const slotEpochCalc = createSlotEpochCalc(await firstValueFrom(eraSummaries$));
115
+
116
+ return slotEpochCalc(observedTx!.blockHeader.slot);
117
+ };
118
+
119
+ describe('multi signature wallet', () => {
120
+ let faucetWallet: PersonalWallet;
121
+ let aliceKeyAgent: InMemoryKeyAgent;
122
+ let bobKeyAgent: InMemoryKeyAgent;
123
+ let charlotteKeyAgent: InMemoryKeyAgent;
124
+ let alicePubKey: Crypto.Ed25519PublicKeyHex;
125
+ let bobPubKey: Crypto.Ed25519PublicKeyHex;
126
+ let charlottePubKey: Crypto.Ed25519PublicKeyHex;
127
+ let faucetAddress: Cardano.PaymentAddress;
128
+ let aliceMultiSigWallet: MultiSigWallet;
129
+ let bobMultiSigWallet: MultiSigWallet;
130
+ let charlotteMultiSigWallet: MultiSigWallet;
131
+ let multiSigParticipants: Crypto.Ed25519PublicKeyHex[];
132
+
133
+ const initializeFaucet = async () => {
134
+ ({ wallet: faucetWallet } = await getWallet({
135
+ env,
136
+ logger,
137
+ name: 'Faucet Wallet',
138
+ polling: { interval: 50 }
139
+ }));
140
+
141
+ await walletReady(faucetWallet);
142
+ };
143
+
144
+ beforeAll(async () => {
145
+ await initializeFaucet();
146
+ const bip32Ed25519 = await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger);
147
+
148
+ ({ keyAgent: aliceKeyAgent, pubKey: alicePubKey } = await getKeyAgent(aliceMnemonics, faucetWallet, bip32Ed25519));
149
+ ({ keyAgent: bobKeyAgent, pubKey: bobPubKey } = await getKeyAgent(bobMnemonics, faucetWallet, bip32Ed25519));
150
+ ({ keyAgent: charlotteKeyAgent, pubKey: charlottePubKey } = await getKeyAgent(
151
+ charlotteMnemonics,
152
+ faucetWallet,
153
+ bip32Ed25519
154
+ ));
155
+
156
+ faucetAddress = (await firstValueFrom(faucetWallet.addresses$))[0].address;
157
+
158
+ multiSigParticipants = [alicePubKey, bobPubKey, charlottePubKey];
159
+
160
+ aliceMultiSigWallet = await createMultiSignWallet(aliceKeyAgent, faucetWallet, multiSigParticipants);
161
+ bobMultiSigWallet = await createMultiSignWallet(bobKeyAgent, faucetWallet, multiSigParticipants);
162
+ charlotteMultiSigWallet = await createMultiSignWallet(charlotteKeyAgent, faucetWallet, multiSigParticipants);
163
+ });
164
+
165
+ afterAll(() => {
166
+ faucetWallet?.shutdown();
167
+ });
168
+
169
+ it('can receive balance and can spend balance', async () => {
170
+ expect(aliceMultiSigWallet.getPaymentAddress()).toEqual(bobMultiSigWallet.getPaymentAddress());
171
+ expect(aliceMultiSigWallet.getPaymentAddress()).toEqual(charlotteMultiSigWallet.getPaymentAddress());
172
+
173
+ await fundMultiSigWallet(faucetWallet, bobMultiSigWallet.getPaymentAddress());
174
+
175
+ const multiSigWalletBalance = await firstValueFrom(
176
+ bobMultiSigWallet.getBalance().pipe(
177
+ map((value) => value.coins),
178
+ filter((value) => value > 0n),
179
+ take(1)
180
+ )
181
+ );
182
+
183
+ expect(multiSigWalletBalance).toBeGreaterThan(0n);
184
+
185
+ // Alice will initiate the transaction on her wallet.
186
+ let tx = await aliceMultiSigWallet.transferFunds(faucetAddress, { coins: 2_000_000n });
187
+
188
+ // Alice then signs the transaction and relay it to Bob.
189
+ tx = await aliceMultiSigWallet.sign(tx);
190
+ const aliceSerializedTx = tx.toCbor();
191
+
192
+ // .... Bob receives the transaction and signs it.
193
+ let bobTx = MultiSigTx.fromCbor(aliceSerializedTx);
194
+ bobTx = await bobMultiSigWallet.sign(bobTx);
195
+
196
+ // Bob can then check if there are any missing signatures. If there are, he can then
197
+ // check who is missing and send the transaction to them.
198
+ expect(bobTx.isFullySigned()).toBe(false);
199
+ expect(bobTx.getMissingSigners()).toEqual([charlottePubKey]);
200
+
201
+ const bobSerializedTx = bobTx.toCbor();
202
+
203
+ // .... Charlotte receives the transaction and signs it.
204
+ let charlotteTx = MultiSigTx.fromCbor(bobSerializedTx);
205
+ charlotteTx = await charlotteMultiSigWallet.sign(charlotteTx);
206
+
207
+ // Charlotte can then check if there are any missing signatures. And if all signatures
208
+ // are complete she can submit it to the network.
209
+ expect(charlotteTx.getMissingSigners()).toEqual([]);
210
+ expect(charlotteTx.isFullySigned()).toBe(true);
211
+ const txId = await charlotteMultiSigWallet.submit(charlotteTx);
212
+
213
+ // Search chain history to see if the transaction is there.
214
+ const txFoundInHistory = await firstValueFrom(
215
+ faucetWallet.transactions.history$.pipe(
216
+ map((txs) => txs.find((hTx) => hTx.id === txId)),
217
+ filter(isNotNil),
218
+ take(1)
219
+ )
220
+ );
221
+
222
+ expect(txFoundInHistory).toBeTruthy();
223
+ });
224
+
225
+ // eslint-disable-next-line max-statements
226
+ it('delegate to a pool and claim rewards', async () => {
227
+ expect(aliceMultiSigWallet.getRewardAccount()).toEqual(bobMultiSigWallet.getRewardAccount());
228
+ expect(aliceMultiSigWallet.getRewardAccount()).toEqual(charlotteMultiSigWallet.getRewardAccount());
229
+
230
+ await fundMultiSigWallet(faucetWallet, bobMultiSigWallet.getPaymentAddress());
231
+
232
+ const multiSigWalletBalance = await firstValueFrom(
233
+ bobMultiSigWallet.getBalance().pipe(
234
+ map((value) => value.coins),
235
+ filter((value) => value > 0n),
236
+ take(1)
237
+ )
238
+ );
239
+
240
+ expect(multiSigWalletBalance).toBeGreaterThan(0n);
241
+
242
+ const [poolId] = await getPoolIds(faucetWallet.stakePoolProvider, 1);
243
+
244
+ // Alice will initiate the delegation transaction on her wallet.
245
+ let tx = await aliceMultiSigWallet.delegate(poolId);
246
+ tx = await aliceMultiSigWallet.sign(tx);
247
+ tx = await bobMultiSigWallet.sign(tx);
248
+ tx = await charlotteMultiSigWallet.sign(tx);
249
+
250
+ const txId = await charlotteMultiSigWallet.submit(tx);
251
+
252
+ // Search chain history to see if the transaction is there.
253
+ const txFoundInHistory = await firstValueFrom(
254
+ charlotteMultiSigWallet.getTransactionHistory().pipe(
255
+ map((txs) => txs.find((hTx) => hTx.id === txId)),
256
+ filter(isNotNil),
257
+ take(1)
258
+ )
259
+ );
260
+
261
+ expect(txFoundInHistory).toBeTruthy();
262
+
263
+ // Delegation is completed. Now we wait for rewards to be available.
264
+ const delegationTxConfirmedAtEpoch = await getTxConfirmationEpoch(
265
+ charlotteMultiSigWallet.getTransactionHistory(),
266
+ tx.getTransaction(),
267
+ faucetWallet.eraSummaries$
268
+ );
269
+
270
+ logger.info(`Delegation tx confirmed at epoch #${delegationTxConfirmedAtEpoch}`);
271
+
272
+ await waitForEpoch(faucetWallet, delegationTxConfirmedAtEpoch + 2);
273
+ await generateTxs(faucetWallet, faucetWallet);
274
+ await waitForEpoch(faucetWallet, delegationTxConfirmedAtEpoch + 4);
275
+
276
+ // Check reward
277
+ const multiSigWalletRewardBalance = await firstValueFrom(
278
+ bobMultiSigWallet.getRewardAccountBalance().pipe(
279
+ filter((value) => value > 0n),
280
+ take(1)
281
+ )
282
+ );
283
+
284
+ expect(multiSigWalletRewardBalance).toBeGreaterThan(0n);
285
+
286
+ logger.info(`Generated rewards: ${multiSigWalletRewardBalance} tLovelace`);
287
+
288
+ tx = await aliceMultiSigWallet.transferFunds(faucetAddress, { coins: 2_000_000n });
289
+ expect(tx.getTransaction().body.withdrawals?.length).toBeGreaterThan(0);
290
+
291
+ tx = await aliceMultiSigWallet.sign(tx);
292
+ tx = await bobMultiSigWallet.sign(tx);
293
+ tx = await charlotteMultiSigWallet.sign(tx);
294
+
295
+ const spendRewardsTx = await charlotteMultiSigWallet.submit(tx);
296
+
297
+ // Search chain history to see if the transaction is there.
298
+ const spendRewardsTxFoundInHistory = await firstValueFrom(
299
+ faucetWallet.transactions.history$.pipe(
300
+ map((txs) => txs.find((hTx) => hTx.id === spendRewardsTx)),
301
+ filter(isNotNil),
302
+ take(1)
303
+ )
304
+ );
305
+
306
+ expect(spendRewardsTxFoundInHistory).toBeTruthy();
307
+
308
+ // Check reward
309
+ const finalRewardBalance = await firstValueFrom(
310
+ bobMultiSigWallet.getRewardAccountBalance().pipe(
311
+ filter((value) => value === 0n),
312
+ take(1)
313
+ )
314
+ );
315
+
316
+ expect(finalRewardBalance).toEqual(0n);
317
+ });
318
+ });
@@ -1,10 +1,10 @@
1
- /* eslint-disable promise/always-return */
2
1
  import * as Postgres from '@cardano-sdk/projection-typeorm';
3
2
  import { BlockDataEntity, BlockEntity, StakeKeyEntity } from '@cardano-sdk/projection-typeorm';
4
3
  import {
5
4
  Bootstrap,
6
5
  InMemory,
7
6
  Mappers,
7
+ ProjectionEvent,
8
8
  ProjectionOperator,
9
9
  StabilityWindowBuffer,
10
10
  WithBlock,
@@ -17,16 +17,18 @@ import {
17
17
  ChainSyncEventType,
18
18
  ChainSyncRollForward,
19
19
  ObservableCardanoNode,
20
- Point
20
+ Point,
21
+ TipOrOrigin
21
22
  } from '@cardano-sdk/core';
22
23
  import { ChainSyncDataSet, chainSyncData, logger } from '@cardano-sdk/util-dev';
23
24
  import { ConnectionConfig } from '@cardano-ogmios/client';
24
- import { Observable, filter, firstValueFrom, lastValueFrom, of, take, takeWhile, toArray } from 'rxjs';
25
+ import { Observable, filter, firstValueFrom, lastValueFrom, map, of, take, takeWhile, toArray } from 'rxjs';
25
26
  import { OgmiosObservableCardanoNode } from '@cardano-sdk/ogmios';
27
+ import { ReconnectionConfig } from '@cardano-sdk/util-rxjs';
26
28
  import { createDatabase } from 'typeorm-extension';
27
29
  import { getEnv } from '../../src';
28
30
 
29
- const dataWithStakeKeyDeregistration = chainSyncData(ChainSyncDataSet.WithStakeKeyDeregistration);
31
+ const dataWithStakeDeregistration = chainSyncData(ChainSyncDataSet.WithStakeKeyDeregistration);
30
32
 
31
33
  const ogmiosConnectionConfig = ((): ConnectionConfig => {
32
34
  const { OGMIOS_URL } = getEnv(['OGMIOS_URL']);
@@ -64,22 +66,22 @@ const createForkProjectionSource = (
64
66
  // eslint-disable-next-line sort-keys-fix/sort-keys-fix
65
67
  findIntersect: (points) => {
66
68
  const intersectionPoint = points[0] as Point;
67
- const someEventsWithStakeKeyRegistration = dataWithStakeKeyDeregistration.allEvents
69
+ const someEventsWithStakeRegistration = dataWithStakeDeregistration.allEvents
68
70
  .filter(
69
71
  (evt): evt is Omit<ChainSyncRollForward, 'requestNext'> =>
70
72
  evt.eventType === ChainSyncEventType.RollForward &&
71
73
  evt.block.body.some((tx) =>
72
- tx.body.certificates?.some((cert) => cert.__typename === Cardano.CertificateType.StakeKeyRegistration)
74
+ tx.body.certificates?.some((cert) => cert.__typename === Cardano.CertificateType.StakeRegistration)
73
75
  )
74
76
  )
75
77
  .slice(0, 2);
76
78
  return of({
77
79
  chainSync$: new Observable<ChainSyncEvent>((subscriber) => {
78
- const events = [...someEventsWithStakeKeyRegistration];
80
+ const events = [...someEventsWithStakeRegistration];
79
81
  const next = () => {
80
82
  const nextEvt = events.shift();
81
83
  if (nextEvt) {
82
- const blockOffset = someEventsWithStakeKeyRegistration.length - events.length;
84
+ const blockOffset = someEventsWithStakeRegistration.length - events.length;
83
85
  const slot = Cardano.Slot(intersectionPoint.slot + blockOffset * 20);
84
86
  const blockNo = Cardano.BlockNo(lastEvt.block.header.blockNo + blockOffset);
85
87
  subscriber.next({
@@ -102,13 +104,21 @@ const createForkProjectionSource = (
102
104
  }),
103
105
  intersection: {
104
106
  point: intersectionPoint,
105
- tip: someEventsWithStakeKeyRegistration[someEventsWithStakeKeyRegistration.length - 1].tip
107
+ tip: someEventsWithStakeRegistration[someEventsWithStakeRegistration.length - 1].tip
106
108
  }
107
109
  });
108
110
  },
109
111
  healthCheck$: new Observable()
110
112
  });
111
113
 
114
+ const fakeTip = <T extends ProjectionEvent>(evt$: Observable<T>): Observable<T> =>
115
+ evt$.pipe(
116
+ map((evt) => ({
117
+ ...evt,
118
+ tip: evt.block.header
119
+ }))
120
+ );
121
+
112
122
  describe('resuming projection when intersection is not local tip', () => {
113
123
  let ogmiosCardanoNode: ObservableCardanoNode;
114
124
 
@@ -119,9 +129,11 @@ describe('resuming projection when intersection is not local tip', () => {
119
129
  const project = (
120
130
  cardanoNode: ObservableCardanoNode,
121
131
  buffer: StabilityWindowBuffer,
132
+ projectedTip$: Observable<TipOrOrigin>,
122
133
  into: ProjectionOperator<Mappers.WithStakeKeys>
123
134
  ) =>
124
- Bootstrap.fromCardanoNode({ blocksBufferLength: 10, buffer, cardanoNode, logger }).pipe(
135
+ Bootstrap.fromCardanoNode({ blocksBufferLength: 10, buffer, cardanoNode, logger, projectedTip$ }).pipe(
136
+ fakeTip,
125
137
  Mappers.withCertificates(),
126
138
  Mappers.withStakeKeys(),
127
139
  into,
@@ -130,13 +142,14 @@ describe('resuming projection when intersection is not local tip', () => {
130
142
 
131
143
  const testRollbackAndContinue = (
132
144
  buffer: StabilityWindowBuffer,
145
+ tip$: Observable<TipOrOrigin>,
133
146
  into: ProjectionOperator<Mappers.WithStakeKeys>,
134
147
  getNumberOfLocalStakeKeys: () => Promise<number>
135
148
  ) => {
136
149
  it('rolls back local data to intersection and resumes projection from there', async () => {
137
150
  // Project some events until we find at least 1 stake key registration
138
151
  const firstEventWithKeyRegistrations = await firstValueFrom(
139
- project(ogmiosCardanoNode, buffer, into).pipe(filter((evt) => evt.stakeKeys.insert.length > 0))
152
+ project(ogmiosCardanoNode, buffer, tip$, into).pipe(filter((evt) => evt.stakeKeys.insert.length > 0))
140
153
  );
141
154
  const lastEventFromOriginalSync = firstEventWithKeyRegistrations;
142
155
  const numStakeKeysBeforeFork = await getNumberOfLocalStakeKeys();
@@ -144,13 +157,13 @@ describe('resuming projection when intersection is not local tip', () => {
144
157
 
145
158
  // Simulate a fork by adding some blocks that are not on the ogmios chain
146
159
  const stubForkCardanoNode = createForkProjectionSource(ogmiosCardanoNode, lastEventFromOriginalSync);
147
- await lastValueFrom(project(stubForkCardanoNode, buffer, into).pipe(take(4)));
160
+ await lastValueFrom(project(stubForkCardanoNode, buffer, tip$, into).pipe(take(4)));
148
161
  const numStakeKeysAfterFork = await getNumberOfLocalStakeKeys();
149
162
  expect(numStakeKeysAfterFork).toBeGreaterThan(numStakeKeysBeforeFork);
150
163
 
151
164
  // Continue projection from ogmios
152
165
  const eventsTilStakeKeyRollback = await firstValueFrom(
153
- project(ogmiosCardanoNode, buffer, into).pipe(
166
+ project(ogmiosCardanoNode, buffer, tip$, into).pipe(
154
167
  takeWhile((evt) => evt.stakeKeys.del.length === 0 && evt.stakeKeys.insert.length === 0, true),
155
168
  toArray()
156
169
  )
@@ -168,7 +181,7 @@ describe('resuming projection when intersection is not local tip', () => {
168
181
 
169
182
  // Continue projection from ogmios
170
183
  const firstRollForwardEvent = await lastValueFrom(
171
- project(ogmiosCardanoNode, buffer, into).pipe(
184
+ project(ogmiosCardanoNode, buffer, tip$, into).pipe(
172
185
  takeWhile((evt) => evt.eventType === ChainSyncEventType.RollBackward, true)
173
186
  )
174
187
  );
@@ -183,20 +196,29 @@ describe('resuming projection when intersection is not local tip', () => {
183
196
  const buffer = new InMemory.InMemoryStabilityWindowBuffer();
184
197
  testRollbackAndContinue(
185
198
  buffer,
199
+ buffer.tip$,
186
200
  (evt$) => evt$.pipe(withStaticContext({ store }), InMemory.storeStakeKeys(), buffer.handleEvents()),
187
201
  async () => store.stakeKeys.size
188
202
  );
189
203
  });
190
204
 
191
205
  describe('typeorm', () => {
192
- const buffer = new Postgres.TypeormStabilityWindowBuffer({ logger });
206
+ const reconnectionConfig: ReconnectionConfig = { initialInterval: 10 };
207
+ const entities = [BlockEntity, BlockDataEntity, StakeKeyEntity];
208
+ const connection$ = Postgres.createObservableConnection({
209
+ connectionConfig$: of(pgConnectionConfig),
210
+ entities,
211
+ logger
212
+ });
213
+ const buffer = new Postgres.TypeormStabilityWindowBuffer({ connection$, logger, reconnectionConfig });
214
+ const tipTracker = Postgres.createTypeormTipTracker({ connection$, reconnectionConfig });
193
215
  const dataSource = Postgres.createDataSource({
194
216
  connectionConfig: pgConnectionConfig,
195
217
  devOptions: {
196
218
  dropSchema: true,
197
219
  synchronize: true
198
220
  },
199
- entities: [BlockEntity, BlockDataEntity, StakeKeyEntity],
221
+ entities,
200
222
  logger,
201
223
  options: {
202
224
  installExtensions: true
@@ -217,19 +239,24 @@ describe('resuming projection when intersection is not local tip', () => {
217
239
  }
218
240
  });
219
241
  await dataSource.initialize();
220
- await buffer.initialize(dataSource.createQueryRunner());
221
242
  });
222
- afterAll(() => dataSource.destroy());
243
+
244
+ afterAll(async () => {
245
+ await dataSource.destroy();
246
+ tipTracker.shutdown();
247
+ });
223
248
 
224
249
  testRollbackAndContinue(
225
250
  buffer,
251
+ tipTracker.tip$,
226
252
  (evt$) =>
227
253
  evt$.pipe(
228
- Postgres.withTypeormTransaction({ dataSource$: of(dataSource), logger }),
254
+ Postgres.withTypeormTransaction({ connection$ }),
229
255
  Postgres.storeBlock(),
230
256
  Postgres.storeStakeKeys(),
231
257
  buffer.storeBlockData(),
232
- Postgres.typeormTransactionCommit()
258
+ Postgres.typeormTransactionCommit(),
259
+ tipTracker.trackProjectedTip()
233
260
  ),
234
261
  getNumberOfLocalStakeKeys
235
262
  );
@@ -6,10 +6,20 @@ import { ConnectionConfig } from '@cardano-ogmios/client';
6
6
  import { DataSource, QueryRunner } from 'typeorm';
7
7
  import { Observable, filter, firstValueFrom, lastValueFrom, of, scan, takeWhile } from 'rxjs';
8
8
  import { OgmiosObservableCardanoNode } from '@cardano-sdk/ogmios';
9
+ import { ReconnectionConfig } from '@cardano-sdk/util-rxjs';
9
10
  import { createDatabase, dropDatabase } from 'typeorm-extension';
10
11
  import { getEnv } from '../../src';
11
12
  import { logger } from '@cardano-sdk/util-dev';
12
13
 
14
+ const entities = [
15
+ Postgres.BlockEntity,
16
+ Postgres.BlockDataEntity,
17
+ Postgres.AssetEntity,
18
+ Postgres.TokensEntity,
19
+ Postgres.OutputEntity,
20
+ Postgres.NftMetadataEntity
21
+ ];
22
+
13
23
  const ogmiosConnectionConfig = ((): ConnectionConfig => {
14
24
  const { OGMIOS_URL } = getEnv(['OGMIOS_URL']);
15
25
  const url = new URL(OGMIOS_URL);
@@ -42,14 +52,7 @@ const createDataSource = () =>
42
52
  dropSchema: true,
43
53
  synchronize: true
44
54
  },
45
- entities: [
46
- Postgres.BlockEntity,
47
- Postgres.BlockDataEntity,
48
- Postgres.AssetEntity,
49
- Postgres.TokensEntity,
50
- Postgres.OutputEntity,
51
- Postgres.NftMetadataEntity
52
- ],
55
+ entities,
53
56
  logger,
54
57
  options: {
55
58
  installExtensions: true
@@ -72,25 +75,32 @@ const countUniqueOutputAddresses = (queryRunner: QueryRunner) =>
72
75
  .getRawMany()
73
76
  .then((results) => results.length);
74
77
 
75
- describe('single-tenant utxo projection', () => {
78
+ describe.skip('single-tenant utxo projection', () => {
76
79
  let cardanoNode: ObservableCardanoNode;
80
+ let connection$: Observable<Postgres.TypeormConnection>;
77
81
  let buffer: Postgres.TypeormStabilityWindowBuffer;
82
+ let tipTracker: Postgres.TypeormTipTracker;
78
83
  let queryRunner: QueryRunner;
79
84
  let dataSource: DataSource;
80
85
 
81
86
  const initialize = async () => {
82
- buffer = new Postgres.TypeormStabilityWindowBuffer({ logger });
83
87
  await createDatabase(databaseOptions);
84
88
  dataSource = createDataSource();
85
89
  await dataSource.initialize();
86
90
  queryRunner = dataSource.createQueryRunner('slave');
87
- await buffer.initialize(queryRunner);
91
+ connection$ = Postgres.createObservableConnection({
92
+ connectionConfig$: of(pgConnectionConfig),
93
+ entities,
94
+ logger
95
+ });
96
+ const reconnectionConfig: ReconnectionConfig = { initialInterval: 10 };
97
+ buffer = new Postgres.TypeormStabilityWindowBuffer({ connection$, logger, reconnectionConfig });
98
+ tipTracker = Postgres.createTypeormTipTracker({ connection$, reconnectionConfig });
88
99
  };
89
100
 
90
101
  const cleanup = async () => {
91
102
  await queryRunner.release();
92
103
  await dataSource.destroy();
93
- buffer.shutdown();
94
104
  };
95
105
 
96
106
  beforeEach(async () => {
@@ -100,22 +110,9 @@ describe('single-tenant utxo projection', () => {
100
110
 
101
111
  afterEach(async () => cleanup());
102
112
 
103
- const projectMultiTenant = () =>
104
- Bootstrap.fromCardanoNode({ blocksBufferLength: 10, buffer, cardanoNode, logger }).pipe(
105
- Mappers.withMint(),
106
- Mappers.withUtxo(),
107
- Postgres.withTypeormTransaction({ dataSource$: of(dataSource), logger }),
108
- Postgres.storeBlock(),
109
- Postgres.storeAssets(),
110
- Postgres.storeUtxo(),
111
- buffer.storeBlockData(),
112
- Postgres.typeormTransactionCommit(),
113
- requestNext()
114
- );
115
-
116
113
  const storeUtxo = (evt$: Observable<ProjectionEvent<Mappers.WithMint & Mappers.WithUtxo>>) =>
117
114
  evt$.pipe(
118
- Postgres.withTypeormTransaction({ dataSource$: of(dataSource), logger }),
115
+ Postgres.withTypeormTransaction({ connection$ }),
119
116
  Postgres.storeBlock(),
120
117
  Postgres.storeAssets(),
121
118
  Postgres.storeUtxo(),
@@ -123,12 +120,28 @@ describe('single-tenant utxo projection', () => {
123
120
  Postgres.typeormTransactionCommit()
124
121
  );
125
122
 
123
+ const projectMultiTenant = () =>
124
+ Bootstrap.fromCardanoNode({
125
+ blocksBufferLength: 10,
126
+ buffer,
127
+ cardanoNode,
128
+ logger,
129
+ projectedTip$: tipTracker.tip$
130
+ }).pipe(Mappers.withMint(), Mappers.withUtxo(), storeUtxo, requestNext(), tipTracker.trackProjectedTip());
131
+
126
132
  const projectSingleTenant = (addresses: Cardano.PaymentAddress[]) =>
127
- Bootstrap.fromCardanoNode({ blocksBufferLength: 10, buffer, cardanoNode, logger }).pipe(
133
+ Bootstrap.fromCardanoNode({
134
+ blocksBufferLength: 10,
135
+ buffer,
136
+ cardanoNode,
137
+ logger,
138
+ projectedTip$: tipTracker.tip$
139
+ }).pipe(
128
140
  Mappers.withMint(),
129
141
  Mappers.withUtxo(),
130
142
  Mappers.filterProducedUtxoByAddresses({ addresses }),
131
143
  storeUtxo,
144
+ tipTracker.trackProjectedTip(),
132
145
  requestNext()
133
146
  );
134
147