@hardkas/localnet 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Rodriguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,60 @@
1
+ // src/receipts.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { existsSync } from "fs";
5
+ function getDefaultReceiptsDir(cwd = process.cwd()) {
6
+ return path.join(cwd, ".hardkas", "receipts");
7
+ }
8
+ function getReceiptPath(txId, cwd) {
9
+ validateTxId(txId);
10
+ return path.join(getDefaultReceiptsDir(cwd), `${txId}.json`);
11
+ }
12
+ function validateTxId(txId) {
13
+ if (txId.includes("/") || txId.includes("\\") || txId.includes("..")) {
14
+ throw new Error(`Invalid txId: ${txId}`);
15
+ }
16
+ }
17
+ async function saveSimulatedReceipt(receipt, options) {
18
+ const dir = getDefaultReceiptsDir(options?.cwd);
19
+ if (!existsSync(dir)) {
20
+ await fs.mkdir(dir, { recursive: true });
21
+ }
22
+ const filePath = getReceiptPath(receipt.txId, options?.cwd);
23
+ await fs.writeFile(filePath, JSON.stringify(receipt, null, 2), "utf-8");
24
+ return filePath;
25
+ }
26
+ async function loadSimulatedReceipt(txId, options) {
27
+ const filePath = getReceiptPath(txId, options?.cwd);
28
+ if (!existsSync(filePath)) {
29
+ throw new Error(`Receipt not found: ${txId}`);
30
+ }
31
+ const data = await fs.readFile(filePath, "utf-8");
32
+ return JSON.parse(data);
33
+ }
34
+ async function listSimulatedReceipts(options) {
35
+ const dir = getDefaultReceiptsDir(options?.cwd);
36
+ if (!existsSync(dir)) {
37
+ return [];
38
+ }
39
+ const files = await fs.readdir(dir);
40
+ const receipts = [];
41
+ for (const file of files) {
42
+ if (file.endsWith(".json")) {
43
+ try {
44
+ const txId = path.basename(file, ".json");
45
+ const receipt = await loadSimulatedReceipt(txId, options);
46
+ receipts.push(receipt);
47
+ } catch (e) {
48
+ }
49
+ }
50
+ }
51
+ return receipts.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
52
+ }
53
+
54
+ export {
55
+ getDefaultReceiptsDir,
56
+ getReceiptPath,
57
+ saveSimulatedReceipt,
58
+ loadSimulatedReceipt,
59
+ listSimulatedReceipts
60
+ };
@@ -0,0 +1,60 @@
1
+ // src/traces.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { existsSync } from "fs";
5
+ function getDefaultTracesDir(cwd = process.cwd()) {
6
+ return path.join(cwd, ".hardkas", "traces");
7
+ }
8
+ function getTracePath(txId, cwd) {
9
+ validateTxId(txId);
10
+ return path.join(getDefaultTracesDir(cwd), `${txId}.trace.json`);
11
+ }
12
+ function validateTxId(txId) {
13
+ if (txId.includes("/") || txId.includes("\\") || txId.includes("..")) {
14
+ throw new Error(`Invalid txId: ${txId}`);
15
+ }
16
+ }
17
+ async function saveSimulatedTrace(trace, options) {
18
+ const dir = getDefaultTracesDir(options?.cwd);
19
+ if (!existsSync(dir)) {
20
+ await fs.mkdir(dir, { recursive: true });
21
+ }
22
+ const filePath = getTracePath(trace.txId, options?.cwd);
23
+ await fs.writeFile(filePath, JSON.stringify(trace, null, 2), "utf-8");
24
+ return filePath;
25
+ }
26
+ async function loadSimulatedTrace(txId, options) {
27
+ const filePath = getTracePath(txId, options?.cwd);
28
+ if (!existsSync(filePath)) {
29
+ throw new Error(`Trace not found: ${txId}`);
30
+ }
31
+ const data = await fs.readFile(filePath, "utf-8");
32
+ return JSON.parse(data);
33
+ }
34
+ async function listSimulatedTraces(options) {
35
+ const dir = getDefaultTracesDir(options?.cwd);
36
+ if (!existsSync(dir)) {
37
+ return [];
38
+ }
39
+ const files = await fs.readdir(dir);
40
+ const traces = [];
41
+ for (const file of files) {
42
+ if (file.endsWith(".trace.json")) {
43
+ try {
44
+ const txId = path.basename(file, ".trace.json");
45
+ const trace = await loadSimulatedTrace(txId, options);
46
+ traces.push(trace);
47
+ } catch (e) {
48
+ }
49
+ }
50
+ }
51
+ return traces.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
52
+ }
53
+
54
+ export {
55
+ getDefaultTracesDir,
56
+ getTracePath,
57
+ saveSimulatedTrace,
58
+ loadSimulatedTrace,
59
+ listSimulatedTraces
60
+ };
@@ -0,0 +1,334 @@
1
+ import { HardkasArtifactBase, Snapshot as Snapshot$1, ARTIFACT_SCHEMAS, TxReceipt, TxPlan } from '@hardkas/artifacts';
2
+ import { ExecutionMode, NetworkId } from '@hardkas/core';
3
+
4
+ interface HardkasAccount {
5
+ readonly name: string;
6
+ readonly address: string;
7
+ readonly balanceSompi: bigint;
8
+ }
9
+ declare function createDeterministicAccounts(input?: {
10
+ readonly count?: number | undefined;
11
+ readonly initialBalanceSompi?: bigint | undefined;
12
+ } | undefined): HardkasAccount[];
13
+ declare function resolveAccountAddress(input: string): string;
14
+
15
+ interface SimulatedUtxo {
16
+ readonly id: string;
17
+ readonly address: string;
18
+ readonly amountSompi: bigint;
19
+ readonly spent: boolean;
20
+ }
21
+ interface Snapshot {
22
+ readonly id: string;
23
+ readonly utxos: readonly SimulatedUtxo[];
24
+ readonly daaScore: bigint;
25
+ }
26
+ declare class SimulatedKaspaChain {
27
+ private utxos;
28
+ private daaScore;
29
+ constructor(accounts: readonly HardkasAccount[]);
30
+ getDaaScore(): bigint;
31
+ mineBlock(): bigint;
32
+ getBalance(address: string): bigint;
33
+ getUtxos(address: string): readonly SimulatedUtxo[];
34
+ fund(address: string, amountSompi: bigint): SimulatedUtxo;
35
+ snapshot(): Snapshot;
36
+ restore(snapshot: Snapshot): void;
37
+ }
38
+
39
+ interface HardkasDevnet {
40
+ readonly mode: "simulated";
41
+ readonly accounts: readonly HardkasAccount[];
42
+ readonly chain: SimulatedKaspaChain;
43
+ stop(): Promise<void>;
44
+ }
45
+ declare function startSimulatedDevnet(input?: {
46
+ readonly accounts?: number | undefined;
47
+ readonly initialBalanceSompi?: bigint | undefined;
48
+ } | undefined): Promise<HardkasDevnet>;
49
+
50
+ interface LocalnetAccount {
51
+ name: string;
52
+ address: string;
53
+ }
54
+ interface LocalnetUtxo {
55
+ id: string;
56
+ address: string;
57
+ amountSompi: string;
58
+ spent: boolean;
59
+ createdAtDaaScore: string;
60
+ spentAtDaaScore?: string;
61
+ }
62
+ interface LocalnetState extends HardkasArtifactBase {
63
+ schema: "hardkas.localnetState.v1";
64
+ mode: ExecutionMode;
65
+ networkId: NetworkId;
66
+ daaScore: string;
67
+ accounts: LocalnetAccount[];
68
+ utxos: LocalnetUtxo[];
69
+ snapshots?: Snapshot$1[];
70
+ dag?: SimulatedDag;
71
+ }
72
+ interface SimulatedBlock {
73
+ id: string;
74
+ parents: string[];
75
+ blueScore: string;
76
+ daaScore: string;
77
+ acceptedTxIds: string[];
78
+ isGenesis?: boolean;
79
+ }
80
+ interface SimulatedDag {
81
+ blocks: Record<string, SimulatedBlock>;
82
+ sink: string;
83
+ selectedPathToSink: string[];
84
+ acceptedTxIds: string[];
85
+ displacedTxIds: string[];
86
+ conflictSet: Array<{
87
+ outpoint: string;
88
+ winnerTxId: string;
89
+ loserTxIds: string[];
90
+ }>;
91
+ }
92
+ interface StateTransition {
93
+ preStateHash: string;
94
+ postStateHash: string;
95
+ daaScore: string;
96
+ }
97
+ interface SimulationResult {
98
+ ok: boolean;
99
+ state: LocalnetState;
100
+ receipt: any;
101
+ planArtifact?: any;
102
+ errors: string[];
103
+ }
104
+ interface ReplayInvariantResult {
105
+ ok: boolean;
106
+ mismatches: string[];
107
+ }
108
+ interface ReplayVerificationReport {
109
+ planOk: boolean;
110
+ receiptOk: boolean;
111
+ invariantsOk: boolean;
112
+ errors: string[];
113
+ }
114
+ interface SnapshotVerificationResult {
115
+ ok: boolean;
116
+ hashes: {
117
+ accountsMatch: boolean;
118
+ utxoSetMatch: boolean;
119
+ stateMatch: boolean;
120
+ contentMatch: boolean;
121
+ };
122
+ errors: string[];
123
+ }
124
+ interface SnapshotRestoreResult {
125
+ ok: boolean;
126
+ previousStateHash?: string;
127
+ newStateHash?: string;
128
+ error?: string;
129
+ }
130
+
131
+ interface CreateInitialStateOptions {
132
+ accounts?: number | undefined;
133
+ initialBalanceSompi?: bigint | undefined;
134
+ }
135
+ declare function createInitialLocalnetState(options?: CreateInitialStateOptions): LocalnetState;
136
+ declare function resolveAccountAddressFromState(state: LocalnetState, nameOrAddress: string): string;
137
+
138
+ declare function getDefaultLocalnetDir(cwd?: string): string;
139
+ declare function getDefaultLocalnetStatePath(cwd?: string): string;
140
+ declare function saveLocalnetState(state: LocalnetState, filePath?: string): Promise<void>;
141
+ declare function loadLocalnetState(filePath?: string): Promise<LocalnetState | null>;
142
+ declare function loadOrCreateLocalnetState(options?: {
143
+ cwd?: string;
144
+ accounts?: number;
145
+ initialBalanceSompi?: bigint;
146
+ }): Promise<LocalnetState>;
147
+ declare function resetLocalnetState(options?: {
148
+ cwd?: string;
149
+ accounts?: number;
150
+ initialBalanceSompi?: bigint;
151
+ }): Promise<LocalnetState>;
152
+
153
+ interface FundAddressInput {
154
+ address: string;
155
+ amountSompi: bigint;
156
+ }
157
+ declare function fundAddress(state: LocalnetState, input: FundAddressInput): LocalnetState;
158
+
159
+ declare function getAddressBalanceSompi(state: LocalnetState, address: string): bigint;
160
+ declare function getAccountBalanceSompi(state: LocalnetState, nameOrAddress: string): bigint;
161
+ declare function getSpendableUtxos(state: LocalnetState, address: string): LocalnetUtxo[];
162
+
163
+ /**
164
+ * Calculates hash of the UTXO set (sorted by outpoint).
165
+ */
166
+ declare function calculateUtxoSetHash(utxos: LocalnetUtxo[]): string;
167
+ /**
168
+ * Calculates hash of the account set (sorted by address).
169
+ */
170
+ declare function calculateAccountsHash(accounts: LocalnetAccount[]): string;
171
+ /**
172
+ * Calculates the state hash (daaScore + accountsHash + utxoSetHash).
173
+ */
174
+ declare function calculateStateHash(state: LocalnetState): string;
175
+ /**
176
+ * Creates a canonical deterministic snapshot.
177
+ */
178
+ declare function createLocalnetSnapshot(state: LocalnetState, name?: string): LocalnetState;
179
+ /**
180
+ * Verifies the integrity of a snapshot.
181
+ */
182
+ declare function verifySnapshot(snapshot: any): SnapshotVerificationResult;
183
+ /**
184
+ * Restores a snapshot with atomic safety and verification.
185
+ */
186
+ declare function restoreLocalnetSnapshot(state: LocalnetState, snapshotIdOrName: string): LocalnetState;
187
+
188
+ interface SimulatedPaymentInput {
189
+ readonly from: string;
190
+ readonly to: string;
191
+ readonly amountSompi: bigint;
192
+ readonly feeRateSompiPerMass?: bigint;
193
+ }
194
+ /**
195
+ * Standard Kaspa dust limit (approximate).
196
+ */
197
+ declare const DUST_LIMIT_SOMPI = 600n;
198
+ /**
199
+ * Applies a simulated payment to the localnet state with atomic safety and validation.
200
+ */
201
+ declare function applySimulatedPayment(state: LocalnetState, input: SimulatedPaymentInput): SimulationResult;
202
+ /**
203
+ * Executes a pre-built transaction plan against the simulated state.
204
+ */
205
+ declare function applySimulatedPlan(state: LocalnetState, planArtifact: any, // TxPlanArtifact
206
+ options?: {
207
+ txId?: string;
208
+ }): SimulationResult;
209
+
210
+ interface StoredSimulatedTxReceipt extends HardkasArtifactBase {
211
+ schema: typeof ARTIFACT_SCHEMAS.TX_RECEIPT;
212
+ version: "1.0.0-alpha";
213
+ txId: string;
214
+ status: "confirmed" | "failed";
215
+ mode: ExecutionMode;
216
+ networkId: NetworkId;
217
+ from: {
218
+ address: string;
219
+ };
220
+ to: {
221
+ address: string;
222
+ };
223
+ amountSompi: string;
224
+ feeSompi: string;
225
+ changeSompi?: string | undefined;
226
+ spentUtxoIds: string[];
227
+ createdUtxoIds: string[];
228
+ daaScore: string;
229
+ }
230
+ declare function getDefaultReceiptsDir(cwd?: string): string;
231
+ declare function getReceiptPath(txId: string, cwd?: string): string;
232
+ declare function saveSimulatedReceipt(receipt: StoredSimulatedTxReceipt, options?: {
233
+ cwd?: string;
234
+ }): Promise<string>;
235
+ declare function loadSimulatedReceipt(txId: string, options?: {
236
+ cwd?: string;
237
+ }): Promise<StoredSimulatedTxReceipt>;
238
+ declare function listSimulatedReceipts(options?: {
239
+ cwd?: string;
240
+ }): Promise<StoredSimulatedTxReceipt[]>;
241
+
242
+ type StoredTraceEvent = {
243
+ readonly type: "phase.started";
244
+ readonly phase: string;
245
+ readonly timestamp: number;
246
+ } | {
247
+ readonly type: "phase.completed";
248
+ readonly phase: string;
249
+ readonly timestamp: number;
250
+ } | {
251
+ readonly type: "note";
252
+ readonly message: string;
253
+ readonly timestamp: number;
254
+ } | {
255
+ readonly type: "tx.failed";
256
+ readonly phase: string;
257
+ readonly reason: string;
258
+ readonly timestamp: number;
259
+ };
260
+ interface StoredSimulatedTxTrace extends HardkasArtifactBase {
261
+ readonly schema: typeof ARTIFACT_SCHEMAS.TX_TRACE;
262
+ readonly txId: string;
263
+ readonly mode: ExecutionMode;
264
+ readonly networkId: NetworkId;
265
+ readonly events: readonly StoredTraceEvent[];
266
+ readonly receiptPath?: string | undefined;
267
+ }
268
+ declare function getDefaultTracesDir(cwd?: string): string;
269
+ declare function getTracePath(txId: string, cwd?: string): string;
270
+ declare function saveSimulatedTrace(trace: StoredSimulatedTxTrace, options?: {
271
+ cwd?: string;
272
+ }): Promise<string>;
273
+ declare function loadSimulatedTrace(txId: string, options?: {
274
+ cwd?: string;
275
+ }): Promise<StoredSimulatedTxTrace>;
276
+ declare function listSimulatedTraces(options?: {
277
+ cwd?: string;
278
+ }): Promise<StoredSimulatedTxTrace[]>;
279
+
280
+ interface SimulatedReplaySummary {
281
+ receipt: TxReceipt;
282
+ trace: StoredSimulatedTxTrace;
283
+ summary: {
284
+ spentCount: number;
285
+ createdCount: number;
286
+ transferredSompi: bigint;
287
+ feeSompi: bigint;
288
+ changeSompi: bigint;
289
+ finalDaaScore: string;
290
+ };
291
+ }
292
+ /**
293
+ * Verifies that a transaction replay matches the original artifacts.
294
+ */
295
+ declare function verifyReplay(state: LocalnetState, originalPlan: TxPlan, originalReceipt: TxReceipt): ReplayVerificationReport;
296
+ /**
297
+ * Loads receipt and trace for a transaction and produces a summary.
298
+ */
299
+ declare function getSimulatedReplaySummary(txId: string, options?: {
300
+ cwd?: string;
301
+ }): Promise<SimulatedReplaySummary>;
302
+
303
+ /**
304
+ * Creates a fresh simulated DAG with a genesis block.
305
+ */
306
+ declare function createSimulatedDag(): SimulatedDag;
307
+ /**
308
+ * Adds a block to the DAG.
309
+ */
310
+ declare function addSimulatedBlock(dag: SimulatedDag, block: SimulatedBlock): SimulatedDag;
311
+ /**
312
+ * Moves the sink and recomputes the accepted transaction set.
313
+ */
314
+ declare function moveSink(dag: SimulatedDag, newSinkId: string, txProvider: (txId: string) => {
315
+ inputs: string[];
316
+ } | undefined): SimulatedDag;
317
+ /**
318
+ * Deterministic Conflict Resolution (Approximation for v0.2-alpha)
319
+ * Priority:
320
+ * 1. sink ancestry priority (is part of selectedPathToSink?)
321
+ * 2. deterministic block order (daaScore then block ID)
322
+ * 3. txId lexicographic tie-break
323
+ */
324
+ declare function resolveConflictsDeterministically(txs: Array<{
325
+ txId: string;
326
+ blockId: string;
327
+ inputs: string[];
328
+ }>, dag: SimulatedDag): {
329
+ accepted: string[];
330
+ displaced: string[];
331
+ conflicts: any[];
332
+ };
333
+
334
+ export { type CreateInitialStateOptions, DUST_LIMIT_SOMPI, type FundAddressInput, type HardkasAccount, type HardkasDevnet, type LocalnetAccount, type LocalnetState, type LocalnetUtxo, type ReplayInvariantResult, type ReplayVerificationReport, type SimulatedBlock, type SimulatedDag, SimulatedKaspaChain, type SimulatedPaymentInput, type SimulatedReplaySummary, type SimulatedUtxo, type SimulationResult, type Snapshot, type SnapshotRestoreResult, type SnapshotVerificationResult, type StateTransition, type StoredSimulatedTxReceipt, type StoredSimulatedTxTrace, type StoredTraceEvent, addSimulatedBlock, applySimulatedPayment, applySimulatedPlan, calculateAccountsHash, calculateStateHash, calculateUtxoSetHash, createDeterministicAccounts, createInitialLocalnetState, createLocalnetSnapshot, createSimulatedDag, fundAddress, getAccountBalanceSompi, getAddressBalanceSompi, getDefaultLocalnetDir, getDefaultLocalnetStatePath, getDefaultReceiptsDir, getDefaultTracesDir, getReceiptPath, getSimulatedReplaySummary, getSpendableUtxos, getTracePath, listSimulatedReceipts, listSimulatedTraces, loadLocalnetState, loadOrCreateLocalnetState, loadSimulatedReceipt, loadSimulatedTrace, moveSink, resetLocalnetState, resolveAccountAddress, resolveAccountAddressFromState, resolveConflictsDeterministically, restoreLocalnetSnapshot, saveLocalnetState, saveSimulatedReceipt, saveSimulatedTrace, startSimulatedDevnet, verifyReplay, verifySnapshot };
package/dist/index.js ADDED
@@ -0,0 +1,861 @@
1
+ import {
2
+ getDefaultReceiptsDir,
3
+ getReceiptPath,
4
+ listSimulatedReceipts,
5
+ loadSimulatedReceipt,
6
+ saveSimulatedReceipt
7
+ } from "./chunk-CXDVB3K4.js";
8
+ import {
9
+ getDefaultTracesDir,
10
+ getTracePath,
11
+ listSimulatedTraces,
12
+ loadSimulatedTrace,
13
+ saveSimulatedTrace
14
+ } from "./chunk-W7ZBGUNK.js";
15
+
16
+ // src/accounts.ts
17
+ function createDeterministicAccounts(input) {
18
+ const count = input?.count ?? 5;
19
+ const initialBalanceSompi = input?.initialBalanceSompi ?? 1000n * 100000000n;
20
+ const names = ["alice", "bob", "carol", "dave", "erin"];
21
+ return Array.from({ length: count }, (_, index) => {
22
+ const name = names[index] ?? `account${index}`;
23
+ return {
24
+ name,
25
+ address: `kaspa:sim_${name}`,
26
+ balanceSompi: initialBalanceSompi
27
+ };
28
+ });
29
+ }
30
+ function resolveAccountAddress(input) {
31
+ if (input.startsWith("kaspa:")) {
32
+ return input;
33
+ }
34
+ const aliases = {
35
+ alice: "kaspa:sim_alice",
36
+ bob: "kaspa:sim_bob",
37
+ carol: "kaspa:sim_carol",
38
+ dave: "kaspa:sim_dave",
39
+ erin: "kaspa:sim_erin"
40
+ };
41
+ const resolved = aliases[input.toLowerCase()];
42
+ if (!resolved) {
43
+ throw new Error(`Unknown account alias: ${input}`);
44
+ }
45
+ return resolved;
46
+ }
47
+
48
+ // src/simulated-chain.ts
49
+ var SimulatedKaspaChain = class {
50
+ utxos = [];
51
+ daaScore = 0n;
52
+ constructor(accounts) {
53
+ for (const account of accounts) {
54
+ this.utxos.push({
55
+ id: `genesis:${account.name}:0`,
56
+ address: account.address,
57
+ amountSompi: account.balanceSompi,
58
+ spent: false
59
+ });
60
+ }
61
+ }
62
+ getDaaScore() {
63
+ return this.daaScore;
64
+ }
65
+ mineBlock() {
66
+ this.daaScore += 1n;
67
+ return this.daaScore;
68
+ }
69
+ getBalance(address) {
70
+ return this.utxos.filter((utxo) => utxo.address === address && !utxo.spent).reduce((sum, utxo) => sum + utxo.amountSompi, 0n);
71
+ }
72
+ getUtxos(address) {
73
+ return this.utxos.filter((utxo) => utxo.address === address && !utxo.spent);
74
+ }
75
+ fund(address, amountSompi) {
76
+ if (amountSompi <= 0n) {
77
+ throw new Error("Faucet amount must be positive.");
78
+ }
79
+ const utxo = {
80
+ id: `faucet:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,
81
+ address,
82
+ amountSompi,
83
+ spent: false
84
+ };
85
+ this.utxos.push(utxo);
86
+ this.mineBlock();
87
+ return utxo;
88
+ }
89
+ snapshot() {
90
+ return {
91
+ id: `snapshot:${Date.now().toString(36)}`,
92
+ utxos: this.utxos.map((utxo) => ({ ...utxo })),
93
+ daaScore: this.daaScore
94
+ };
95
+ }
96
+ restore(snapshot) {
97
+ this.utxos = snapshot.utxos.map((utxo) => ({ ...utxo }));
98
+ this.daaScore = snapshot.daaScore;
99
+ }
100
+ };
101
+
102
+ // src/devnet.ts
103
+ async function startSimulatedDevnet(input) {
104
+ const accounts = createDeterministicAccounts({
105
+ count: input?.accounts,
106
+ initialBalanceSompi: input?.initialBalanceSompi
107
+ });
108
+ const chain = new SimulatedKaspaChain(accounts);
109
+ return {
110
+ mode: "simulated",
111
+ accounts,
112
+ chain,
113
+ async stop() {
114
+ }
115
+ };
116
+ }
117
+
118
+ // src/state.ts
119
+ import { SOMPI_PER_KAS } from "@hardkas/core";
120
+ import { HARDKAS_VERSION, ARTIFACT_SCHEMAS } from "@hardkas/artifacts";
121
+ function createInitialLocalnetState(options = {}) {
122
+ const accountCount = options.accounts ?? 5;
123
+ const initialBalanceSompi = options.initialBalanceSompi ?? 1000n * SOMPI_PER_KAS;
124
+ const accounts = createDeterministicAccounts({
125
+ count: accountCount,
126
+ initialBalanceSompi
127
+ });
128
+ return {
129
+ schema: ARTIFACT_SCHEMAS.LOCALNET_STATE,
130
+ hardkasVersion: HARDKAS_VERSION,
131
+ version: "1.0.0-alpha",
132
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
133
+ mode: "simulated",
134
+ networkId: "simnet",
135
+ daaScore: "0",
136
+ accounts: accounts.map((a) => ({
137
+ name: a.name,
138
+ address: a.address
139
+ })),
140
+ utxos: accounts.map((a) => ({
141
+ id: `genesis:${a.name}:0`,
142
+ address: a.address,
143
+ amountSompi: a.balanceSompi.toString(),
144
+ spent: false,
145
+ createdAtDaaScore: "0"
146
+ })),
147
+ snapshots: []
148
+ };
149
+ }
150
+ function resolveAccountAddressFromState(state, nameOrAddress) {
151
+ if (nameOrAddress.startsWith("kaspa:")) {
152
+ return nameOrAddress;
153
+ }
154
+ const account = state.accounts.find((a) => a.name === nameOrAddress);
155
+ if (account) {
156
+ return account.address;
157
+ }
158
+ return nameOrAddress;
159
+ }
160
+
161
+ // src/store.ts
162
+ import fs from "fs/promises";
163
+ import path from "path";
164
+ function getDefaultLocalnetDir(cwd = process.cwd()) {
165
+ return path.join(cwd, ".hardkas");
166
+ }
167
+ function getDefaultLocalnetStatePath(cwd = process.cwd()) {
168
+ return path.join(getDefaultLocalnetDir(cwd), "localnet.json");
169
+ }
170
+ async function saveLocalnetState(state, filePath) {
171
+ const targetPath = filePath ?? getDefaultLocalnetStatePath();
172
+ const dir = path.dirname(targetPath);
173
+ await fs.mkdir(dir, { recursive: true });
174
+ await fs.writeFile(targetPath, JSON.stringify(state, null, 2), "utf-8");
175
+ }
176
+ async function loadLocalnetState(filePath) {
177
+ const targetPath = filePath ?? getDefaultLocalnetStatePath();
178
+ try {
179
+ const content = await fs.readFile(targetPath, "utf-8");
180
+ return JSON.parse(content);
181
+ } catch (error) {
182
+ return null;
183
+ }
184
+ }
185
+ async function loadOrCreateLocalnetState(options = {}) {
186
+ const path2 = getDefaultLocalnetStatePath(options.cwd);
187
+ let state = await loadLocalnetState(path2);
188
+ if (!state) {
189
+ state = createInitialLocalnetState({
190
+ accounts: options.accounts,
191
+ initialBalanceSompi: options.initialBalanceSompi
192
+ });
193
+ await saveLocalnetState(state, path2);
194
+ }
195
+ return state;
196
+ }
197
+ async function resetLocalnetState(options = {}) {
198
+ const path2 = getDefaultLocalnetStatePath(options.cwd);
199
+ const state = createInitialLocalnetState({
200
+ accounts: options.accounts,
201
+ initialBalanceSompi: options.initialBalanceSompi
202
+ });
203
+ await saveLocalnetState(state, path2);
204
+ return state;
205
+ }
206
+
207
+ // src/faucet.ts
208
+ function fundAddress(state, input) {
209
+ if (input.amountSompi <= 0n) {
210
+ throw new Error("Faucet amount must be positive.");
211
+ }
212
+ const nextDaaScore = (BigInt(state.daaScore) + 1n).toString();
213
+ const newUtxo = {
214
+ id: `faucet:${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`,
215
+ address: input.address,
216
+ amountSompi: input.amountSompi.toString(),
217
+ spent: false,
218
+ createdAtDaaScore: nextDaaScore
219
+ };
220
+ return {
221
+ ...state,
222
+ daaScore: nextDaaScore,
223
+ utxos: [...state.utxos, newUtxo]
224
+ };
225
+ }
226
+
227
+ // src/balance.ts
228
+ function getAddressBalanceSompi(state, address) {
229
+ return state.utxos.filter((u) => u.address === address && !u.spent).reduce((sum, u) => sum + BigInt(u.amountSompi), 0n);
230
+ }
231
+ function getAccountBalanceSompi(state, nameOrAddress) {
232
+ const address = resolveAccountAddressFromState(state, nameOrAddress);
233
+ return getAddressBalanceSompi(state, address);
234
+ }
235
+ function getSpendableUtxos(state, address) {
236
+ return state.utxos.filter((u) => u.address === address && !u.spent);
237
+ }
238
+
239
+ // src/snapshot.ts
240
+ import {
241
+ HARDKAS_VERSION as HARDKAS_VERSION2,
242
+ ARTIFACT_VERSION,
243
+ calculateContentHash,
244
+ sortUtxosByOutpoint
245
+ } from "@hardkas/artifacts";
246
+ function calculateUtxoSetHash(utxos) {
247
+ const sorted = sortUtxosByOutpoint(utxos);
248
+ return calculateContentHash(sorted);
249
+ }
250
+ function calculateAccountsHash(accounts) {
251
+ const sorted = [...accounts].sort((a, b) => a.address.localeCompare(b.address));
252
+ return calculateContentHash(sorted);
253
+ }
254
+ function calculateStateHash(state) {
255
+ const accountsHash = calculateAccountsHash(state.accounts);
256
+ const utxoSetHash = calculateUtxoSetHash(state.utxos);
257
+ return calculateContentHash({
258
+ daaScore: state.daaScore,
259
+ accountsHash,
260
+ utxoSetHash
261
+ });
262
+ }
263
+ function createLocalnetSnapshot(state, name) {
264
+ const accountsHash = calculateAccountsHash(state.accounts);
265
+ const utxoSetHash = calculateUtxoSetHash(state.utxos);
266
+ const stateHash = calculateStateHash(state);
267
+ const snapshot = {
268
+ schema: "hardkas.snapshot",
269
+ hardkasVersion: HARDKAS_VERSION2,
270
+ version: ARTIFACT_VERSION,
271
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
272
+ name,
273
+ daaScore: state.daaScore,
274
+ accountsHash,
275
+ utxoSetHash,
276
+ stateHash,
277
+ accounts: JSON.parse(JSON.stringify(state.accounts)),
278
+ utxos: JSON.parse(JSON.stringify(sortUtxosByOutpoint(state.utxos)))
279
+ };
280
+ snapshot.contentHash = calculateContentHash(snapshot);
281
+ return {
282
+ ...state,
283
+ snapshots: [...state.snapshots || [], snapshot]
284
+ };
285
+ }
286
+ function verifySnapshot(snapshot) {
287
+ const errors = [];
288
+ const currentContentHash = calculateContentHash(snapshot);
289
+ const contentMatch = snapshot.contentHash === currentContentHash;
290
+ if (!contentMatch) errors.push(`Content hash mismatch: expected ${snapshot.contentHash}, got ${currentContentHash}`);
291
+ const currentAccountsHash = calculateAccountsHash(snapshot.accounts);
292
+ const accountsMatch = snapshot.accountsHash === currentAccountsHash;
293
+ if (!accountsMatch) errors.push(`Accounts hash mismatch: expected ${snapshot.accountsHash}, got ${currentAccountsHash}`);
294
+ const currentUtxoSetHash = calculateUtxoSetHash(snapshot.utxos);
295
+ const utxoSetMatch = snapshot.utxoSetHash === currentUtxoSetHash;
296
+ if (!utxoSetMatch) errors.push(`UTXO set hash mismatch: expected ${snapshot.utxoSetHash}, got ${currentUtxoSetHash}`);
297
+ const currentStateHash = calculateContentHash({
298
+ daaScore: snapshot.daaScore,
299
+ accountsHash: currentAccountsHash,
300
+ utxoSetHash: currentUtxoSetHash
301
+ });
302
+ const stateMatch = snapshot.stateHash === currentStateHash;
303
+ if (!stateMatch) errors.push(`State hash mismatch: expected ${snapshot.stateHash}, got ${currentStateHash}`);
304
+ return {
305
+ ok: errors.length === 0,
306
+ hashes: {
307
+ accountsMatch,
308
+ utxoSetMatch,
309
+ stateMatch,
310
+ contentMatch
311
+ },
312
+ errors
313
+ };
314
+ }
315
+ function restoreLocalnetSnapshot(state, snapshotIdOrName) {
316
+ const snapshot = state.snapshots?.find(
317
+ (s) => s.id === snapshotIdOrName || s.name === snapshotIdOrName || s.contentHash === snapshotIdOrName
318
+ );
319
+ if (!snapshot) {
320
+ throw new Error(`Snapshot not found: ${snapshotIdOrName}`);
321
+ }
322
+ const verification = verifySnapshot(snapshot);
323
+ if (!verification.ok) {
324
+ throw new Error(`Corrupted snapshot: ${verification.errors.join(", ")}`);
325
+ }
326
+ return {
327
+ ...state,
328
+ daaScore: snapshot.daaScore,
329
+ accounts: JSON.parse(JSON.stringify(snapshot.accounts)),
330
+ utxos: JSON.parse(JSON.stringify(snapshot.utxos))
331
+ };
332
+ }
333
+
334
+ // src/transactions.ts
335
+ import { createHash } from "crypto";
336
+ import { buildPaymentPlan } from "@hardkas/tx-builder";
337
+ import {
338
+ createTxPlanArtifact,
339
+ createSimulatedTxReceipt,
340
+ calculateContentHash as calculateContentHash2
341
+ } from "@hardkas/artifacts";
342
+ function generateDeterministicTxId(planArtifact, preStateHash, daaScore) {
343
+ const planHash = planArtifact.contentHash || calculateContentHash2(planArtifact);
344
+ const input = `${planHash}:${preStateHash}:${daaScore}`;
345
+ const hash = createHash("sha256").update(input).digest("hex").slice(0, 32);
346
+ return `simtx_${hash}`;
347
+ }
348
+ var DUST_LIMIT_SOMPI = 600n;
349
+ function applySimulatedPayment(state, input) {
350
+ const errors = [];
351
+ const preStateHash = calculateStateHash(state);
352
+ try {
353
+ const fromAddress = resolveAccountAddressFromState(state, input.from);
354
+ const toAddress = resolveAccountAddressFromState(state, input.to);
355
+ const amountSompi = input.amountSompi;
356
+ const feeRateSompiPerMass = input.feeRateSompiPerMass ?? 1n;
357
+ if (amountSompi <= 0n) {
358
+ throw new Error("Amount must be greater than 0");
359
+ }
360
+ if (amountSompi < DUST_LIMIT_SOMPI) {
361
+ errors.push(`Amount ${amountSompi} is below dust limit (${DUST_LIMIT_SOMPI})`);
362
+ }
363
+ const unspent = getSpendableUtxos(state, fromAddress);
364
+ if (unspent.length === 0) {
365
+ throw new Error(`Insufficient funds: no unspent UTXOs for ${fromAddress}`);
366
+ }
367
+ const availableUtxos = unspent.map((u) => {
368
+ const parts = u.id.split(":");
369
+ const index = Number(parts[parts.length - 1]);
370
+ const transactionId = parts.slice(0, -1).join(":");
371
+ return {
372
+ outpoint: { transactionId, index },
373
+ address: u.address,
374
+ amountSompi: BigInt(u.amountSompi),
375
+ scriptPublicKey: "mock-script"
376
+ };
377
+ });
378
+ const plan = buildPaymentPlan({
379
+ fromAddress,
380
+ outputs: [{ address: toAddress, amountSompi }],
381
+ availableUtxos,
382
+ feeRateSompiPerMass
383
+ });
384
+ const spentUtxoIds = plan.inputs.map((i) => `${i.outpoint.transactionId}:${i.outpoint.index}`);
385
+ const uniqueSpentIds = new Set(spentUtxoIds);
386
+ if (uniqueSpentIds.size !== spentUtxoIds.length) {
387
+ throw new Error("Duplicate inputs detected in transaction plan");
388
+ }
389
+ for (const id of spentUtxoIds) {
390
+ const utxo = state.utxos.find((u) => u.id === id);
391
+ if (!utxo) throw new Error(`UTXO not found: ${id}`);
392
+ if (utxo.spent) throw new Error(`UTXO already spent: ${id}`);
393
+ }
394
+ const planArtifact = createTxPlanArtifact({
395
+ networkId: state.networkId || "simnet",
396
+ mode: "simulated",
397
+ from: { input: input.from, address: fromAddress },
398
+ to: { input: input.to, address: toAddress },
399
+ amountSompi,
400
+ plan
401
+ });
402
+ const nextDaaScore = (BigInt(state.daaScore) + 1n).toString();
403
+ const txId = generateDeterministicTxId(planArtifact, preStateHash, nextDaaScore);
404
+ const nextUtxos = state.utxos.map((u) => {
405
+ if (spentUtxoIds.includes(u.id)) {
406
+ return {
407
+ ...u,
408
+ spent: true,
409
+ spentAtDaaScore: nextDaaScore
410
+ };
411
+ }
412
+ return u;
413
+ });
414
+ const createdUtxoIds = [];
415
+ const recipientUtxo = {
416
+ id: `${txId}:0`,
417
+ address: toAddress,
418
+ amountSompi: amountSompi.toString(),
419
+ spent: false,
420
+ createdAtDaaScore: nextDaaScore
421
+ };
422
+ nextUtxos.push(recipientUtxo);
423
+ createdUtxoIds.push(recipientUtxo.id);
424
+ if (plan.change) {
425
+ const changeUtxo = {
426
+ id: `${txId}:1`,
427
+ address: fromAddress,
428
+ amountSompi: plan.change.amountSompi.toString(),
429
+ spent: false,
430
+ createdAtDaaScore: nextDaaScore
431
+ };
432
+ nextUtxos.push(changeUtxo);
433
+ createdUtxoIds.push(changeUtxo.id);
434
+ }
435
+ const nextState = {
436
+ ...state,
437
+ daaScore: nextDaaScore,
438
+ utxos: nextUtxos
439
+ };
440
+ const postStateHash = calculateStateHash(nextState);
441
+ const receipt = createSimulatedTxReceipt(planArtifact, txId, {
442
+ spentUtxoIds,
443
+ createdUtxoIds,
444
+ daaScore: nextDaaScore
445
+ });
446
+ receipt.preStateHash = preStateHash;
447
+ receipt.postStateHash = postStateHash;
448
+ if (state.dag) {
449
+ receipt.dagContext = {
450
+ mode: "dag-light",
451
+ sink: state.dag.sink,
452
+ acceptedTxIds: state.dag.acceptedTxIds,
453
+ displacedTxIds: state.dag.displacedTxIds,
454
+ conflictSet: state.dag.conflictSet
455
+ };
456
+ } else {
457
+ receipt.dagContext = { mode: "linear", sink: "linear-pseudo-sink" };
458
+ }
459
+ return {
460
+ ok: true,
461
+ state: nextState,
462
+ receipt,
463
+ planArtifact,
464
+ errors
465
+ };
466
+ } catch (error) {
467
+ const txId = "failed-" + Date.now();
468
+ const receipt = {
469
+ schema: "hardkas.txReceipt",
470
+ status: "failed",
471
+ mode: "simulated",
472
+ txId,
473
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
474
+ errors: [error.message],
475
+ preStateHash,
476
+ postStateHash: preStateHash
477
+ };
478
+ return {
479
+ ok: false,
480
+ state,
481
+ // No mutation
482
+ receipt,
483
+ errors: [error.message]
484
+ };
485
+ }
486
+ }
487
+ function applySimulatedPlan(state, planArtifact, options) {
488
+ const errors = [];
489
+ const preStateHash = calculateStateHash(state);
490
+ try {
491
+ const spentUtxoIds = planArtifact.inputs.map((i) => `${i.outpoint.transactionId}:${i.outpoint.index}`);
492
+ for (const id of spentUtxoIds) {
493
+ const utxo = state.utxos.find((u) => u.id === id);
494
+ if (!utxo) throw new Error(`UTXO not found: ${id}`);
495
+ if (utxo.spent) throw new Error(`UTXO already spent: ${id}`);
496
+ }
497
+ const nextDaaScore = (BigInt(state.daaScore) + 1n).toString();
498
+ const txId = options?.txId || generateDeterministicTxId(planArtifact, preStateHash, nextDaaScore);
499
+ const nextUtxos = state.utxos.map((u) => {
500
+ if (spentUtxoIds.includes(u.id)) {
501
+ return { ...u, spent: true, spentAtDaaScore: nextDaaScore };
502
+ }
503
+ return u;
504
+ });
505
+ const createdUtxoIds = [];
506
+ planArtifact.outputs.forEach((o, idx) => {
507
+ const utxo = {
508
+ id: `${txId}:${idx}`,
509
+ address: o.address,
510
+ amountSompi: o.amountSompi.toString(),
511
+ spent: false,
512
+ createdAtDaaScore: nextDaaScore
513
+ };
514
+ nextUtxos.push(utxo);
515
+ createdUtxoIds.push(utxo.id);
516
+ });
517
+ if (planArtifact.change) {
518
+ const changeUtxo = {
519
+ id: `${txId}:${planArtifact.outputs.length}`,
520
+ address: planArtifact.change.address,
521
+ amountSompi: planArtifact.change.amountSompi.toString(),
522
+ spent: false,
523
+ createdAtDaaScore: nextDaaScore
524
+ };
525
+ nextUtxos.push(changeUtxo);
526
+ createdUtxoIds.push(changeUtxo.id);
527
+ }
528
+ const nextState = { ...state, daaScore: nextDaaScore, utxos: nextUtxos };
529
+ const postStateHash = calculateStateHash(nextState);
530
+ const receipt = createSimulatedTxReceipt(planArtifact, txId, {
531
+ spentUtxoIds,
532
+ createdUtxoIds,
533
+ daaScore: nextDaaScore
534
+ });
535
+ receipt.preStateHash = preStateHash;
536
+ receipt.postStateHash = postStateHash;
537
+ if (state.dag) {
538
+ receipt.dagContext = {
539
+ mode: "dag-light",
540
+ sink: state.dag.sink,
541
+ acceptedTxIds: state.dag.acceptedTxIds,
542
+ displacedTxIds: state.dag.displacedTxIds,
543
+ conflictSet: state.dag.conflictSet
544
+ };
545
+ } else {
546
+ receipt.dagContext = { mode: "linear", sink: "linear-pseudo-sink" };
547
+ }
548
+ return { ok: true, state: nextState, receipt, planArtifact, errors };
549
+ } catch (error) {
550
+ const receipt = {
551
+ schema: "hardkas.txReceipt",
552
+ status: "failed",
553
+ mode: "simulated",
554
+ txId: "failed-replay",
555
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
556
+ errors: [error.message],
557
+ preStateHash,
558
+ postStateHash: preStateHash
559
+ };
560
+ return { ok: false, state, receipt, errors: [error.message] };
561
+ }
562
+ }
563
+
564
+ // src/replay.ts
565
+ import {
566
+ calculateContentHash as calculateContentHash3
567
+ } from "@hardkas/artifacts";
568
+ import { coreEvents } from "@hardkas/core";
569
+ function verifyReplay(state, originalPlan, originalReceipt) {
570
+ const errors = [];
571
+ const currentPlanHash = calculateContentHash3(originalPlan);
572
+ if (originalPlan.contentHash && currentPlanHash !== originalPlan.contentHash) {
573
+ const errorMsg = `TxPlan contentHash mismatch: expected ${originalPlan.contentHash}, got ${currentPlanHash}`;
574
+ errors.push(errorMsg);
575
+ coreEvents.normalizeAndEmit({
576
+ kind: "replay.divergence",
577
+ txId: originalReceipt.txId,
578
+ field: "planHash",
579
+ expected: originalPlan.contentHash,
580
+ actual: currentPlanHash
581
+ });
582
+ }
583
+ const result = applySimulatedPlan(state, originalPlan, { txId: originalReceipt.txId });
584
+ const replayReceipt = result.receipt;
585
+ const checks = [
586
+ { field: "status", expected: originalReceipt.status, actual: replayReceipt.status },
587
+ { field: "mass", expected: String(originalReceipt.mass), actual: String(replayReceipt.mass) },
588
+ { field: "feeSompi", expected: String(originalReceipt.feeSompi), actual: String(replayReceipt.feeSompi) },
589
+ { field: "preStateHash", expected: String(originalReceipt.preStateHash), actual: String(replayReceipt.preStateHash) },
590
+ { field: "postStateHash", expected: String(originalReceipt.postStateHash), actual: String(replayReceipt.postStateHash) },
591
+ { field: "spentUtxos", expected: String(originalReceipt.spentUtxoIds?.length), actual: String(replayReceipt.spentUtxoIds?.length) }
592
+ ];
593
+ for (const check of checks) {
594
+ if (check.expected !== check.actual) {
595
+ errors.push(`${check.field} mismatch: expected ${check.expected}, got ${check.actual}`);
596
+ coreEvents.normalizeAndEmit({
597
+ kind: "replay.divergence",
598
+ txId: originalReceipt.txId,
599
+ field: check.field,
600
+ expected: check.expected,
601
+ actual: check.actual
602
+ });
603
+ }
604
+ }
605
+ if (errors.length === 0) {
606
+ coreEvents.normalizeAndEmit({
607
+ kind: "replay.verified",
608
+ txId: originalReceipt.txId
609
+ });
610
+ }
611
+ return {
612
+ planOk: !errors.some((e) => e.includes("TxPlan")),
613
+ receiptOk: !errors.some((e) => e.includes("Status") || e.includes("Hash")),
614
+ invariantsOk: errors.length === 0,
615
+ errors
616
+ };
617
+ }
618
+ async function getSimulatedReplaySummary(txId, options = {}) {
619
+ const { loadSimulatedReceipt: loadSimulatedReceipt2 } = await import("./receipts-ICI3GEJW.js");
620
+ const { loadSimulatedTrace: loadSimulatedTrace2 } = await import("./traces-DSACQ53V.js");
621
+ const receipt = await loadSimulatedReceipt2(txId, options);
622
+ const trace = await loadSimulatedTrace2(txId, options);
623
+ if (!receipt || !trace) {
624
+ throw new Error(`Receipt or trace not found for transaction: ${txId}`);
625
+ }
626
+ return {
627
+ receipt,
628
+ trace,
629
+ summary: {
630
+ spentCount: receipt.spentUtxoIds?.length || 0,
631
+ createdCount: receipt.createdUtxoIds?.length || 0,
632
+ transferredSompi: BigInt(receipt.amountSompi),
633
+ feeSompi: BigInt(receipt.feeSompi || "0"),
634
+ changeSompi: BigInt(receipt.changeSompi || "0"),
635
+ finalDaaScore: receipt.daaScore || "0"
636
+ }
637
+ };
638
+ }
639
+
640
+ // src/dag.ts
641
+ function createSimulatedDag() {
642
+ const genesis = {
643
+ id: "genesis",
644
+ parents: [],
645
+ blueScore: "0",
646
+ daaScore: "0",
647
+ acceptedTxIds: [],
648
+ isGenesis: true
649
+ };
650
+ return {
651
+ blocks: { [genesis.id]: genesis },
652
+ sink: genesis.id,
653
+ selectedPathToSink: [genesis.id],
654
+ acceptedTxIds: [],
655
+ displacedTxIds: [],
656
+ conflictSet: []
657
+ };
658
+ }
659
+ function addSimulatedBlock(dag, block) {
660
+ const newBlocks = { ...dag.blocks, [block.id]: block };
661
+ return {
662
+ ...dag,
663
+ blocks: newBlocks
664
+ };
665
+ }
666
+ function moveSink(dag, newSinkId, txProvider) {
667
+ if (!dag.blocks[newSinkId]) {
668
+ throw new Error(`Block ${newSinkId} not found in DAG.`);
669
+ }
670
+ const selectedPath = calculateSelectedPath(dag, newSinkId);
671
+ const reachableBlocks = identifyReachableBlocks(dag, newSinkId);
672
+ const sortedBlocks = reachableBlocks.sort((a, b) => {
673
+ const daaA = BigInt(a.daaScore);
674
+ const daaB = BigInt(b.daaScore);
675
+ if (daaA !== daaB) return daaA < daaB ? -1 : 1;
676
+ return a.id.localeCompare(b.id);
677
+ });
678
+ const acceptedTxIds = [];
679
+ const displacedTxIds = [];
680
+ const conflictSet = [];
681
+ const spentOutpoints = /* @__PURE__ */ new Map();
682
+ for (const block of sortedBlocks) {
683
+ for (const txId of block.acceptedTxIds) {
684
+ const tx = txProvider(txId);
685
+ if (!tx) {
686
+ continue;
687
+ }
688
+ let conflictFound = false;
689
+ for (const input of tx.inputs) {
690
+ if (spentOutpoints.has(input)) {
691
+ const winnerTxId = spentOutpoints.get(input);
692
+ let entry = conflictSet.find((c) => c.outpoint === input);
693
+ if (!entry) {
694
+ entry = { outpoint: input, winnerTxId, loserTxIds: [] };
695
+ conflictSet.push(entry);
696
+ }
697
+ entry.loserTxIds.push(txId);
698
+ conflictFound = true;
699
+ break;
700
+ }
701
+ }
702
+ if (conflictFound) {
703
+ displacedTxIds.push(txId);
704
+ } else {
705
+ acceptedTxIds.push(txId);
706
+ for (const input of tx.inputs) {
707
+ spentOutpoints.set(input, txId);
708
+ }
709
+ }
710
+ }
711
+ }
712
+ const newlyDisplaced = dag.acceptedTxIds.filter((id) => !acceptedTxIds.includes(id));
713
+ for (const id of newlyDisplaced) {
714
+ if (!displacedTxIds.includes(id)) {
715
+ displacedTxIds.push(id);
716
+ }
717
+ const tx = txProvider(id);
718
+ if (tx) {
719
+ for (const input of tx.inputs) {
720
+ if (spentOutpoints.has(input)) {
721
+ const winnerTxId = spentOutpoints.get(input);
722
+ let entry = conflictSet.find((c) => c.outpoint === input);
723
+ if (!entry) {
724
+ entry = { outpoint: input, winnerTxId, loserTxIds: [] };
725
+ conflictSet.push(entry);
726
+ }
727
+ if (!entry.loserTxIds.includes(id)) {
728
+ entry.loserTxIds.push(id);
729
+ }
730
+ break;
731
+ }
732
+ }
733
+ }
734
+ }
735
+ return {
736
+ ...dag,
737
+ sink: newSinkId,
738
+ selectedPathToSink: selectedPath.map((b) => b.id),
739
+ acceptedTxIds,
740
+ displacedTxIds,
741
+ conflictSet
742
+ };
743
+ }
744
+ function calculateSelectedPath(dag, sinkId) {
745
+ const path2 = [];
746
+ let currentId = sinkId;
747
+ while (currentId) {
748
+ const current = dag.blocks[currentId];
749
+ if (!current) break;
750
+ path2.unshift(current);
751
+ if (current.parents.length === 0) break;
752
+ currentId = current.parents[0];
753
+ }
754
+ return path2;
755
+ }
756
+ function identifyReachableBlocks(dag, sinkId) {
757
+ const reachable = /* @__PURE__ */ new Set();
758
+ const stack = [sinkId];
759
+ while (stack.length > 0) {
760
+ const id = stack.pop();
761
+ if (reachable.has(id)) continue;
762
+ reachable.add(id);
763
+ const block = dag.blocks[id];
764
+ if (block) {
765
+ for (const p of block.parents) {
766
+ stack.push(p);
767
+ }
768
+ }
769
+ }
770
+ return Array.from(reachable).map((id) => dag.blocks[id]).filter((b) => b !== void 0);
771
+ }
772
+ function resolveConflictsDeterministically(txs, dag) {
773
+ const sortedTxs = txs.sort((a, b) => {
774
+ const blockA = dag.blocks[a.blockId];
775
+ const blockB = dag.blocks[b.blockId];
776
+ if (!blockA || !blockB) {
777
+ if (!blockA && !blockB) return a.txId.localeCompare(b.txId);
778
+ return !blockA ? 1 : -1;
779
+ }
780
+ const inPathA = dag.selectedPathToSink.includes(a.blockId);
781
+ const inPathB = dag.selectedPathToSink.includes(b.blockId);
782
+ if (inPathA !== inPathB) return inPathA ? -1 : 1;
783
+ const daaA = BigInt(blockA.daaScore);
784
+ const daaB = BigInt(blockB.daaScore);
785
+ if (daaA !== daaB) return daaA < daaB ? -1 : 1;
786
+ if (a.blockId !== b.blockId) return a.blockId.localeCompare(b.blockId);
787
+ return a.txId.localeCompare(b.txId);
788
+ });
789
+ const accepted = [];
790
+ const displaced = [];
791
+ const conflicts = [];
792
+ const spent = /* @__PURE__ */ new Map();
793
+ for (const tx of sortedTxs) {
794
+ let conflictFound = false;
795
+ for (const input of tx.inputs) {
796
+ if (spent.has(input)) {
797
+ const winner = spent.get(input);
798
+ let c = conflicts.find((x) => x.outpoint === input);
799
+ if (!c) {
800
+ c = { outpoint: input, winnerTxId: winner, loserTxIds: [] };
801
+ conflicts.push(c);
802
+ }
803
+ c.loserTxIds.push(tx.txId);
804
+ conflictFound = true;
805
+ break;
806
+ }
807
+ }
808
+ if (conflictFound) {
809
+ displaced.push(tx.txId);
810
+ } else {
811
+ accepted.push(tx.txId);
812
+ for (const input of tx.inputs) {
813
+ spent.set(input, tx.txId);
814
+ }
815
+ }
816
+ }
817
+ return { accepted, displaced, conflicts };
818
+ }
819
+ export {
820
+ DUST_LIMIT_SOMPI,
821
+ SimulatedKaspaChain,
822
+ addSimulatedBlock,
823
+ applySimulatedPayment,
824
+ applySimulatedPlan,
825
+ calculateAccountsHash,
826
+ calculateStateHash,
827
+ calculateUtxoSetHash,
828
+ createDeterministicAccounts,
829
+ createInitialLocalnetState,
830
+ createLocalnetSnapshot,
831
+ createSimulatedDag,
832
+ fundAddress,
833
+ getAccountBalanceSompi,
834
+ getAddressBalanceSompi,
835
+ getDefaultLocalnetDir,
836
+ getDefaultLocalnetStatePath,
837
+ getDefaultReceiptsDir,
838
+ getDefaultTracesDir,
839
+ getReceiptPath,
840
+ getSimulatedReplaySummary,
841
+ getSpendableUtxos,
842
+ getTracePath,
843
+ listSimulatedReceipts,
844
+ listSimulatedTraces,
845
+ loadLocalnetState,
846
+ loadOrCreateLocalnetState,
847
+ loadSimulatedReceipt,
848
+ loadSimulatedTrace,
849
+ moveSink,
850
+ resetLocalnetState,
851
+ resolveAccountAddress,
852
+ resolveAccountAddressFromState,
853
+ resolveConflictsDeterministically,
854
+ restoreLocalnetSnapshot,
855
+ saveLocalnetState,
856
+ saveSimulatedReceipt,
857
+ saveSimulatedTrace,
858
+ startSimulatedDevnet,
859
+ verifyReplay,
860
+ verifySnapshot
861
+ };
@@ -0,0 +1,14 @@
1
+ import {
2
+ getDefaultReceiptsDir,
3
+ getReceiptPath,
4
+ listSimulatedReceipts,
5
+ loadSimulatedReceipt,
6
+ saveSimulatedReceipt
7
+ } from "./chunk-CXDVB3K4.js";
8
+ export {
9
+ getDefaultReceiptsDir,
10
+ getReceiptPath,
11
+ listSimulatedReceipts,
12
+ loadSimulatedReceipt,
13
+ saveSimulatedReceipt
14
+ };
@@ -0,0 +1,14 @@
1
+ import {
2
+ getDefaultTracesDir,
3
+ getTracePath,
4
+ listSimulatedTraces,
5
+ loadSimulatedTrace,
6
+ saveSimulatedTrace
7
+ } from "./chunk-W7ZBGUNK.js";
8
+ export {
9
+ getDefaultTracesDir,
10
+ getTracePath,
11
+ listSimulatedTraces,
12
+ loadSimulatedTrace,
13
+ saveSimulatedTrace
14
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@hardkas/localnet",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@hardkas/core": "0.1.0",
12
+ "@hardkas/query": "0.1.0",
13
+ "@hardkas/artifacts": "0.1.0",
14
+ "@hardkas/tx-builder": "0.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "tsup": "^8.3.5",
18
+ "typescript": "^5.7.2",
19
+ "vitest": "^2.1.8"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Javier Rodriguez",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/jrodrg92/Hardkas.git",
26
+ "directory": "packages/localnet"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/jrodrg92/Hardkas/issues"
30
+ },
31
+ "homepage": "https://github.com/jrodrg92/Hardkas/tree/main/packages/localnet#readme",
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup src/index.ts --format esm --dts --clean --external kaspa",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint ."
42
+ }
43
+ }