@midnight-ntwrk/wallet-sdk-capabilities 4.0.0-beta.2 → 4.0.0-beta.3
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 +27 -3
- package/dist/chainVersion/chainVersionProbe.d.ts +95 -0
- package/dist/chainVersion/chainVersionProbe.js +83 -0
- package/dist/chainVersion/index.d.ts +1 -0
- package/dist/chainVersion/index.js +13 -0
- package/dist/codecs/index.d.ts +1 -0
- package/dist/codecs/index.js +13 -0
- package/dist/codecs/ledgerParameters.d.ts +81 -0
- package/dist/codecs/ledgerParameters.js +63 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/pendingTransactions/pendingTransactions.d.ts +113 -9
- package/dist/pendingTransactions/pendingTransactions.js +147 -30
- package/dist/pendingTransactions/pendingTransactionsService.d.ts +25 -9
- package/dist/pendingTransactions/pendingTransactionsService.js +33 -21
- package/dist/proving/index.d.ts +2 -0
- package/dist/proving/index.js +2 -0
- package/dist/proving/provingService.d.ts +175 -15
- package/dist/proving/provingService.js +104 -11
- package/dist/proving/v8ProvingService.d.ts +53 -0
- package/dist/proving/v8ProvingService.js +71 -0
- package/dist/proving/versionedProving.d.ts +42 -0
- package/dist/proving/versionedProving.js +103 -0
- package/dist/signatures/index.d.ts +2 -0
- package/dist/signatures/index.js +14 -0
- package/dist/signatures/signing.d.ts +37 -0
- package/dist/signatures/signing.js +13 -0
- package/dist/signatures/v8Signatures.d.ts +54 -0
- package/dist/signatures/v8Signatures.js +62 -0
- package/dist/simulation/ForkSimulator.d.ts +114 -0
- package/dist/simulation/ForkSimulator.js +209 -0
- package/dist/simulation/LedgerTranslation.d.ts +53 -0
- package/dist/simulation/LedgerTranslation.js +56 -0
- package/dist/simulation/core/VersionTimeline.d.ts +54 -0
- package/dist/simulation/core/VersionTimeline.js +56 -0
- package/dist/simulation/core/blocks.d.ts +38 -0
- package/dist/simulation/core/blocks.js +52 -0
- package/dist/simulation/core/index.d.ts +13 -0
- package/dist/simulation/core/index.js +25 -0
- package/dist/simulation/core/strictness.d.ts +29 -0
- package/dist/simulation/core/strictness.js +39 -0
- package/dist/simulation/index.d.ts +22 -2
- package/dist/simulation/index.js +25 -14
- package/dist/simulation/v8/Simulator.d.ts +231 -0
- package/dist/simulation/v8/Simulator.js +503 -0
- package/dist/simulation/{SimulatorState.d.ts → v8/SimulatorState.d.ts} +31 -44
- package/dist/simulation/v8/SimulatorState.js +290 -0
- package/dist/simulation/v8/index.d.ts +2 -0
- package/dist/simulation/v8/index.js +26 -0
- package/dist/simulation/{Simulator.d.ts → v9/Simulator.d.ts} +57 -6
- package/dist/simulation/{Simulator.js → v9/Simulator.js} +68 -18
- package/dist/simulation/v9/SimulatorState.d.ts +336 -0
- package/dist/simulation/{SimulatorState.js → v9/SimulatorState.js} +34 -67
- package/dist/simulation/v9/index.d.ts +2 -0
- package/dist/simulation/v9/index.js +26 -0
- package/dist/submission/submissionService.d.ts +2 -1
- package/dist/submission/submissionService.js +1 -1
- package/dist/validation/blockData.d.ts +42 -2
- package/dist/validation/blockData.js +55 -8
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -0
- package/dist/validation/v8ValidationService.d.ts +29 -0
- package/dist/validation/v8ValidationService.js +48 -0
- package/dist/validation/validationService.d.ts +132 -17
- package/dist/validation/validationService.js +88 -42
- package/dist/validation/versionedValidation.d.ts +46 -0
- package/dist/validation/versionedValidation.js +55 -0
- package/package.json +23 -9
|
@@ -0,0 +1,503 @@
|
|
|
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
|
+
* Unified Simulator for wallet testing.
|
|
15
|
+
*
|
|
16
|
+
* This module provides a simulated ledger environment for testing wallet functionality without requiring a real
|
|
17
|
+
* blockchain node. It supports:
|
|
18
|
+
*
|
|
19
|
+
* - Optional genesis mints for pre-funded accounts (shielded, unshielded, or Night tokens)
|
|
20
|
+
* - Transaction submission with configurable strictness
|
|
21
|
+
* - Night token rewards via rewardNight()
|
|
22
|
+
* - Time advancement for TTL and time-sensitive tests
|
|
23
|
+
*/
|
|
24
|
+
import { Effect, Either, pipe, Stream, SubscriptionRef } from 'effect';
|
|
25
|
+
import { addressFromKey, ClaimRewardsTransaction, createShieldedCoinInfo, Intent, LedgerState, SignatureErased, Transaction, TransactionContext, UnshieldedOffer, ZswapOffer, ZswapOutput, } from '@midnight-ntwrk/ledger-v8';
|
|
26
|
+
import { DateOps, LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
27
|
+
import { NetworkId, ProtocolVersion, Token } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
28
|
+
import { addToMempool, allMempoolTransactions, blankState, blockHash, createBlock, createEmptyBlock, createStrictness, defaultStrictness, genesisStrictness, getLastBlock, hasPendingTransactions, nextBlockContextFromBlock, processTransactions, removeFromMempool, resolveFullness, scheduleFork, setProtocolVersion, } from './SimulatorState.js';
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Type Guards and Helpers for Genesis Mints
|
|
31
|
+
// =============================================================================
|
|
32
|
+
/** Type guard for shielded genesis mint. */
|
|
33
|
+
const isShieldedMint = (mint) => mint.type === 'shielded';
|
|
34
|
+
/** Type guard for unshielded genesis mint. */
|
|
35
|
+
const isUnshieldedMint = (mint) => mint.type === 'unshielded';
|
|
36
|
+
/**
|
|
37
|
+
* Check if an unshielded mint is for the native Night token. Night is auto-detected by comparing tokenType with the
|
|
38
|
+
* SDK's own `Token.night`, which is what both ledger versions call the native token.
|
|
39
|
+
*/
|
|
40
|
+
const isNightToken = (tokenType) => tokenType === Token.night;
|
|
41
|
+
// Re-export state accessors for backward compatibility
|
|
42
|
+
export { getLastBlock, getCurrentBlockNumber, getBlockByNumber, getLastBlockResults, getLastBlockEvents, hasPendingTransactions, getCurrentTime, getProtocolVersion, protocolVersionAt, applyTransaction, defaultStrictness, genesisStrictness, createStrictness, } from './SimulatorState.js';
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Block Producers
|
|
45
|
+
// =============================================================================
|
|
46
|
+
/**
|
|
47
|
+
* Default block producer: produces a block for each state change with non-empty mempool.
|
|
48
|
+
*
|
|
49
|
+
* By default, uses post-genesis strictness (balancing, signatures, limits enforced). This ensures realistic simulation
|
|
50
|
+
* where transactions must be properly balanced (pay fees).
|
|
51
|
+
*
|
|
52
|
+
* @param fullness - Static fullness (0-1) or callback based on state
|
|
53
|
+
* @param strictness - Strictness config (defaults to defaultStrictness)
|
|
54
|
+
*/
|
|
55
|
+
export const immediateBlockProducer = (fullness = 0.5, strictness = defaultStrictness) => (states) => states.pipe(Stream.filter(hasPendingTransactions), Stream.map((s) => allMempoolTransactions(s, resolveFullness(fullness, s), createStrictness(strictness))));
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Simulator Class
|
|
58
|
+
// =============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Unified simulator for wallet testing.
|
|
61
|
+
*
|
|
62
|
+
* Provides a simulated ledger environment for testing wallet functionality without a real blockchain. Optionally
|
|
63
|
+
* pre-funds accounts via genesis mints.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // Empty ledger (useful for dust/Night token testing via rewardNight)
|
|
68
|
+
* const simulator = yield* Simulator.init({});
|
|
69
|
+
*
|
|
70
|
+
* // Pre-funded accounts (useful for token transfer testing)
|
|
71
|
+
* const simulator = yield* Simulator.init({
|
|
72
|
+
* genesisMints: [{ amount: 1000n, tokenType, shieldedRecipient: secretKeys }],
|
|
73
|
+
* });
|
|
74
|
+
* ```;
|
|
75
|
+
*/
|
|
76
|
+
export class Simulator {
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
// Static Methods
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
/**
|
|
81
|
+
* Initialize a new simulator.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Empty ledger - use rewardNight() for Night tokens
|
|
86
|
+
* const simulator = yield* Simulator.init({});
|
|
87
|
+
*
|
|
88
|
+
* // Pre-funded accounts for token transfer testing
|
|
89
|
+
* const simulator = yield* Simulator.init({
|
|
90
|
+
* genesisMints: [{ amount: 1000n, tokenType, shieldedRecipient: secretKeys }],
|
|
91
|
+
* });
|
|
92
|
+
*
|
|
93
|
+
* // With custom network ID
|
|
94
|
+
* const simulator = yield* Simulator.init({
|
|
95
|
+
* networkId: NetworkId.Preview,
|
|
96
|
+
* genesisMints: [...],
|
|
97
|
+
* });
|
|
98
|
+
* ```;
|
|
99
|
+
*
|
|
100
|
+
* @param config - Configuration options (all optional)
|
|
101
|
+
* @returns Effect that produces a Simulator instance
|
|
102
|
+
*/
|
|
103
|
+
static init(config = {}) {
|
|
104
|
+
const networkId = config.networkId ?? NetworkId.NetworkId.Undeployed;
|
|
105
|
+
const genesis = {
|
|
106
|
+
protocolVersion: config.protocolVersion ?? ProtocolVersion.MinSupportedVersion,
|
|
107
|
+
blockNumber: config.genesisBlockNumber ?? 0n,
|
|
108
|
+
time: config.genesisTime ?? new Date(0),
|
|
109
|
+
};
|
|
110
|
+
return config.genesisMints !== undefined && config.genesisMints.length > 0
|
|
111
|
+
? Simulator.initWithGenesis(config.genesisMints, networkId, genesis, config.blockProducer)
|
|
112
|
+
: Simulator.initBlank(networkId, genesis, config.blockProducer);
|
|
113
|
+
}
|
|
114
|
+
/** Initialize simulator with blank ledger state. */
|
|
115
|
+
static initBlank(networkId, genesis, blockProducer) {
|
|
116
|
+
return pipe(Effect.promise(() => blankState(networkId, {
|
|
117
|
+
protocolVersion: genesis.protocolVersion,
|
|
118
|
+
genesisBlockNumber: genesis.blockNumber,
|
|
119
|
+
genesisTime: genesis.time,
|
|
120
|
+
})), Effect.flatMap((state) => Simulator.fromState(state, blockProducer)));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Initialize simulator with genesis mints (pre-funded accounts). Supports shielded and unshielded token mints. Night
|
|
124
|
+
* tokens are auto-detected by comparing tokenType with `Token.night`.
|
|
125
|
+
*/
|
|
126
|
+
static initWithGenesis(genesisMints, networkId = NetworkId.NetworkId.Undeployed, genesis, blockProducer) {
|
|
127
|
+
const noStrictness = createStrictness();
|
|
128
|
+
// Pure function to extract shielded mint data (returns array for flatMap)
|
|
129
|
+
const toShieldedMint = (mint) => isShieldedMint(mint) ? [{ tokenType: mint.tokenType, amount: mint.amount, keys: mint.recipient }] : [];
|
|
130
|
+
// Pure function to extract non-Night unshielded mint data (returns array for flatMap)
|
|
131
|
+
// Night is auto-detected by tokenType and excluded from regular unshielded minting
|
|
132
|
+
const toCustomUnshieldedMint = (mint) => isUnshieldedMint(mint) && !isNightToken(mint.tokenType)
|
|
133
|
+
? [{ tokenType: mint.tokenType, amount: mint.amount, recipient: mint.recipient }]
|
|
134
|
+
: [];
|
|
135
|
+
// Pure function to extract Night mint data from unshielded mints (returns array for flatMap)
|
|
136
|
+
// Night is auto-detected by comparing tokenType with `Token.night`
|
|
137
|
+
const toNightMint = (mint) => isUnshieldedMint(mint) && isNightToken(mint.tokenType) && mint.verifyingKey !== undefined
|
|
138
|
+
? [{ amount: mint.amount, recipient: mint.recipient, verifyingKey: mint.verifyingKey }]
|
|
139
|
+
: [];
|
|
140
|
+
// Pure function to create a ZswapOffer from shielded mint data
|
|
141
|
+
const createShieldedOffer = (transfer) => {
|
|
142
|
+
const coin = createShieldedCoinInfo(transfer.tokenType, transfer.amount);
|
|
143
|
+
const output = ZswapOutput.new(coin, 0, transfer.keys.coinPublicKey, transfer.keys.encryptionPublicKey);
|
|
144
|
+
return ZswapOffer.fromOutput(output, transfer.tokenType, transfer.amount);
|
|
145
|
+
};
|
|
146
|
+
// Pure function to create an Intent with UnshieldedOffer
|
|
147
|
+
// Note: Intent API requires mutation, isolated here
|
|
148
|
+
const createUnshieldedIntent = (mints, ttl) => {
|
|
149
|
+
const outputs = mints.map((mint) => ({ type: mint.tokenType, value: mint.amount, owner: mint.recipient }));
|
|
150
|
+
const intent = Intent.new(ttl);
|
|
151
|
+
intent.guaranteedUnshieldedOffer = UnshieldedOffer.new([], outputs, []);
|
|
152
|
+
return intent;
|
|
153
|
+
};
|
|
154
|
+
// Process shielded and custom unshielded mints in initial block (Night handled separately)
|
|
155
|
+
const makeInitialTransactions = (ledgerState, context, blockTime) => {
|
|
156
|
+
const verificationTime = blockTime;
|
|
157
|
+
// Separate mints by type using flatMap (pure, no mutation)
|
|
158
|
+
// Night tokens are excluded and handled separately via reward/claim mechanism
|
|
159
|
+
const shieldedMints = genesisMints.flatMap(toShieldedMint);
|
|
160
|
+
const customUnshieldedMints = genesisMints.flatMap(toCustomUnshieldedMint);
|
|
161
|
+
// If no shielded or custom unshielded mints, return empty result
|
|
162
|
+
if (shieldedMints.length === 0 && customUnshieldedMints.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
initialState: ledgerState,
|
|
165
|
+
transactions: [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Create ZswapOffer for shielded mints (if any)
|
|
169
|
+
const zswapOffer = shieldedMints.length > 0
|
|
170
|
+
? shieldedMints.map(createShieldedOffer).reduce((acc, offer) => acc.merge(offer))
|
|
171
|
+
: undefined;
|
|
172
|
+
// Create Intent with UnshieldedOffer for custom unshielded mints (if any)
|
|
173
|
+
const ttl = new Date(blockTime.getTime() + 3600 * 1000); // 1 hour TTL from block time
|
|
174
|
+
const intent = customUnshieldedMints.length > 0 ? createUnshieldedIntent(customUnshieldedMints, ttl) : undefined;
|
|
175
|
+
// Build transaction from parts
|
|
176
|
+
const proofErasedTx = Transaction.fromParts(networkId, zswapOffer, undefined, intent).eraseProofs();
|
|
177
|
+
const verifiedTx = proofErasedTx.wellFormed(ledgerState, noStrictness, verificationTime);
|
|
178
|
+
const [newState, result] = ledgerState.apply(verifiedTx, new TransactionContext(ledgerState, context));
|
|
179
|
+
return {
|
|
180
|
+
initialState: newState,
|
|
181
|
+
transactions: [{ tx: proofErasedTx, result }],
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
// Process a single Night mint by distributing Night and creating claim transaction
|
|
185
|
+
const processNightMint = (ledgerState, mint, blockTime, context) => {
|
|
186
|
+
// Distribute Night tokens (makes them claimable)
|
|
187
|
+
const ledgerWithReward = ledgerState.testingDistributeNight(mint.recipient, mint.amount, blockTime);
|
|
188
|
+
// Create claim transaction
|
|
189
|
+
const signature = new SignatureErased();
|
|
190
|
+
const claimTx = new ClaimRewardsTransaction(signature.instance, networkId, mint.amount, mint.verifyingKey, LedgerOps.randomNonce(), signature);
|
|
191
|
+
const proofErasedTx = Transaction.fromRewards(claimTx).eraseProofs();
|
|
192
|
+
const verifiedTx = proofErasedTx.wellFormed(ledgerWithReward, noStrictness, blockTime);
|
|
193
|
+
const [newState, result] = ledgerWithReward.apply(verifiedTx, new TransactionContext(ledgerWithReward, context));
|
|
194
|
+
return { ledgerState: newState, tx: proofErasedTx, result };
|
|
195
|
+
};
|
|
196
|
+
return Effect.gen(function* () {
|
|
197
|
+
const genesisTime = genesis.time;
|
|
198
|
+
const emptyState = LedgerState.blank(networkId);
|
|
199
|
+
const context = yield* Effect.promise(() => nextBlockContextFromBlock(undefined, genesisTime, genesis.blockNumber));
|
|
200
|
+
// Process shielded and unshielded mints first
|
|
201
|
+
const init = makeInitialTransactions(emptyState, context, genesisTime);
|
|
202
|
+
// Apply post-block update before processing Night mints
|
|
203
|
+
// Night distribution requires the ledger to be in a consistent post-block state
|
|
204
|
+
const postBlockState = init.initialState.postBlockUpdate(genesisTime);
|
|
205
|
+
// Process Night mints sequentially using reduce (pure functional fold)
|
|
206
|
+
const nightMints = genesisMints.flatMap(toNightMint);
|
|
207
|
+
const nightResults = nightMints.reduce((acc, mint) => {
|
|
208
|
+
const result = processNightMint(acc.ledgerState, mint, genesisTime, context);
|
|
209
|
+
return {
|
|
210
|
+
ledgerState: result.ledgerState,
|
|
211
|
+
transactions: [...acc.transactions, { tx: result.tx, result: result.result }],
|
|
212
|
+
};
|
|
213
|
+
}, {
|
|
214
|
+
ledgerState: postBlockState,
|
|
215
|
+
transactions: [],
|
|
216
|
+
});
|
|
217
|
+
// Apply final post-block update
|
|
218
|
+
const finalLedger = nightResults.ledgerState.postBlockUpdate(genesisTime);
|
|
219
|
+
// Combine all transactions
|
|
220
|
+
const allTransactions = [...init.transactions, ...nightResults.transactions];
|
|
221
|
+
// Create genesis block with all transactions
|
|
222
|
+
const genesisBlock = {
|
|
223
|
+
number: genesis.blockNumber,
|
|
224
|
+
hash: context.parentBlockHash,
|
|
225
|
+
timestamp: genesisTime,
|
|
226
|
+
transactions: allTransactions,
|
|
227
|
+
protocolVersion: genesis.protocolVersion,
|
|
228
|
+
};
|
|
229
|
+
const initialState = {
|
|
230
|
+
networkId,
|
|
231
|
+
ledger: finalLedger,
|
|
232
|
+
blocks: [genesisBlock],
|
|
233
|
+
mempool: [],
|
|
234
|
+
currentTime: genesisTime, // Time stays at genesis; next block will advance it
|
|
235
|
+
protocolVersion: genesis.protocolVersion,
|
|
236
|
+
scheduledForks: [],
|
|
237
|
+
};
|
|
238
|
+
return yield* Simulator.fromState(initialState, blockProducer);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Create a Simulator from an initial state with proper stream setup.
|
|
243
|
+
*
|
|
244
|
+
* The low-level constructor behind {@link Simulator.init}. Use it when the initial state cannot be expressed as a
|
|
245
|
+
* config — most importantly a ledger-v9 chain, whose ledger is handed over from the chain before it.
|
|
246
|
+
*
|
|
247
|
+
* @param initialState - State the simulator starts from
|
|
248
|
+
* @param blockProducer - Custom block producer (defaults to immediateBlockProducer())
|
|
249
|
+
*/
|
|
250
|
+
static fromState(initialState, blockProducer) {
|
|
251
|
+
return Effect.gen(function* () {
|
|
252
|
+
const stateRef = yield* SubscriptionRef.make(initialState);
|
|
253
|
+
// Create a shared stream of state changes.
|
|
254
|
+
// Note: SubscriptionRef.changes only emits on updates, not the initial value.
|
|
255
|
+
// Consumers (sync services) should get the initial state via getLatestState() if needed.
|
|
256
|
+
const stateChangesStream = yield* Stream.share(stateRef.changes, {
|
|
257
|
+
capacity: 'unbounded',
|
|
258
|
+
replay: 1,
|
|
259
|
+
});
|
|
260
|
+
// Create instance first so we can use instance method in the stream
|
|
261
|
+
const simulator = new Simulator(stateRef, stateChangesStream);
|
|
262
|
+
// Set up block production stream
|
|
263
|
+
const effectiveProducer = blockProducer ?? immediateBlockProducer();
|
|
264
|
+
const statesForProducer = Stream.concat(Stream.succeed(initialState), stateRef.changes);
|
|
265
|
+
const productionRequests = effectiveProducer(statesForProducer);
|
|
266
|
+
yield* Effect.forkScoped(productionRequests.pipe(Stream.runForEach((request) => simulator.#produceBlock(request))));
|
|
267
|
+
return simulator;
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// ===========================================================================
|
|
271
|
+
// Instance Properties
|
|
272
|
+
// ===========================================================================
|
|
273
|
+
#stateRef;
|
|
274
|
+
/** Observable stream of simulator state changes. */
|
|
275
|
+
state$;
|
|
276
|
+
constructor(stateRef, state$) {
|
|
277
|
+
this.#stateRef = stateRef;
|
|
278
|
+
this.state$ = state$;
|
|
279
|
+
}
|
|
280
|
+
// ===========================================================================
|
|
281
|
+
// Instance Methods
|
|
282
|
+
// ===========================================================================
|
|
283
|
+
/** Get the current simulator state. */
|
|
284
|
+
getLatestState() {
|
|
285
|
+
return SubscriptionRef.get(this.#stateRef);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Distribute Night tokens to a recipient and submit claim transaction to mempool. Used for testing dust token
|
|
289
|
+
* generation.
|
|
290
|
+
*
|
|
291
|
+
* This method:
|
|
292
|
+
*
|
|
293
|
+
* 1. Modifies the ledger to make Night tokens claimable
|
|
294
|
+
* 2. Creates and submits a ClaimRewardsTransaction to the mempool
|
|
295
|
+
* 3. The block producer will process the transaction
|
|
296
|
+
*
|
|
297
|
+
* @param verifyingKey - Signature verifying key (recipient address is derived from it)
|
|
298
|
+
* @param amount - Amount of Night tokens to distribute
|
|
299
|
+
*/
|
|
300
|
+
rewardNight(verifyingKey, amount) {
|
|
301
|
+
const stateRef = this.#stateRef;
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
303
|
+
const simulator = this;
|
|
304
|
+
const recipient = addressFromKey(verifyingKey);
|
|
305
|
+
return Effect.gen(function* () {
|
|
306
|
+
// First, modify ledger state to make Night tokens claimable
|
|
307
|
+
yield* SubscriptionRef.updateEffect(stateRef, (simulatorState) => pipe(LedgerOps.ledgerTry(() => simulatorState.ledger.testingDistributeNight(recipient, amount, simulatorState.currentTime)), Effect.map((newLedgerState) => ({
|
|
308
|
+
...simulatorState,
|
|
309
|
+
ledger: newLedgerState,
|
|
310
|
+
}))));
|
|
311
|
+
// Create and submit the claim transaction through submitTransaction
|
|
312
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
313
|
+
const signature = new SignatureErased();
|
|
314
|
+
const claimRewardsTransaction = new ClaimRewardsTransaction(signature.instance, currentState.networkId, amount, verifyingKey, LedgerOps.randomNonce(), signature);
|
|
315
|
+
const tx = Transaction.fromRewards(claimRewardsTransaction).eraseProofs();
|
|
316
|
+
// Submit transaction with genesisStrictness - reward claims are not balanced transactions
|
|
317
|
+
return yield* simulator.submitTransaction(tx, { strictness: genesisStrictness });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Submit a transaction and wait for it to be included in a block.
|
|
322
|
+
*
|
|
323
|
+
* This method adds the transaction to the mempool and blocks until the block producer includes it in a block. Use
|
|
324
|
+
* this when you need confirmation that the transaction was processed.
|
|
325
|
+
*
|
|
326
|
+
* For fire-and-forget scenarios where you don't need to wait for block inclusion, use `submitAndForget` instead.
|
|
327
|
+
*
|
|
328
|
+
* @param tx - Transaction to submit (proofs erased)
|
|
329
|
+
* @param options - Optional submission options
|
|
330
|
+
* @param options.strictness - Override well-formedness strictness
|
|
331
|
+
* @returns The block containing the transaction
|
|
332
|
+
*/
|
|
333
|
+
submitTransaction(tx, options) {
|
|
334
|
+
// Only set strictness if explicitly provided; otherwise let block producer assign default
|
|
335
|
+
const pendingTx = options?.strictness !== undefined ? { tx, strictness: createStrictness(options.strictness) } : { tx };
|
|
336
|
+
const stateRef = this.#stateRef;
|
|
337
|
+
return Effect.gen(function* () {
|
|
338
|
+
// Add to mempool
|
|
339
|
+
yield* SubscriptionRef.update(stateRef, (s) => addToMempool(s, pendingTx));
|
|
340
|
+
// Wait for the transaction to be processed (removed from mempool)
|
|
341
|
+
// by watching state changes
|
|
342
|
+
const finalState = yield* pipe(stateRef.changes, Stream.filter((s) => !s.mempool.includes(pendingTx)), Stream.take(1), Stream.runHead);
|
|
343
|
+
if (finalState._tag === 'None') {
|
|
344
|
+
return yield* Effect.die(new Error('State stream ended unexpectedly'));
|
|
345
|
+
}
|
|
346
|
+
// Find the block containing our transaction
|
|
347
|
+
const block = finalState.value.blocks.find((b) => b.transactions.some((bt) => bt.tx === tx));
|
|
348
|
+
if (!block) {
|
|
349
|
+
// Transaction was removed from mempool but not in any block - it failed
|
|
350
|
+
return yield* Effect.fail({
|
|
351
|
+
_tag: 'LedgerError',
|
|
352
|
+
message: 'Transaction was discarded',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return block;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Submit a transaction without waiting for block inclusion.
|
|
360
|
+
*
|
|
361
|
+
* This method adds the transaction to the mempool and returns immediately. The block producer will process it
|
|
362
|
+
* asynchronously. Use this for fire-and-forget scenarios or when testing custom block producers with batched
|
|
363
|
+
* transactions.
|
|
364
|
+
*
|
|
365
|
+
* To wait for block inclusion, use `submitTransaction` instead.
|
|
366
|
+
*
|
|
367
|
+
* @param tx - Transaction to submit (proofs erased)
|
|
368
|
+
* @param options - Optional options
|
|
369
|
+
* @param options.strictness - Override well-formedness strictness
|
|
370
|
+
*/
|
|
371
|
+
submitAndForget(tx, options) {
|
|
372
|
+
// Only set strictness if explicitly provided; otherwise let block producer assign default
|
|
373
|
+
const pendingTx = options?.strictness !== undefined ? { tx, strictness: createStrictness(options.strictness) } : { tx };
|
|
374
|
+
return SubscriptionRef.update(this.#stateRef, (s) => addToMempool(s, pendingTx));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Produce a block with no transactions, advancing the chain by one height.
|
|
378
|
+
*
|
|
379
|
+
* Bypasses the configured block producer, which only produces when there is something in the mempool. Use it to reach
|
|
380
|
+
* a given height without traffic — a fork happens at a height whether or not transactions were submitted.
|
|
381
|
+
*
|
|
382
|
+
* @returns The produced block
|
|
383
|
+
*/
|
|
384
|
+
produceEmptyBlock() {
|
|
385
|
+
return this.#produceBlock({ transactions: [], fullness: 0 });
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Switch the chain to a protocol version, effective for blocks produced from now on. Already-produced blocks keep the
|
|
389
|
+
* version they were produced under.
|
|
390
|
+
*
|
|
391
|
+
* @param version - Version to switch the chain to
|
|
392
|
+
*/
|
|
393
|
+
setProtocolVersion(version) {
|
|
394
|
+
return SubscriptionRef.update(this.#stateRef, (state) => setProtocolVersion(state, version));
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Schedule a protocol version to activate at a block height — a fork on the simulated chain. The block produced at
|
|
398
|
+
* that height, and every block after it, is stamped with `version`.
|
|
399
|
+
*
|
|
400
|
+
* @param atBlock - Height of the first block produced under `version`
|
|
401
|
+
* @param version - Version that activates at `atBlock`
|
|
402
|
+
*/
|
|
403
|
+
scheduleFork(atBlock, version) {
|
|
404
|
+
return SubscriptionRef.update(this.#stateRef, (state) => scheduleFork(state, atBlock, version));
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Fast-forward the simulator time by the given number of seconds. Does not produce a block - only advances the
|
|
408
|
+
* internal clock. Useful for testing time-sensitive functionality like TTL.
|
|
409
|
+
*
|
|
410
|
+
* @param seconds - Number of seconds to advance (must be positive)
|
|
411
|
+
*/
|
|
412
|
+
fastForward(seconds) {
|
|
413
|
+
return SubscriptionRef.update(this.#stateRef, (simulatorState) => ({
|
|
414
|
+
...simulatorState,
|
|
415
|
+
currentTime: DateOps.addSeconds(simulatorState.currentTime, seconds),
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
// ===========================================================================
|
|
419
|
+
// Internal Block Production
|
|
420
|
+
// ===========================================================================
|
|
421
|
+
/**
|
|
422
|
+
* Produce a block from the given block production request. Internal method used by the block producer stream.
|
|
423
|
+
*
|
|
424
|
+
* This method orchestrates block production by:
|
|
425
|
+
*
|
|
426
|
+
* 1. Computing the block hash (async)
|
|
427
|
+
* 2. Processing transactions using pure functions from SimulatorState
|
|
428
|
+
* 3. Creating the block and updating state atomically
|
|
429
|
+
*
|
|
430
|
+
* Each transaction in the request has its own strictness assigned by the block producer.
|
|
431
|
+
*
|
|
432
|
+
* @param request - Block production request with transactions and fullness
|
|
433
|
+
*/
|
|
434
|
+
#produceBlock(request) {
|
|
435
|
+
const stateRef = this.#stateRef;
|
|
436
|
+
const { transactions, fullness } = request;
|
|
437
|
+
return Effect.gen(function* () {
|
|
438
|
+
const blockResult = yield* SubscriptionRef.modifyEffect(stateRef, (simulatorState) => Effect.gen(function* () {
|
|
439
|
+
// Advance time first, then use it for the block
|
|
440
|
+
const blockTime = DateOps.addSeconds(simulatorState.currentTime, 1);
|
|
441
|
+
const previousBlock = getLastBlock(simulatorState);
|
|
442
|
+
const nextBlockNumber = previousBlock !== undefined ? previousBlock.number + 1n : 0n;
|
|
443
|
+
const hash = yield* Effect.promise(() => blockHash(nextBlockNumber));
|
|
444
|
+
if (transactions.length === 0) {
|
|
445
|
+
// No transactions to process, return empty block using pure function
|
|
446
|
+
const [emptyBlock, newState] = createEmptyBlock(simulatorState, hash, blockTime, transactions);
|
|
447
|
+
return [Either.right(emptyBlock), newState];
|
|
448
|
+
}
|
|
449
|
+
const context = yield* Effect.promise(() => nextBlockContextFromBlock(previousBlock, blockTime));
|
|
450
|
+
// Process all transactions - each has its own strictness assigned by block producer
|
|
451
|
+
const processingResult = processTransactions(simulatorState.ledger, transactions, blockTime, context, fullness);
|
|
452
|
+
if (Either.isLeft(processingResult)) {
|
|
453
|
+
// Transaction failed, remove from mempool and return error
|
|
454
|
+
const newState = removeFromMempool(simulatorState, transactions);
|
|
455
|
+
return [Either.left(processingResult.left), newState];
|
|
456
|
+
}
|
|
457
|
+
const { blockTransactions, finalLedger } = processingResult.right;
|
|
458
|
+
// Create block using pure function
|
|
459
|
+
const [block, newState] = createBlock(simulatorState, blockTransactions, hash, blockTime, finalLedger, transactions);
|
|
460
|
+
return [Either.right(block), newState];
|
|
461
|
+
}));
|
|
462
|
+
// Return the result
|
|
463
|
+
if (Either.isLeft(blockResult)) {
|
|
464
|
+
return yield* Effect.fail(blockResult.left);
|
|
465
|
+
}
|
|
466
|
+
return blockResult.right;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// ===========================================================================
|
|
470
|
+
// Query Method
|
|
471
|
+
// ===========================================================================
|
|
472
|
+
/**
|
|
473
|
+
* Query the simulator state with a custom function. This is a generic query mechanism that allows extracting any
|
|
474
|
+
* information from the current state without modifying it.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```typescript
|
|
478
|
+
* // Query fee prices
|
|
479
|
+
* const feePrices = yield* simulator.query(state => state.ledger.parameters.feePrices);
|
|
480
|
+
*
|
|
481
|
+
* // Use composable state accessors
|
|
482
|
+
* const blockNumber = yield* simulator.query(getCurrentBlockNumber);
|
|
483
|
+
* const lastBlock = yield* simulator.query(getLastBlock);
|
|
484
|
+
* const events = yield* simulator.query(getLastBlockEvents);
|
|
485
|
+
*
|
|
486
|
+
* // Query UTXOs for an address
|
|
487
|
+
* const utxos = yield* simulator.query(state => Array.from(state.ledger.utxo.filter(address)));
|
|
488
|
+
*
|
|
489
|
+
* // Complex query returning multiple values
|
|
490
|
+
* const info = yield* simulator.query(state => ({
|
|
491
|
+
* networkId: state.networkId,
|
|
492
|
+
* blockNumber: getCurrentBlockNumber(state),
|
|
493
|
+
* feePrices: state.ledger.parameters.feePrices,
|
|
494
|
+
* }));
|
|
495
|
+
* ```;
|
|
496
|
+
*
|
|
497
|
+
* @param fn - Function that receives the current state and returns a result
|
|
498
|
+
* @returns The result of applying the function to the current state
|
|
499
|
+
*/
|
|
500
|
+
query(fn) {
|
|
501
|
+
return Effect.map(SubscriptionRef.get(this.#stateRef), fn);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* synchronous and side-effect free.
|
|
6
6
|
*/
|
|
7
7
|
import { Either, type Stream, Array as EArray } from 'effect';
|
|
8
|
-
import { LedgerState,
|
|
8
|
+
import { LedgerState, WellFormedStrictness, type TransactionResult, type ProofErasedTransaction, type SyntheticCost, type RawTokenType, type ZswapSecretKeys, type UserAddress, type SignatureVerifyingKey } from '@midnight-ntwrk/ledger-v8';
|
|
9
9
|
import { LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
10
|
-
import { type NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
10
|
+
import { type NetworkId, ProtocolVersion } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
11
|
+
import { type BlockContext, type ScheduledFork, type StrictnessConfig } from '../core/index.js';
|
|
12
|
+
export { blockHash, defaultStrictness, genesisStrictness, getProtocolVersion, nextBlockContextFromBlock, protocolVersionAt, scheduleFork, setProtocolVersion, type BlockContext, type ScheduledFork, type StrictnessConfig, } from '../core/index.js';
|
|
11
13
|
/** A transaction included in a block with its execution result. */
|
|
12
14
|
export type BlockTransaction = Readonly<{
|
|
13
15
|
/** The transaction that was executed */
|
|
@@ -25,6 +27,12 @@ export type Block = Readonly<{
|
|
|
25
27
|
timestamp: Date;
|
|
26
28
|
/** Transactions in this block, ordered by execution */
|
|
27
29
|
transactions: readonly BlockTransaction[];
|
|
30
|
+
/**
|
|
31
|
+
* Protocol version this block was produced under. Stamped at production time and never revised afterwards, so the
|
|
32
|
+
* chain's history records which version each block belongs to — exactly what a syncing wallet reads to decide which
|
|
33
|
+
* codec applies and when to migrate.
|
|
34
|
+
*/
|
|
35
|
+
protocolVersion: ProtocolVersion.ProtocolVersion;
|
|
28
36
|
}>;
|
|
29
37
|
/**
|
|
30
38
|
* Pending transaction waiting for block production. Strictness is optional - if not specified, block producer assigns
|
|
@@ -54,6 +62,10 @@ export type SimulatorState = Readonly<{
|
|
|
54
62
|
mempool: readonly PendingTransaction[];
|
|
55
63
|
/** Current simulator time (independent of block numbers) */
|
|
56
64
|
currentTime: Date;
|
|
65
|
+
/** Protocol version the chain is currently on — stamped onto blocks as they are produced */
|
|
66
|
+
protocolVersion: ProtocolVersion.ProtocolVersion;
|
|
67
|
+
/** Version activations scheduled at block heights, applied when those heights are produced */
|
|
68
|
+
scheduledForks: readonly ScheduledFork[];
|
|
57
69
|
}>;
|
|
58
70
|
/** Result of a successful block production. */
|
|
59
71
|
export type BlockInfo = Readonly<{
|
|
@@ -170,29 +182,6 @@ export type UnshieldedGenesisMint = Readonly<{
|
|
|
170
182
|
* - Night tokens: auto-detected by tokenType, uses reward/claim mechanism (verifyingKey required)
|
|
171
183
|
*/
|
|
172
184
|
export type GenesisMint = ShieldedGenesisMint | UnshieldedGenesisMint;
|
|
173
|
-
/** Configuration for well-formedness strictness checks. All options default to false for testing flexibility. */
|
|
174
|
-
export type StrictnessConfig = Readonly<{
|
|
175
|
-
enforceBalancing?: boolean;
|
|
176
|
-
verifyNativeProofs?: boolean;
|
|
177
|
-
verifyContractProofs?: boolean;
|
|
178
|
-
enforceLimits?: boolean;
|
|
179
|
-
verifySignatures?: boolean;
|
|
180
|
-
}>;
|
|
181
|
-
/**
|
|
182
|
-
* Default strictness for post-genesis blocks.
|
|
183
|
-
*
|
|
184
|
-
* In a realistic simulation:
|
|
185
|
-
*
|
|
186
|
-
* - Signatures should be verified (verifySignatures: true)
|
|
187
|
-
* - Proofs cannot be verified because they're erased (verifyNativeProofs/verifyContractProofs: false)
|
|
188
|
-
* - Limits should be enforced (enforceLimits: true)
|
|
189
|
-
* - Balancing must be enforced (enforceBalancing: true) - transactions must pay fees
|
|
190
|
-
*
|
|
191
|
-
* Note: Genesis blocks typically disable all strictness to allow initial token distribution.
|
|
192
|
-
*/
|
|
193
|
-
export declare const defaultStrictness: StrictnessConfig;
|
|
194
|
-
/** Strictness for genesis blocks - all checks disabled to allow initial token distribution. */
|
|
195
|
-
export declare const genesisStrictness: StrictnessConfig;
|
|
196
185
|
/**
|
|
197
186
|
* Assign strictness to a pending transaction, creating a ready transaction. If the pending transaction already has
|
|
198
187
|
* strictness, use it; otherwise use the provided default.
|
|
@@ -244,8 +233,21 @@ export declare const getCurrentTime: (state: SimulatorState) => Date;
|
|
|
244
233
|
export declare const resolveFullness: (spec: FullnessSpec, state: SimulatorState) => number;
|
|
245
234
|
/** Create a block production request that includes all mempool transactions. */
|
|
246
235
|
export declare const allMempoolTransactions: (state: SimulatorState, fullness: number, defaultStrictness: WellFormedStrictness) => BlockProductionRequest;
|
|
247
|
-
/**
|
|
248
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Create a blank initial state.
|
|
238
|
+
*
|
|
239
|
+
* @param networkId - Network identifier
|
|
240
|
+
* @param options - Optional genesis parameters
|
|
241
|
+
* @param options.protocolVersion - Version the chain starts on (defaults to the minimum supported version)
|
|
242
|
+
* @param options.genesisBlockNumber - Height of the genesis block (defaults to 0; a ledger-v9 chain continues numbering
|
|
243
|
+
* from the fork height instead)
|
|
244
|
+
* @param options.genesisTime - Timestamp of the genesis block (defaults to the epoch)
|
|
245
|
+
*/
|
|
246
|
+
export declare const blankState: (networkId: NetworkId.NetworkId, options?: {
|
|
247
|
+
protocolVersion?: ProtocolVersion.ProtocolVersion;
|
|
248
|
+
genesisBlockNumber?: bigint;
|
|
249
|
+
genesisTime?: Date;
|
|
250
|
+
}) => Promise<SimulatorState>;
|
|
249
251
|
/** Add a pending transaction to the mempool. */
|
|
250
252
|
export declare const addToMempool: (state: SimulatorState, pendingTx: PendingTransaction) => SimulatorState;
|
|
251
253
|
/** Remove transactions from the mempool. */
|
|
@@ -280,10 +282,10 @@ export type TransactionProcessingResult = Readonly<{
|
|
|
280
282
|
newLedger: LedgerState;
|
|
281
283
|
}>;
|
|
282
284
|
/**
|
|
283
|
-
* Process a single pending transaction against the
|
|
285
|
+
* Process a single pending transaction against the latest ledger state. Returns Either with the processing result or an
|
|
284
286
|
* error.
|
|
285
287
|
*
|
|
286
|
-
* @param ledger -
|
|
288
|
+
* @param ledger - The ledger state to process against
|
|
287
289
|
* @param readyTx - Transaction to process
|
|
288
290
|
* @param blockTime - Block timestamp
|
|
289
291
|
* @param blockContext - Block context
|
|
@@ -332,18 +334,3 @@ export declare const createEmptyBlock: (state: SimulatorState, blockHashValue: s
|
|
|
332
334
|
* unavoidable given the external API design.
|
|
333
335
|
*/
|
|
334
336
|
export declare const createStrictness: (config?: StrictnessConfig) => WellFormedStrictness;
|
|
335
|
-
/**
|
|
336
|
-
* Compute block hash from block number. Uses a deterministic hash based on block number for easy recomputation.
|
|
337
|
-
*
|
|
338
|
-
* @param blockNumber - The block number to compute hash for
|
|
339
|
-
* @returns A deterministic 64-character hex hash
|
|
340
|
-
*/
|
|
341
|
-
export declare const blockHash: (blockNumber: bigint) => Promise<string>;
|
|
342
|
-
/**
|
|
343
|
-
* Create the next block context from the previous block.
|
|
344
|
-
*
|
|
345
|
-
* @param previousBlock - The previous block (or undefined for genesis)
|
|
346
|
-
* @param blockTime - The timestamp for the new block
|
|
347
|
-
* @returns A BlockContext suitable for transaction processing
|
|
348
|
-
*/
|
|
349
|
-
export declare const nextBlockContextFromBlock: (previousBlock: Block | undefined, blockTime: Date) => Promise<BlockContext>;
|