@sundaeswap/sprinkles 0.3.0 → 0.5.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.
Files changed (88) hide show
  1. package/dist/cjs/Sprinkle/__tests__/encryption.test.js +20 -8
  2. package/dist/cjs/Sprinkle/__tests__/encryption.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +41 -16
  4. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  5. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
  6. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  7. package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js +120 -0
  8. package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
  9. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +93 -7
  10. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  11. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js +21 -0
  12. package/dist/cjs/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  13. package/dist/cjs/Sprinkle/encryption.js +131 -0
  14. package/dist/cjs/Sprinkle/encryption.js.map +1 -0
  15. package/dist/cjs/Sprinkle/index.js +318 -352
  16. package/dist/cjs/Sprinkle/index.js.map +1 -1
  17. package/dist/cjs/Sprinkle/prompts.js +393 -0
  18. package/dist/cjs/Sprinkle/prompts.js.map +1 -0
  19. package/dist/cjs/Sprinkle/schemas.js +97 -0
  20. package/dist/cjs/Sprinkle/schemas.js.map +1 -0
  21. package/dist/cjs/Sprinkle/tx-dialog.js +101 -0
  22. package/dist/cjs/Sprinkle/tx-dialog.js.map +1 -0
  23. package/dist/cjs/Sprinkle/type-guards.js +42 -0
  24. package/dist/cjs/Sprinkle/type-guards.js.map +1 -0
  25. package/dist/cjs/Sprinkle/types.js +49 -0
  26. package/dist/cjs/Sprinkle/types.js.map +1 -0
  27. package/dist/cjs/Sprinkle/wallet.js +98 -0
  28. package/dist/cjs/Sprinkle/wallet.js.map +1 -0
  29. package/dist/esm/Sprinkle/__tests__/encryption.test.js +20 -8
  30. package/dist/esm/Sprinkle/__tests__/encryption.test.js.map +1 -1
  31. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +41 -16
  32. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  33. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +85 -38
  34. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  35. package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js +120 -0
  36. package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
  37. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +94 -8
  38. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  39. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js +21 -0
  40. package/dist/esm/Sprinkle/__tests__/tx-dialog.test.js.map +1 -1
  41. package/dist/esm/Sprinkle/encryption.js +117 -0
  42. package/dist/esm/Sprinkle/encryption.js.map +1 -0
  43. package/dist/esm/Sprinkle/index.js +172 -337
  44. package/dist/esm/Sprinkle/index.js.map +1 -1
  45. package/dist/esm/Sprinkle/prompts.js +385 -0
  46. package/dist/esm/Sprinkle/prompts.js.map +1 -0
  47. package/dist/esm/Sprinkle/schemas.js +91 -0
  48. package/dist/esm/Sprinkle/schemas.js.map +1 -0
  49. package/dist/esm/Sprinkle/tx-dialog.js +90 -0
  50. package/dist/esm/Sprinkle/tx-dialog.js.map +1 -0
  51. package/dist/esm/Sprinkle/type-guards.js +24 -0
  52. package/dist/esm/Sprinkle/type-guards.js.map +1 -0
  53. package/dist/esm/Sprinkle/types.js +42 -0
  54. package/dist/esm/Sprinkle/types.js.map +1 -0
  55. package/dist/esm/Sprinkle/wallet.js +90 -0
  56. package/dist/esm/Sprinkle/wallet.js.map +1 -0
  57. package/dist/types/Sprinkle/encryption.d.ts +43 -0
  58. package/dist/types/Sprinkle/encryption.d.ts.map +1 -0
  59. package/dist/types/Sprinkle/index.d.ts +13 -174
  60. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  61. package/dist/types/Sprinkle/prompts.d.ts +94 -0
  62. package/dist/types/Sprinkle/prompts.d.ts.map +1 -0
  63. package/dist/types/Sprinkle/schemas.d.ts +125 -0
  64. package/dist/types/Sprinkle/schemas.d.ts.map +1 -0
  65. package/dist/types/Sprinkle/tx-dialog.d.ts +37 -0
  66. package/dist/types/Sprinkle/tx-dialog.d.ts.map +1 -0
  67. package/dist/types/Sprinkle/type-guards.d.ts +22 -0
  68. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -0
  69. package/dist/types/Sprinkle/types.d.ts +62 -0
  70. package/dist/types/Sprinkle/types.d.ts.map +1 -0
  71. package/dist/types/Sprinkle/wallet.d.ts +27 -0
  72. package/dist/types/Sprinkle/wallet.d.ts.map +1 -0
  73. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  74. package/package.json +1 -1
  75. package/src/Sprinkle/__tests__/encryption.test.ts +21 -8
  76. package/src/Sprinkle/__tests__/enhancements.test.ts +41 -15
  77. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +104 -38
  78. package/src/Sprinkle/__tests__/settings-persistence.test.ts +108 -0
  79. package/src/Sprinkle/__tests__/show-menu.test.ts +96 -8
  80. package/src/Sprinkle/__tests__/tx-dialog.test.ts +21 -0
  81. package/src/Sprinkle/encryption.ts +130 -0
  82. package/src/Sprinkle/index.ts +265 -478
  83. package/src/Sprinkle/prompts.ts +481 -0
  84. package/src/Sprinkle/schemas.ts +111 -0
  85. package/src/Sprinkle/tx-dialog.ts +100 -0
  86. package/src/Sprinkle/type-guards.ts +51 -0
  87. package/src/Sprinkle/types.ts +73 -0
  88. package/src/Sprinkle/wallet.ts +133 -0
@@ -1,189 +1,126 @@
1
- import { Blockfrost, type Provider } from "@blaze-cardano/query";
1
+ import { type Provider } from "@blaze-cardano/query";
2
2
  import {
3
3
  Blaze,
4
- ColdWallet,
5
4
  Core,
6
5
  HotWallet,
7
6
  type Wallet,
8
7
  } from "@blaze-cardano/sdk";
