@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.
- package/.env.example +1 -7
- package/CHANGELOG.md +24 -0
- package/README.md +0 -29
- package/dist/cjs/environment.d.ts.map +1 -1
- package/dist/cjs/environment.js.map +1 -1
- package/dist/cjs/factories.d.ts.map +1 -1
- package/dist/cjs/factories.js +7 -1
- package/dist/cjs/factories.js.map +1 -1
- package/dist/cjs/measurement-util.d.ts.map +1 -1
- package/dist/cjs/measurement-util.js.map +1 -1
- package/dist/cjs/scripts/is-local-network-ready.js.map +1 -1
- package/dist/cjs/scripts/mnemonic.js.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/files.d.ts.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/files.js.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.d.ts.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.js.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/utils.d.ts.map +1 -1
- package/dist/cjs/tools/multi-delegation-data-gen/utils/utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/environment.d.ts.map +1 -1
- package/dist/esm/environment.js.map +1 -1
- package/dist/esm/factories.d.ts.map +1 -1
- package/dist/esm/factories.js +7 -1
- package/dist/esm/factories.js.map +1 -1
- package/dist/esm/measurement-util.d.ts.map +1 -1
- package/dist/esm/measurement-util.js.map +1 -1
- package/dist/esm/scripts/is-local-network-ready.js.map +1 -1
- package/dist/esm/scripts/mnemonic.js.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/files.d.ts.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/files.js.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.d.ts.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.js.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/utils.d.ts.map +1 -1
- package/dist/esm/tools/multi-delegation-data-gen/utils/utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/docker-compose.yml +4 -0
- package/jest.config.js +0 -1
- package/local-network/scripts/common.sh +19 -0
- package/local-network/scripts/make-babbage.sh +2 -1
- package/local-network/scripts/mint-handles.sh +2 -18
- package/local-network/scripts/mint-tokens.sh +2 -18
- package/local-network/scripts/mnemonic_keys.sh +0 -0
- package/local-network/scripts/setup-wallets.sh +2 -17
- package/local-network/templates/babbage/submit-api-config.json +115 -0
- package/package.json +27 -29
- package/src/environment.ts +2 -6
- package/src/factories.ts +11 -5
- package/src/measurement-util.ts +1 -4
- package/src/scripts/is-local-network-ready.ts +1 -3
- package/src/scripts/mnemonic.ts +1 -3
- package/src/tools/multi-delegation-data-gen/utils/files.ts +1 -3
- package/src/tools/multi-delegation-data-gen/utils/terminal-progress-monitor.ts +2 -6
- package/src/tools/multi-delegation-data-gen/utils/utils.ts +1 -3
- package/test/artillery/StakePoolSearch.ts +5 -16
- package/test/artillery/artillery.ts +20 -61
- package/test/artillery/wallet-restoration/WalletRestoration.ts +1 -3
- package/test/artillery/wallet-restoration/queries.ts +1 -3
- package/test/artillery/wallet-restoration/types.ts +1 -3
- package/test/load-test-custom/stake-pool-search/stake-pool-search.test.ts +1 -3
- package/test/load-test-custom/wallet-init/wallet-init.test.ts +10 -1
- package/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts +1 -4
- package/test/local-network/register-pool.test.ts +24 -14
- package/test/long-running/cache-invalidation.test.ts +7 -4
- package/test/long-running/multisig-wallet/MultiSigTx.ts +117 -0
- package/test/long-running/multisig-wallet/MultiSigWallet.ts +491 -0
- package/test/long-running/multisig-wallet/multisig-delegation-rewards.test.ts +318 -0
- package/test/projection/offline-fork.test.ts +47 -20
- package/test/projection/single-tenant-utxo.test.ts +40 -27
- package/test/providers/StakePoolProvider.test.ts +22 -17
- package/test/tsconfig.json +3 -0
- package/test/wallet/PersonalWallet/delegation.test.ts +6 -3
- package/test/wallet/PersonalWallet/handle.test.ts +2 -1
- package/test/wallet/PersonalWallet/mint.test.ts +2 -1
- package/test/wallet/PersonalWallet/multiAddress.test.ts +2 -1
- package/test/wallet/PersonalWallet/multisignature.test.ts +4 -2
- package/test/wallet/PersonalWallet/nft.test.ts +2 -1
- package/test/web-extension/extension/ui.ts +2 -8
- 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
|
|
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
|
|
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.
|
|
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 = [...
|
|
80
|
+
const events = [...someEventsWithStakeRegistration];
|
|
79
81
|
const next = () => {
|
|
80
82
|
const nextEvt = events.shift();
|
|
81
83
|
if (nextEvt) {
|
|
82
|
-
const blockOffset =
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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
|
|