@hardkas/testing 0.2.2-alpha → 0.2.2-alpha.1

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/index.d.ts CHANGED
@@ -1,25 +1,69 @@
1
- import { MockKaspaRpcClient } from '@hardkas/kaspa-rpc';
1
+ import { Hardkas } from '@hardkas/sdk';
2
+ import { CoreEvent, NetworkId } from '@hardkas/core';
3
+ import { BaseArtifact } from '@hardkas/artifacts';
2
4
 
3
- interface TestWallet {
4
- readonly name: string;
5
- readonly address: string;
5
+ /**
6
+ * System Invariant Verifiers
7
+ *
8
+ * Instead of just unit tests, these provide "System Guarantees".
9
+ */
10
+ declare const Invariants: {
11
+ /**
12
+ * Guarantee: An artifact's content hash MUST match its current state.
13
+ */
14
+ verifyArtifactIntegrity(artifact: BaseArtifact<any>, calculateHash: (a: any) => string): Promise<boolean>;
15
+ /**
16
+ * Guarantee: A Lineage chain MUST be strictly monotonic and linked.
17
+ */
18
+ verifyLineageContinuity(artifacts: BaseArtifact<any>[]): boolean;
19
+ /**
20
+ * Guarantee: Every emission to the event bus MUST have a timestamp.
21
+ */
22
+ verifyEventCompliance(event: CoreEvent): boolean;
23
+ };
24
+ /**
25
+ * Invariant Watcher
26
+ * Can be hooked into the event bus to detect violations in real-time.
27
+ */
28
+ declare class InvariantWatcher {
29
+ private violations;
30
+ constructor();
31
+ getViolations(): string[];
32
+ hasViolations(): boolean;
6
33
  }
7
- interface HardkasTestContext {
8
- readonly wallets: {
9
- readonly alice: TestWallet;
10
- readonly bob: TestWallet;
11
- readonly carol: TestWallet;
12
- readonly faucet: TestWallet;
13
- };
14
- readonly rpc: MockKaspaRpcClient;
15
- readonly faucet: {
16
- fund(address: string, amountSompi: bigint): Promise<void>;
17
- };
18
- reset(): Promise<void>;
34
+
35
+ interface FuzzResult {
36
+ ok: boolean;
37
+ iterations: number;
38
+ violations: string[];
19
39
  }
20
- declare function createHardkasTestContext(): Promise<HardkasTestContext>;
21
- declare const hardkas: {
22
- localnet: typeof createHardkasTestContext;
23
- };
40
+ /**
41
+ * Custom Scenario Fuzzer for UTXO Invariants.
42
+ * Verifies that sum(inputs) == sum(outputs) + fee across random transaction sequences.
43
+ */
44
+ declare function runUtxoFuzzer(iterations?: number): Promise<FuzzResult>;
45
+
46
+ /**
47
+ * HardKAS Testing Runtime
48
+ */
49
+ interface HardkasTestRuntime {
50
+ readonly hardkas: Hardkas;
51
+ readonly network: NetworkId;
52
+ readonly accounts: Hardkas["accounts"];
53
+ readonly tx: Hardkas["tx"];
54
+ readonly localnet: Hardkas["localnet"];
55
+ readonly query: Hardkas["query"];
56
+ }
57
+ interface HardkasTestOptions {
58
+ cwd?: string;
59
+ network?: NetworkId;
60
+ autoStartLocalnet?: boolean;
61
+ resetBetweenTests?: boolean;
62
+ }
63
+ /**
64
+ * The main helper for HardKAS tests.
65
+ * Injects hooks for Vitest and provides access to the SDK.
66
+ */
67
+ declare function hardkasTest(options?: HardkasTestOptions): HardkasTestRuntime;
24
68
 
25
- export { type HardkasTestContext, type TestWallet, createHardkasTestContext, hardkas };
69
+ export { type FuzzResult, type HardkasTestOptions, type HardkasTestRuntime, InvariantWatcher, Invariants, hardkasTest, runUtxoFuzzer };
package/dist/index.js CHANGED
@@ -1,50 +1,170 @@
1
1
  // src/index.ts
2
- import { MockKaspaRpcClient } from "@hardkas/kaspa-rpc";
3
- import { createDeterministicAccounts } from "@hardkas/localnet";
4
- import { createMockUtxo } from "@hardkas/tx-builder";
5
- async function createHardkasTestContext() {
6
- const rpc = new MockKaspaRpcClient("simnet");
7
- const accounts = createDeterministicAccounts({
8
- count: 4,
9
- initialBalanceSompi: 0n
10
- });
11
- const alice = accounts[0];
12
- const bob = accounts[1];
13
- const carol = accounts[2];
14
- if (!alice || !bob || !carol) {
15
- throw new Error("Failed to create deterministic test accounts.");
2
+ import { Hardkas } from "@hardkas/sdk";
3
+ import { beforeAll, beforeEach } from "vitest";
4
+
5
+ // src/invariants.ts
6
+ import { coreEvents } from "@hardkas/core";
7
+ var Invariants = {
8
+ /**
9
+ * Guarantee: An artifact's content hash MUST match its current state.
10
+ */
11
+ async verifyArtifactIntegrity(artifact, calculateHash) {
12
+ if (!artifact.contentHash) return true;
13
+ const actual = calculateHash(artifact);
14
+ return artifact.contentHash === actual;
15
+ },
16
+ /**
17
+ * Guarantee: A Lineage chain MUST be strictly monotonic and linked.
18
+ */
19
+ verifyLineageContinuity(artifacts) {
20
+ if (artifacts.length < 2) return true;
21
+ const sorted = [...artifacts].sort(
22
+ (a, b) => (a.lineage?.sequence ?? 0) - (b.lineage?.sequence ?? 0)
23
+ );
24
+ for (let i = 1; i < sorted.length; i++) {
25
+ const prev = sorted[i - 1];
26
+ const curr = sorted[i];
27
+ if (!prev || !curr) continue;
28
+ if (curr.lineage?.parentArtifactId !== prev.lineage?.artifactId) {
29
+ return false;
30
+ }
31
+ if (curr.lineage?.lineageId !== prev.lineage?.lineageId) {
32
+ return false;
33
+ }
34
+ }
35
+ return true;
36
+ },
37
+ /**
38
+ * Guarantee: Every emission to the event bus MUST have a timestamp.
39
+ */
40
+ verifyEventCompliance(event) {
41
+ return !!event.timestamp;
42
+ }
43
+ };
44
+ var InvariantWatcher = class {
45
+ violations = [];
46
+ constructor() {
47
+ coreEvents.on((event) => {
48
+ if (!Invariants.verifyEventCompliance(event)) {
49
+ this.violations.push(`Event compliance violation: ${event.kind}`);
50
+ }
51
+ if (event.kind === "integrity.hash_mismatch") {
52
+ const payload = event.payload;
53
+ this.violations.push(`Hash mismatch detected: ${payload.artifactId || event.artifactId}`);
54
+ }
55
+ });
16
56
  }
17
- const wallets = {
18
- alice: { name: alice.name, address: alice.address },
19
- bob: { name: bob.name, address: bob.address },
20
- carol: { name: carol.name, address: carol.address },
21
- faucet: { name: "faucet", address: "kaspa:sim_faucet" }
57
+ getViolations() {
58
+ return this.violations;
59
+ }
60
+ hasViolations() {
61
+ return this.violations.length > 0;
62
+ }
63
+ };
64
+
65
+ // src/utxo-fuzzer.ts
66
+ import { SOMPI_PER_KAS } from "@hardkas/core";
67
+ import { buildPaymentPlan } from "@hardkas/tx-builder";
68
+ import { applySimulatedPayment, createInitialLocalnetState } from "@hardkas/localnet";
69
+ async function runUtxoFuzzer(iterations = 50) {
70
+ let state = createInitialLocalnetState({ accounts: 5, initialBalanceSompi: 1000n * SOMPI_PER_KAS });
71
+ const violations = [];
72
+ for (let i = 0; i < iterations; i++) {
73
+ const fromIdx = Math.floor(Math.random() * state.accounts.length);
74
+ let toIdx = Math.floor(Math.random() * state.accounts.length);
75
+ if (fromIdx === toIdx) toIdx = (toIdx + 1) % state.accounts.length;
76
+ const fromAccount = state.accounts[fromIdx];
77
+ const toAccount = state.accounts[toIdx];
78
+ const amountSompi = BigInt(Math.floor(Math.random() * 10)) * SOMPI_PER_KAS + BigInt(Math.floor(Math.random() * 1e6));
79
+ try {
80
+ const unspent = state.utxos.filter((u) => u.address === fromAccount.address && !u.spent);
81
+ if (unspent.length === 0) continue;
82
+ const builderUtxos = unspent.map((u) => ({
83
+ outpoint: { transactionId: u.id.split(":")[0], index: 0 },
84
+ address: u.address,
85
+ amountSompi: BigInt(u.amountSompi),
86
+ scriptPublicKey: "mock"
87
+ }));
88
+ const plan = buildPaymentPlan({
89
+ fromAddress: fromAccount.address,
90
+ availableUtxos: builderUtxos,
91
+ outputs: [{ address: toAccount.address, amountSompi }],
92
+ feeRateSompiPerMass: 1n
93
+ });
94
+ const inputSum = plan.inputs.reduce((s, x) => s + x.amountSompi, 0n);
95
+ const outputSum = plan.outputs.reduce((s, x) => s + x.amountSompi, 0n) + (plan.change?.amountSompi || 0n);
96
+ const fee = plan.estimatedFeeSompi;
97
+ if (inputSum !== outputSum + fee) {
98
+ violations.push(`Iteration ${i}: Planning Invariant Violated! ${inputSum} != ${outputSum} + ${fee}`);
99
+ }
100
+ const result = applySimulatedPayment(state, {
101
+ from: fromAccount.name,
102
+ to: toAccount.name,
103
+ amountSompi
104
+ });
105
+ state = result.state;
106
+ const totalInState = state.utxos.filter((u) => !u.spent).reduce((s, x) => s + BigInt(x.amountSompi), 0n);
107
+ const expectedTotal = BigInt(state.accounts.length) * 1000n * SOMPI_PER_KAS - BigInt(i + 1) * fee;
108
+ const utxoIds = state.utxos.map((u) => u.id);
109
+ const uniqueIds = new Set(utxoIds);
110
+ if (utxoIds.length !== uniqueIds.size) {
111
+ violations.push(`Iteration ${i}: Duplicate UTXO IDs detected in state!`);
112
+ }
113
+ } catch (e) {
114
+ if (!e.message.includes("Insufficient funds")) {
115
+ violations.push(`Iteration ${i}: Unexpected Error: ${e.message}`);
116
+ }
117
+ }
118
+ }
119
+ return {
120
+ ok: violations.length === 0,
121
+ iterations,
122
+ violations
22
123
  };
124
+ }
125
+
126
+ // src/index.ts
127
+ function hardkasTest(options = {}) {
128
+ const cwd = options.cwd || process.env.HARDKAS_CWD || ".";
129
+ const network = options.network || process.env.HARDKAS_NETWORK || "simnet";
130
+ const autoStart = options.autoStartLocalnet ?? true;
131
+ const autoReset = options.resetBetweenTests ?? true;
132
+ let sdk;
133
+ beforeAll(async () => {
134
+ sdk = await Hardkas.open(cwd);
135
+ if (autoStart && network === "simnet") {
136
+ await sdk.localnet.start();
137
+ }
138
+ });
139
+ beforeEach(async () => {
140
+ if (autoReset && network === "simnet") {
141
+ await sdk.localnet.reset();
142
+ }
143
+ });
23
144
  return {
24
- wallets,
25
- rpc,
26
- faucet: {
27
- async fund(address, amountSompi) {
28
- rpc.setUtxos(address, [
29
- createMockUtxo({
30
- address,
31
- amountSompi,
32
- index: 0
33
- })
34
- ]);
35
- }
145
+ get hardkas() {
146
+ return sdk;
147
+ },
148
+ get network() {
149
+ return network;
150
+ },
151
+ get accounts() {
152
+ return sdk.accounts;
36
153
  },
37
- async reset() {
38
- rpc.setUtxos(wallets.alice.address, []);
39
- rpc.setUtxos(wallets.bob.address, []);
40
- rpc.setUtxos(wallets.carol.address, []);
154
+ get tx() {
155
+ return sdk.tx;
156
+ },
157
+ get localnet() {
158
+ return sdk.localnet;
159
+ },
160
+ get query() {
161
+ return sdk.query;
41
162
  }
42
163
  };
43
164
  }
44
- var hardkas = {
45
- localnet: createHardkasTestContext
46
- };
47
165
  export {
48
- createHardkasTestContext,
49
- hardkas
166
+ InvariantWatcher,
167
+ Invariants,
168
+ hardkasTest,
169
+ runUtxoFuzzer
50
170
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardkas/testing",
3
- "version": "0.2.2-alpha",
3
+ "version": "0.2.2-alpha.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -8,12 +8,13 @@
8
8
  ".": "./dist/index.js"
9
9
  },
10
10
  "dependencies": {
11
- "@hardkas/artifacts": "0.2.2-alpha",
12
- "@hardkas/core": "0.2.2-alpha",
13
- "@hardkas/kaspa-rpc": "0.2.2-alpha",
14
- "@hardkas/simulator": "0.2.2-alpha",
15
- "@hardkas/localnet": "0.2.2-alpha",
16
- "@hardkas/tx-builder": "0.2.2-alpha"
11
+ "@hardkas/artifacts": "0.2.2-alpha.1",
12
+ "@hardkas/sdk": "0.2.2-alpha.1",
13
+ "@hardkas/kaspa-rpc": "0.2.2-alpha.1",
14
+ "@hardkas/core": "0.2.2-alpha.1",
15
+ "@hardkas/localnet": "0.2.2-alpha.1",
16
+ "@hardkas/simulator": "0.2.2-alpha.1",
17
+ "@hardkas/tx-builder": "0.2.2-alpha.1"
17
18
  },
18
19
  "devDependencies": {
19
20
  "tsup": "^8.3.5",
@@ -37,7 +38,7 @@
37
38
  "README.md"
38
39
  ],
39
40
  "scripts": {
40
- "build": "tsup src/index.ts --format esm --dts --clean",
41
+ "build": "tsup src/index.ts --format esm --dts --clean --external vitest",
41
42
  "test": "vitest run",
42
43
  "typecheck": "tsc --noEmit",
43
44
  "lint": "eslint ."