@hardkas/toolkit 0.11.1-alpha
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/.turbo/turbo-build.log +4 -0
- package/LICENSE +21 -0
- package/dist/dag/consensus.d.ts +20 -0
- package/dist/dag/consensus.d.ts.map +1 -0
- package/dist/dag/consensus.js +131 -0
- package/dist/dag/consensus.js.map +1 -0
- package/dist/dag/index.d.ts +28 -0
- package/dist/dag/index.d.ts.map +1 -0
- package/dist/dag/index.js +57 -0
- package/dist/dag/index.js.map +1 -0
- package/dist/dag/store.d.ts +11 -0
- package/dist/dag/store.d.ts.map +1 -0
- package/dist/dag/store.js +32 -0
- package/dist/dag/store.js.map +1 -0
- package/dist/dag/topology.d.ts +14 -0
- package/dist/dag/topology.d.ts.map +1 -0
- package/dist/dag/topology.js +98 -0
- package/dist/dag/topology.js.map +1 -0
- package/dist/dag/types.d.ts +21 -0
- package/dist/dag/types.d.ts.map +1 -0
- package/dist/dag/types.js +2 -0
- package/dist/dag/types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +34 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +111 -0
- package/dist/indexer.js.map +1 -0
- package/dist/jobs.d.ts +18 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +33 -0
- package/dist/jobs.js.map +1 -0
- package/dist/payment.d.ts +25 -0
- package/dist/payment.d.ts.map +1 -0
- package/dist/payment.js +66 -0
- package/dist/payment.js.map +1 -0
- package/dist/silver/index.d.ts +20 -0
- package/dist/silver/index.d.ts.map +1 -0
- package/dist/silver/index.js +94 -0
- package/dist/silver/index.js.map +1 -0
- package/dist/silver/templates.d.ts +2 -0
- package/dist/silver/templates.d.ts.map +1 -0
- package/dist/silver/templates.js +28 -0
- package/dist/silver/templates.js.map +1 -0
- package/dist/silver/types.d.ts +34 -0
- package/dist/silver/types.d.ts.map +1 -0
- package/dist/silver/types.js +2 -0
- package/dist/silver/types.js.map +1 -0
- package/dist/snapshot/backends/FsSnapshotBackend.d.ts +14 -0
- package/dist/snapshot/backends/FsSnapshotBackend.d.ts.map +1 -0
- package/dist/snapshot/backends/FsSnapshotBackend.js +82 -0
- package/dist/snapshot/backends/FsSnapshotBackend.js.map +1 -0
- package/dist/snapshot/backends/MemorySnapshotBackend.d.ts +13 -0
- package/dist/snapshot/backends/MemorySnapshotBackend.d.ts.map +1 -0
- package/dist/snapshot/backends/MemorySnapshotBackend.js +55 -0
- package/dist/snapshot/backends/MemorySnapshotBackend.js.map +1 -0
- package/dist/snapshot/index.d.ts +19 -0
- package/dist/snapshot/index.d.ts.map +1 -0
- package/dist/snapshot/index.js +84 -0
- package/dist/snapshot/index.js.map +1 -0
- package/dist/snapshot/serialization.d.ts +18 -0
- package/dist/snapshot/serialization.d.ts.map +1 -0
- package/dist/snapshot/serialization.js +31 -0
- package/dist/snapshot/serialization.js.map +1 -0
- package/dist/snapshot/types.d.ts +44 -0
- package/dist/snapshot/types.d.ts.map +1 -0
- package/dist/snapshot/types.js +2 -0
- package/dist/snapshot/types.js.map +1 -0
- package/dist/stores/invoice-store.d.ts +26 -0
- package/dist/stores/invoice-store.d.ts.map +1 -0
- package/dist/stores/invoice-store.js +34 -0
- package/dist/stores/invoice-store.js.map +1 -0
- package/dist/stores/utxo-control-store.d.ts +23 -0
- package/dist/stores/utxo-control-store.d.ts.map +1 -0
- package/dist/stores/utxo-control-store.js +64 -0
- package/dist/stores/utxo-control-store.js.map +1 -0
- package/dist/utxos.d.ts +128 -0
- package/dist/utxos.d.ts.map +1 -0
- package/dist/utxos.js +331 -0
- package/dist/utxos.js.map +1 -0
- package/dist/wallet.d.ts +34 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +72 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +28 -0
- package/src/dag/consensus.ts +150 -0
- package/src/dag/index.ts +73 -0
- package/src/dag/store.ts +40 -0
- package/src/dag/topology.ts +108 -0
- package/src/dag/types.ts +20 -0
- package/src/index.ts +9 -0
- package/src/indexer.ts +128 -0
- package/src/jobs.ts +46 -0
- package/src/payment.ts +81 -0
- package/src/silver/index.ts +102 -0
- package/src/silver/templates.ts +27 -0
- package/src/silver/types.ts +37 -0
- package/src/snapshot/backends/FsSnapshotBackend.ts +100 -0
- package/src/snapshot/backends/MemorySnapshotBackend.ts +65 -0
- package/src/snapshot/index.ts +102 -0
- package/src/snapshot/serialization.ts +35 -0
- package/src/snapshot/types.ts +45 -0
- package/src/stores/invoice-store.ts +53 -0
- package/src/stores/utxo-control-store.ts +78 -0
- package/src/utxos.ts +451 -0
- package/src/wallet.ts +93 -0
- package/test/silver.test.ts +59 -0
- package/test/snapshot.test.ts +99 -0
- package/test/toolkit.test.ts +109 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SnapshotBackend, SnapshotManifest, SnapshotState } from "../types.js";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
import { snapshotReplacer, snapshotReviver } from "../serialization.js";
|
|
6
|
+
|
|
7
|
+
export class FsSnapshotBackend implements SnapshotBackend {
|
|
8
|
+
private baseDir: string;
|
|
9
|
+
|
|
10
|
+
constructor(baseDir: string = ".hardkas-snapshots") {
|
|
11
|
+
this.baseDir = baseDir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async save(id: string, name: string, state: Record<string, SnapshotState>): Promise<SnapshotManifest> {
|
|
15
|
+
const snapshotDir = path.join(this.baseDir, id);
|
|
16
|
+
await fs.ensureDir(snapshotDir);
|
|
17
|
+
|
|
18
|
+
const stateHashes: Record<string, string> = {};
|
|
19
|
+
const participants = Object.keys(state);
|
|
20
|
+
|
|
21
|
+
// Write each participant's state to a separate file
|
|
22
|
+
for (const [pId, pState] of Object.entries(state)) {
|
|
23
|
+
const data = JSON.stringify(pState, snapshotReplacer, 2);
|
|
24
|
+
stateHashes[pId] = crypto.createHash("sha256").update(data).digest("hex");
|
|
25
|
+
await fs.writeFile(path.join(snapshotDir, `${pId}.json`), data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const manifest: SnapshotManifest = {
|
|
29
|
+
schema: "hardkas.snapshot.v1",
|
|
30
|
+
snapshotId: id,
|
|
31
|
+
name,
|
|
32
|
+
createdAt: Date.now(),
|
|
33
|
+
backend: "filesystem",
|
|
34
|
+
path: snapshotDir,
|
|
35
|
+
participants,
|
|
36
|
+
stateHashes,
|
|
37
|
+
claims: {
|
|
38
|
+
containsFullStateInline: false,
|
|
39
|
+
restorableLocalState: true,
|
|
40
|
+
networkStateCaptured: false
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Write manifest
|
|
45
|
+
await fs.writeFile(path.join(snapshotDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
46
|
+
|
|
47
|
+
return manifest;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async load(id: string): Promise<{ manifest: SnapshotManifest; state: Record<string, SnapshotState> }> {
|
|
51
|
+
const snapshotDir = path.join(this.baseDir, id);
|
|
52
|
+
if (!await fs.pathExists(snapshotDir)) {
|
|
53
|
+
throw new Error(`Snapshot ${id} not found in filesystem backend at ${snapshotDir}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const manifestData = await fs.readFile(path.join(snapshotDir, "manifest.json"), "utf8");
|
|
57
|
+
const manifest: SnapshotManifest = JSON.parse(manifestData);
|
|
58
|
+
|
|
59
|
+
const state: Record<string, SnapshotState> = {};
|
|
60
|
+
for (const pId of manifest.participants) {
|
|
61
|
+
const stateData = await fs.readFile(path.join(snapshotDir, `${pId}.json`), "utf8");
|
|
62
|
+
state[pId] = JSON.parse(stateData, snapshotReviver);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { manifest, state };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async list(): Promise<SnapshotManifest[]> {
|
|
69
|
+
if (!await fs.pathExists(this.baseDir)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entries = await fs.readdir(this.baseDir, { withFileTypes: true });
|
|
74
|
+
const manifests: SnapshotManifest[] = [];
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
const manifestPath = path.join(this.baseDir, entry.name, "manifest.json");
|
|
79
|
+
if (await fs.pathExists(manifestPath)) {
|
|
80
|
+
const data = await fs.readFile(manifestPath, "utf8");
|
|
81
|
+
manifests.push(JSON.parse(data));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return manifests;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(id: string): Promise<void> {
|
|
90
|
+
const snapshotDir = path.join(this.baseDir, id);
|
|
91
|
+
if (await fs.pathExists(snapshotDir)) {
|
|
92
|
+
await fs.remove(snapshotDir);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async branch(baseId: string, newId: string, newName: string): Promise<SnapshotManifest> {
|
|
97
|
+
const { state } = await this.load(baseId);
|
|
98
|
+
return this.save(newId, newName, state);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SnapshotBackend, SnapshotManifest, SnapshotState } from "../types.js";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { snapshotReplacer, snapshotReviver } from "../serialization.js";
|
|
4
|
+
|
|
5
|
+
export class MemorySnapshotBackend implements SnapshotBackend {
|
|
6
|
+
private snapshots = new Map<string, { manifest: SnapshotManifest; state: Record<string, SnapshotState> }>();
|
|
7
|
+
|
|
8
|
+
async save(id: string, name: string, state: Record<string, SnapshotState>): Promise<SnapshotManifest> {
|
|
9
|
+
const stateHashes: Record<string, string> = {};
|
|
10
|
+
const participants = Object.keys(state);
|
|
11
|
+
|
|
12
|
+
for (const [pId, pState] of Object.entries(state)) {
|
|
13
|
+
stateHashes[pId] = crypto.createHash("sha256").update(JSON.stringify(pState, snapshotReplacer)).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const manifest: SnapshotManifest = {
|
|
17
|
+
schema: "hardkas.snapshot.v1",
|
|
18
|
+
snapshotId: id,
|
|
19
|
+
name,
|
|
20
|
+
createdAt: Date.now(),
|
|
21
|
+
backend: "memory",
|
|
22
|
+
participants,
|
|
23
|
+
stateHashes,
|
|
24
|
+
claims: {
|
|
25
|
+
containsFullStateInline: true,
|
|
26
|
+
restorableLocalState: true,
|
|
27
|
+
networkStateCaptured: false
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Deep copy state to ensure isolation
|
|
32
|
+
const deepCopiedState = JSON.parse(JSON.stringify(state, snapshotReplacer), snapshotReviver);
|
|
33
|
+
this.snapshots.set(id, { manifest, state: deepCopiedState });
|
|
34
|
+
|
|
35
|
+
return manifest;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async load(id: string): Promise<{ manifest: SnapshotManifest; state: Record<string, SnapshotState> }> {
|
|
39
|
+
const entry = this.snapshots.get(id);
|
|
40
|
+
if (!entry) {
|
|
41
|
+
throw new Error(`Snapshot ${id} not found in memory backend`);
|
|
42
|
+
}
|
|
43
|
+
// Return a deep copy to prevent mutation of the snapshot itself
|
|
44
|
+
return {
|
|
45
|
+
manifest: JSON.parse(JSON.stringify(entry.manifest)),
|
|
46
|
+
state: JSON.parse(JSON.stringify(entry.state, snapshotReplacer), snapshotReviver)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async list(): Promise<SnapshotManifest[]> {
|
|
51
|
+
return Array.from(this.snapshots.values()).map(s => s.manifest);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(id: string): Promise<void> {
|
|
55
|
+
this.snapshots.delete(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async branch(baseId: string, newId: string, newName: string): Promise<SnapshotManifest> {
|
|
59
|
+
const entry = this.snapshots.get(baseId);
|
|
60
|
+
if (!entry) throw new Error(`Base snapshot ${baseId} not found`);
|
|
61
|
+
|
|
62
|
+
const newState = JSON.parse(JSON.stringify(entry.state, snapshotReplacer), snapshotReviver);
|
|
63
|
+
return this.save(newId, newName, newState);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SnapshotBackend, SnapshotManifest, SnapshotParticipant, SnapshotState } from "./types.js";
|
|
2
|
+
import { MemorySnapshotBackend } from "./backends/MemorySnapshotBackend.js";
|
|
3
|
+
import { FsSnapshotBackend } from "./backends/FsSnapshotBackend.js";
|
|
4
|
+
import crypto from "crypto";
|
|
5
|
+
|
|
6
|
+
export interface SnapshotToolkitOptions {
|
|
7
|
+
backend?: "memory" | "filesystem";
|
|
8
|
+
dir?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SnapshotToolkit {
|
|
12
|
+
private backend: SnapshotBackend;
|
|
13
|
+
private participants = new Map<string, SnapshotParticipant>();
|
|
14
|
+
|
|
15
|
+
private constructor(options: SnapshotToolkitOptions) {
|
|
16
|
+
if (options.backend === "filesystem") {
|
|
17
|
+
this.backend = new FsSnapshotBackend(options.dir);
|
|
18
|
+
} else {
|
|
19
|
+
this.backend = new MemorySnapshotBackend();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static open(options: SnapshotToolkitOptions = { backend: "memory" }): SnapshotToolkit {
|
|
24
|
+
return new SnapshotToolkit(options);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public register(id: string, participant: SnapshotParticipant): void {
|
|
28
|
+
this.participants.set(id, participant);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async create(name: string): Promise<SnapshotManifest> {
|
|
32
|
+
const id = crypto.randomUUID();
|
|
33
|
+
const state: Record<string, SnapshotState> = {};
|
|
34
|
+
|
|
35
|
+
// Snapshot all participants
|
|
36
|
+
for (const [pId, participant] of this.participants.entries()) {
|
|
37
|
+
state[pId] = await participant.snapshot();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.backend.save(id, name, state);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async restore(id: string): Promise<void> {
|
|
44
|
+
const { state } = await this.backend.load(id);
|
|
45
|
+
|
|
46
|
+
// 1. load state & restore (but don't reload yet in case something fails)
|
|
47
|
+
for (const [pId, pState] of Object.entries(state)) {
|
|
48
|
+
const participant = this.participants.get(pId);
|
|
49
|
+
if (!participant) {
|
|
50
|
+
throw new Error(`Cannot restore state for participant '${pId}' because it is not registered`);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await participant.restore(pState);
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
throw new Error(`Failed to restore participant '${pId}': ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. reload all
|
|
60
|
+
for (const [pId, _] of Object.entries(state)) {
|
|
61
|
+
const participant = this.participants.get(pId)!;
|
|
62
|
+
try {
|
|
63
|
+
await participant.reload();
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
throw new Error(`Failed to reload participant '${pId}' after restoration: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public async branch(baseId: string, newName: string): Promise<SnapshotManifest> {
|
|
71
|
+
const newId = crypto.randomUUID();
|
|
72
|
+
return this.backend.branch(baseId, newId, newName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public async diff(idA: string, idB: string): Promise<Record<string, any>> {
|
|
76
|
+
const [a, b] = await Promise.all([this.backend.load(idA), this.backend.load(idB)]);
|
|
77
|
+
|
|
78
|
+
const differences: Record<string, any> = {};
|
|
79
|
+
const allParticipants = new Set([...a.manifest.participants, ...b.manifest.participants]);
|
|
80
|
+
|
|
81
|
+
for (const pId of allParticipants) {
|
|
82
|
+
const hashA = a.manifest.stateHashes[pId];
|
|
83
|
+
const hashB = b.manifest.stateHashes[pId];
|
|
84
|
+
if (hashA !== hashB) {
|
|
85
|
+
differences[pId] = {
|
|
86
|
+
a: a.state[pId] || null,
|
|
87
|
+
b: b.state[pId] || null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return differences;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async compare(idA: string, idB: string): Promise<boolean> {
|
|
95
|
+
const diff = await this.diff(idA, idB);
|
|
96
|
+
return Object.keys(diff).length === 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async list(): Promise<SnapshotManifest[]> {
|
|
100
|
+
return this.backend.list();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SnapshotState } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom JSON Replacer to safely serialize BigInt values.
|
|
5
|
+
*/
|
|
6
|
+
export function snapshotReplacer(key: string, value: any): any {
|
|
7
|
+
if (typeof value === "bigint") {
|
|
8
|
+
return { $bigint: value.toString() };
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom JSON Reviver to safely deserialize BigInt values.
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotReviver(key: string, value: any): any {
|
|
17
|
+
if (value !== null && typeof value === "object" && typeof value.$bigint === "string") {
|
|
18
|
+
return BigInt(value.$bigint);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Serializes a SnapshotState map to a string safely supporting BigInt.
|
|
25
|
+
*/
|
|
26
|
+
export function serializeState(state: Record<string, SnapshotState>): string {
|
|
27
|
+
return JSON.stringify(state, snapshotReplacer, 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deserializes a SnapshotState map from a string safely supporting BigInt.
|
|
32
|
+
*/
|
|
33
|
+
export function deserializeState(data: string): Record<string, SnapshotState> {
|
|
34
|
+
return JSON.parse(data, snapshotReviver);
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface SnapshotState {
|
|
2
|
+
[key: string]: any;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface SnapshotParticipant {
|
|
6
|
+
/**
|
|
7
|
+
* Extracts the current internal state of the toolkit/store
|
|
8
|
+
*/
|
|
9
|
+
snapshot(): Promise<SnapshotState>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Restores the internal state from the provided snapshot state
|
|
13
|
+
*/
|
|
14
|
+
restore(state: SnapshotState): Promise<void>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reloads any caches, queues, or in-memory representations
|
|
18
|
+
* based on the newly restored state
|
|
19
|
+
*/
|
|
20
|
+
reload(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SnapshotManifest {
|
|
24
|
+
schema: "hardkas.snapshot.v1";
|
|
25
|
+
snapshotId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
backend: "memory" | "filesystem";
|
|
29
|
+
path?: string;
|
|
30
|
+
participants: string[];
|
|
31
|
+
stateHashes: Record<string, string>;
|
|
32
|
+
claims: {
|
|
33
|
+
containsFullStateInline: boolean;
|
|
34
|
+
restorableLocalState: boolean;
|
|
35
|
+
networkStateCaptured: boolean;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SnapshotBackend {
|
|
40
|
+
save(id: string, name: string, state: Record<string, SnapshotState>): Promise<SnapshotManifest>;
|
|
41
|
+
load(id: string): Promise<{ manifest: SnapshotManifest; state: Record<string, SnapshotState> }>;
|
|
42
|
+
list(): Promise<SnapshotManifest[]>;
|
|
43
|
+
delete(id: string): Promise<void>;
|
|
44
|
+
branch(baseId: string, newId: string, newName: string): Promise<SnapshotManifest>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DomainStoreJson } from '@hardkas/query-store';
|
|
2
|
+
|
|
3
|
+
export interface InvoiceRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
merchantId: string;
|
|
6
|
+
amount: bigint;
|
|
7
|
+
currency: string;
|
|
8
|
+
status: 'pending' | 'paid' | 'cancelled';
|
|
9
|
+
uri: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class InvoiceStoreJson {
|
|
15
|
+
private store: DomainStoreJson<InvoiceRecord>;
|
|
16
|
+
|
|
17
|
+
constructor(options: { filePath: string }) {
|
|
18
|
+
this.store = new DomainStoreJson<InvoiceRecord>(options);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public save(invoice: InvoiceRecord): void {
|
|
22
|
+
invoice.updatedAt = new Date().toISOString();
|
|
23
|
+
this.store.save(invoice.id, invoice);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public get(id: string): InvoiceRecord | undefined {
|
|
27
|
+
return this.store.get(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public list(): InvoiceRecord[] {
|
|
31
|
+
return this.store.list();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public listByMerchant(merchantId: string): InvoiceRecord[] {
|
|
35
|
+
return this.store.list().filter(i => i.merchantId === merchantId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public listPaid(): InvoiceRecord[] {
|
|
39
|
+
return this.store.list().filter(i => i.status === 'paid');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public stats(merchantId?: string) {
|
|
43
|
+
let invoices = this.store.list();
|
|
44
|
+
if (merchantId) {
|
|
45
|
+
invoices = invoices.filter(i => i.merchantId === merchantId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
totalInvoices: invoices.length,
|
|
50
|
+
paidInvoices: invoices.filter(i => i.status === 'paid').length
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface UtxoControlState {
|
|
5
|
+
frozen: Record<string, { reason?: string | undefined; timestamp: number }>;
|
|
6
|
+
labels: Record<string, string>;
|
|
7
|
+
notes: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class UtxoControlStore {
|
|
11
|
+
private filePath: string;
|
|
12
|
+
private state: UtxoControlState;
|
|
13
|
+
|
|
14
|
+
constructor(filePath: string) {
|
|
15
|
+
this.filePath = filePath;
|
|
16
|
+
this.state = this.load();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private load(): UtxoControlState {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(this.filePath)) {
|
|
22
|
+
const data = fs.readFileSync(this.filePath, 'utf-8');
|
|
23
|
+
return JSON.parse(data) as UtxoControlState;
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Ignore read errors, initialize default
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
frozen: {},
|
|
30
|
+
labels: {},
|
|
31
|
+
notes: {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private save(): void {
|
|
36
|
+
try {
|
|
37
|
+
const dir = path.dirname(this.filePath);
|
|
38
|
+
if (!fs.existsSync(dir)) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.warn(`Failed to save UTXO control state to ${this.filePath}`, e);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public getState(): UtxoControlState {
|
|
48
|
+
return JSON.parse(JSON.stringify(this.state)); // Deep copy
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public freeze(utxoId: string, reason?: string): void {
|
|
52
|
+
this.state.frozen[utxoId] = { reason, timestamp: Date.now() };
|
|
53
|
+
this.save();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public unfreeze(utxoId: string): void {
|
|
57
|
+
delete this.state.frozen[utxoId];
|
|
58
|
+
this.save();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public setLabel(utxoId: string, label: string): void {
|
|
62
|
+
this.state.labels[utxoId] = label;
|
|
63
|
+
this.save();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getLabel(utxoId: string): string | undefined {
|
|
67
|
+
return this.state.labels[utxoId];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public setNote(utxoId: string, note: string): void {
|
|
71
|
+
this.state.notes[utxoId] = note;
|
|
72
|
+
this.save();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public getNote(utxoId: string): string | undefined {
|
|
76
|
+
return this.state.notes[utxoId];
|
|
77
|
+
}
|
|
78
|
+
}
|