@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 +65 -21
- package/dist/index.js +160 -40
- package/package.json +9 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,69 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Hardkas } from '@hardkas/sdk';
|
|
2
|
+
import { CoreEvent, NetworkId } from '@hardkas/core';
|
|
3
|
+
import { BaseArtifact } from '@hardkas/artifacts';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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/
|
|
13
|
-
"@hardkas/kaspa-rpc": "0.2.2-alpha",
|
|
14
|
-
"@hardkas/
|
|
15
|
-
"@hardkas/localnet": "0.2.2-alpha",
|
|
16
|
-
"@hardkas/
|
|
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 ."
|