@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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SimulatorState types and pure functions for state manipulation.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the core data types and pure functions for working with simulator state. All functions are
|
|
5
|
+
* synchronous and side-effect free.
|
|
6
|
+
*/
|
|
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 '@midnight-ntwrk/ledger-v8';
|
|
9
|
+
import { LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
10
|
+
import { type NetworkId } from '@midnight-ntwrk/wallet-sdk-abstractions';
|
|
11
|
+
/** A transaction included in a block with its execution result. */
|
|
12
|
+
export type BlockTransaction = Readonly<{
|
|
13
|
+
/** The transaction that was executed */
|
|
14
|
+
tx: ProofErasedTransaction;
|
|
15
|
+
/** The result of executing the transaction */
|
|
16
|
+
result: TransactionResult;
|
|
17
|
+
}>;
|
|
18
|
+
/** A produced block containing transactions and metadata. */
|
|
19
|
+
export type Block = Readonly<{
|
|
20
|
+
/** Block number (height) */
|
|
21
|
+
number: bigint;
|
|
22
|
+
/** Block hash */
|
|
23
|
+
hash: string;
|
|
24
|
+
/** Block timestamp */
|
|
25
|
+
timestamp: Date;
|
|
26
|
+
/** Transactions in this block, ordered by execution */
|
|
27
|
+
transactions: readonly BlockTransaction[];
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Pending transaction waiting for block production. Strictness is optional - if not specified, block producer assigns
|
|
31
|
+
* default when creating block.
|
|
32
|
+
*/
|
|
33
|
+
export type PendingTransaction = Readonly<{
|
|
34
|
+
tx: ProofErasedTransaction;
|
|
35
|
+
/** Optional per-transaction strictness. If not specified, block producer assigns default. */
|
|
36
|
+
strictness?: WellFormedStrictness;
|
|
37
|
+
}>;
|
|
38
|
+
/**
|
|
39
|
+
* Transaction ready for block production with strictness assigned. Block producer ensures all transactions have
|
|
40
|
+
* strictness before block creation.
|
|
41
|
+
*/
|
|
42
|
+
export type ReadyTransaction = Readonly<{
|
|
43
|
+
tx: ProofErasedTransaction;
|
|
44
|
+
/** Strictness for validation (assigned by block producer if not specified on pending tx). */
|
|
45
|
+
strictness: WellFormedStrictness;
|
|
46
|
+
}>;
|
|
47
|
+
/** Simulator state containing the ledger, block history, and pending mempool. */
|
|
48
|
+
export type SimulatorState = Readonly<{
|
|
49
|
+
networkId: NetworkId.NetworkId;
|
|
50
|
+
ledger: LedgerState;
|
|
51
|
+
/** All produced blocks, ordered by block number */
|
|
52
|
+
blocks: EArray.NonEmptyArray<Block>;
|
|
53
|
+
/** Pending transactions waiting for block production */
|
|
54
|
+
mempool: readonly PendingTransaction[];
|
|
55
|
+
/** Current simulator time (independent of block numbers) */
|
|
56
|
+
currentTime: Date;
|
|
57
|
+
}>;
|
|
58
|
+
/** Result of a successful block production. */
|
|
59
|
+
export type BlockInfo = Readonly<{
|
|
60
|
+
blockNumber: bigint;
|
|
61
|
+
blockHash: string;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Request to produce a block with specific transactions, fullness, and optional strictness override. This mimics how
|
|
65
|
+
* real nodes work - selecting which transactions to include and how to validate them.
|
|
66
|
+
*
|
|
67
|
+
* The block producer can optionally specify a strictness that overrides per-transaction strictness. This enables
|
|
68
|
+
* patterns like:
|
|
69
|
+
*
|
|
70
|
+
* - Enforcing balancing for all blocks after genesis
|
|
71
|
+
* - Using a decorator pattern to modify strictness behavior
|
|
72
|
+
* - Testing specific validation scenarios
|
|
73
|
+
*/
|
|
74
|
+
export type BlockProductionRequest = Readonly<{
|
|
75
|
+
/** Transactions to include in this block with strictness assigned */
|
|
76
|
+
transactions: readonly ReadyTransaction[];
|
|
77
|
+
/** Block fullness (0-1) for fee calculation */
|
|
78
|
+
fullness: number;
|
|
79
|
+
}>;
|
|
80
|
+
/**
|
|
81
|
+
* A block producer is a stream transformer that decides when blocks should be produced.
|
|
82
|
+
*
|
|
83
|
+
* It receives a stream of simulator state changes and transforms it into a stream of block production requests. Each
|
|
84
|
+
* request specifies which transactions to include and the block fullness for fee calculation.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* // Custom producer: produce block when mempool has 5+ transactions
|
|
89
|
+
* const batchedProducer: BlockProducer = (states) =>
|
|
90
|
+
* states.pipe(
|
|
91
|
+
* Stream.filter((s) => s.mempool.length >= 5),
|
|
92
|
+
* Stream.map((s) => ({
|
|
93
|
+
* transactions: [...s.mempool],
|
|
94
|
+
* fullness: 0.5,
|
|
95
|
+
* }))
|
|
96
|
+
* );
|
|
97
|
+
* ```;
|
|
98
|
+
*/
|
|
99
|
+
export type BlockProducer = (states: Stream.Stream<SimulatorState>) => Stream.Stream<BlockProductionRequest>;
|
|
100
|
+
/** Fullness specification: static number or callback based on state. */
|
|
101
|
+
export type FullnessSpec = number | ((state: SimulatorState) => number);
|
|
102
|
+
/**
|
|
103
|
+
* Genesis mint specification for shielded tokens.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const mint: ShieldedGenesisMint = {
|
|
108
|
+
* type: 'shielded',
|
|
109
|
+
* tokenType: ledger.shieldedToken().raw,
|
|
110
|
+
* amount: 1000n,
|
|
111
|
+
* recipient: secretKeys,
|
|
112
|
+
* };
|
|
113
|
+
* ```;
|
|
114
|
+
*/
|
|
115
|
+
export type ShieldedGenesisMint = Readonly<{
|
|
116
|
+
type: 'shielded';
|
|
117
|
+
tokenType: RawTokenType;
|
|
118
|
+
amount: bigint;
|
|
119
|
+
recipient: ZswapSecretKeys;
|
|
120
|
+
}>;
|
|
121
|
+
/**
|
|
122
|
+
* Genesis mint specification for unshielded tokens.
|
|
123
|
+
*
|
|
124
|
+
* For **custom tokens**: Minted directly from nothing (enforceBalancing disabled).
|
|
125
|
+
*
|
|
126
|
+
* For **Night tokens** (native token): Requires `verifyingKey` field. Night cannot be minted from nothing due to supply
|
|
127
|
+
* invariant, so the reward/claim mechanism is used internally. Night is auto-detected by comparing `tokenType` with
|
|
128
|
+
* `ledger.nativeToken().raw`.
|
|
129
|
+
*
|
|
130
|
+
* Note: Night claims have a minimum amount requirement (~14077). Use larger amounts to ensure the claim transaction
|
|
131
|
+
* succeeds.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* // Custom unshielded token
|
|
136
|
+
* const customMint: UnshieldedGenesisMint = {
|
|
137
|
+
* type: 'unshielded',
|
|
138
|
+
* tokenType: customToken,
|
|
139
|
+
* amount: 1000n,
|
|
140
|
+
* recipient: userAddress,
|
|
141
|
+
* };
|
|
142
|
+
*
|
|
143
|
+
* // Night token (native token) - requires verifyingKey
|
|
144
|
+
* const nightMint: UnshieldedGenesisMint = {
|
|
145
|
+
* type: 'unshielded',
|
|
146
|
+
* tokenType: ledger.nativeToken().raw,
|
|
147
|
+
* amount: 1_000_000n, // Must exceed minimum claim amount (~14077)
|
|
148
|
+
* recipient: userAddress,
|
|
149
|
+
* verifyingKey: signatureVerifyingKey,
|
|
150
|
+
* };
|
|
151
|
+
* ```;
|
|
152
|
+
*/
|
|
153
|
+
export type UnshieldedGenesisMint = Readonly<{
|
|
154
|
+
type: 'unshielded';
|
|
155
|
+
tokenType: RawTokenType;
|
|
156
|
+
amount: bigint;
|
|
157
|
+
recipient: UserAddress;
|
|
158
|
+
/** Required for Night tokens (native token). Used for the claim transaction signature. */
|
|
159
|
+
verifyingKey?: SignatureVerifyingKey;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* Genesis mint specification for initializing the simulator with pre-funded accounts.
|
|
163
|
+
*
|
|
164
|
+
* Uses a tagged union pattern consistent with the facade API:
|
|
165
|
+
*
|
|
166
|
+
* - **Shielded**: `{ type: 'shielded', tokenType, amount, recipient: ZswapSecretKeys }`
|
|
167
|
+
* - **Unshielded**: `{ type: 'unshielded', tokenType, amount, recipient, verifyingKey? }`
|
|
168
|
+
*
|
|
169
|
+
* - Custom tokens: minted directly (verifyingKey not needed)
|
|
170
|
+
* - Night tokens: auto-detected by tokenType, uses reward/claim mechanism (verifyingKey required)
|
|
171
|
+
*/
|
|
172
|
+
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
|
+
/**
|
|
197
|
+
* Assign strictness to a pending transaction, creating a ready transaction. If the pending transaction already has
|
|
198
|
+
* strictness, use it; otherwise use the provided default.
|
|
199
|
+
*/
|
|
200
|
+
export declare const assignStrictness: (pendingTx: PendingTransaction, defaultStrictness: WellFormedStrictness) => ReadyTransaction;
|
|
201
|
+
/**
|
|
202
|
+
* Assign strictness to all pending transactions, creating ready transactions. Transactions with existing strictness
|
|
203
|
+
* keep their strictness; others get the default.
|
|
204
|
+
*/
|
|
205
|
+
export declare const assignStrictnessToAll: (transactions: readonly PendingTransaction[], defaultStrictness: WellFormedStrictness) => readonly ReadyTransaction[];
|
|
206
|
+
/** Get the last produced block, or undefined if no blocks yet. */
|
|
207
|
+
export declare const getLastBlock: (state: SimulatorState) => Block;
|
|
208
|
+
/** Get the current block number (height of the last block, or 0 if no blocks). */
|
|
209
|
+
export declare const getCurrentBlockNumber: (state: SimulatorState) => bigint;
|
|
210
|
+
/** Get a block by its number. */
|
|
211
|
+
export declare const getBlockByNumber: {
|
|
212
|
+
(number: bigint): (state: SimulatorState) => Block | undefined;
|
|
213
|
+
(state: SimulatorState, number: bigint): Block | undefined;
|
|
214
|
+
};
|
|
215
|
+
/** Get all transaction results from the last block. */
|
|
216
|
+
export declare const getLastBlockResults: (state: SimulatorState) => readonly TransactionResult[];
|
|
217
|
+
/** Get all events from the last block (flattened from all transactions). */
|
|
218
|
+
export declare const getLastBlockEvents: (state: SimulatorState) => readonly TransactionResult["events"][number][];
|
|
219
|
+
/**
|
|
220
|
+
* Get all events from blocks with number >= fromBlockNumber. Returns events ordered by block number, with each block's
|
|
221
|
+
* transactions flattened.
|
|
222
|
+
*
|
|
223
|
+
* Use this with the wallet's next-to-process index: `appliedIndex` after processing should be set to `lastBlockNumber +
|
|
224
|
+
* 1`, not `lastBlockNumber`.
|
|
225
|
+
*/
|
|
226
|
+
export declare const getBlockEventsFrom: {
|
|
227
|
+
(fromBlockNumber: bigint): (state: SimulatorState) => readonly TransactionResult['events'][number][];
|
|
228
|
+
(state: SimulatorState, fromBlockNumber: bigint): readonly TransactionResult['events'][number][];
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* @deprecated Use getBlockEventsFrom instead with proper appliedIndex semantics Get all events from blocks with number
|
|
232
|
+
*
|
|
233
|
+
* > AfterBlockNumber.
|
|
234
|
+
*/
|
|
235
|
+
export declare const getBlockEventsSince: {
|
|
236
|
+
(afterBlockNumber: bigint): (state: SimulatorState) => readonly TransactionResult['events'][number][];
|
|
237
|
+
(state: SimulatorState, afterBlockNumber: bigint): readonly TransactionResult['events'][number][];
|
|
238
|
+
};
|
|
239
|
+
/** Check if there are pending transactions in the mempool. */
|
|
240
|
+
export declare const hasPendingTransactions: (state: SimulatorState) => boolean;
|
|
241
|
+
/** Get the current simulator time. */
|
|
242
|
+
export declare const getCurrentTime: (state: SimulatorState) => Date;
|
|
243
|
+
/** Resolve fullness from spec and state. */
|
|
244
|
+
export declare const resolveFullness: (spec: FullnessSpec, state: SimulatorState) => number;
|
|
245
|
+
/** Create a block production request that includes all mempool transactions. */
|
|
246
|
+
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>;
|
|
249
|
+
/** Add a pending transaction to the mempool. */
|
|
250
|
+
export declare const addToMempool: (state: SimulatorState, pendingTx: PendingTransaction) => SimulatorState;
|
|
251
|
+
/** Remove transactions from the mempool. */
|
|
252
|
+
export declare const removeFromMempool: (state: SimulatorState, transactions: readonly ReadyTransaction[]) => SimulatorState;
|
|
253
|
+
/** Advance the simulator time by the given number of seconds. */
|
|
254
|
+
export declare const advanceTime: (state: SimulatorState, seconds: bigint) => SimulatorState;
|
|
255
|
+
/** Update the ledger state. */
|
|
256
|
+
export declare const updateLedger: (state: SimulatorState, ledger: LedgerState) => SimulatorState;
|
|
257
|
+
/** Append a block to the state and update time. */
|
|
258
|
+
export declare const appendBlock: (state: SimulatorState, block: Block, newLedger: LedgerState) => SimulatorState;
|
|
259
|
+
/**
|
|
260
|
+
* Pure state transition: apply a transaction to the simulator state. Returns Either with the new state or an error.
|
|
261
|
+
*
|
|
262
|
+
* @param state - Current simulator state
|
|
263
|
+
* @param tx - Transaction to apply
|
|
264
|
+
* @param strictness - Well-formedness strictness options
|
|
265
|
+
* @param blockContext - Block context for the transaction
|
|
266
|
+
* @param options - Optional parameters
|
|
267
|
+
* @param options.blockNumber - Override block number (defaults to last block + 1)
|
|
268
|
+
* @param options.blockFullness - Override detailed block fullness (SyntheticCost)
|
|
269
|
+
* @param options.overallBlockFullness - Override overall block fullness (0-1 value)
|
|
270
|
+
*/
|
|
271
|
+
export declare const applyTransaction: (state: SimulatorState, tx: ProofErasedTransaction, strictness: WellFormedStrictness, blockContext: BlockContext, options?: {
|
|
272
|
+
blockNumber?: bigint;
|
|
273
|
+
blockFullness?: SyntheticCost;
|
|
274
|
+
overallBlockFullness?: number;
|
|
275
|
+
}) => Either.Either<[Block, SimulatorState], LedgerOps.LedgerError>;
|
|
276
|
+
/** Result of processing a single transaction. */
|
|
277
|
+
export type TransactionProcessingResult = Readonly<{
|
|
278
|
+
tx: ProofErasedTransaction;
|
|
279
|
+
result: TransactionResult;
|
|
280
|
+
newLedger: LedgerState;
|
|
281
|
+
}>;
|
|
282
|
+
/**
|
|
283
|
+
* Process a single pending transaction against the current ledger. Returns Either with the processing result or an
|
|
284
|
+
* error.
|
|
285
|
+
*
|
|
286
|
+
* @param ledger - Current ledger state
|
|
287
|
+
* @param readyTx - Transaction to process
|
|
288
|
+
* @param blockTime - Block timestamp
|
|
289
|
+
* @param blockContext - Block context
|
|
290
|
+
* @param minFullness - Minimum block fullness to use
|
|
291
|
+
*/
|
|
292
|
+
export declare const processTransaction: (ledger: LedgerState, readyTx: ReadyTransaction, blockTime: Date, blockContext: BlockContext, minFullness: number) => Either.Either<TransactionProcessingResult, LedgerOps.LedgerError>;
|
|
293
|
+
/**
|
|
294
|
+
* Process multiple transactions in sequence, accumulating results. Returns Either with all results and final ledger, or
|
|
295
|
+
* first error. Each transaction uses its assigned strictness (from ReadyTransaction).
|
|
296
|
+
*
|
|
297
|
+
* @param ledger - Initial ledger state
|
|
298
|
+
* @param transactions - Transactions to process (each with assigned strictness)
|
|
299
|
+
* @param blockTime - Block timestamp
|
|
300
|
+
* @param blockContext - Block context
|
|
301
|
+
* @param fullness - Block fullness (0-1)
|
|
302
|
+
*/
|
|
303
|
+
export declare const processTransactions: (ledger: LedgerState, transactions: readonly ReadyTransaction[], blockTime: Date, blockContext: BlockContext, fullness: number) => Either.Either<{
|
|
304
|
+
blockTransactions: readonly BlockTransaction[];
|
|
305
|
+
finalLedger: LedgerState;
|
|
306
|
+
}, LedgerOps.LedgerError>;
|
|
307
|
+
/**
|
|
308
|
+
* Create a block from processed transactions and update state. Pure function that takes pre-computed block hash.
|
|
309
|
+
*
|
|
310
|
+
* @param state - Current simulator state
|
|
311
|
+
* @param blockTransactions - Processed transactions to include
|
|
312
|
+
* @param blockHash - Pre-computed block hash
|
|
313
|
+
* @param blockTime - Block timestamp
|
|
314
|
+
* @param newLedger - New ledger state after processing transactions
|
|
315
|
+
* @param processedTxs - Ready transactions to remove from mempool
|
|
316
|
+
*/
|
|
317
|
+
export declare const createBlock: (state: SimulatorState, blockTransactions: readonly BlockTransaction[], blockHashValue: string, blockTime: Date, newLedger: LedgerState, processedTxs: readonly ReadyTransaction[]) => [Block, SimulatorState];
|
|
318
|
+
/**
|
|
319
|
+
* Create an empty block (no transactions) and update state.
|
|
320
|
+
*
|
|
321
|
+
* @param state - Current simulator state
|
|
322
|
+
* @param blockHashValue - Pre-computed block hash
|
|
323
|
+
* @param blockTime - Block timestamp
|
|
324
|
+
* @param processedTxs - Original pending transactions to remove from mempool (if any)
|
|
325
|
+
*/
|
|
326
|
+
export declare const createEmptyBlock: (state: SimulatorState, blockHashValue: string, blockTime: Date, processedTxs?: readonly ReadyTransaction[]) => [Block, SimulatorState];
|
|
327
|
+
/**
|
|
328
|
+
* Create a WellFormedStrictness instance with configurable options. All options default to false for maximum testing
|
|
329
|
+
* flexibility.
|
|
330
|
+
*
|
|
331
|
+
* Note: WellFormedStrictness is a class from the ledger library that requires mutation to configure. This is
|
|
332
|
+
* unavoidable given the external API design.
|
|
333
|
+
*/
|
|
334
|
+
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>;
|
|
@@ -0,0 +1,323 @@
|
|
|
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
|
+
* SimulatorState types and pure functions for state manipulation.
|
|
15
|
+
*
|
|
16
|
+
* This module contains the core data types and pure functions for working with simulator state. All functions are
|
|
17
|
+
* synchronous and side-effect free.
|
|
18
|
+
*/
|
|
19
|
+
import { Either, Function as EFunction, Array as EArray } from 'effect';
|
|
20
|
+
import { LedgerState, WellFormedStrictness, TransactionContext, } from '@midnight-ntwrk/ledger-v8';
|
|
21
|
+
import { DateOps, LedgerOps } from '@midnight-ntwrk/wallet-sdk-utilities';
|
|
22
|
+
/**
|
|
23
|
+
* Default strictness for post-genesis blocks.
|
|
24
|
+
*
|
|
25
|
+
* In a realistic simulation:
|
|
26
|
+
*
|
|
27
|
+
* - Signatures should be verified (verifySignatures: true)
|
|
28
|
+
* - Proofs cannot be verified because they're erased (verifyNativeProofs/verifyContractProofs: false)
|
|
29
|
+
* - Limits should be enforced (enforceLimits: true)
|
|
30
|
+
* - Balancing must be enforced (enforceBalancing: true) - transactions must pay fees
|
|
31
|
+
*
|
|
32
|
+
* Note: Genesis blocks typically disable all strictness to allow initial token distribution.
|
|
33
|
+
*/
|
|
34
|
+
export const defaultStrictness = {
|
|
35
|
+
enforceBalancing: true,
|
|
36
|
+
verifyNativeProofs: false,
|
|
37
|
+
verifyContractProofs: false,
|
|
38
|
+
enforceLimits: true,
|
|
39
|
+
verifySignatures: true,
|
|
40
|
+
};
|
|
41
|
+
/** Strictness for genesis blocks - all checks disabled to allow initial token distribution. */
|
|
42
|
+
export const genesisStrictness = {
|
|
43
|
+
enforceBalancing: false,
|
|
44
|
+
verifyNativeProofs: false,
|
|
45
|
+
verifyContractProofs: false,
|
|
46
|
+
enforceLimits: false,
|
|
47
|
+
verifySignatures: false,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Assign strictness to a pending transaction, creating a ready transaction. If the pending transaction already has
|
|
51
|
+
* strictness, use it; otherwise use the provided default.
|
|
52
|
+
*/
|
|
53
|
+
export const assignStrictness = (pendingTx, defaultStrictness) => ({
|
|
54
|
+
tx: pendingTx.tx,
|
|
55
|
+
strictness: pendingTx.strictness ?? defaultStrictness,
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Assign strictness to all pending transactions, creating ready transactions. Transactions with existing strictness
|
|
59
|
+
* keep their strictness; others get the default.
|
|
60
|
+
*/
|
|
61
|
+
export const assignStrictnessToAll = (transactions, defaultStrictness) => transactions.map((tx) => assignStrictness(tx, defaultStrictness));
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// State Accessors (Pure Functions)
|
|
64
|
+
// =============================================================================
|
|
65
|
+
/** Get the last produced block, or undefined if no blocks yet. */
|
|
66
|
+
export const getLastBlock = (state) => EArray.lastNonEmpty(state.blocks);
|
|
67
|
+
/** Get the current block number (height of the last block, or 0 if no blocks). */
|
|
68
|
+
export const getCurrentBlockNumber = (state) => getLastBlock(state)?.number;
|
|
69
|
+
/** Get a block by its number. */
|
|
70
|
+
export const getBlockByNumber = EFunction.dual(2, (state, number) => state.blocks.find((b) => b.number === number));
|
|
71
|
+
/** Get all transaction results from the last block. */
|
|
72
|
+
export const getLastBlockResults = (state) => getLastBlock(state)?.transactions.map((t) => t.result) ?? [];
|
|
73
|
+
/** Get all events from the last block (flattened from all transactions). */
|
|
74
|
+
export const getLastBlockEvents = (state) => getLastBlockResults(state).flatMap((r) => r.events);
|
|
75
|
+
/**
|
|
76
|
+
* Get all events from blocks with number >= fromBlockNumber. Returns events ordered by block number, with each block's
|
|
77
|
+
* transactions flattened.
|
|
78
|
+
*
|
|
79
|
+
* Use this with the wallet's next-to-process index: `appliedIndex` after processing should be set to `lastBlockNumber +
|
|
80
|
+
* 1`, not `lastBlockNumber`.
|
|
81
|
+
*/
|
|
82
|
+
export const getBlockEventsFrom = EFunction.dual(2, (state, fromBlockNumber) => state.blocks
|
|
83
|
+
.filter((b) => b.number >= fromBlockNumber)
|
|
84
|
+
.flatMap((b) => b.transactions.flatMap((t) => t.result.events)));
|
|
85
|
+
/**
|
|
86
|
+
* @deprecated Use getBlockEventsFrom instead with proper appliedIndex semantics Get all events from blocks with number
|
|
87
|
+
*
|
|
88
|
+
* > AfterBlockNumber.
|
|
89
|
+
*/
|
|
90
|
+
export const getBlockEventsSince = EFunction.dual(2, (state, afterBlockNumber) => state.blocks
|
|
91
|
+
.filter((b) => b.number > afterBlockNumber)
|
|
92
|
+
.flatMap((b) => b.transactions.flatMap((t) => t.result.events)));
|
|
93
|
+
/** Check if there are pending transactions in the mempool. */
|
|
94
|
+
export const hasPendingTransactions = (state) => state.mempool.length > 0;
|
|
95
|
+
/** Get the current simulator time. */
|
|
96
|
+
export const getCurrentTime = (state) => state.currentTime;
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// State Transformations (Pure Functions)
|
|
99
|
+
// =============================================================================
|
|
100
|
+
/** Resolve fullness from spec and state. */
|
|
101
|
+
export const resolveFullness = (spec, state) => typeof spec === 'function' ? spec(state) : spec;
|
|
102
|
+
/** Create a block production request that includes all mempool transactions. */
|
|
103
|
+
export const allMempoolTransactions = (state, fullness, defaultStrictness) => ({
|
|
104
|
+
transactions: assignStrictnessToAll(state.mempool, defaultStrictness),
|
|
105
|
+
fullness,
|
|
106
|
+
});
|
|
107
|
+
/** Create a blank initial state. */
|
|
108
|
+
export const blankState = async (networkId) => {
|
|
109
|
+
const blankGenesis = {
|
|
110
|
+
number: 0n,
|
|
111
|
+
hash: await blockHash(0n),
|
|
112
|
+
timestamp: new Date(0),
|
|
113
|
+
transactions: [],
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
networkId,
|
|
117
|
+
ledger: LedgerState.blank(networkId),
|
|
118
|
+
blocks: [blankGenesis],
|
|
119
|
+
mempool: [],
|
|
120
|
+
currentTime: new Date(0),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** Add a pending transaction to the mempool. */
|
|
124
|
+
export const addToMempool = (state, pendingTx) => ({
|
|
125
|
+
...state,
|
|
126
|
+
mempool: [...state.mempool, pendingTx],
|
|
127
|
+
});
|
|
128
|
+
/** Remove transactions from the mempool. */
|
|
129
|
+
export const removeFromMempool = (state, transactions) => {
|
|
130
|
+
const txsToRemove = new Set(transactions.map((t) => t.tx));
|
|
131
|
+
return {
|
|
132
|
+
...state,
|
|
133
|
+
mempool: state.mempool.filter((pending) => !txsToRemove.has(pending.tx)),
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/** Advance the simulator time by the given number of seconds. */
|
|
137
|
+
export const advanceTime = (state, seconds) => ({
|
|
138
|
+
...state,
|
|
139
|
+
currentTime: DateOps.addSeconds(state.currentTime, seconds),
|
|
140
|
+
});
|
|
141
|
+
/** Update the ledger state. */
|
|
142
|
+
export const updateLedger = (state, ledger) => ({
|
|
143
|
+
...state,
|
|
144
|
+
ledger,
|
|
145
|
+
});
|
|
146
|
+
/** Append a block to the state and update time. */
|
|
147
|
+
export const appendBlock = (state, block, newLedger) => ({
|
|
148
|
+
...state,
|
|
149
|
+
ledger: newLedger,
|
|
150
|
+
blocks: [...state.blocks, block],
|
|
151
|
+
currentTime: block.timestamp,
|
|
152
|
+
});
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// Transaction Application (Pure Function)
|
|
155
|
+
// =============================================================================
|
|
156
|
+
/**
|
|
157
|
+
* Pure state transition: apply a transaction to the simulator state. Returns Either with the new state or an error.
|
|
158
|
+
*
|
|
159
|
+
* @param state - Current simulator state
|
|
160
|
+
* @param tx - Transaction to apply
|
|
161
|
+
* @param strictness - Well-formedness strictness options
|
|
162
|
+
* @param blockContext - Block context for the transaction
|
|
163
|
+
* @param options - Optional parameters
|
|
164
|
+
* @param options.blockNumber - Override block number (defaults to last block + 1)
|
|
165
|
+
* @param options.blockFullness - Override detailed block fullness (SyntheticCost)
|
|
166
|
+
* @param options.overallBlockFullness - Override overall block fullness (0-1 value)
|
|
167
|
+
*/
|
|
168
|
+
export const applyTransaction = (state, tx, strictness, blockContext, options) => {
|
|
169
|
+
return LedgerOps.ledgerTry(() => {
|
|
170
|
+
const computedFullness = options?.blockFullness ?? tx.cost(state.ledger.parameters);
|
|
171
|
+
const detailedBlockFullness = state.ledger.parameters.normalizeFullness(computedFullness);
|
|
172
|
+
const computedBlockFullness = options?.overallBlockFullness ??
|
|
173
|
+
Math.max(detailedBlockFullness.readTime, detailedBlockFullness.computeTime, detailedBlockFullness.blockUsage, detailedBlockFullness.bytesWritten, detailedBlockFullness.bytesChurned);
|
|
174
|
+
const blockNumber = options?.blockNumber ?? getCurrentBlockNumber(state) + 1n;
|
|
175
|
+
const blockTime = state.currentTime;
|
|
176
|
+
const verifiedTransaction = tx.wellFormed(state.ledger, strictness, blockTime);
|
|
177
|
+
const transactionContext = new TransactionContext(state.ledger, blockContext);
|
|
178
|
+
const [newLedgerState, txResult] = state.ledger.apply(verifiedTransaction, transactionContext);
|
|
179
|
+
const newBlock = {
|
|
180
|
+
number: blockNumber,
|
|
181
|
+
hash: blockContext.parentBlockHash,
|
|
182
|
+
timestamp: blockTime,
|
|
183
|
+
transactions: [{ tx, result: txResult }],
|
|
184
|
+
};
|
|
185
|
+
const newState = {
|
|
186
|
+
...state,
|
|
187
|
+
ledger: newLedgerState.postBlockUpdate(blockTime, detailedBlockFullness, computedBlockFullness),
|
|
188
|
+
blocks: [...state.blocks, newBlock],
|
|
189
|
+
};
|
|
190
|
+
return [newBlock, newState];
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Process a single pending transaction against the current ledger. Returns Either with the processing result or an
|
|
195
|
+
* error.
|
|
196
|
+
*
|
|
197
|
+
* @param ledger - Current ledger state
|
|
198
|
+
* @param readyTx - Transaction to process
|
|
199
|
+
* @param blockTime - Block timestamp
|
|
200
|
+
* @param blockContext - Block context
|
|
201
|
+
* @param minFullness - Minimum block fullness to use
|
|
202
|
+
*/
|
|
203
|
+
export const processTransaction = (ledger, readyTx, blockTime, blockContext, minFullness) => {
|
|
204
|
+
return LedgerOps.ledgerTry(() => {
|
|
205
|
+
const computedFullness = readyTx.tx.cost(ledger.parameters);
|
|
206
|
+
const detailedBlockFullness = ledger.parameters.normalizeFullness(computedFullness);
|
|
207
|
+
const computedBlockFullness = Math.max(minFullness, detailedBlockFullness.readTime, detailedBlockFullness.computeTime, detailedBlockFullness.blockUsage, detailedBlockFullness.bytesWritten, detailedBlockFullness.bytesChurned);
|
|
208
|
+
// Use the assigned strictness
|
|
209
|
+
const verifiedTransaction = readyTx.tx.wellFormed(ledger, readyTx.strictness, blockTime);
|
|
210
|
+
const transactionContext = new TransactionContext(ledger, blockContext);
|
|
211
|
+
const [newLedgerState, txResult] = ledger.apply(verifiedTransaction, transactionContext);
|
|
212
|
+
const postBlockLedger = newLedgerState.postBlockUpdate(blockTime, detailedBlockFullness, computedBlockFullness);
|
|
213
|
+
return {
|
|
214
|
+
tx: readyTx.tx,
|
|
215
|
+
result: txResult,
|
|
216
|
+
newLedger: postBlockLedger,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Process multiple transactions in sequence, accumulating results. Returns Either with all results and final ledger, or
|
|
222
|
+
* first error. Each transaction uses its assigned strictness (from ReadyTransaction).
|
|
223
|
+
*
|
|
224
|
+
* @param ledger - Initial ledger state
|
|
225
|
+
* @param transactions - Transactions to process (each with assigned strictness)
|
|
226
|
+
* @param blockTime - Block timestamp
|
|
227
|
+
* @param blockContext - Block context
|
|
228
|
+
* @param fullness - Block fullness (0-1)
|
|
229
|
+
*/
|
|
230
|
+
export const processTransactions = (ledger, transactions, blockTime, blockContext, fullness) => transactions.reduce((acc, readyTx) => Either.flatMap(acc, ({ blockTransactions, finalLedger }) => Either.map(processTransaction(finalLedger, readyTx, blockTime, blockContext, fullness), (result) => ({
|
|
231
|
+
blockTransactions: [...blockTransactions, { tx: result.tx, result: result.result }],
|
|
232
|
+
finalLedger: result.newLedger,
|
|
233
|
+
}))), Either.right({ blockTransactions: [], finalLedger: ledger }));
|
|
234
|
+
/**
|
|
235
|
+
* Create a block from processed transactions and update state. Pure function that takes pre-computed block hash.
|
|
236
|
+
*
|
|
237
|
+
* @param state - Current simulator state
|
|
238
|
+
* @param blockTransactions - Processed transactions to include
|
|
239
|
+
* @param blockHash - Pre-computed block hash
|
|
240
|
+
* @param blockTime - Block timestamp
|
|
241
|
+
* @param newLedger - New ledger state after processing transactions
|
|
242
|
+
* @param processedTxs - Ready transactions to remove from mempool
|
|
243
|
+
*/
|
|
244
|
+
export const createBlock = (state, blockTransactions, blockHashValue, blockTime, newLedger, processedTxs) => {
|
|
245
|
+
const nextBlockNumber = getCurrentBlockNumber(state) + 1n;
|
|
246
|
+
const block = {
|
|
247
|
+
number: nextBlockNumber,
|
|
248
|
+
hash: blockHashValue,
|
|
249
|
+
timestamp: blockTime,
|
|
250
|
+
transactions: blockTransactions,
|
|
251
|
+
};
|
|
252
|
+
const txsToRemove = new Set(processedTxs.map((t) => t.tx));
|
|
253
|
+
const newState = {
|
|
254
|
+
...state,
|
|
255
|
+
ledger: newLedger,
|
|
256
|
+
blocks: [...state.blocks, block],
|
|
257
|
+
mempool: state.mempool.filter((pending) => !txsToRemove.has(pending.tx)),
|
|
258
|
+
currentTime: blockTime,
|
|
259
|
+
};
|
|
260
|
+
return [block, newState];
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Create an empty block (no transactions) and update state.
|
|
264
|
+
*
|
|
265
|
+
* @param state - Current simulator state
|
|
266
|
+
* @param blockHashValue - Pre-computed block hash
|
|
267
|
+
* @param blockTime - Block timestamp
|
|
268
|
+
* @param processedTxs - Original pending transactions to remove from mempool (if any)
|
|
269
|
+
*/
|
|
270
|
+
export const createEmptyBlock = (state, blockHashValue, blockTime, processedTxs = []) => {
|
|
271
|
+
return createBlock(state, [], blockHashValue, blockTime, state.ledger, processedTxs);
|
|
272
|
+
};
|
|
273
|
+
// =============================================================================
|
|
274
|
+
// Helper Functions
|
|
275
|
+
// =============================================================================
|
|
276
|
+
/**
|
|
277
|
+
* Create a WellFormedStrictness instance with configurable options. All options default to false for maximum testing
|
|
278
|
+
* flexibility.
|
|
279
|
+
*
|
|
280
|
+
* Note: WellFormedStrictness is a class from the ledger library that requires mutation to configure. This is
|
|
281
|
+
* unavoidable given the external API design.
|
|
282
|
+
*/
|
|
283
|
+
export const createStrictness = (config = {}) => {
|
|
284
|
+
const strictness = new WellFormedStrictness();
|
|
285
|
+
strictness.enforceBalancing = config.enforceBalancing ?? false;
|
|
286
|
+
strictness.verifyNativeProofs = config.verifyNativeProofs ?? false;
|
|
287
|
+
strictness.verifyContractProofs = config.verifyContractProofs ?? false;
|
|
288
|
+
strictness.enforceLimits = config.enforceLimits ?? false;
|
|
289
|
+
strictness.verifySignatures = config.verifySignatures ?? false;
|
|
290
|
+
return strictness;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Compute block hash from block number. Uses a deterministic hash based on block number for easy recomputation.
|
|
294
|
+
*
|
|
295
|
+
* @param blockNumber - The block number to compute hash for
|
|
296
|
+
* @returns A deterministic 64-character hex hash
|
|
297
|
+
*/
|
|
298
|
+
export const blockHash = async (blockNumber) => {
|
|
299
|
+
const input = `block-${blockNumber.toString()}`;
|
|
300
|
+
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(input));
|
|
301
|
+
const { Encoding } = await import('effect');
|
|
302
|
+
return Encoding.encodeHex(new Uint8Array(hashBuffer));
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Create the next block context from the previous block.
|
|
306
|
+
*
|
|
307
|
+
* @param previousBlock - The previous block (or undefined for genesis)
|
|
308
|
+
* @param blockTime - The timestamp for the new block
|
|
309
|
+
* @returns A BlockContext suitable for transaction processing
|
|
310
|
+
*/
|
|
311
|
+
export const nextBlockContextFromBlock = async (previousBlock, blockTime) => {
|
|
312
|
+
const nextBlockNumber = previousBlock !== undefined ? previousBlock.number + 1n : 0n;
|
|
313
|
+
const hash = await blockHash(nextBlockNumber);
|
|
314
|
+
const blockSeconds = DateOps.dateToSeconds(blockTime);
|
|
315
|
+
const previousSeconds = previousBlock !== undefined ? DateOps.dateToSeconds(previousBlock.timestamp) : blockSeconds - 1n;
|
|
316
|
+
const timeSinceLastBlock = blockSeconds - previousSeconds;
|
|
317
|
+
return {
|
|
318
|
+
parentBlockHash: hash,
|
|
319
|
+
secondsSinceEpoch: blockSeconds,
|
|
320
|
+
secondsSinceEpochErr: 1, // Clock error tolerance in seconds (reasonable default for simulator)
|
|
321
|
+
lastBlockTime: timeSinceLastBlock > 0n ? timeSinceLastBlock : 1n,
|
|
322
|
+
};
|
|
323
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { getLastBlock, getCurrentBlockNumber, getCurrentTime, getBlockByNumber, getLastBlockResults, getLastBlockEvents, getBlockEventsFrom, getBlockEventsSince, hasPendingTransactions, resolveFullness, allMempoolTransactions, blankState, addToMempool, removeFromMempool, advanceTime, updateLedger, appendBlock, applyTransaction, processTransaction, processTransactions, createBlock, createEmptyBlock, type TransactionProcessingResult, createStrictness, blockHash, assignStrictness, assignStrictnessToAll, defaultStrictness, genesisStrictness, type SimulatorState, type Block, type BlockTransaction, type BlockInfo, type PendingTransaction, type ReadyTransaction, type BlockProductionRequest, type BlockProducer, type FullnessSpec, type GenesisMint, type StrictnessConfig, } from './SimulatorState.js';
|
|
2
|
+
export { Simulator, immediateBlockProducer, type SimulatorConfig } from './Simulator.js';
|