@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.
Files changed (113) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +21 -0
  3. package/dist/dag/consensus.d.ts +20 -0
  4. package/dist/dag/consensus.d.ts.map +1 -0
  5. package/dist/dag/consensus.js +131 -0
  6. package/dist/dag/consensus.js.map +1 -0
  7. package/dist/dag/index.d.ts +28 -0
  8. package/dist/dag/index.d.ts.map +1 -0
  9. package/dist/dag/index.js +57 -0
  10. package/dist/dag/index.js.map +1 -0
  11. package/dist/dag/store.d.ts +11 -0
  12. package/dist/dag/store.d.ts.map +1 -0
  13. package/dist/dag/store.js +32 -0
  14. package/dist/dag/store.js.map +1 -0
  15. package/dist/dag/topology.d.ts +14 -0
  16. package/dist/dag/topology.d.ts.map +1 -0
  17. package/dist/dag/topology.js +98 -0
  18. package/dist/dag/topology.js.map +1 -0
  19. package/dist/dag/types.d.ts +21 -0
  20. package/dist/dag/types.d.ts.map +1 -0
  21. package/dist/dag/types.js +2 -0
  22. package/dist/dag/types.js.map +1 -0
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +10 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/indexer.d.ts +34 -0
  28. package/dist/indexer.d.ts.map +1 -0
  29. package/dist/indexer.js +111 -0
  30. package/dist/indexer.js.map +1 -0
  31. package/dist/jobs.d.ts +18 -0
  32. package/dist/jobs.d.ts.map +1 -0
  33. package/dist/jobs.js +33 -0
  34. package/dist/jobs.js.map +1 -0
  35. package/dist/payment.d.ts +25 -0
  36. package/dist/payment.d.ts.map +1 -0
  37. package/dist/payment.js +66 -0
  38. package/dist/payment.js.map +1 -0
  39. package/dist/silver/index.d.ts +20 -0
  40. package/dist/silver/index.d.ts.map +1 -0
  41. package/dist/silver/index.js +94 -0
  42. package/dist/silver/index.js.map +1 -0
  43. package/dist/silver/templates.d.ts +2 -0
  44. package/dist/silver/templates.d.ts.map +1 -0
  45. package/dist/silver/templates.js +28 -0
  46. package/dist/silver/templates.js.map +1 -0
  47. package/dist/silver/types.d.ts +34 -0
  48. package/dist/silver/types.d.ts.map +1 -0
  49. package/dist/silver/types.js +2 -0
  50. package/dist/silver/types.js.map +1 -0
  51. package/dist/snapshot/backends/FsSnapshotBackend.d.ts +14 -0
  52. package/dist/snapshot/backends/FsSnapshotBackend.d.ts.map +1 -0
  53. package/dist/snapshot/backends/FsSnapshotBackend.js +82 -0
  54. package/dist/snapshot/backends/FsSnapshotBackend.js.map +1 -0
  55. package/dist/snapshot/backends/MemorySnapshotBackend.d.ts +13 -0
  56. package/dist/snapshot/backends/MemorySnapshotBackend.d.ts.map +1 -0
  57. package/dist/snapshot/backends/MemorySnapshotBackend.js +55 -0
  58. package/dist/snapshot/backends/MemorySnapshotBackend.js.map +1 -0
  59. package/dist/snapshot/index.d.ts +19 -0
  60. package/dist/snapshot/index.d.ts.map +1 -0
  61. package/dist/snapshot/index.js +84 -0
  62. package/dist/snapshot/index.js.map +1 -0
  63. package/dist/snapshot/serialization.d.ts +18 -0
  64. package/dist/snapshot/serialization.d.ts.map +1 -0
  65. package/dist/snapshot/serialization.js +31 -0
  66. package/dist/snapshot/serialization.js.map +1 -0
  67. package/dist/snapshot/types.d.ts +44 -0
  68. package/dist/snapshot/types.d.ts.map +1 -0
  69. package/dist/snapshot/types.js +2 -0
  70. package/dist/snapshot/types.js.map +1 -0
  71. package/dist/stores/invoice-store.d.ts +26 -0
  72. package/dist/stores/invoice-store.d.ts.map +1 -0
  73. package/dist/stores/invoice-store.js +34 -0
  74. package/dist/stores/invoice-store.js.map +1 -0
  75. package/dist/stores/utxo-control-store.d.ts +23 -0
  76. package/dist/stores/utxo-control-store.d.ts.map +1 -0
  77. package/dist/stores/utxo-control-store.js +64 -0
  78. package/dist/stores/utxo-control-store.js.map +1 -0
  79. package/dist/utxos.d.ts +128 -0
  80. package/dist/utxos.d.ts.map +1 -0
  81. package/dist/utxos.js +331 -0
  82. package/dist/utxos.js.map +1 -0
  83. package/dist/wallet.d.ts +34 -0
  84. package/dist/wallet.d.ts.map +1 -0
  85. package/dist/wallet.js +72 -0
  86. package/dist/wallet.js.map +1 -0
  87. package/package.json +28 -0
  88. package/src/dag/consensus.ts +150 -0
  89. package/src/dag/index.ts +73 -0
  90. package/src/dag/store.ts +40 -0
  91. package/src/dag/topology.ts +108 -0
  92. package/src/dag/types.ts +20 -0
  93. package/src/index.ts +9 -0
  94. package/src/indexer.ts +128 -0
  95. package/src/jobs.ts +46 -0
  96. package/src/payment.ts +81 -0
  97. package/src/silver/index.ts +102 -0
  98. package/src/silver/templates.ts +27 -0
  99. package/src/silver/types.ts +37 -0
  100. package/src/snapshot/backends/FsSnapshotBackend.ts +100 -0
  101. package/src/snapshot/backends/MemorySnapshotBackend.ts +65 -0
  102. package/src/snapshot/index.ts +102 -0
  103. package/src/snapshot/serialization.ts +35 -0
  104. package/src/snapshot/types.ts +45 -0
  105. package/src/stores/invoice-store.ts +53 -0
  106. package/src/stores/utxo-control-store.ts +78 -0
  107. package/src/utxos.ts +451 -0
  108. package/src/wallet.ts +93 -0
  109. package/test/silver.test.ts +59 -0
  110. package/test/snapshot.test.ts +99 -0
  111. package/test/toolkit.test.ts +109 -0
  112. package/tsconfig.json +9 -0
  113. 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
+ }