@midnightntwrk/wallet-sdk-testkit 0.2.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/README.md +68 -0
- package/dist/addresses.d.ts +12 -0
- package/dist/addresses.js +30 -0
- package/dist/core.d.ts +7 -0
- package/dist/core.js +24 -0
- package/dist/environment.d.ts +37 -0
- package/dist/environment.js +101 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +26 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +36 -0
- package/dist/network.d.ts +6 -0
- package/dist/network.js +43 -0
- package/dist/primitives.d.ts +2 -0
- package/dist/primitives.js +14 -0
- package/dist/scenarios/dust.d.ts +14 -0
- package/dist/scenarios/dust.js +91 -0
- package/dist/scenarios/index.d.ts +2 -0
- package/dist/scenarios/index.js +19 -0
- package/dist/scenarios/token-transfer.d.ts +39 -0
- package/dist/scenarios/token-transfer.js +212 -0
- package/dist/seeds.d.ts +3 -0
- package/dist/seeds.js +27 -0
- package/dist/state-waiters.d.ts +69 -0
- package/dist/state-waiters.js +100 -0
- package/dist/testcontainers.d.ts +18 -0
- package/dist/testcontainers.js +87 -0
- package/dist/tx-history-asserts.d.ts +25 -0
- package/dist/tx-history-asserts.js +92 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/dist/vitest.d.ts +15 -0
- package/dist/vitest.js +62 -0
- package/dist/wallet.d.ts +34 -0
- package/dist/wallet.js +213 -0
- package/package.json +85 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
//
|
|
14
|
+
// Token-transfer healthcheck scenario. Only the `@healthcheck`-tagged test lives here — it is the
|
|
15
|
+
// one downstream monitoring (sentinel) consumes and so must be single-sourced. The remaining
|
|
16
|
+
// token-transfer tests (self-transaction, swap, error cases, dev TODOs) stay in the upstream
|
|
17
|
+
// e2e-tests suite; they share the two-wallet setup via the exported `useTokenTransferWallets`.
|
|
18
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
19
|
+
import { firstValueFrom } from 'rxjs';
|
|
20
|
+
import { inspect } from 'node:util';
|
|
21
|
+
import * as ledger from '@midnight-ntwrk/ledger-v8';
|
|
22
|
+
import { provideWallet, saveState } from '../wallet.js';
|
|
23
|
+
import { tNightAmount } from '../primitives.js';
|
|
24
|
+
import { getShieldedAddress, getUnshieldedAddress } from '../addresses.js';
|
|
25
|
+
import { waitForTxInHistory } from '../state-waiters.js';
|
|
26
|
+
import { expectReceiverShieldedTxHistory, expectReceiverUnshieldedTxHistory, expectSenderShieldedTxHistory, expectSenderUnshieldedTxHistory, } from '../tx-history-asserts.js';
|
|
27
|
+
import { logger } from '../logger.js';
|
|
28
|
+
/**
|
|
29
|
+
* Registers the shared two-wallet `beforeEach`/`afterEach` used by every token-transfer test and returns accessors.
|
|
30
|
+
* Call once inside a `describe`. Both the healthcheck scenario below and the upstream-only token-transfer tests reuse
|
|
31
|
+
* this so the sender/receiver selection lives in one place.
|
|
32
|
+
*/
|
|
33
|
+
export function useTokenTransferWallets({ getEnv, fundedSeed, secondSeed, syncCacheDir, syncTimeout = 60 * 60 * 1000, timeout = 600_000, }) {
|
|
34
|
+
const shieldedTokenRaw = ledger.shieldedToken().raw;
|
|
35
|
+
let sender;
|
|
36
|
+
let receiver;
|
|
37
|
+
let wallet;
|
|
38
|
+
let wallet2;
|
|
39
|
+
let networkId;
|
|
40
|
+
let filenameWallet;
|
|
41
|
+
let filenameWallet2;
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
const env = getEnv();
|
|
44
|
+
networkId = env.endpoints.networkId;
|
|
45
|
+
filenameWallet = `${fundedSeed.substring(0, 7)}-${env.network}.state`;
|
|
46
|
+
filenameWallet2 = `${secondSeed.substring(0, 7)}-${env.network}.state`;
|
|
47
|
+
wallet = await provideWallet(env, { seed: fundedSeed, syncCacheDir, filename: filenameWallet });
|
|
48
|
+
wallet2 = await provideWallet(env, { seed: secondSeed, syncCacheDir, filename: filenameWallet2 });
|
|
49
|
+
logger.info('Two wallets started');
|
|
50
|
+
const [state1, state2] = await Promise.all([
|
|
51
|
+
wallet.wallet.waitForSyncedState(),
|
|
52
|
+
wallet2.wallet.waitForSyncedState(),
|
|
53
|
+
]);
|
|
54
|
+
const balance1 = state1.shielded.balances[shieldedTokenRaw] ?? 0n;
|
|
55
|
+
const balance2 = state2.shielded.balances[shieldedTokenRaw] ?? 0n;
|
|
56
|
+
logger.info(`Wallet 1 shielded balance: ${balance1}, Wallet 2 shielded balance: ${balance2}`);
|
|
57
|
+
if (balance1 >= balance2) {
|
|
58
|
+
logger.info('Wallet 1 (SEED) has more funds — using as sender');
|
|
59
|
+
sender = wallet;
|
|
60
|
+
receiver = wallet2;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
logger.info('Wallet 2 (SEED2) has more funds — using as sender');
|
|
64
|
+
sender = wallet2;
|
|
65
|
+
receiver = wallet;
|
|
66
|
+
}
|
|
67
|
+
}, syncTimeout);
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
if (syncCacheDir) {
|
|
70
|
+
await saveState(wallet.wallet, syncCacheDir, filenameWallet);
|
|
71
|
+
await saveState(wallet2.wallet, syncCacheDir, filenameWallet2);
|
|
72
|
+
}
|
|
73
|
+
await sender.wallet.stop();
|
|
74
|
+
await receiver.wallet.stop();
|
|
75
|
+
logger.info('Wallets stopped');
|
|
76
|
+
}, timeout);
|
|
77
|
+
const requireWallet = (w, label) => {
|
|
78
|
+
if (!w)
|
|
79
|
+
throw new Error(`${label} accessed before beforeEach completed`);
|
|
80
|
+
return w;
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
getSender: () => requireWallet(sender, 'sender'),
|
|
84
|
+
getReceiver: () => requireWallet(receiver, 'receiver'),
|
|
85
|
+
getNetworkId: () => networkId,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Registers the single `@healthcheck`-tagged token-transfer test under a `describe('Token transfer')` block. This is
|
|
90
|
+
* the only token-transfer test shipped in the testkit; downstream monitoring selects it with `vitest run -t
|
|
91
|
+
*
|
|
92
|
+
* @healthcheck`.
|
|
93
|
+
*/
|
|
94
|
+
export function registerTokenTransferHealthchecks(deps) {
|
|
95
|
+
describe('Token transfer', () => {
|
|
96
|
+
const { getSender, getReceiver, getNetworkId } = useTokenTransferWallets(deps);
|
|
97
|
+
const shieldedTokenRaw = ledger.shieldedToken().raw;
|
|
98
|
+
const unshieldedTokenRaw = ledger.unshieldedToken().raw;
|
|
99
|
+
const outputValue = tNightAmount(10n);
|
|
100
|
+
const syncTimeout = deps.syncTimeout ?? 60 * 60 * 1000;
|
|
101
|
+
test('Is working for valid transfer @healthcheck', async () => {
|
|
102
|
+
const sender = getSender();
|
|
103
|
+
const receiver = getReceiver();
|
|
104
|
+
const networkId = getNetworkId();
|
|
105
|
+
await Promise.all([sender.wallet.waitForSyncedState(), receiver.wallet.waitForSyncedState()]);
|
|
106
|
+
const senderInitialState = await firstValueFrom(sender.wallet.state());
|
|
107
|
+
const initialShieldedBalance = senderInitialState.shielded.balances[shieldedTokenRaw];
|
|
108
|
+
const initialUnshieldedBalance = senderInitialState.unshielded.balances[unshieldedTokenRaw] ?? 0n;
|
|
109
|
+
const initialDustBalance = senderInitialState.dust.balance(new Date());
|
|
110
|
+
logger.info(`Wallet 1: ${initialShieldedBalance} shielded tokens`);
|
|
111
|
+
logger.info(`Wallet 1: ${initialUnshieldedBalance} unshielded tokens`);
|
|
112
|
+
logger.info(`Wallet 1 available dust: ${initialDustBalance}`);
|
|
113
|
+
logger.info(`Wallet 1 shielded address: ${getShieldedAddress(networkId, senderInitialState.shielded.address)}`);
|
|
114
|
+
logger.info(`Wallet 1 available shielded coins: ${senderInitialState.shielded.availableCoins.length}`);
|
|
115
|
+
logger.info(inspect(senderInitialState.shielded.availableCoins, { depth: null }));
|
|
116
|
+
logger.info(`Wallet 1 available unshielded coins: ${senderInitialState.unshielded.availableCoins.length}`);
|
|
117
|
+
logger.info(inspect(senderInitialState.unshielded.availableCoins, { depth: null }));
|
|
118
|
+
logger.info(`Wallet 1 unshielded address: ${getUnshieldedAddress(networkId, senderInitialState.unshielded.address)}`);
|
|
119
|
+
const initialReceiverState = await firstValueFrom(receiver.wallet.state());
|
|
120
|
+
const initialReceiverShieldedBalance = initialReceiverState.shielded.balances[shieldedTokenRaw] ?? 0n;
|
|
121
|
+
const initialReceiverUnshieldedBalance = initialReceiverState.unshielded.balances[unshieldedTokenRaw] ?? 0n;
|
|
122
|
+
logger.info(`Wallet 2: ${initialReceiverShieldedBalance} shielded tokens`);
|
|
123
|
+
logger.info(`Wallet 2: ${initialReceiverUnshieldedBalance} unshielded tokens`);
|
|
124
|
+
logger.info(`Wallet 2 unshielded address: ${getUnshieldedAddress(networkId, initialReceiverState.unshielded.address)}`);
|
|
125
|
+
logger.info(`Wallet 2 shielded address: ${getShieldedAddress(networkId, initialReceiverState.shielded.address)}`);
|
|
126
|
+
const senderInitialTxHistory = await sender.wallet.getAllFromTxHistory();
|
|
127
|
+
const receiverInitialTxHistory = await receiver.wallet.getAllFromTxHistory();
|
|
128
|
+
const outputsToCreate = [
|
|
129
|
+
{
|
|
130
|
+
type: 'shielded',
|
|
131
|
+
outputs: [
|
|
132
|
+
{
|
|
133
|
+
type: shieldedTokenRaw,
|
|
134
|
+
amount: outputValue,
|
|
135
|
+
receiverAddress: initialReceiverState.shielded.address,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'unshielded',
|
|
141
|
+
outputs: [
|
|
142
|
+
{
|
|
143
|
+
type: unshieldedTokenRaw,
|
|
144
|
+
amount: outputValue,
|
|
145
|
+
receiverAddress: initialReceiverState.unshielded.address,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const txRecipe = await sender.wallet.transferTransaction(outputsToCreate, {
|
|
151
|
+
shieldedSecretKeys: sender.shieldedSecretKeys,
|
|
152
|
+
dustSecretKey: sender.dustSecretKey,
|
|
153
|
+
}, {
|
|
154
|
+
ttl: new Date(Date.now() + 30 * 60 * 1000),
|
|
155
|
+
});
|
|
156
|
+
logger.info('Signing tx...');
|
|
157
|
+
logger.info(txRecipe);
|
|
158
|
+
const signedTxRecipe = await sender.wallet.signRecipe(txRecipe, (payload) => sender.unshieldedKeystore.signData(payload));
|
|
159
|
+
logger.info('Transaction to prove...');
|
|
160
|
+
logger.info(signedTxRecipe);
|
|
161
|
+
const finalizedTx = await sender.wallet.finalizeRecipe(signedTxRecipe);
|
|
162
|
+
logger.info('Submitting transaction...');
|
|
163
|
+
logger.info(finalizedTx.toString());
|
|
164
|
+
const txId = await sender.wallet.submitTransaction(finalizedTx);
|
|
165
|
+
const txHash = finalizedTx.transactionHash();
|
|
166
|
+
logger.info('txProcessing');
|
|
167
|
+
logger.info('Transaction id: ' + txId);
|
|
168
|
+
logger.info('waiting for tx in history');
|
|
169
|
+
const senderTxEntry = await waitForTxInHistory(txHash, sender.wallet, (e) => e.shielded !== undefined && e.unshielded !== undefined);
|
|
170
|
+
const senderFinalState = await sender.wallet.waitForSyncedState();
|
|
171
|
+
const senderFinalShieldedBalance = senderFinalState.shielded.balances[shieldedTokenRaw];
|
|
172
|
+
const senderFinalUnshieldedBalance = senderFinalState.unshielded.balances[unshieldedTokenRaw];
|
|
173
|
+
const senderFinalDustBalance = senderFinalState.dust.balance(new Date(3 * 1000));
|
|
174
|
+
logger.info(`Wallet 1 final available dust: ${senderFinalDustBalance}`);
|
|
175
|
+
logger.info(`Wallet 1 final available shielded coins: ${senderFinalShieldedBalance}`);
|
|
176
|
+
logger.info(`Wallet 1 final available unshielded coins: ${senderFinalUnshieldedBalance}`);
|
|
177
|
+
// assert balance after tx history is fixed
|
|
178
|
+
expect(senderFinalShieldedBalance).toBe(initialShieldedBalance - outputValue);
|
|
179
|
+
expect(senderFinalUnshieldedBalance).toBe(initialUnshieldedBalance - outputValue);
|
|
180
|
+
expect(senderFinalShieldedBalance).toBeLessThan(initialShieldedBalance);
|
|
181
|
+
expect(senderFinalUnshieldedBalance).toBeLessThan(initialUnshieldedBalance);
|
|
182
|
+
expect(senderFinalState.shielded.availableCoins.length).toBeLessThanOrEqual(senderInitialState.shielded.availableCoins.length);
|
|
183
|
+
expect(senderFinalState.dust.pendingCoins.length).toBe(0);
|
|
184
|
+
expect(senderFinalState.shielded.pendingCoins.length).toBe(0);
|
|
185
|
+
expect(senderFinalState.shielded.totalCoins.length).toBeLessThanOrEqual(senderInitialState.shielded.totalCoins.length);
|
|
186
|
+
expect(senderFinalState.unshielded.pendingCoins.length).toBe(0);
|
|
187
|
+
expect(senderFinalState.unshielded.availableCoins.length).toBeLessThanOrEqual(senderInitialState.unshielded.availableCoins.length);
|
|
188
|
+
expect(senderFinalState.unshielded.totalCoins.length).toBeLessThanOrEqual(senderInitialState.unshielded.totalCoins.length);
|
|
189
|
+
// Verify sender unshielded transaction history grew and contains the specific transaction
|
|
190
|
+
const senderFinalTxHistory = await sender.wallet.getAllFromTxHistory();
|
|
191
|
+
expect(senderFinalTxHistory.length).toBeGreaterThanOrEqual(senderInitialTxHistory.length + 1);
|
|
192
|
+
expectSenderShieldedTxHistory(senderTxEntry);
|
|
193
|
+
expectSenderUnshieldedTxHistory(senderTxEntry);
|
|
194
|
+
const receiverFinalState = await receiver.wallet.waitForSyncedState();
|
|
195
|
+
const receiverFinalShieldedBalance = receiverFinalState.shielded.balances[shieldedTokenRaw] ?? 0n;
|
|
196
|
+
const receiverFinalUnshieldedBalance = receiverFinalState.unshielded.balances[unshieldedTokenRaw] ?? 0n;
|
|
197
|
+
logger.info(`Wallet 2 final available shielded coins: ${receiverFinalShieldedBalance}`);
|
|
198
|
+
logger.info(`Wallet 2 final available unshielded coins: ${receiverFinalUnshieldedBalance}`);
|
|
199
|
+
expect(receiverFinalShieldedBalance).toBe(initialReceiverShieldedBalance + outputValue);
|
|
200
|
+
expect(receiverFinalUnshieldedBalance).toBe(initialReceiverUnshieldedBalance + outputValue);
|
|
201
|
+
expect(receiverFinalState.shielded.pendingCoins.length).toBe(0);
|
|
202
|
+
expect(receiverFinalState.shielded.availableCoins.length).toBeGreaterThanOrEqual(initialReceiverState.shielded.availableCoins.length + 1);
|
|
203
|
+
expect(receiverFinalState.shielded.totalCoins.length).toBeGreaterThanOrEqual(initialReceiverState.shielded.totalCoins.length + 1);
|
|
204
|
+
// Verify receiver unshielded transaction history grew and contains the specific transaction
|
|
205
|
+
const receiverFinalTxHistory = await receiver.wallet.getAllFromTxHistory();
|
|
206
|
+
expect(receiverFinalTxHistory.length).toBeGreaterThanOrEqual(receiverInitialTxHistory.length + 1);
|
|
207
|
+
const receiverTxEntry = await waitForTxInHistory(txHash, receiver.wallet, (entry) => entry.shielded !== undefined && entry.unshielded !== undefined);
|
|
208
|
+
expectReceiverShieldedTxHistory(receiverTxEntry, outputValue);
|
|
209
|
+
expectReceiverUnshieldedTxHistory(receiverTxEntry, outputValue);
|
|
210
|
+
}, syncTimeout);
|
|
211
|
+
});
|
|
212
|
+
}
|
package/dist/seeds.d.ts
ADDED
package/dist/seeds.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
import { HDWallet, Roles } from '@midnightntwrk/wallet-sdk-hd';
|
|
14
|
+
const deriveKey = (seed, role) => {
|
|
15
|
+
const seedBuffer = Buffer.from(seed, 'hex');
|
|
16
|
+
const hdWalletResult = HDWallet.fromSeed(seedBuffer);
|
|
17
|
+
const { hdWallet } = hdWalletResult;
|
|
18
|
+
const derivationResult = hdWallet.selectAccount(0).selectRole(role).deriveKeyAt(0);
|
|
19
|
+
if (derivationResult.type === 'keyOutOfBounds') {
|
|
20
|
+
throw new Error('Key derivation out of bounds');
|
|
21
|
+
}
|
|
22
|
+
hdWallet.clear();
|
|
23
|
+
return derivationResult.key;
|
|
24
|
+
};
|
|
25
|
+
export const getShieldedSeed = (seed) => Buffer.from(deriveKey(seed, Roles.Zswap));
|
|
26
|
+
export const getUnshieldedSeed = (seed) => deriveKey(seed, Roles.NightExternal);
|
|
27
|
+
export const getDustSeed = (seed) => deriveKey(seed, Roles.Dust);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type * as ledger from '@midnight-ntwrk/ledger-v8';
|
|
2
|
+
import { type ShieldedWalletAPI } from '@midnightntwrk/wallet-sdk-shielded';
|
|
3
|
+
import { type UnshieldedWallet } from '@midnightntwrk/wallet-sdk-unshielded-wallet';
|
|
4
|
+
import { type WalletFacade, type WalletEntry } from '@midnightntwrk/wallet-sdk-facade';
|
|
5
|
+
/** Default dust-balance threshold to wait for: 7 * 10^14. */
|
|
6
|
+
export declare const DEFAULT_DUST_BALANCE_THRESHOLD: bigint;
|
|
7
|
+
export declare const waitForSyncUnshielded: (wallet: UnshieldedWallet) => Promise<import("@midnightntwrk/wallet-sdk-unshielded-wallet").UnshieldedWalletState<string>>;
|
|
8
|
+
export declare const waitForFacadePending: (wallet: WalletFacade) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
9
|
+
export declare const waitForFacadePendingClear: (wallet: WalletFacade) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
10
|
+
export declare const waitForDustBalance: (wallet: WalletFacade, threshold?: bigint) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
11
|
+
export declare const waitForFinalizedShieldedBalance: (wallet: ShieldedWalletAPI) => Promise<import("@midnightntwrk/wallet-sdk-shielded").ShieldedWalletState<string, ledger.FinalizedTransaction>>;
|
|
12
|
+
export declare const waitForUnshieldedCoinUpdate: (wallet: WalletFacade, initialNumAvailableCoins: number) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
13
|
+
export declare const waitForStateAfterDustRegistration: (wallet: WalletFacade, finalizedTx: ledger.FinalizedTransaction) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
14
|
+
export declare const waitForRegisteredTokens: (wallet: WalletFacade) => Promise<import("@midnightntwrk/wallet-sdk-facade").FacadeState>;
|
|
15
|
+
export declare const waitForTxInHistory: (txHash: string, wallet: WalletFacade, ready?: (entry: WalletEntry) => boolean) => Promise<{
|
|
16
|
+
readonly hash: string;
|
|
17
|
+
readonly protocolVersion: number;
|
|
18
|
+
readonly status: "SUCCESS" | "FAILURE" | "PARTIAL_SUCCESS";
|
|
19
|
+
readonly identifiers?: readonly string[] | undefined;
|
|
20
|
+
readonly timestamp?: Date | undefined;
|
|
21
|
+
readonly fees?: bigint | null | undefined;
|
|
22
|
+
readonly shielded?: {
|
|
23
|
+
readonly receivedCoins: readonly {
|
|
24
|
+
readonly type: string;
|
|
25
|
+
readonly nonce: string;
|
|
26
|
+
readonly value: bigint;
|
|
27
|
+
readonly mtIndex: bigint;
|
|
28
|
+
}[];
|
|
29
|
+
readonly spentCoins: readonly {
|
|
30
|
+
readonly type: string;
|
|
31
|
+
readonly nonce: string;
|
|
32
|
+
readonly value: bigint;
|
|
33
|
+
readonly mtIndex: bigint;
|
|
34
|
+
}[];
|
|
35
|
+
} | undefined;
|
|
36
|
+
readonly unshielded?: {
|
|
37
|
+
readonly id: number;
|
|
38
|
+
readonly createdUtxos: readonly {
|
|
39
|
+
readonly value: bigint;
|
|
40
|
+
readonly owner: string;
|
|
41
|
+
readonly tokenType: string;
|
|
42
|
+
readonly intentHash: string;
|
|
43
|
+
readonly outputIndex: number;
|
|
44
|
+
}[];
|
|
45
|
+
readonly spentUtxos: readonly {
|
|
46
|
+
readonly value: bigint;
|
|
47
|
+
readonly owner: string;
|
|
48
|
+
readonly tokenType: string;
|
|
49
|
+
readonly intentHash: string;
|
|
50
|
+
readonly outputIndex: number;
|
|
51
|
+
}[];
|
|
52
|
+
} | undefined;
|
|
53
|
+
readonly dust?: {
|
|
54
|
+
readonly spentUtxos: readonly {
|
|
55
|
+
readonly nonce: bigint;
|
|
56
|
+
readonly mtIndex: bigint;
|
|
57
|
+
readonly initialValue: bigint;
|
|
58
|
+
readonly seq: number;
|
|
59
|
+
readonly backingNight: string;
|
|
60
|
+
}[];
|
|
61
|
+
readonly receivedUtxos: readonly {
|
|
62
|
+
readonly nonce: bigint;
|
|
63
|
+
readonly mtIndex: bigint;
|
|
64
|
+
readonly initialValue: bigint;
|
|
65
|
+
readonly seq: number;
|
|
66
|
+
readonly backingNight: string;
|
|
67
|
+
}[];
|
|
68
|
+
} | undefined;
|
|
69
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
14
|
+
import * as rx from 'rxjs';
|
|
15
|
+
import { expect } from 'vitest';
|
|
16
|
+
import { logger } from './logger.js';
|
|
17
|
+
/** Default dust-balance threshold to wait for: 7 * 10^14. */
|
|
18
|
+
export const DEFAULT_DUST_BALANCE_THRESHOLD = 7n * 10n ** 14n;
|
|
19
|
+
export const waitForSyncUnshielded = (wallet) => rx.firstValueFrom(wallet.state.pipe(rx.throttleTime(5_000), rx.tap((state) => {
|
|
20
|
+
const applyGap = state.state.progress.highestTransactionId - state.state.progress.appliedId;
|
|
21
|
+
logger.info(`Wallet behind by ${applyGap} indices`);
|
|
22
|
+
}), rx.filter((state) => state.state.progress.isStrictlyComplete())));
|
|
23
|
+
export const waitForFacadePending = (wallet) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
24
|
+
const shieldedPending = state.shielded.pendingCoins.length;
|
|
25
|
+
logger.info(`Shielded wallet pending coins: ${shieldedPending}, waiting for pending coins...`);
|
|
26
|
+
const unshieldedPending = state.unshielded.pendingCoins.length;
|
|
27
|
+
logger.info(`Unshielded wallet pending coins: ${unshieldedPending}, waiting for pending coins...`);
|
|
28
|
+
}), rx.filter((state) => state.shielded.pendingCoins.length > 0 || state.unshielded.pendingCoins.length > 0)));
|
|
29
|
+
export const waitForFacadePendingClear = (wallet) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
30
|
+
const shieldedPending = state.shielded.pendingCoins.length;
|
|
31
|
+
logger.info(`Shielded wallet pending coins: ${shieldedPending}, waiting for pending coins to clear...`);
|
|
32
|
+
const unshieldedPending = state.unshielded.pendingCoins.length;
|
|
33
|
+
logger.info(`Unshielded wallet pending coins: ${unshieldedPending}, waiting for pending coins to clear...`);
|
|
34
|
+
const dustPending = state.dust.pendingCoins.length;
|
|
35
|
+
logger.info(`Dust wallet pending coins: ${dustPending}, waiting for pending coins to clear...`);
|
|
36
|
+
}), rx.debounceTime(10_000), rx.filter((state) => state.shielded.pendingCoins.length == 0 &&
|
|
37
|
+
state.unshielded.pendingCoins.length == 0 &&
|
|
38
|
+
state.dust.pendingCoins.length == 0)));
|
|
39
|
+
export const waitForDustBalance = (wallet, threshold = DEFAULT_DUST_BALANCE_THRESHOLD) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
40
|
+
const dustBalance = state.dust.balance(new Date());
|
|
41
|
+
logger.info(`Dust balance: ${dustBalance}, waiting for dust balance > ${threshold}...`);
|
|
42
|
+
}), rx.filter((state) => state.dust.balance(new Date()) > threshold)));
|
|
43
|
+
export const waitForFinalizedShieldedBalance = (wallet) => rx.firstValueFrom(wallet.state.pipe(rx.tap((state) => {
|
|
44
|
+
const pending = state.pendingCoins.length;
|
|
45
|
+
logger.info(`Wallet pending coins: ${pending}, waiting for pending coins cleared...`);
|
|
46
|
+
}), rx.filter((state) => state.pendingCoins.length === 0)));
|
|
47
|
+
export const waitForUnshieldedCoinUpdate = (wallet, initialNumAvailableCoins) => rx.firstValueFrom(wallet.state().pipe(rx.tap((state) => {
|
|
48
|
+
const currentNumAvailableCoins = state.unshielded.availableCoins.length;
|
|
49
|
+
logger.info(`Unshielded available coins: ${currentNumAvailableCoins}, waiting for more than ${initialNumAvailableCoins}...`);
|
|
50
|
+
}), rx.debounceTime(10_000), rx.filter((s) => s.isSynced), rx.filter((s) => s.unshielded.availableCoins.length > initialNumAvailableCoins)));
|
|
51
|
+
export const waitForStateAfterDustRegistration = (wallet, finalizedTx) => rx.firstValueFrom(wallet.state().pipe(rx.mergeMap(async (state) => {
|
|
52
|
+
const txInHistory = await wallet.queryTxHistoryByHash(finalizedTx.transactionHash());
|
|
53
|
+
return {
|
|
54
|
+
state,
|
|
55
|
+
txFound: txInHistory !== undefined,
|
|
56
|
+
};
|
|
57
|
+
}), rx.filter(({ state, txFound }) => txFound && state.isSynced && state.dust.availableCoins.length > 0), rx.map(({ state }) => state)));
|
|
58
|
+
export const waitForRegisteredTokens = (wallet) => rx.firstValueFrom(wallet.state().pipe(rx.tap((s) => {
|
|
59
|
+
const registeredTokens = s.unshielded.availableCoins.filter((coin) => coin.meta.registeredForDustGeneration === true);
|
|
60
|
+
logger.info(`registered tokens: ${registeredTokens.length}`);
|
|
61
|
+
}), rx.filter((s) => s.unshielded.availableCoins.filter((coin) => coin.meta.registeredForDustGeneration === true).length > 0)));
|
|
62
|
+
export const waitForTxInHistory = async (txHash, wallet, ready) => {
|
|
63
|
+
const isReady = ready ?? (() => true);
|
|
64
|
+
const describeSections = (e) => ['shielded', 'unshielded', 'dust'].filter((k) => e[k] !== undefined).join(',');
|
|
65
|
+
let pollsSinceDump = 0;
|
|
66
|
+
const txEntry = await rx.firstValueFrom(rx.merge(wallet.state().pipe(rx.filter((state) => state.isSynced)), rx.interval(500)).pipe(rx.mergeMap(async () => {
|
|
67
|
+
const entry = await wallet.queryTxHistoryByHash(txHash);
|
|
68
|
+
if (entry !== undefined && entry.status !== 'SUCCESS') {
|
|
69
|
+
logger.info(`Waiting for tx ${txHash} in history: found, status=${entry.status}, sections=[${describeSections(entry)}] — non-SUCCESS, aborting wait`);
|
|
70
|
+
return entry;
|
|
71
|
+
}
|
|
72
|
+
const notReady = entry === undefined || !isReady(entry);
|
|
73
|
+
const needsDump = notReady && pollsSinceDump >= 20;
|
|
74
|
+
if (entry === undefined) {
|
|
75
|
+
logger.info(`Waiting for tx ${txHash} in history: not found yet`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
logger.info(`Waiting for tx ${txHash} in history: found, status=${entry.status}, ready=${isReady(entry)}`);
|
|
79
|
+
}
|
|
80
|
+
if (needsDump) {
|
|
81
|
+
const all = await wallet.getAllFromTxHistory();
|
|
82
|
+
const summary = all.map((e) => ({
|
|
83
|
+
hash: e.hash,
|
|
84
|
+
status: e.status,
|
|
85
|
+
sections: describeSections(e),
|
|
86
|
+
identifiers: e.identifiers ?? [],
|
|
87
|
+
}));
|
|
88
|
+
logger.info(`Storage snapshot (${all.length} entries): ${JSON.stringify(summary, null, 2)}`);
|
|
89
|
+
pollsSinceDump = 0;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
pollsSinceDump = notReady ? pollsSinceDump + 1 : 0;
|
|
93
|
+
}
|
|
94
|
+
return entry;
|
|
95
|
+
}), rx.filter((entry) => entry !== undefined && (entry.status !== 'SUCCESS' || isReady(entry)))));
|
|
96
|
+
expect(txEntry).toBeDefined();
|
|
97
|
+
expect(txEntry.hash).toBe(txHash);
|
|
98
|
+
expect(txEntry.status).toBe('SUCCESS');
|
|
99
|
+
return txEntry;
|
|
100
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type MidnightNetwork, type WalletTestEnvironment } from './types.js';
|
|
2
|
+
/** Configuration for {@link createTestContainersEnvironment}. */
|
|
3
|
+
export interface TestContainersEnvironmentConfig {
|
|
4
|
+
network: MidnightNetwork;
|
|
5
|
+
/** Environment variables forwarded into the compose environment. Default: `['APP_INFRA_SECRET']`. */
|
|
6
|
+
passEnv?: readonly string[];
|
|
7
|
+
/** Startup timeout for the remote-network proof-server, in ms. Default: 100000. */
|
|
8
|
+
startupTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Spins up the local docker-compose stack and returns a {@link WalletTestEnvironment}.
|
|
12
|
+
*
|
|
13
|
+
* - `undeployed`: full local stack (proof-server + node + indexer), endpoints resolved from mapped ports.
|
|
14
|
+
* - Remote networks: a local proof-server only, combined with the public indexer/node preset. This is the case the
|
|
15
|
+
* downstream `PROOF_SERVER_URL` patch used to bypass; downstream consumers should instead use
|
|
16
|
+
* `createRemoteEnvironment` with an explicit `proverUrl` and skip Docker entirely.
|
|
17
|
+
*/
|
|
18
|
+
export declare const createTestContainersEnvironment: (config: TestContainersEnvironmentConfig) => Promise<WalletTestEnvironment>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
//
|
|
14
|
+
// Docker-backed environment provisioning. Kept behind the `@midnightntwrk/wallet-sdk-testkit/
|
|
15
|
+
// testcontainers` entry point so consumers that only target remote networks never load
|
|
16
|
+
// `testcontainers` or `@midnightntwrk/wallet-sdk-utilities` (both declared as optional peers).
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { DockerComposeEnvironment, Wait } from 'testcontainers';
|
|
19
|
+
import { NetworkId } from '@midnightntwrk/wallet-sdk-abstractions';
|
|
20
|
+
import { buildTestEnvironmentVariables, getComposeDirectory } from '@midnightntwrk/wallet-sdk-utilities/testing';
|
|
21
|
+
import { NETWORK_PRESETS, makeEnvironment } from './environment.js';
|
|
22
|
+
import { sleep } from './network.js';
|
|
23
|
+
import { logger } from './logger.js';
|
|
24
|
+
const PROOF_SERVER_PORT = 6300;
|
|
25
|
+
const NODE_PORT_RPC = 9944;
|
|
26
|
+
const INDEXER_PORT = 8088;
|
|
27
|
+
const mappedPort = (env, container, port) => env.getContainer(container).getMappedPort(port);
|
|
28
|
+
const resolveUndeployedEndpoints = (env, uid) => {
|
|
29
|
+
const indexerPort = mappedPort(env, `indexer_${uid}`, INDEXER_PORT);
|
|
30
|
+
return {
|
|
31
|
+
networkId: NetworkId.NetworkId.Undeployed,
|
|
32
|
+
proverUrl: `http://localhost:${mappedPort(env, `proof-server_${uid}`, PROOF_SERVER_PORT)}`,
|
|
33
|
+
indexerHttpUrl: `http://localhost:${indexerPort}/api/v3/graphql`,
|
|
34
|
+
indexerWsUrl: `ws://localhost:${indexerPort}/api/v4/graphql/ws`,
|
|
35
|
+
nodeUrl: `ws://localhost:${mappedPort(env, `node_${uid}`, NODE_PORT_RPC)}`,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Spins up the local docker-compose stack and returns a {@link WalletTestEnvironment}.
|
|
40
|
+
*
|
|
41
|
+
* - `undeployed`: full local stack (proof-server + node + indexer), endpoints resolved from mapped ports.
|
|
42
|
+
* - Remote networks: a local proof-server only, combined with the public indexer/node preset. This is the case the
|
|
43
|
+
* downstream `PROOF_SERVER_URL` patch used to bypass; downstream consumers should instead use
|
|
44
|
+
* `createRemoteEnvironment` with an explicit `proverUrl` and skip Docker entirely.
|
|
45
|
+
*/
|
|
46
|
+
export const createTestContainersEnvironment = async (config) => {
|
|
47
|
+
const { network } = config;
|
|
48
|
+
const passEnv = config.passEnv ?? ['APP_INFRA_SECRET'];
|
|
49
|
+
const uid = randomUUID();
|
|
50
|
+
logger.info(`Spinning up ${network} test environment...`);
|
|
51
|
+
if (network === 'undeployed') {
|
|
52
|
+
const environmentVars = buildTestEnvironmentVariables(passEnv, { additionalVars: { TESTCONTAINERS_UID: uid } });
|
|
53
|
+
const composeEnvironment = await new DockerComposeEnvironment(getComposeDirectory(), 'docker-compose-dynamic.yml')
|
|
54
|
+
.withWaitStrategy(`proof-server_${uid}`, Wait.forListeningPorts())
|
|
55
|
+
.withWaitStrategy(`node_${uid}`, Wait.forListeningPorts())
|
|
56
|
+
.withWaitStrategy(`indexer_${uid}`, Wait.forListeningPorts())
|
|
57
|
+
.withEnvironment(environmentVars)
|
|
58
|
+
.up();
|
|
59
|
+
// wait for another block to be produced
|
|
60
|
+
await sleep(6);
|
|
61
|
+
logger.info('Test environment started');
|
|
62
|
+
return makeEnvironment('undeployed', resolveUndeployedEndpoints(composeEnvironment, uid), {
|
|
63
|
+
down: async () => {
|
|
64
|
+
await composeEnvironment.down({ timeout: 10_000, removeVolumes: true });
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const environmentVars = buildTestEnvironmentVariables(passEnv, {
|
|
69
|
+
additionalVars: { TESTCONTAINERS_UID: uid, NETWORK_ID: network },
|
|
70
|
+
});
|
|
71
|
+
const composeEnvironment = await new DockerComposeEnvironment(getComposeDirectory(), 'docker-compose-remote-dynamic.yml')
|
|
72
|
+
.withWaitStrategy(`proof-server_${uid}`, Wait.forLogMessage('Actix runtime found; starting in Actix runtime'))
|
|
73
|
+
.withEnvironment(environmentVars)
|
|
74
|
+
.withStartupTimeout(config.startupTimeoutMs ?? 100_000)
|
|
75
|
+
.up();
|
|
76
|
+
logger.info('Test environment started');
|
|
77
|
+
const preset = NETWORK_PRESETS[network];
|
|
78
|
+
const endpoints = {
|
|
79
|
+
...preset,
|
|
80
|
+
proverUrl: `http://localhost:${mappedPort(composeEnvironment, `proof-server_${uid}`, PROOF_SERVER_PORT)}`,
|
|
81
|
+
};
|
|
82
|
+
return makeEnvironment(network, endpoints, {
|
|
83
|
+
down: async () => {
|
|
84
|
+
await composeEnvironment.down({ timeout: 10_000, removeVolumes: true });
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type WalletEntry } from '@midnightntwrk/wallet-sdk-facade';
|
|
2
|
+
export declare function expectValidShieldedCoinFields(coin: NonNullable<WalletEntry['shielded']>['receivedCoins'][number]): void;
|
|
3
|
+
export declare function expectValidShieldedTxHistoryEntry(entry: WalletEntry): void;
|
|
4
|
+
export declare function expectValidUnshieldedTxHistoryEntry(entry: WalletEntry): void;
|
|
5
|
+
/** Asserts a sender's shielded tx history entry has valid spentCoins. */
|
|
6
|
+
export declare function expectSenderShieldedTxHistory(entry: WalletEntry): void;
|
|
7
|
+
/**
|
|
8
|
+
* Asserts a receiver's shielded tx history entry has valid receivedCoins, and that a coin matching the expected value
|
|
9
|
+
* exists with valid fields.
|
|
10
|
+
*/
|
|
11
|
+
export declare function expectReceiverShieldedTxHistory(entry: WalletEntry, expectedValue: bigint): void;
|
|
12
|
+
/** Asserts a sender's unshielded tx history entry has valid spentUtxos. */
|
|
13
|
+
export declare function expectSenderUnshieldedTxHistory(entry: WalletEntry): void;
|
|
14
|
+
/**
|
|
15
|
+
* Asserts a receiver's unshielded tx history entry has valid createdUtxos, and that a UTXO matching the expected value
|
|
16
|
+
* exists with valid fields.
|
|
17
|
+
*/
|
|
18
|
+
export declare function expectReceiverUnshieldedTxHistory(entry: WalletEntry, expectedValue: bigint): void;
|
|
19
|
+
/**
|
|
20
|
+
* Asserts that tx history entries from a storage contain at least one entry with the specified section ('shielded' or
|
|
21
|
+
* 'unshielded').
|
|
22
|
+
*/
|
|
23
|
+
export declare function expectTxHistoryHasSection(storage: {
|
|
24
|
+
getAll(): Promise<readonly Record<string, unknown>[]>;
|
|
25
|
+
}, section: 'shielded' | 'unshielded'): Promise<readonly Record<string, unknown>[]>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// This file is part of MIDNIGHT-WALLET-SDK.
|
|
2
|
+
// Copyright (C) Midnight Foundation
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// You may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
14
|
+
import { expect } from 'vitest';
|
|
15
|
+
function expectValidUnshieldedUtxoFields(utxo) {
|
|
16
|
+
expect(typeof utxo.value).toBe('bigint');
|
|
17
|
+
expect(typeof utxo.owner).toBe('string');
|
|
18
|
+
expect(typeof utxo.tokenType).toBe('string');
|
|
19
|
+
expect(typeof utxo.intentHash).toBe('string');
|
|
20
|
+
expect(typeof utxo.outputIndex).toBe('number');
|
|
21
|
+
}
|
|
22
|
+
export function expectValidShieldedCoinFields(coin) {
|
|
23
|
+
expect(typeof coin.type).toBe('string');
|
|
24
|
+
expect(coin.type.length).toBeGreaterThan(0);
|
|
25
|
+
expect(typeof coin.nonce).toBe('string');
|
|
26
|
+
expect(coin.nonce.length).toBeGreaterThan(0);
|
|
27
|
+
expect(typeof coin.value).toBe('bigint');
|
|
28
|
+
expect(typeof coin.mtIndex).toBe('bigint');
|
|
29
|
+
}
|
|
30
|
+
export function expectValidShieldedTxHistoryEntry(entry) {
|
|
31
|
+
expect(entry.shielded).toBeDefined();
|
|
32
|
+
expect(Array.isArray(entry.shielded.receivedCoins)).toBe(true);
|
|
33
|
+
expect(Array.isArray(entry.shielded.spentCoins)).toBe(true);
|
|
34
|
+
for (const coin of [...entry.shielded.receivedCoins, ...entry.shielded.spentCoins]) {
|
|
35
|
+
expectValidShieldedCoinFields(coin);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function expectValidUnshieldedTxHistoryEntry(entry) {
|
|
39
|
+
expect(entry.unshielded).toBeDefined();
|
|
40
|
+
expect(Array.isArray(entry.unshielded.createdUtxos)).toBe(true);
|
|
41
|
+
expect(Array.isArray(entry.unshielded.spentUtxos)).toBe(true);
|
|
42
|
+
for (const utxo of [...entry.unshielded.createdUtxos, ...entry.unshielded.spentUtxos]) {
|
|
43
|
+
expectValidUnshieldedUtxoFields(utxo);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Asserts a sender's shielded tx history entry has valid spentCoins. */
|
|
47
|
+
export function expectSenderShieldedTxHistory(entry) {
|
|
48
|
+
expect(entry.shielded).toBeDefined();
|
|
49
|
+
expect(entry.shielded.spentCoins.length).toBeGreaterThan(0);
|
|
50
|
+
expectValidShieldedTxHistoryEntry(entry);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Asserts a receiver's shielded tx history entry has valid receivedCoins, and that a coin matching the expected value
|
|
54
|
+
* exists with valid fields.
|
|
55
|
+
*/
|
|
56
|
+
export function expectReceiverShieldedTxHistory(entry, expectedValue) {
|
|
57
|
+
expect(entry.shielded).toBeDefined();
|
|
58
|
+
expect(entry.shielded.receivedCoins.length).toBeGreaterThan(0);
|
|
59
|
+
const receivedCoin = entry.shielded.receivedCoins.find((c) => c.value === expectedValue);
|
|
60
|
+
expect(receivedCoin).toBeDefined();
|
|
61
|
+
expectValidShieldedCoinFields(receivedCoin);
|
|
62
|
+
expectValidShieldedTxHistoryEntry(entry);
|
|
63
|
+
}
|
|
64
|
+
/** Asserts a sender's unshielded tx history entry has valid spentUtxos. */
|
|
65
|
+
export function expectSenderUnshieldedTxHistory(entry) {
|
|
66
|
+
expect(entry.unshielded).toBeDefined();
|
|
67
|
+
expect(entry.unshielded.spentUtxos.length).toBeGreaterThan(0);
|
|
68
|
+
expectValidUnshieldedTxHistoryEntry(entry);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Asserts a receiver's unshielded tx history entry has valid createdUtxos, and that a UTXO matching the expected value
|
|
72
|
+
* exists with valid fields.
|
|
73
|
+
*/
|
|
74
|
+
export function expectReceiverUnshieldedTxHistory(entry, expectedValue) {
|
|
75
|
+
expect(entry.unshielded).toBeDefined();
|
|
76
|
+
expect(entry.unshielded.createdUtxos.length).toBeGreaterThan(0);
|
|
77
|
+
const receivedUtxo = entry.unshielded.createdUtxos.find((u) => u.value === expectedValue);
|
|
78
|
+
expect(receivedUtxo).toBeDefined();
|
|
79
|
+
expectValidUnshieldedUtxoFields(receivedUtxo);
|
|
80
|
+
expectValidUnshieldedTxHistoryEntry(entry);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Asserts that tx history entries from a storage contain at least one entry with the specified section ('shielded' or
|
|
84
|
+
* 'unshielded').
|
|
85
|
+
*/
|
|
86
|
+
export async function expectTxHistoryHasSection(storage, section) {
|
|
87
|
+
const entries = await storage.getAll();
|
|
88
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
89
|
+
const matching = entries.filter((e) => e[section] !== undefined);
|
|
90
|
+
expect(matching.length).toBeGreaterThan(0);
|
|
91
|
+
return entries;
|
|
92
|
+
}
|