8
+ import { CborSet, VkeyWitness, TxCBOR } from "@blaze-cardano/core";
9
9
  import {
10
- CborSet,
11
- VkeyWitness,
12
- blake2b_256,
13
- TxCBOR,
14
- wordlist,
15
- } from "@blaze-cardano/core";
16
- import { confirm, input, password, search, select } from "@inquirer/prompts";
17
- import {
18
- Kind,
19
- type Static,
20
- type TBigInt,
21
- type TLiteral,
22
- type TObject,
23
- type TSchema,
24
- type TString,
25
- type TTuple,
26
- type TUnion,
27
- Type,
28
- type TArray,
29
- type TThis,
30
- type TRef,
31
- type TImport,
32
- type TOptional,
33
- OptionalKind,
34
- } from "@sinclair/typebox";
10
+ selectCancellable,
11
+ inputCancellable,
12
+ passwordCancellable,
13
+ confirmCancellable,
14
+ searchCancellable,
15
+ select,
16
+ } from "./prompts.js";
17
+ import { type TSchema, Type, OptionalKind } from "@sinclair/typebox";
35
18
  import * as fs from "fs";
36
19
  import * as path from "path";
37
20
  export * from "@sinclair/typebox";
38
21
 
39
- export type TExact<T> = T extends TSchema ? Static<T> : T;
40
-
41
- const isOptional = <T extends TSchema>(t: T): t is TOptional<T> =>
42
- t[OptionalKind] === "Optional";
43
- const isImport = (t: TSchema): t is TImport => t[Kind] === "Import";
44
- const isArray = (t: TSchema): t is TArray => t[Kind] === "Array";
45
- const isBigInt = (t: TSchema): t is TBigInt => t[Kind] === "BigInt";
46
- // const isBoolean = (t: TSchema): t is TBoolean => t[Kind] === "Boolean";
47
- const isLiteral = (t: TSchema): t is TLiteral => t[Kind] === "Literal";
48
- // const isNumber = (t: TSchema): t is TNumber => t[Kind] === "Number";
49
- const isObject = (t: TSchema): t is TObject => t[Kind] === "Object";
50
- // const isRecord = (t: TSchema): t is TRecord => t[Kind] === "Record";
51
- const isRef = (t: TSchema): t is TRef => t[Kind] === "Ref";
52
- const isString = (t: TSchema): t is TString => t[Kind] === "String";
53
- const isThis = (t: TSchema): t is TThis => t[Kind] === "This";
54
- const isTuple = (t: TSchema): t is TTuple => t[Kind] === "Tuple";
55
- const isUnion = (t: TSchema): t is TUnion => t[Kind] === "Union";
56
- // const isAny = (t: TSchema): t is TAny => t[Kind] === "Any";
57
-
58
- const isSensitive = (t: TSchema): boolean =>
59
- isString(t) && t.sensitive === true;
60
-
61
- export interface IEncryptionOptions {
62
- encrypt: (plaintext: string) => string;
63
- decrypt: (ciphertext: string) => Promise<string>;
64
- }
65
-
66
- export interface ISprinkleOptions {
67
- encryption?: IEncryptionOptions;
68
- }
69
-
70
- export interface IProfileMeta {
71
- name: string;
72
- description?: string;
73
- createdAt: string;
74
- updatedAt: string;
75
- }
76
-
77
- export interface IProfileEntry {
78
- id: string;
79
- meta: IProfileMeta;
80
- }
81
-
82
- export interface TxDialogResult {
83
- action: "submitted" | "signed" | "cancelled";
84
- txId?: string; // present only if action === 'submitted'
85
- tx: Core.Transaction; // the (potentially signed) transaction
86
- }
87
-
88
- export interface TxDialogOptions {
89
- beforeSign?: () => Promise<void>;
90
- }
22
+ // Re-export types from types.ts
23
+ export type { TExact } from "./types.js";
24
+ export type {
25
+ IEncryptionOptions,
26
+ ISprinkleOptions,
27
+ IProfileMeta,
28
+ ICurrentProfile,
29
+ IProfileEntry,
30
+ TxDialogResult,
31
+ TxDialogOptions,
32
+ } from "./types.js";
33
+ export { UserCancelledError } from "./types.js";
34
+ import type {
35
+ TExact,
36
+ IEncryptionOptions,
37
+ ISprinkleOptions,
38
+ IProfileMeta,
39
+ ICurrentProfile,
40
+ IProfileEntry,
41
+ TxDialogResult,
42
+ TxDialogOptions,
43
+ } from "./types.js";
44
+ import { UserCancelledError } from "./types.js";
45
+
46
+ // Re-export schemas from schemas.ts
47
+ export {
48
+ NetworkSchema,
49
+ MultisigScriptModule,
50
+ MultisigScript,
51
+ ProviderSettingsSchema,
52
+ WalletSettingsSchema,
53
+ } from "./schemas.js";
54
+ export type { TMultisigScript } from "./schemas.js";
55
+
56
+ // Import and re-export type guards
57
+ import {
58
+ isOptional,
59
+ isImport,
60
+ isArray,
61
+ isBigInt,
62
+ isLiteral,
63
+ isObject,
64
+ isRef,
65
+ isString,
66
+ isThis,
67
+ isTuple,
68
+ isUnion,
69
+ isSensitive,
70
+ } from "./type-guards.js";
71
+ export {
72
+ isOptional,
73
+ isImport,
74
+ isArray,
75
+ isBigInt,
76
+ isLiteral,
77
+ isObject,
78
+ isRef,
79
+ isString,
80
+ isThis,
81
+ isTuple,
82
+ isUnion,
83
+ isSensitive,
84
+ } from "./type-guards.js";
85
+
86
+ // Import schemas for use in this file
87
+ import {
88
+ NetworkSchema,
89
+ ProviderSettingsSchema,
90
+ WalletSettingsSchema,
91
+ } from "./schemas.js";
91
92
 
