@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.
Files changed (68) hide show
  1. package/README.md +27 -3
  2. package/dist/chainVersion/chainVersionProbe.d.ts +95 -0
  3. package/dist/chainVersion/chainVersionProbe.js +83 -0
  4. package/dist/chainVersion/index.d.ts +1 -0
  5. package/dist/chainVersion/index.js +13 -0
  6. package/dist/codecs/index.d.ts +1 -0
  7. package/dist/codecs/index.js +13 -0
  8. package/dist/codecs/ledgerParameters.d.ts +81 -0
  9. package/dist/codecs/ledgerParameters.js +63 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +3 -0
  12. package/dist/pendingTransactions/pendingTransactions.d.ts +113 -9
  13. package/dist/pendingTransactions/pendingTransactions.js +147 -30
  14. package/dist/pendingTransactions/pendingTransactionsService.d.ts +25 -9
  15. package/dist/pendingTransactions/pendingTransactionsService.js +33 -21
  16. package/dist/proving/index.d.ts +2 -0
  17. package/dist/proving/index.js +2 -0
  18. package/dist/proving/provingService.d.ts +175 -15
  19. package/dist/proving/provingService.js +104 -11
  20. package/dist/proving/v8ProvingService.d.ts +53 -0
  21. package/dist/proving/v8ProvingService.js +71 -0
  22. package/dist/proving/versionedProving.d.ts +42 -0
  23. package/dist/proving/versionedProving.js +103 -0
  24. package/dist/signatures/index.d.ts +2 -0
  25. package/dist/signatures/index.js +14 -0
  26. package/dist/signatures/signing.d.ts +37 -0
  27. package/dist/signatures/signing.js +13 -0
  28. package/dist/signatures/v8Signatures.d.ts +54 -0
  29. package/dist/signatures/v8Signatures.js +62 -0
  30. package/dist/simulation/ForkSimulator.d.ts +114 -0
  31. package/dist/simulation/ForkSimulator.js +209 -0
  32. package/dist/simulation/LedgerTranslation.d.ts +53 -0
  33. package/dist/simulation/LedgerTranslation.js +56 -0
  34. package/dist/simulation/core/VersionTimeline.d.ts +54 -0
  35. package/dist/simulation/core/VersionTimeline.js +56 -0
  36. package/dist/simulation/core/blocks.d.ts +38 -0
  37. package/dist/simulation/core/blocks.js +52 -0
  38. package/dist/simulation/core/index.d.ts +13 -0
  39. package/dist/simulation/core/index.js +25 -0
  40. package/dist/simulation/core/strictness.d.ts +29 -0
  41. package/dist/simulation/core/strictness.js +39 -0
  42. package/dist/simulation/index.d.ts +22 -2
  43. package/dist/simulation/index.js +25 -14
  44. package/dist/simulation/v8/Simulator.d.ts +231 -0
  45. package/dist/simulation/v8/Simulator.js +503 -0
  46. package/dist/simulation/{SimulatorState.d.ts → v8/SimulatorState.d.ts} +31 -44
  47. package/dist/simulation/v8/SimulatorState.js +290 -0
  48. package/dist/simulation/v8/index.d.ts +2 -0
  49. package/dist/simulation/v8/index.js +26 -0
  50. package/dist/simulation/{Simulator.d.ts → v9/Simulator.d.ts} +57 -6
  51. package/dist/simulation/{Simulator.js → v9/Simulator.js} +68 -18
  52. package/dist/simulation/v9/SimulatorState.d.ts +336 -0
  53. package/dist/simulation/{SimulatorState.js → v9/SimulatorState.js} +34 -67
  54. package/dist/simulation/v9/index.d.ts +2 -0
  55. package/dist/simulation/v9/index.js +26 -0
  56. package/dist/submission/submissionService.d.ts +2 -1
  57. package/dist/submission/submissionService.js +1 -1
  58. package/dist/validation/blockData.d.ts +42 -2
  59. package/dist/validation/blockData.js +55 -8
  60. package/dist/validation/index.d.ts +2 -0
  61. package/dist/validation/index.js +2 -0
  62. package/dist/validation/v8ValidationService.d.ts +29 -0
  63. package/dist/validation/v8ValidationService.js +48 -0
  64. package/dist/validation/validationService.d.ts +132 -17
  65. package/dist/validation/validationService.js +88 -42
  66. package/dist/validation/versionedValidation.d.ts +46 -0
  67. package/dist/validation/versionedValidation.js +55 -0
  68. 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, type BlockContext, WellFormedStrictness, type TransactionResult, type ProofErasedTransaction, type SyntheticCost, type RawTokenType, type ZswapSecretKeys, type UserAddress, type SignatureVerifyingKey } from '@midnightntwrk/ledger-v9';
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
- /** Create a blank initial state. */
248
- export declare const blankState: (networkId: NetworkId.NetworkId) => Promise<SimulatorState>;
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 current ledger. Returns Either with the processing result or an
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 - Current ledger state
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>;