@midnight-ntwrk/wallet-sdk-capabilities 3.2.0 → 3.3.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/dist/balancer/Balancer.d.ts +2 -2
- package/dist/balancer/CounterOffer.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pendingTransactions/pendingTransactions.d.ts +1 -1
- package/dist/pendingTransactions/pendingTransactionsService.d.ts +3 -3
- package/dist/proving/provingService.d.ts +2 -2
- package/dist/proving/provingService.js +1 -1
- package/dist/simulation/Simulator.d.ts +180 -0
- package/dist/simulation/Simulator.js +453 -0
- package/dist/simulation/SimulatorState.d.ts +349 -0
- package/dist/simulation/SimulatorState.js +323 -0
- package/dist/simulation/index.d.ts +2 -0
- package/dist/simulation/index.js +26 -0
- package/dist/submission/submissionService.d.ts +4 -5
- package/dist/submission/submissionService.js +11 -5
- package/package.json +18 -14
|
@@ -0,0 +1,453 @@
|
|
|
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, nativeToken, SignatureErased, Transaction, TransactionContext, UnshieldedOffer, ZswapOffer, ZswapOutput, } from '@midnight-ntwrk/ledger-v8';
|
|
26
|
+
import { DateOps, LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
27
|
+
import { NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
28
|
+
import { addToMempool, allMempoolTransactions, blankState, blockHash, createBlock, createEmptyBlock, createStrictness, defaultStrictness, genesisStrictness, getLastBlock, hasPendingTransactions, nextBlockContextFromBlock, processTransactions, removeFromMempool, resolveFullness, } 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
|
|
38
|
+
* ledger.nativeToken().raw.
|
|
39
|
+
*/
|
|
40
|
+
const isNightToken = (tokenType) => tokenType === nativeToken().raw;
|
|
41
|
+
// Re-export state accessors for backward compatibility
|
|
42
|
+
export { getLastBlock, getCurrentBlockNumber, getBlockByNumber, getLastBlockResults, getLastBlockEvents, hasPendingTransactions, getCurrentTime, 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
|
+
return config.genesisMints !== undefined && config.genesisMints.length > 0
|
|
106
|
+
? Simulator.initWithGenesis(config.genesisMints, networkId, config.blockProducer)
|
|
107
|
+
: Simulator.initBlank(networkId, config.blockProducer);
|
|
108
|
+
}
|
|
109
|
+
/** Initialize simulator with blank ledger state. */
|
|
110
|
+
static initBlank(networkId, blockProducer) {
|
|
111
|
+
return pipe(Effect.promise(() => blankState(networkId)), Effect.flatMap((state) => Simulator.fromState(state, blockProducer)));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Initialize simulator with genesis mints (pre-funded accounts). Supports shielded and unshielded token mints. Night
|
|
115
|
+
* tokens are auto-detected by comparing tokenType with nativeToken().raw.
|
|
116
|
+
*/
|
|
117
|
+
static initWithGenesis(genesisMints, networkId = NetworkId.NetworkId.Undeployed, blockProducer) {
|
|
118
|
+
const noStrictness = createStrictness();
|
|
119
|
+
// Pure function to extract shielded mint data (returns array for flatMap)
|
|
120
|
+
const toShieldedMint = (mint) => isShieldedMint(mint) ? [{ tokenType: mint.tokenType, amount: mint.amount, keys: mint.recipient }] : [];
|
|
121
|
+
// Pure function to extract non-Night unshielded mint data (returns array for flatMap)
|
|
122
|
+
// Night is auto-detected by tokenType and excluded from regular unshielded minting
|
|
123
|
+
const toCustomUnshieldedMint = (mint) => isUnshieldedMint(mint) && !isNightToken(mint.tokenType)
|
|
124
|
+
? [{ tokenType: mint.tokenType, amount: mint.amount, recipient: mint.recipient }]
|
|
125
|
+
: [];
|
|
126
|
+
// Pure function to extract Night mint data from unshielded mints (returns array for flatMap)
|
|
127
|
+
// Night is auto-detected by comparing tokenType with nativeToken().raw
|
|
128
|
+
const toNightMint = (mint) => isUnshieldedMint(mint) && isNightToken(mint.tokenType) && mint.verifyingKey !== undefined
|
|
129
|
+
? [{ amount: mint.amount, recipient: mint.recipient, verifyingKey: mint.verifyingKey }]
|
|
130
|
+
: [];
|
|
131
|
+
// Pure function to create a ZswapOffer from shielded mint data
|
|
132
|
+
const createShieldedOffer = (transfer) => {
|
|
133
|
+
const coin = createShieldedCoinInfo(transfer.tokenType, transfer.amount);
|
|
134
|
+
const output = ZswapOutput.new(coin, 0, transfer.keys.coinPublicKey, transfer.keys.encryptionPublicKey);
|
|
135
|
+
return ZswapOffer.fromOutput(output, transfer.tokenType, transfer.amount);
|
|
136
|
+
};
|
|
137
|
+
// Pure function to create an Intent with UnshieldedOffer
|
|
138
|
+
// Note: Intent API requires mutation, isolated here
|
|
139
|
+
const createUnshieldedIntent = (mints, ttl) => {
|
|
140
|
+
const outputs = mints.map((mint) => ({ type: mint.tokenType, value: mint.amount, owner: mint.recipient }));
|
|
141
|
+
const intent = Intent.new(ttl);
|
|
142
|
+
intent.guaranteedUnshieldedOffer = UnshieldedOffer.new([], outputs, []);
|
|
143
|
+
return intent;
|
|
144
|
+
};
|
|
145
|
+
// Process shielded and custom unshielded mints in initial block (Night handled separately)
|
|
146
|
+
const makeInitialTransactions = (ledgerState, context, blockTime) => {
|
|
147
|
+
const verificationTime = blockTime;
|
|
148
|
+
// Separate mints by type using flatMap (pure, no mutation)
|
|
149
|
+
// Night tokens are excluded and handled separately via reward/claim mechanism
|
|
150
|
+
const shieldedMints = genesisMints.flatMap(toShieldedMint);
|
|
151
|
+
const customUnshieldedMints = genesisMints.flatMap(toCustomUnshieldedMint);
|
|
152
|
+
// If no shielded or custom unshielded mints, return empty result
|
|
153
|
+
if (shieldedMints.length === 0 && customUnshieldedMints.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
initialState: ledgerState,
|
|
156
|
+
transactions: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// Create ZswapOffer for shielded mints (if any)
|
|
160
|
+
const zswapOffer = shieldedMints.length > 0
|
|
161
|
+
? shieldedMints.map(createShieldedOffer).reduce((acc, offer) => acc.merge(offer))
|
|
162
|
+
: undefined;
|
|
163
|
+
// Create Intent with UnshieldedOffer for custom unshielded mints (if any)
|
|
164
|
+
const ttl = new Date(blockTime.getTime() + 3600 * 1000); // 1 hour TTL from block time
|
|
165
|
+
const intent = customUnshieldedMints.length > 0 ? createUnshieldedIntent(customUnshieldedMints, ttl) : undefined;
|
|
166
|
+
// Build transaction from parts
|
|
167
|
+
const proofErasedTx = Transaction.fromParts(networkId, zswapOffer, undefined, intent).eraseProofs();
|
|
168
|
+
const verifiedTx = proofErasedTx.wellFormed(ledgerState, noStrictness, verificationTime);
|
|
169
|
+
const [newState, result] = ledgerState.apply(verifiedTx, new TransactionContext(ledgerState, context));
|
|
170
|
+
return {
|
|
171
|
+
initialState: newState,
|
|
172
|
+
transactions: [{ tx: proofErasedTx, result }],
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
// Process a single Night mint by distributing Night and creating claim transaction
|
|
176
|
+
const processNightMint = (ledgerState, mint, blockTime, context) => {
|
|
177
|
+
// Distribute Night tokens (makes them claimable)
|
|
178
|
+
const ledgerWithReward = ledgerState.testingDistributeNight(mint.recipient, mint.amount, blockTime);
|
|
179
|
+
// Create claim transaction
|
|
180
|
+
const signature = new SignatureErased();
|
|
181
|
+
const claimTx = new ClaimRewardsTransaction(signature.instance, networkId, mint.amount, mint.verifyingKey, LedgerOps.randomNonce(), signature);
|
|
182
|
+
const proofErasedTx = Transaction.fromRewards(claimTx).eraseProofs();
|
|
183
|
+
const verifiedTx = proofErasedTx.wellFormed(ledgerWithReward, noStrictness, blockTime);
|
|
184
|
+
const [newState, result] = ledgerWithReward.apply(verifiedTx, new TransactionContext(ledgerWithReward, context));
|
|
185
|
+
return { ledgerState: newState, tx: proofErasedTx, result };
|
|
186
|
+
};
|
|
187
|
+
return Effect.gen(function* () {
|
|
188
|
+
const genesisTime = new Date(0);
|
|
189
|
+
const emptyState = LedgerState.blank(networkId);
|
|
190
|
+
const context = yield* Effect.promise(() => nextBlockContextFromBlock(undefined, genesisTime));
|
|
191
|
+
// Process shielded and unshielded mints first
|
|
192
|
+
const init = makeInitialTransactions(emptyState, context, genesisTime);
|
|
193
|
+
// Apply post-block update before processing Night mints
|
|
194
|
+
// Night distribution requires the ledger to be in a consistent post-block state
|
|
195
|
+
const postBlockState = init.initialState.postBlockUpdate(genesisTime);
|
|
196
|
+
// Process Night mints sequentially using reduce (pure functional fold)
|
|
197
|
+
const nightMints = genesisMints.flatMap(toNightMint);
|
|
198
|
+
const nightResults = nightMints.reduce((acc, mint) => {
|
|
199
|
+
const result = processNightMint(acc.ledgerState, mint, genesisTime, context);
|
|
200
|
+
return {
|
|
201
|
+
ledgerState: result.ledgerState,
|
|
202
|
+
transactions: [...acc.transactions, { tx: result.tx, result: result.result }],
|
|
203
|
+
};
|
|
204
|
+
}, {
|
|
205
|
+
ledgerState: postBlockState,
|
|
206
|
+
transactions: [],
|
|
207
|
+
});
|
|
208
|
+
// Apply final post-block update
|
|
209
|
+
const finalLedger = nightResults.ledgerState.postBlockUpdate(genesisTime);
|
|
210
|
+
// Combine all transactions
|
|
211
|
+
const allTransactions = [...init.transactions, ...nightResults.transactions];
|
|
212
|
+
// Create genesis block with all transactions
|
|
213
|
+
const genesisBlock = {
|
|
214
|
+
number: 0n,
|
|
215
|
+
hash: context.parentBlockHash,
|
|
216
|
+
timestamp: genesisTime,
|
|
217
|
+
transactions: allTransactions,
|
|
218
|
+
};
|
|
219
|
+
const initialState = {
|
|
220
|
+
networkId,
|
|
221
|
+
ledger: finalLedger,
|
|
222
|
+
blocks: [genesisBlock],
|
|
223
|
+
mempool: [],
|
|
224
|
+
currentTime: genesisTime, // Time stays at genesis; next block will advance it
|
|
225
|
+
};
|
|
226
|
+
return yield* Simulator.fromState(initialState, blockProducer);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/** Create a Simulator from an initial state with proper stream setup. */
|
|
230
|
+
static fromState(initialState, blockProducer) {
|
|
231
|
+
return Effect.gen(function* () {
|
|
232
|
+
const stateRef = yield* SubscriptionRef.make(initialState);
|
|
233
|
+
// Create a shared stream of state changes.
|
|
234
|
+
// Note: SubscriptionRef.changes only emits on updates, not the initial value.
|
|
235
|
+
// Consumers (sync services) should get the initial state via getLatestState() if needed.
|
|
236
|
+
const stateChangesStream = yield* Stream.share(stateRef.changes, {
|
|
237
|
+
capacity: 'unbounded',
|
|
238
|
+
replay: 1,
|
|
239
|
+
});
|
|
240
|
+
// Create instance first so we can use instance method in the stream
|
|
241
|
+
const simulator = new Simulator(stateRef, stateChangesStream);
|
|
242
|
+
// Set up block production stream
|
|
243
|
+
const effectiveProducer = blockProducer ?? immediateBlockProducer();
|
|
244
|
+
const statesForProducer = Stream.concat(Stream.succeed(initialState), stateRef.changes);
|
|
245
|
+
const productionRequests = effectiveProducer(statesForProducer);
|
|
246
|
+
yield* Effect.forkScoped(productionRequests.pipe(Stream.runForEach((request) => simulator.#produceBlock(request))));
|
|
247
|
+
return simulator;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// ===========================================================================
|
|
251
|
+
// Instance Properties
|
|
252
|
+
// ===========================================================================
|
|
253
|
+
#stateRef;
|
|
254
|
+
/** Observable stream of simulator state changes. */
|
|
255
|
+
state$;
|
|
256
|
+
constructor(stateRef, state$) {
|
|
257
|
+
this.#stateRef = stateRef;
|
|
258
|
+
this.state$ = state$;
|
|
259
|
+
}
|
|
260
|
+
// ===========================================================================
|
|
261
|
+
// Instance Methods
|
|
262
|
+
// ===========================================================================
|
|
263
|
+
/** Get the current simulator state. */
|
|
264
|
+
getLatestState() {
|
|
265
|
+
return SubscriptionRef.get(this.#stateRef);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Distribute Night tokens to a recipient and submit claim transaction to mempool. Used for testing dust token
|
|
269
|
+
* generation.
|
|
270
|
+
*
|
|
271
|
+
* This method:
|
|
272
|
+
*
|
|
273
|
+
* 1. Modifies the ledger to make Night tokens claimable
|
|
274
|
+
* 2. Creates and submits a ClaimRewardsTransaction to the mempool
|
|
275
|
+
* 3. The block producer will process the transaction
|
|
276
|
+
*
|
|
277
|
+
* @param verifyingKey - Signature verifying key (recipient address is derived from it)
|
|
278
|
+
* @param amount - Amount of Night tokens to distribute
|
|
279
|
+
*/
|
|
280
|
+
rewardNight(verifyingKey, amount) {
|
|
281
|
+
const stateRef = this.#stateRef;
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
283
|
+
const simulator = this;
|
|
284
|
+
const recipient = addressFromKey(verifyingKey);
|
|
285
|
+
return Effect.gen(function* () {
|
|
286
|
+
// First, modify ledger state to make Night tokens claimable
|
|
287
|
+
yield* SubscriptionRef.updateEffect(stateRef, (simulatorState) => pipe(LedgerOps.ledgerTry(() => simulatorState.ledger.testingDistributeNight(recipient, amount, simulatorState.currentTime)), Effect.map((newLedgerState) => ({
|
|
288
|
+
...simulatorState,
|
|
289
|
+
ledger: newLedgerState,
|
|
290
|
+
}))));
|
|
291
|
+
// Create and submit the claim transaction through submitTransaction
|
|
292
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
293
|
+
const signature = new SignatureErased();
|
|
294
|
+
const claimRewardsTransaction = new ClaimRewardsTransaction(signature.instance, currentState.networkId, amount, verifyingKey, LedgerOps.randomNonce(), signature);
|
|
295
|
+
const tx = Transaction.fromRewards(claimRewardsTransaction).eraseProofs();
|
|
296
|
+
// Submit transaction with genesisStrictness - reward claims are not balanced transactions
|
|
297
|
+
return yield* simulator.submitTransaction(tx, { strictness: genesisStrictness });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Submit a transaction and wait for it to be included in a block.
|
|
302
|
+
*
|
|
303
|
+
* This method adds the transaction to the mempool and blocks until the block producer includes it in a block. Use
|
|
304
|
+
* this when you need confirmation that the transaction was processed.
|
|
305
|
+
*
|
|
306
|
+
* For fire-and-forget scenarios where you don't need to wait for block inclusion, use `submitAndForget` instead.
|
|
307
|
+
*
|
|
308
|
+
* @param tx - Transaction to submit (proofs erased)
|
|
309
|
+
* @param options - Optional submission options
|
|
310
|
+
* @param options.strictness - Override well-formedness strictness
|
|
311
|
+
* @returns The block containing the transaction
|
|
312
|
+
*/
|
|
313
|
+
submitTransaction(tx, options) {
|
|
314
|
+
// Only set strictness if explicitly provided; otherwise let block producer assign default
|
|
315
|
+
const pendingTx = options?.strictness !== undefined ? { tx, strictness: createStrictness(options.strictness) } : { tx };
|
|
316
|
+
const stateRef = this.#stateRef;
|
|
317
|
+
return Effect.gen(function* () {
|
|
318
|
+
// Add to mempool
|
|
319
|
+
yield* SubscriptionRef.update(stateRef, (s) => addToMempool(s, pendingTx));
|
|
320
|
+
// Wait for the transaction to be processed (removed from mempool)
|
|
321
|
+
// by watching state changes
|
|
322
|
+
const finalState = yield* pipe(stateRef.changes, Stream.filter((s) => !s.mempool.includes(pendingTx)), Stream.take(1), Stream.runHead);
|
|
323
|
+
if (finalState._tag === 'None') {
|
|
324
|
+
return yield* Effect.die(new Error('State stream ended unexpectedly'));
|
|
325
|
+
}
|
|
326
|
+
// Find the block containing our transaction
|
|
327
|
+
const block = finalState.value.blocks.find((b) => b.transactions.some((bt) => bt.tx === tx));
|
|
328
|
+
if (!block) {
|
|
329
|
+
// Transaction was removed from mempool but not in any block - it failed
|
|
330
|
+
return yield* Effect.fail({
|
|
331
|
+
_tag: 'LedgerError',
|
|
332
|
+
message: 'Transaction was discarded',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return block;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Submit a transaction without waiting for block inclusion.
|
|
340
|
+
*
|
|
341
|
+
* This method adds the transaction to the mempool and returns immediately. The block producer will process it
|
|
342
|
+
* asynchronously. Use this for fire-and-forget scenarios or when testing custom block producers with batched
|
|
343
|
+
* transactions.
|
|
344
|
+
*
|
|
345
|
+
* To wait for block inclusion, use `submitTransaction` instead.
|
|
346
|
+
*
|
|
347
|
+
* @param tx - Transaction to submit (proofs erased)
|
|
348
|
+
* @param options - Optional options
|
|
349
|
+
* @param options.strictness - Override well-formedness strictness
|
|
350
|
+
*/
|
|
351
|
+
submitAndForget(tx, options) {
|
|
352
|
+
// Only set strictness if explicitly provided; otherwise let block producer assign default
|
|
353
|
+
const pendingTx = options?.strictness !== undefined ? { tx, strictness: createStrictness(options.strictness) } : { tx };
|
|
354
|
+
return SubscriptionRef.update(this.#stateRef, (s) => addToMempool(s, pendingTx));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Fast-forward the simulator time by the given number of seconds. Does not produce a block - only advances the
|
|
358
|
+
* internal clock. Useful for testing time-sensitive functionality like TTL.
|
|
359
|
+
*
|
|
360
|
+
* @param seconds - Number of seconds to advance (must be positive)
|
|
361
|
+
*/
|
|
362
|
+
fastForward(seconds) {
|
|
363
|
+
return SubscriptionRef.update(this.#stateRef, (simulatorState) => ({
|
|
364
|
+
...simulatorState,
|
|
365
|
+
currentTime: DateOps.addSeconds(simulatorState.currentTime, seconds),
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
// ===========================================================================
|
|
369
|
+
// Internal Block Production
|
|
370
|
+
// ===========================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Produce a block from the given block production request. Internal method used by the block producer stream.
|
|
373
|
+
*
|
|
374
|
+
* This method orchestrates block production by:
|
|
375
|
+
*
|
|
376
|
+
* 1. Computing the block hash (async)
|
|
377
|
+
* 2. Processing transactions using pure functions from SimulatorState
|
|
378
|
+
* 3. Creating the block and updating state atomically
|
|
379
|
+
*
|
|
380
|
+
* Each transaction in the request has its own strictness assigned by the block producer.
|
|
381
|
+
*
|
|
382
|
+
* @param request - Block production request with transactions and fullness
|
|
383
|
+
*/
|
|
384
|
+
#produceBlock(request) {
|
|
385
|
+
const stateRef = this.#stateRef;
|
|
386
|
+
const { transactions, fullness } = request;
|
|
387
|
+
return Effect.gen(function* () {
|
|
388
|
+
const blockResult = yield* SubscriptionRef.modifyEffect(stateRef, (simulatorState) => Effect.gen(function* () {
|
|
389
|
+
// Advance time first, then use it for the block
|
|
390
|
+
const blockTime = DateOps.addSeconds(simulatorState.currentTime, 1);
|
|
391
|
+
const previousBlock = getLastBlock(simulatorState);
|
|
392
|
+
const nextBlockNumber = previousBlock !== undefined ? previousBlock.number + 1n : 0n;
|
|
393
|
+
const hash = yield* Effect.promise(() => blockHash(nextBlockNumber));
|
|
394
|
+
if (transactions.length === 0) {
|
|
395
|
+
// No transactions to process, return empty block using pure function
|
|
396
|
+
const [emptyBlock, newState] = createEmptyBlock(simulatorState, hash, blockTime, transactions);
|
|
397
|
+
return [Either.right(emptyBlock), newState];
|
|
398
|
+
}
|
|
399
|
+
const context = yield* Effect.promise(() => nextBlockContextFromBlock(previousBlock, blockTime));
|
|
400
|
+
// Process all transactions - each has its own strictness assigned by block producer
|
|
401
|
+
const processingResult = processTransactions(simulatorState.ledger, transactions, blockTime, context, fullness);
|
|
402
|
+
if (Either.isLeft(processingResult)) {
|
|
403
|
+
// Transaction failed, remove from mempool and return error
|
|
404
|
+
const newState = removeFromMempool(simulatorState, transactions);
|
|
405
|
+
return [Either.left(processingResult.left), newState];
|
|
406
|
+
}
|
|
407
|
+
const { blockTransactions, finalLedger } = processingResult.right;
|
|
408
|
+
// Create block using pure function
|
|
409
|
+
const [block, newState] = createBlock(simulatorState, blockTransactions, hash, blockTime, finalLedger, transactions);
|
|
410
|
+
return [Either.right(block), newState];
|
|
411
|
+
}));
|
|
412
|
+
// Return the result
|
|
413
|
+
if (Either.isLeft(blockResult)) {
|
|
414
|
+
return yield* Effect.fail(blockResult.left);
|
|
415
|
+
}
|
|
416
|
+
return blockResult.right;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
// ===========================================================================
|
|
420
|
+
// Query Method
|
|
421
|
+
// ===========================================================================
|
|
422
|
+
/**
|
|
423
|
+
* Query the simulator state with a custom function. This is a generic query mechanism that allows extracting any
|
|
424
|
+
* information from the current state without modifying it.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```typescript
|
|
428
|
+
* // Query fee prices
|
|
429
|
+
* const feePrices = yield* simulator.query(state => state.ledger.parameters.feePrices);
|
|
430
|
+
*
|
|
431
|
+
* // Use composable state accessors
|
|
432
|
+
* const blockNumber = yield* simulator.query(getCurrentBlockNumber);
|
|
433
|
+
* const lastBlock = yield* simulator.query(getLastBlock);
|
|
434
|
+
* const events = yield* simulator.query(getLastBlockEvents);
|
|
435
|
+
*
|
|
436
|
+
* // Query UTXOs for an address
|
|
437
|
+
* const utxos = yield* simulator.query(state => Array.from(state.ledger.utxo.filter(address)));
|
|
438
|
+
*
|
|
439
|
+
* // Complex query returning multiple values
|
|
440
|
+
* const info = yield* simulator.query(state => ({
|
|
441
|
+
* networkId: state.networkId,
|
|
442
|
+
* blockNumber: getCurrentBlockNumber(state),
|
|
443
|
+
* feePrices: state.ledger.parameters.feePrices,
|
|
444
|
+
* }));
|
|
445
|
+
* ```;
|
|
446
|
+
*
|
|
447
|
+
* @param fn - Function that receives the current state and returns a result
|
|
448
|
+
* @returns The result of applying the function to the current state
|
|
449
|
+
*/
|
|
450
|
+
query(fn) {
|
|
451
|
+
return Effect.map(SubscriptionRef.get(this.#stateRef), fn);
|
|
452
|
+
}
|
|
453
|
+
}
|