92
- export const NetworkSchema = Type.Union([
93
- Type.Literal("mainnet"),
94
- Type.Literal("preview"),
95
- Type.Literal("preprod"),
96
- ]);
97
-
98
- export const MultisigScriptModule = Type.Module({
99
- MultisigScript: Type.Union([
100
- Type.Object({
101
- Signature: Type.Object(
102
- {
103
- key_hash: Type.String(),
104
- },
105
- { ctor: 0n },
106
- ),
107
- }),
108
- Type.Object({
109
- AllOf: Type.Object(
110
- {
111
- scripts: Type.Array(Type.Ref("MultisigScript")),
112
- },
113
- { ctor: 1n },
114
- ),
115
- }),
116
- Type.Object({
117
- AnyOf: Type.Object(
118
- {
119
- scripts: Type.Array(Type.Ref("MultisigScript")),
120
- },
121
- { ctor: 2n },
122
- ),
123
- }),
124
- Type.Object({
125
- AtLeast: Type.Object(
126
- {
127
- required: Type.BigInt(),
128
- scripts: Type.Array(Type.Ref("MultisigScript")),
129
- },
130
- { ctor: 3n },
131
- ),
132
- }),
133
- Type.Object({
134
- Before: Type.Object(
135
- {
136
- time: Type.BigInt(),
137
- },
138
- { ctor: 4n },
139
- ),
140
- }),
141
- Type.Object({
142
- After: Type.Object(
143
- {
144
- time: Type.BigInt(),
145
- },
146
- { ctor: 5n },
147
- ),
148
- }),
149
- Type.Object({
150
- Script: Type.Object(
151
- {
152
- script_hash: Type.String(),
153
- },
154
- { ctor: 6n },
155
- ),
156
- }),
157
- ]),
158
- });
159
- export const MultisigScript = MultisigScriptModule.Import("MultisigScript");
160
- export type TMultisigScript = TExact<typeof MultisigScript>;
161
-
162
- export const ProviderSettingsSchema = Type.Union([
163
- Type.Object({
164
- type: Type.Literal("blockfrost"),
165
- projectId: Type.String({ minLength: 1, title: "Blockfrost Project ID" }),
166
- }),
167
- Type.Object({
168
- type: Type.Literal("maestro"),
169
- apiKey: Type.String({ minLength: 1, title: "Maestro API Key" }),
170
- }),
171
- ]);
172
-
173
- export const WalletSettingsSchema = Type.Union([
174
- Type.Object({
175
- type: Type.Literal("hot"),
176
- privateKey: Type.String({
177
- minLength: 1,
178
- title: "Hot Wallet Private Key",
179
- sensitive: true,
180
- }),
181
- }),
182
- Type.Object({
183
- type: Type.Literal("cold"),
184
- address: Type.String({ minLength: 1, title: "Cold Wallet Address" }),
185
- }),
186
- ]);
93
+ // Import and re-export wallet utilities
94
+ import {
95
+ GetProvider as GetProviderFn,
96
+ GetWallet as GetWalletFn,
97
+ GetBlaze as GetBlazeFn,
98
+ generateWalletFromMnemonic,
99
+ } from "./wallet.js";
100
+ export { GetProvider, GetWallet, GetBlaze } from "./wallet.js";
101
+
102
+ // Import encryption utilities
103
+ import {
104
+ collectSensitivePaths,
105
+ getNestedValue,
106
+ setNestedValue,
107
+ bigIntReplacer,
108
+ bigIntReviver,
109
+ encryptSensitiveFields,
110
+ decryptSensitiveFields,
111
+ maskSensitiveFields,
112
+ } from "./encryption.js";
113
+
114
+ // Import tx-dialog utilities
115
+ import {
116
+ getWalletPaymentKeyHash,
117
+ countSignatures,
118
+ hasVkeySigned,
119
+ getRequiredSigners,
120
+ getTxBodyHash,
121
+ formatHash,
122
+ mergeSignatures,
123
+ } from "./tx-dialog.js";
187
124
 
188
125
  export interface IMenuAction<S extends TSchema> {
189
126
  title: string;
@@ -211,6 +148,16 @@ export class Sprinkle<S extends TSchema> {
211
148
  this.options = options ?? {};
212
149
  }
213
150
 
151
+ // --- Current Profile Accessor ---
152
+
153
+ get currentProfile(): ICurrentProfile | null {
154
+ if (!this.profileId) return null;
155
+ return {
156
+ id: this.profileId,
157
+ ...this.profileMeta,
158
+ };
159
+ }
160
+
214
161
  // --- Profile path helpers ---
215
162
 
216
163
  static sanitizeProfileId(name: string): string {
@@ -304,7 +251,7 @@ export class Sprinkle<S extends TSchema> {
304
251
  settings: settingsToSave,
305
252
  defaults: this.defaults,
306
253
  },
307
- Sprinkle.bigIntReplacer,
254
+ bigIntReplacer,
308
255
  2,
309
256
  );
310
257
  fs.writeFileSync(filePath, jsonContent, "utf-8");
@@ -313,31 +260,54 @@ export class Sprinkle<S extends TSchema> {
313
260
  private async promptProfileMeta(
314
261
  defaultName?: string,
315
262
  defaultDescription?: string,
316
- ): Promise<{ name: string; description?: string }> {
317
- const name = await input({
263
+ ): Promise<{ name: string; description?: string } | null> {
264
+ const name = await inputCancellable({
318
265
  message: "Profile name:",
319
266
  default: defaultName,
320
267
  validate: (v) => (v.trim().length > 0 ? true : "Name cannot be empty"),
321
268
  });
322
- const description = await input({
269
+ if (name === null) return null; // User cancelled
270
+ const description = await inputCancellable({
323
271
  message: "Profile description (optional):",
324
272
  default: defaultDescription ?? "",
325
273
  });
274
+ if (description === null) return null; // User cancelled
326
275
  return { name, description: description || undefined };
327
276
  }
328
277
 
329
278
  private async createProfile(): Promise<void> {
330
- const { name, description } = await this.promptProfileMeta();
279
+ const result = await this.promptProfileMeta();
280
+ if (result === null) return; // User cancelled
281
+ const { name, description } = result;
331
282
  const profilesDir = Sprinkle.profilesDir(this.storagePath);
332
283
  const id = Sprinkle.findAvailableId(
333
284
  profilesDir,
334
285
  Sprinkle.sanitizeProfileId(name),
335
286
  );
336
287
  const now = new Date().toISOString();
288
+
289
+ // Snapshot current state in case we need to restore on cancellation
290
+ const prevProfileId = this.profileId;
291
+ const prevProfileMeta = this.profileMeta;
292
+ const prevDefaults = this.defaults;
293
+ const prevSettings = this.settings;
294
+
337
295
  this.profileId = id;
338
296
  this.profileMeta = { name, description, createdAt: now, updatedAt: now };
339
297
  this.defaults = {};
340
- this.settings = await this.FillInStruct(this.type);
298
+ try {
299
+ this.settings = await this.FillInStruct(this.type);
300
+ } catch (e) {
301
+ // Restore previous state on cancellation
302
+ if (e instanceof UserCancelledError) {
303
+ this.profileId = prevProfileId;
304
+ this.profileMeta = prevProfileMeta;
305
+ this.defaults = prevDefaults;
306
+ this.settings = prevSettings;
307
+ return;
308
+ }
309
+ throw e;
310
+ }
341
311
  this.saveProfile();
342
312
  fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), id, "utf-8");
343
313
  }
@@ -364,7 +334,7 @@ export class Sprinkle<S extends TSchema> {
364
334
  fs.mkdirSync(profilesDir, { recursive: true });
365
335
  fs.writeFileSync(
366
336
  Sprinkle.profilePath(this.storagePath, "default"),
367
- JSON.stringify(profileData, Sprinkle.bigIntReplacer, 2),
337
+ JSON.stringify(profileData, bigIntReplacer, 2),
368
338
  "utf-8",
369
339
  );
370
340
  fs.writeFileSync(
@@ -409,7 +379,7 @@ export class Sprinkle<S extends TSchema> {
409
379
 
410
380
  private async selectProfile(profiles?: IProfileEntry[]): Promise<void> {
411
381
  const available = profiles ?? this.scanProfiles();
412
- const selection = await select({
382
+ const selection = await selectCancellable({
413
383
  message: "Select a profile:",
414
384
  choices: available.map((p) => ({
415
385
  name: p.meta.description
@@ -418,16 +388,19 @@ export class Sprinkle<S extends TSchema> {
418
388
  value: p.id,
419
389
  })),
420
390
  });
421
- await this.loadProfile(selection);
391
+ if (selection === null) return; // User cancelled
392
+ await this.loadProfile(selection as string);
422
393
  }
423
394
 
424
395
  // --- Profile management (CRUD) ---
425
396
 
426
397
  private async duplicateProfile(): Promise<void> {
427
- const { name, description } = await this.promptProfileMeta(
398
+ const result = await this.promptProfileMeta(
428
399
  `${this.profileMeta.name} (copy)`,
429
400
  this.profileMeta.description,
430
401
  );
402
+ if (result === null) return; // User cancelled
403
+ const { name, description } = result;
431
404
  const profilesDir = Sprinkle.profilesDir(this.storagePath);
432
405
  const id = Sprinkle.findAvailableId(
433
406
  profilesDir,
@@ -442,7 +415,7 @@ export class Sprinkle<S extends TSchema> {
442
415
  settings: settingsToSave,
443
416
  defaults: this.defaults,
444
417
  },
445
- Sprinkle.bigIntReplacer,
418
+ bigIntReplacer,
446
419
  2,
447
420
  );
448
421
  fs.writeFileSync(
@@ -454,10 +427,12 @@ export class Sprinkle<S extends TSchema> {
454
427
  }
455
428
 
456
429
  private async renameProfile(): Promise<void> {
457
- const { name, description } = await this.promptProfileMeta(
430
+ const result = await this.promptProfileMeta(
458
431
  this.profileMeta.name,
459
432
  this.profileMeta.description,
460
433
  );
434
+ if (result === null) return; // User cancelled
435
+ const { name, description } = result;
461
436
  const newId = Sprinkle.sanitizeProfileId(name);
462
437
  const oldId = this.profileId;
463
438
 
@@ -496,31 +471,31 @@ export class Sprinkle<S extends TSchema> {
496
471
  return;
497
472
  }
498
473
 
499
- const toDelete = await select({
474
+ const toDelete = await selectCancellable({
500
475
  message: "Select a profile to delete:",
501
- choices: [
502
- ...others.map((p) => ({
503
- name: p.meta.description
504
- ? `${p.meta.name} - ${p.meta.description}`
505
- : p.meta.name,
506
- value: p.id,
507
- })),
508
- { name: "Cancel", value: "__cancel__" },
509
- ],
476
+ choices: others.map((p) => ({
477
+ name: p.meta.description
478
+ ? `${p.meta.name} - ${p.meta.description}`
479
+ : p.meta.name,
480
+ value: p.id,
481
+ })),
510
482
  });
511
483
 
512
- if (toDelete === "__cancel__") return;
484
+ if (toDelete === null) return; // User cancelled
513
485
 
514
- const profileToDelete = others.find((p) => p.id === toDelete);
515
- const confirmed = await confirm({
516
- message: `Delete profile "${profileToDelete?.meta.name ?? toDelete}"? This cannot be undone.`,
486
+ const profileId = toDelete as string;
487
+ const profileToDelete = others.find((p) => p.id === profileId);
488
+ const confirmed = await confirmCancellable({
489
+ message: `Delete profile "${profileToDelete?.meta.name ?? profileId}"? This cannot be undone.`,
517
490
  default: false,
518
491
  });
519
492
 
493
+ if (confirmed === null) return; // User cancelled
494
+
520
495
  if (confirmed) {
521
- fs.unlinkSync(Sprinkle.profilePath(this.storagePath, toDelete));
496
+ fs.unlinkSync(Sprinkle.profilePath(this.storagePath, profileId));
522
497
  console.log(
523
- `Profile "${profileToDelete?.meta.name ?? toDelete}" deleted.`,
498
+ `Profile "${profileToDelete?.meta.name ?? profileId}" deleted.`,
524
499
  );
525
500
  }
526
501
  }
@@ -548,14 +523,27 @@ export class Sprinkle<S extends TSchema> {
548
523
  choices.push({ name: "Settings & Profiles", value: -5 });
549
524
  choices.push({ name: "Exit", value: -1 });
550
525
  }
551
- const selection = await select({
526
+ const selectionResult = await selectCancellable({
552
527
  message: "Select an option:",
553
528
  choices: choices,
554
529
  });
530
+ // Handle escape (null) as Back
531
+ if (selectionResult === null) {
532
+ return;
533
+ }
534
+ const selection = selectionResult as number;
555
535
  if (selection === -5) {
556
536
  const settingsMenu: IMenu<S> = {
557
537
  title: "Settings & Profiles",
558
538
  items: [
539
+ {
540
+ title: "View settings",
541
+ action: async () => {
542
+ console.log(
543
+ JSON.stringify(this.getDisplaySettings(), bigIntReplacer, 2),
544
+ );
545
+ },
546
+ },
559
547
  {
560
548
  title: "Edit settings",
561
549
  action: async () => {
@@ -641,49 +629,14 @@ export class Sprinkle<S extends TSchema> {
641
629
  network: TExact<typeof NetworkSchema>,
642
630
  settings: TExact<typeof ProviderSettingsSchema>,
643
631
  ): Promise<Provider> {
644
- switch (settings.type) {
645
- case "blockfrost":
646
- return new Blockfrost({
647
- network: `cardano-${network}`,
648
- projectId: settings.projectId,
649
- });
650
- case "maestro":
651
- // Dynamic import - Maestro may or may not be exported depending on @blaze-cardano/query version
652
- const queryModule = (await import("@blaze-cardano/query")) as any;
653
- if (!queryModule.Maestro) {
654
- throw new Error(
655
- "Maestro is not available in the installed version of @blaze-cardano/query. Please install a version that includes Maestro support.",
656
- );
657
- }
658
- return new queryModule.Maestro({
659
- network: network as "mainnet" | "preview" | "preprod",
660
- apiKey: settings.apiKey,
661
- });
662
- default:
663
- throw new Error("Invalid provider type");
664
- }
632
+ return GetProviderFn(network, settings);
665
633
  }
666
634
 
667
635
  static async GetWallet(
668
636
  settings: TExact<typeof WalletSettingsSchema>,
669
637
  provider: Provider,
670
638
  ): Promise<Wallet> {
671
- switch (settings.type) {
672
- case "hot":
673
- return HotWallet.fromMasterkey(
674
- Core.Bip32PrivateKeyHex(settings.privateKey),
675
- provider,
676
- provider.network,
677
- );
678
- case "cold":
679
- return new ColdWallet(
680
- Core.Address.fromBech32(settings.address),
681
- provider.network,
682
- provider,
683
- );
684
- default:
685
- throw new Error("Invalid wallet type");
686
- }
639
+ return GetWalletFn(settings, provider);
687
640
  }
688
641
 
689
642
  static async GetBlaze(
@@ -691,9 +644,7 @@ export class Sprinkle<S extends TSchema> {
691
644
  providerSettings: TExact<typeof ProviderSettingsSchema>,
692
645
  walletSettings: TExact<typeof WalletSettingsSchema>,
693
646
  ): Promise<Blaze<Provider, Wallet>> {
694
- const provider = await Sprinkle.GetProvider(network, providerSettings);
695
- const wallet = await Sprinkle.GetWallet(walletSettings, provider);
696
- return Blaze.from(provider, wallet);
647
+ return GetBlazeFn(network, providerSettings, walletSettings);
697
648
  }
698
649
 
699
650
  /**
@@ -702,39 +653,7 @@ export class Sprinkle<S extends TSchema> {
702
653
  * @returns The Bip32PrivateKey hex string for storage
703
654
  */
704
655
  private static async generateWalletFromMnemonic(): Promise<string> {
705
- const mnemonic = Core.generateMnemonic(wordlist, 256); // 24 words
706
- const words = mnemonic.split(" ");
707
-
708
- console.log("\n=== NEW WALLET GENERATED ===\n");
709
- console.log("IMPORTANT: Save these 24 words in a secure location.");
710
- console.log("This is the ONLY way to recover your wallet.\n");
711
-
712
- // Display in 4 columns
713
- for (let i = 0; i < 6; i++) {
714
- console.log(
715
- `${(i + 1).toString().padStart(2)}. ${words[i]!.padEnd(12)} ` +
716
- `${(i + 7).toString().padStart(2)}. ${words[i + 6]!.padEnd(12)} ` +
717
- `${(i + 13).toString().padStart(2)}. ${words[i + 12]!.padEnd(12)} ` +
718
- `${(i + 19).toString().padStart(2)}. ${words[i + 18]}`,
719
- );
720
- }
721
- console.log("");
722
-
723
- const confirmed = await confirm({
724
- message: "Have you saved your recovery phrase?",
725
- default: false,
726
- });
727
-
728
- if (!confirmed) {
729
- throw new Error("Wallet generation cancelled - recovery phrase not saved");
730
- }
731
-
732
- const entropy = Core.mnemonicToEntropy(mnemonic, wordlist);
733
- const masterKey = Core.Bip32PrivateKey.fromBip39Entropy(
734
- Buffer.from(entropy),
735
- "",
736
- );
737
- return masterKey.hex();
656
+ return generateWalletFromMnemonic();
738
657
  }
739
658
 
740
659
  static async SearchSelect<T>(opts: {
@@ -742,8 +661,8 @@ export class Sprinkle<S extends TSchema> {
742
661
  source: (
743
662
  term: string | undefined,
744
663
  ) => Promise<{ name: string; value: T }[]> | { name: string; value: T }[];
745
- }): Promise<T> {
746
- return search(opts) as Promise<T>;
664
+ }): Promise<T | null> {
665
+ return searchCancellable(opts) as Promise<T | null>;
747
666
  }
748
667
 
749
668
  static SettingsPath(storagePath: string): string {
@@ -775,191 +694,25 @@ export class Sprinkle<S extends TSchema> {
775
694
  }
776
695
 
777
696
  static bigIntReviver(key: string, value: unknown): unknown {
778
- if (typeof value === "string" && /^\d+n$/.test(value)) {
779
- return BigInt(value.slice(0, -1));
780
- }
781
- return value;
782
- }
783
-
784
- private static bigIntReplacer(_key: string, value: unknown): unknown {
785
- return typeof value === "bigint" ? `${value.toString()}n` : value;
786
- }
787
-
788
- private static collectSensitivePaths(
789
- type: TSchema,
790
- prefix: string = "",
791
- ): string[] {
792
- const paths: string[] = [];
793
- if (isObject(type)) {
794
- const fields = type["properties"] as Record<string, TSchema>;
795
- for (const [field, fieldType] of Object.entries(fields)) {
796
- const fieldPath = prefix ? `${prefix}.${field}` : field;
797
- if (isSensitive(fieldType)) {
798
- paths.push(fieldPath);
799
- }
800
- paths.push(...Sprinkle.collectSensitivePaths(fieldType, fieldPath));
801
- }
802
- }
803
- if (isUnion(type)) {
804
- for (const variant of type.anyOf) {
805
- paths.push(...Sprinkle.collectSensitivePaths(variant, prefix));
806
- }
807
- }
808
- return paths;
809
- }
810
-
811
- private static getNestedValue(obj: any, path: string): unknown {
812
- return path.split(".").reduce((o, k) => o?.[k], obj);
813
- }
814
-
815
- private static setNestedValue(obj: any, path: string, value: unknown): void {
816
- const keys = path.split(".");
817
- const last = keys.pop()!;
818
- const parent = keys.reduce((o, k) => o?.[k], obj);
819
- if (parent && typeof parent === "object") {
820
- parent[last] = value;
821
- }
697
+ return bigIntReviver(key, value);
822
698
  }
823
699
 
824
700
  private encryptSettings(settings: TExact<S>): TExact<S> {
825
701
  if (!this.options.encryption) return settings;
826
- const clone = JSON.parse(
827
- JSON.stringify(settings, Sprinkle.bigIntReplacer),
828
- Sprinkle.bigIntReviver,
829
- );
830
- const sensitivePaths = Sprinkle.collectSensitivePaths(this.type);
831
- for (const p of sensitivePaths) {
832
- const value = Sprinkle.getNestedValue(clone, p);
833
- if (typeof value === "string" && value.length > 0) {
834
- Sprinkle.setNestedValue(
835
- clone,
836
- p,
837
- this.options.encryption.encrypt(value),
838
- );
839
- }
840
- }
841
- return clone;
702
+ return encryptSensitiveFields(settings, this.type, this.options.encryption);
842
703
  }
843
704
 
844
705
  private async decryptSettings(settings: TExact<S>): Promise<TExact<S>> {
845
706
  if (!this.options.encryption) return settings;
846
- const clone = JSON.parse(
847
- JSON.stringify(settings, Sprinkle.bigIntReplacer),
848
- Sprinkle.bigIntReviver,
849
- );
850
- const sensitivePaths = Sprinkle.collectSensitivePaths(this.type);
851
- for (const p of sensitivePaths) {
852
- const value = Sprinkle.getNestedValue(clone, p);
853
- if (typeof value === "string" && value.length > 0) {
854
- Sprinkle.setNestedValue(
855
- clone,
856
- p,
857
- await this.options.encryption.decrypt(value),
858
- );
859
- }
860
- }
861
- return clone;
707
+ return decryptSensitiveFields(settings, this.type, this.options.encryption);
862
708
  }
863
709
 
864
710
  saveSettings(): void {
865
711
  this.saveProfile();
866
712
  }
867
713
 
868
- // --- TxDialog Helpers ---
869
-
870
- /**
871
- * Get the payment key hash from a HotWallet's first address
872
- */
873
- private async getWalletPaymentKeyHash(
874
- wallet: HotWallet,
875
- ): Promise<string | null> {
876
- try {
877
- const addresses = await wallet.getUsedAddresses();
878
- const address = addresses[0];
879
- if (!address) return null;
880
- const paymentCredential = address.asBase()?.getPaymentCredential();
881
- return paymentCredential?.hash?.toString() ?? null;
882
- } catch {
883
- return null;
884
- }
885
- }
886
-
887
- /**
888
- * Count the number of vkey signatures in a transaction's witness set
889
- */
890
- private countSignatures(tx: Core.Transaction): number {
891
- const vkeys = tx.witnessSet().vkeys();
892
- return vkeys ? vkeys.size() : 0;
893
- }
894
-
895
- /**
896
- * Check if a specific public key has already signed the transaction
897
- * Compares by vkey (public key bytes)
898
- */
899
- private hasVkeySigned(tx: Core.Transaction, vkeyHex: string): boolean {
900
- const vkeys = tx.witnessSet().vkeys();
901
- if (!vkeys) return false;
902
- const vkeyArray = vkeys.toCore();
903
- return vkeyArray.some(([vkey]) => vkey === vkeyHex);
904
- }
905
-
906
- /**
907
- * Get the list of required signer key hashes from the transaction body
908
- */
909
- private getRequiredSigners(tx: Core.Transaction): string[] {
910
- const requiredSigners = tx.body().requiredSigners();
911
- if (!requiredSigners) return [];
912
- return Array.from(requiredSigners.values()).map((s) => s.toString());
913
- }
914
-
915
- /**
916
- * Compute the transaction body hash for display
917
- */
918
- private getTxBodyHash(tx: Core.Transaction): string {
919
- const bodyCbor = tx.body().toCbor();
920
- return blake2b_256(bodyCbor);
921
- }
922
-
923
- /**
924
- * Format a hash for display: first 8 chars + ... + last 8 chars
925
- */
926
- private formatHash(hash: string): string {
927
- if (hash.length <= 20) return hash;
928
- return `${hash.slice(0, 8)}...${hash.slice(-8)}`;
929
- }
930
-
931
- /**
932
- * Merge signatures from source transaction into target transaction.
933
- * Prevents duplicate signatures by comparing vkey (public key).
934
- * Returns the count of newly added signatures.
935
- */
936
- private mergeSignatures(
937
- target: Core.Transaction,
938
- source: Core.Transaction,
939
- ): number {
940
- const targetWs = target.witnessSet();
941
- const sourceWs = source.witnessSet();
942
-
943
- const targetVkeys = targetWs.vkeys()?.toCore() ?? [];
944
- const sourceVkeys = sourceWs.vkeys()?.toCore() ?? [];
945
-
946
- // Find vkeys in source that aren't in target (by comparing public key)
947
- const existingPubKeys = new Set(targetVkeys.map(([vkey]) => vkey));
948
- const newVkeys = sourceVkeys.filter(
949
- ([vkey]) => !existingPubKeys.has(vkey),
950
- );
951
-
952
- if (newVkeys.length === 0) {
953
- return 0;
954
- }
955
-
956
- // Merge the new vkeys into target
957
- targetWs.setVkeys(
958
- CborSet.fromCore([...targetVkeys, ...newVkeys], VkeyWitness.fromCore),
959
- );
960
- target.setWitnessSet(targetWs);
961
-
962
- return newVkeys.length;
714
+ getDisplaySettings(): TExact<S> {
715
+ return maskSensitiveFields(this.settings, this.type);
963
716
  }
964
717
 
965
718
  async TxDialog<P extends Provider, W extends Wallet>(
@@ -993,17 +746,17 @@ export class Sprinkle<S extends TSchema> {
993
746
 
994
747
  while (true) {
995
748
  // Display transaction status
996
- const txHash = this.getTxBodyHash(currentTx);
997
- const sigCount = this.countSignatures(currentTx);
998
- const requiredSigners = this.getRequiredSigners(currentTx);
749
+ const txHash = getTxBodyHash(currentTx);
750
+ const sigCount = countSignatures(currentTx);
751
+ const requiredSigners = getRequiredSigners(currentTx);
999
752
 
1000
753
  console.log("");
1001
- console.log(`Transaction: ${this.formatHash(txHash)}`);
754
+ console.log(`Transaction: ${formatHash(txHash)}`);
1002
755
  if (requiredSigners.length > 0) {
1003
756
  console.log(`Signatures: ${sigCount} of ${requiredSigners.length} required`);
1004
757
  console.log("Required signers:");
1005
758
  for (const signer of requiredSigners) {
1006
- console.log(` - ${this.formatHash(signer)}`);
759
+ console.log(` - ${formatHash(signer)}`);
1007
760
  }
1008
761
  } else {
1009
762
  console.log(`Signatures: ${sigCount}`);
@@ -1036,11 +789,19 @@ export class Sprinkle<S extends TSchema> {
1036
789
  choices.push({ name: "Submit transaction", value: "submit" });
1037
790
  choices.push({ name: "Cancel", value: "cancel" });
1038
791
 
1039
- const selection = await select({
792
+ const selection = await selectCancellable({
1040
793
  message: "Select an option:",
1041
794
  choices,
1042
795
  });
1043
796
 
797
+ // Handle escape/cancel as cancel action
798
+ if (selection === null) {
799
+ if (hasSignedThisSession) {
800
+ return { action: "signed", tx: currentTx };
801
+ }
802
+ return { action: "cancelled", tx: currentTx };
803
+ }
804
+
1044
805
  // Handle selection
1045
806
  if (selection === "sign") {
1046
807
  if (opts?.beforeSign) {
@@ -1126,7 +887,7 @@ export class Sprinkle<S extends TSchema> {
1126
887
  } else {
1127
888
  const signedTx = await blaze.signTransaction(currentTx);
1128
889
  // Merge signatures from signed tx into current tx
1129
- const added = this.mergeSignatures(currentTx, signedTx);
890
+ const added = mergeSignatures(currentTx, signedTx);
1130
891
  if (added > 0) {
1131
892
  console.log(`Added ${added} signature(s).`);
1132
893
  hasSignedThisSession = true;
@@ -1159,11 +920,11 @@ export class Sprinkle<S extends TSchema> {
1159
920
  }
1160
921
 
1161
922
  if (selection === "import") {
1162
- const cborInput = await input({
923
+ const cborInput = await inputCancellable({
1163
924
  message: "Paste transaction CBOR (hex):",
1164
925
  });
1165
926
 
1166
- if (!cborInput || cborInput.trim() === "") {
927
+ if (cborInput === null || cborInput.trim() === "") {
1167
928
  console.log("No CBOR provided.");
1168
929
  continue;
1169
930
  }
@@ -1174,22 +935,22 @@ export class Sprinkle<S extends TSchema> {
1174
935
  );
1175
936
 
1176
937
  // Validate body hash matches
1177
- const currentHash = this.getTxBodyHash(currentTx);
1178
- const importedHash = this.getTxBodyHash(importedTx);
938
+ const currentHash = getTxBodyHash(currentTx);
939
+ const importedHash = getTxBodyHash(importedTx);
1179
940
 
1180
941
  if (currentHash !== importedHash) {
1181
- const proceed = await confirm({
1182
- message: `Warning: Imported transaction has different body hash.\nCurrent: ${this.formatHash(currentHash)}\nImported: ${this.formatHash(importedHash)}\nProceed anyway?`,
942
+ const proceed = await confirmCancellable({
943
+ message: `Warning: Imported transaction has different body hash.\nCurrent: ${formatHash(currentHash)}\nImported: ${formatHash(importedHash)}\nProceed anyway?`,
1183
944
  default: false,
1184
945
  });
1185
- if (!proceed) {
946
+ if (proceed === null || !proceed) {
1186
947
  console.log("Import cancelled.");
1187
948
  continue;
1188
949
  }
1189
950
  }
1190
951
 
1191
952
  // Merge signatures
1192
- const added = this.mergeSignatures(currentTx, importedTx);
953
+ const added = mergeSignatures(currentTx, importedTx);
1193
954
  const sourceVkeys = importedTx.witnessSet().vkeys();
1194
955
  const sourceCount = sourceVkeys ? sourceVkeys.size() : 0;
1195
956
  const skipped = sourceCount - added;
@@ -1210,13 +971,13 @@ export class Sprinkle<S extends TSchema> {
1210
971
  }
1211
972
 
1212
973
  if (selection === "submit") {
1213
- const sigCount = this.countSignatures(currentTx);
974
+ const sigCount = countSignatures(currentTx);
1214
975
  if (sigCount === 0) {
1215
- const proceed = await confirm({
976
+ const proceed = await confirmCancellable({
1216
977
  message: "Warning: Transaction has no signatures. Submit anyway?",
1217
978
  default: false,
1218
979
  });
1219
- if (!proceed) {
980
+ if (proceed === null || !proceed) {
1220
981
  continue;
1221
982
  }
1222
983
  }
@@ -1331,7 +1092,7 @@ export class Sprinkle<S extends TSchema> {
1331
1092
  >;
1332
1093
  }
1333
1094
  if (isOptional(type)) {
1334
- const shouldSet = await select({
1095
+ const shouldSet = await selectCancellable({
1335
1096
  message: Sprinkle.ExtractMessage(
1336
1097
  type,
1337
1098
  `Set value for ${path.join(".")}?`,
@@ -1342,6 +1103,9 @@ export class Sprinkle<S extends TSchema> {
1342
1103
  ],
1343
1104
  default: def !== undefined,
1344
1105
  });
1106
+ if (shouldSet === null) {
1107
+ throw new UserCancelledError();
1108
+ }
1345
1109
  if (!shouldSet) {
1346
1110
  return undefined as TExact<U>;
1347
1111
  }
@@ -1360,7 +1124,7 @@ export class Sprinkle<S extends TSchema> {
1360
1124
  value: variant,
1361
1125
  });
1362
1126
  }
1363
- const selection = await select({
1127
+ const selectionResult = await selectCancellable({
1364
1128
  message: Sprinkle.ExtractMessage(
1365
1129
  resolved,
1366
1130
  `Enter a choice for ${path.join(".")}`,
@@ -1368,13 +1132,17 @@ export class Sprinkle<S extends TSchema> {
1368
1132
  choices: choices,
1369
1133
  default: def ? `${def}` : undefined,
1370
1134
  });
1135
+ if (selectionResult === null) {
1136
+ throw new UserCancelledError();
1137
+ }
1138
+ const selection = selectionResult as TSchema;
1371
1139
  return this._fillInStruct(selection, path, defs) as Promise<TExact<U>>;
1372
1140
  }
1373
1141
 
1374
1142
  if (isString(type)) {
1375
1143
  // Special handling for hot wallet private key - offer generation option
1376
1144
  if (type.title === "Hot Wallet Private Key") {
1377
- const choice = await select({
1145
+ const choice = await selectCancellable({
1378
1146
  message: "Hot wallet setup:",
1379
1147
  choices: [
1380
1148
  { name: "Enter existing private key", value: "existing" },
@@ -1382,11 +1150,19 @@ export class Sprinkle<S extends TSchema> {
1382
1150
  ],
1383
1151
  });
1384
1152
 
1153
+ if (choice === null) {
1154
+ throw new UserCancelledError();
1155
+ }
1385
1156
  if (choice === "generate") {
1386
1157
  return Sprinkle.generateWalletFromMnemonic() as Promise<TExact<U>>;
1387
1158
  }
1388
1159
  // Fall through to password prompt for "existing" choice
1389
- const answer = await password({ message: "Enter your private key:" });
1160
+ const answer = await passwordCancellable({
1161
+ message: "Enter your private key:",
1162
+ });
1163
+ if (answer === null) {
1164
+ throw new UserCancelledError();
1165
+ }
1390
1166
  return answer as TExact<U>;
1391
1167
  }
1392
1168
 
@@ -1397,18 +1173,23 @@ export class Sprinkle<S extends TSchema> {
1397
1173
  type,
1398
1174
  `Enter a string for ${path.join(".")}`,
1399
1175
  );
1400
- let answer: string;
1176
+ let answer: string | null;
1401
1177
  if (isSensitive(type)) {
1402
- answer = await password({ message });
1178
+ answer = await passwordCancellable({ message });
1403
1179
  } else {
1404
- answer = await input({ message, default: defaultString });
1405
- this.defaults["string"] = answer;
1180
+ answer = await inputCancellable({ message, default: defaultString });
1181
+ if (answer !== null) {
1182
+ this.defaults["string"] = answer;
1183
+ }
1184
+ }
1185
+ if (answer === null) {
1186
+ throw new UserCancelledError();
1406
1187
  }
1407
1188
  return answer as TExact<U>;
1408
1189
  }
1409
1190
 
1410
1191
  if (isBigInt(type)) {
1411
- const answer = await input({
1192
+ const answer = await inputCancellable({
1412
1193
  message: Sprinkle.ExtractMessage(
1413
1194
  type,
1414
1195
  `Enter a bigint for ${path.join(".")}`,
@@ -1423,6 +1204,9 @@ export class Sprinkle<S extends TSchema> {
1423
1204
  }
1424
1205
  },
1425
1206
  });
1207
+ if (answer === null) {
1208
+ throw new UserCancelledError();
1209
+ }
1426
1210
  return BigInt(answer) as TExact<U>;
1427
1211
  }
1428
1212
 
@@ -1459,14 +1243,17 @@ export class Sprinkle<S extends TSchema> {
1459
1243
  defs,
1460
1244
  );
1461
1245
  arr.push(itemValue);
1462
- const continueAnswer = await select({
1246
+ const continueAnswer = await selectCancellable({
1463
1247
  message: `Add another item to ${path.join(".")}?`,
1464
1248
  choices: [
1465
1249
  { name: "Yes", value: true },
1466
1250
  { name: "No", value: false },
1467
1251
  ],
1468
1252
  });
1469
- addMore = continueAnswer;
1253
+ if (continueAnswer === null) {
1254
+ throw new UserCancelledError();
1255
+ }
1256
+ addMore = continueAnswer as boolean;
1470
1257
  }
1471
1258
  return arr as TExact<U>;
1472
1259
  }