@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,106 +1,36 @@
1
1
  function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
2
2
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
3
3
  function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
4
- import { Blockfrost } from "@blaze-cardano/query";
5
- import { Blaze, ColdWallet, Core, HotWallet } from "@blaze-cardano/sdk";
6
- import { CborSet, VkeyWitness, blake2b_256, TxCBOR, wordlist } from "@blaze-cardano/core";
7
- import { confirm, input, password, search, select } from "@inquirer/prompts";
8
- import { Kind, Type, OptionalKind } from "@sinclair/typebox";
4
+ import { Core, HotWallet } from "@blaze-cardano/sdk";
5
+ import { CborSet, VkeyWitness, TxCBOR } from "@blaze-cardano/core";
6
+ import { selectCancellable, inputCancellable, passwordCancellable, confirmCancellable, searchCancellable } from "./prompts.js";
7
+ import { OptionalKind } from "@sinclair/typebox";
9
8
  import * as fs from "fs";
10
9
  import * as path from "path";
11
10
  export * from "@sinclair/typebox";
12
- const isOptional = t => t[OptionalKind] === "Optional";
13
- const isImport = t => t[Kind] === "Import";
14
- const isArray = t => t[Kind] === "Array";
15
- const isBigInt = t => t[Kind] === "BigInt";
16
- // const isBoolean = (t: TSchema): t is TBoolean => t[Kind] === "Boolean";
17
- const isLiteral = t => t[Kind] === "Literal";
18
- // const isNumber = (t: TSchema): t is TNumber => t[Kind] === "Number";
19
- const isObject = t => t[Kind] === "Object";
20
- // const isRecord = (t: TSchema): t is TRecord => t[Kind] === "Record";
21
- const isRef = t => t[Kind] === "Ref";
22
- const isString = t => t[Kind] === "String";
23
- const isThis = t => t[Kind] === "This";
24
- const isTuple = t => t[Kind] === "Tuple";
25
- const isUnion = t => t[Kind] === "Union";
26
- // const isAny = (t: TSchema): t is TAny => t[Kind] === "Any";
27
11
 
28
- const isSensitive = t => isString(t) && t.sensitive === true;
29
- export const NetworkSchema = Type.Union([Type.Literal("mainnet"), Type.Literal("preview"), Type.Literal("preprod")]);
30
- export const MultisigScriptModule = Type.Module({
31
- MultisigScript: Type.Union([Type.Object({
32
- Signature: Type.Object({
33
- key_hash: Type.String()
34
- }, {
35
- ctor: 0n
36
- })
37
- }), Type.Object({
38
- AllOf: Type.Object({
39
- scripts: Type.Array(Type.Ref("MultisigScript"))
40
- }, {
41
- ctor: 1n
42
- })
43
- }), Type.Object({
44
- AnyOf: Type.Object({
45
- scripts: Type.Array(Type.Ref("MultisigScript"))
46
- }, {
47
- ctor: 2n
48
- })
49
- }), Type.Object({
50
- AtLeast: Type.Object({
51
- required: Type.BigInt(),
52
- scripts: Type.Array(Type.Ref("MultisigScript"))
53
- }, {
54
- ctor: 3n
55
- })
56
- }), Type.Object({
57
- Before: Type.Object({
58
- time: Type.BigInt()
59
- }, {
60
- ctor: 4n
61
- })
62
- }), Type.Object({
63
- After: Type.Object({
64
- time: Type.BigInt()
65
- }, {
66
- ctor: 5n
67
- })
68
- }), Type.Object({
69
- Script: Type.Object({
70
- script_hash: Type.String()
71
- }, {
72
- ctor: 6n
73
- })
74
- })])
75
- });
76
- export const MultisigScript = MultisigScriptModule.Import("MultisigScript");
77
- export const ProviderSettingsSchema = Type.Union([Type.Object({
78
- type: Type.Literal("blockfrost"),
79
- projectId: Type.String({
80
- minLength: 1,
81
- title: "Blockfrost Project ID"
82
- })
83
- }), Type.Object({
84
- type: Type.Literal("maestro"),
85
- apiKey: Type.String({
86
- minLength: 1,
87
- title: "Maestro API Key"
88
- })
89
- })]);
90
- export const WalletSettingsSchema = Type.Union([Type.Object({
91
- type: Type.Literal("hot"),
92
- privateKey: Type.String({
93
- minLength: 1,
94
- title: "Hot Wallet Private Key",
95
- sensitive: true
96
- })
97
- }), Type.Object({
98
- type: Type.Literal("cold"),
99
- address: Type.String({
100
- minLength: 1,
101
- title: "Cold Wallet Address"
102
- })
103
- })]);
12
+ // Re-export types from types.ts
13
+
14
+ export { UserCancelledError } from "./types.js";
15
+ import { UserCancelledError } from "./types.js";
16
+
17
+ // Re-export schemas from schemas.ts
18
+ export { NetworkSchema, MultisigScriptModule, MultisigScript, ProviderSettingsSchema, WalletSettingsSchema } from "./schemas.js";
19
+ // Import and re-export type guards
20
+ import { isOptional, isImport, isArray, isBigInt, isLiteral, isObject, isRef, isString, isThis, isTuple, isUnion, isSensitive } from "./type-guards.js";
21
+ export { isOptional, isImport, isArray, isBigInt, isLiteral, isObject, isRef, isString, isThis, isTuple, isUnion, isSensitive } from "./type-guards.js";
22
+
23
+ // Import schemas for use in this file
24
+
25
+ // Import and re-export wallet utilities
26
+ import { GetProvider as GetProviderFn, GetWallet as GetWalletFn, GetBlaze as GetBlazeFn, generateWalletFromMnemonic } from "./wallet.js";
27
+ export { GetProvider, GetWallet, GetBlaze } from "./wallet.js";
28
+
29
+ // Import encryption utilities
30
+ import { bigIntReplacer, bigIntReviver, encryptSensitiveFields, decryptSensitiveFields, maskSensitiveFields } from "./encryption.js";
31
+
32
+ // Import tx-dialog utilities
33
+ import { countSignatures, getRequiredSigners, getTxBodyHash, formatHash, mergeSignatures } from "./tx-dialog.js";
104
34
  export class Sprinkle {
105
35
  constructor(type, storagePath, options) {
106
36
  _defineProperty(this, "storagePath", void 0);
@@ -119,6 +49,16 @@ export class Sprinkle {
119
49
  this.options = options ?? {};
120
50
  }
121
51
 
52
+ // --- Current Profile Accessor ---
53
+
54
+ get currentProfile() {
55
+ if (!this.profileId) return null;
56
+ return {
57
+ id: this.profileId,
58
+ ...this.profileMeta
59
+ };
60
+ }
61
+
122
62
  // --- Profile path helpers ---
123
63
 
124
64
  static sanitizeProfileId(name) {
@@ -197,32 +137,42 @@ export class Sprinkle {
197
137
  meta: this.profileMeta,
198
138
  settings: settingsToSave,
199
139
  defaults: this.defaults
200
- }, Sprinkle.bigIntReplacer, 2);
140
+ }, bigIntReplacer, 2);
201
141
  fs.writeFileSync(filePath, jsonContent, "utf-8");
202
142
  }
203
143
  async promptProfileMeta(defaultName, defaultDescription) {
204
- const name = await input({
144
+ const name = await inputCancellable({
205
145
  message: "Profile name:",
206
146
  default: defaultName,
207
147
  validate: v => v.trim().length > 0 ? true : "Name cannot be empty"
208
148
  });
209
- const description = await input({
149
+ if (name === null) return null; // User cancelled
150
+ const description = await inputCancellable({
210
151
  message: "Profile description (optional):",
211
152
  default: defaultDescription ?? ""
212
153
  });
154
+ if (description === null) return null; // User cancelled
213
155
  return {
214
156
  name,
215
157
  description: description || undefined
216
158
  };
217
159
  }
218
160
  async createProfile() {
161
+ const result = await this.promptProfileMeta();
162
+ if (result === null) return; // User cancelled
219
163
  const {
220
164
  name,
221
165
  description
222
- } = await this.promptProfileMeta();
166
+ } = result;
223
167
  const profilesDir = Sprinkle.profilesDir(this.storagePath);
224
168
  const id = Sprinkle.findAvailableId(profilesDir, Sprinkle.sanitizeProfileId(name));
225
169
  const now = new Date().toISOString();
170
+
171
+ // Snapshot current state in case we need to restore on cancellation
172
+ const prevProfileId = this.profileId;
173
+ const prevProfileMeta = this.profileMeta;
174
+ const prevDefaults = this.defaults;
175
+ const prevSettings = this.settings;
226
176
  this.profileId = id;
227
177
  this.profileMeta = {
228
178
  name,
@@ -231,7 +181,19 @@ export class Sprinkle {
231
181
  updatedAt: now
232
182
  };
233
183
  this.defaults = {};
234
- this.settings = await this.FillInStruct(this.type);
184
+ try {
185
+ this.settings = await this.FillInStruct(this.type);
186
+ } catch (e) {
187
+ // Restore previous state on cancellation
188
+ if (e instanceof UserCancelledError) {
189
+ this.profileId = prevProfileId;
190
+ this.profileMeta = prevProfileMeta;
191
+ this.defaults = prevDefaults;
192
+ this.settings = prevSettings;
193
+ return;
194
+ }
195
+ throw e;
196
+ }
235
197
  this.saveProfile();
236
198
  fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), id, "utf-8");
237
199
  }
@@ -256,7 +218,7 @@ export class Sprinkle {
256
218
  fs.mkdirSync(profilesDir, {
257
219
  recursive: true
258
220
  });
259
- fs.writeFileSync(Sprinkle.profilePath(this.storagePath, "default"), JSON.stringify(profileData, Sprinkle.bigIntReplacer, 2), "utf-8");
221
+ fs.writeFileSync(Sprinkle.profilePath(this.storagePath, "default"), JSON.stringify(profileData, bigIntReplacer, 2), "utf-8");
260
222
  fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), "default", "utf-8");
261
223
  // Backup legacy file
262
224
  fs.renameSync(legacyPath, `${legacyPath}.bak`);
@@ -293,23 +255,26 @@ export class Sprinkle {
293
255
  }
294
256
  async selectProfile(profiles) {
295
257
  const available = profiles ?? this.scanProfiles();
296
- const selection = await select({
258
+ const selection = await selectCancellable({
297
259
  message: "Select a profile:",
298
260
  choices: available.map(p => ({
299
261
  name: p.meta.description ? `${p.meta.name} - ${p.meta.description}` : p.meta.name,
300
262
  value: p.id
301
263
  }))
302
264
  });
265
+ if (selection === null) return; // User cancelled
303
266
  await this.loadProfile(selection);
304
267
  }
305
268
 
306
269
  // --- Profile management (CRUD) ---
307
270
 
308
271
  async duplicateProfile() {
272
+ const result = await this.promptProfileMeta(`${this.profileMeta.name} (copy)`, this.profileMeta.description);
273
+ if (result === null) return; // User cancelled
309
274
  const {
310
275
  name,
311
276
  description
312
- } = await this.promptProfileMeta(`${this.profileMeta.name} (copy)`, this.profileMeta.description);
277
+ } = result;
313
278
  const profilesDir = Sprinkle.profilesDir(this.storagePath);
314
279
  const id = Sprinkle.findAvailableId(profilesDir, Sprinkle.sanitizeProfileId(name));
315
280
  const now = new Date().toISOString();
@@ -324,15 +289,17 @@ export class Sprinkle {
324
289
  },
325
290
  settings: settingsToSave,
326
291
  defaults: this.defaults
327
- }, Sprinkle.bigIntReplacer, 2);
292
+ }, bigIntReplacer, 2);
328
293
  fs.writeFileSync(path.join(profilesDir, `${id}.json`), jsonContent, "utf-8");
329
294
  console.log(`Profile "${name}" created as a copy.`);
330
295
  }
331
296
  async renameProfile() {
297
+ const result = await this.promptProfileMeta(this.profileMeta.name, this.profileMeta.description);
298
+ if (result === null) return; // User cancelled
332
299
  const {
333
300
  name,
334
301
  description
335
- } = await this.promptProfileMeta(this.profileMeta.name, this.profileMeta.description);
302
+ } = result;
336
303
  const newId = Sprinkle.sanitizeProfileId(name);
337
304
  const oldId = this.profileId;
338
305
  this.profileMeta.name = name;
@@ -360,25 +327,26 @@ export class Sprinkle {
360
327
  console.log("Cannot delete the only profile.");
361
328
  return;
362
329
  }
363
- const toDelete = await select({
330
+ const toDelete = await selectCancellable({
364
331
  message: "Select a profile to delete:",
365
- choices: [...others.map(p => ({
332
+ choices: others.map(p => ({
366
333
  name: p.meta.description ? `${p.meta.name} - ${p.meta.description}` : p.meta.name,
367
334
  value: p.id
368
- })), {
369
- name: "Cancel",
370
- value: "__cancel__"
371
- }]
335
+ }))
372
336
  });
373
- if (toDelete === "__cancel__") return;
374
- const profileToDelete = others.find(p => p.id === toDelete);
375
- const confirmed = await confirm({
376
- message: `Delete profile "${profileToDelete?.meta.name ?? toDelete}"? This cannot be undone.`,
337
+ if (toDelete === null) return; // User cancelled
338
+
339
+ const profileId = toDelete;
340
+ const profileToDelete = others.find(p => p.id === profileId);
341
+ const confirmed = await confirmCancellable({
342
+ message: `Delete profile "${profileToDelete?.meta.name ?? profileId}"? This cannot be undone.`,
377
343
  default: false
378
344
  });
345
+ if (confirmed === null) return; // User cancelled
346
+
379
347
  if (confirmed) {
380
- fs.unlinkSync(Sprinkle.profilePath(this.storagePath, toDelete));
381
- console.log(`Profile "${profileToDelete?.meta.name ?? toDelete}" deleted.`);
348
+ fs.unlinkSync(Sprinkle.profilePath(this.storagePath, profileId));
349
+ console.log(`Profile "${profileToDelete?.meta.name ?? profileId}" deleted.`);
382
350
  }
383
351
  }
384
352
 
@@ -419,14 +387,24 @@ export class Sprinkle {
419
387
  value: -1
420
388
  });
421
389
  }
422
- const selection = await select({
390
+ const selectionResult = await selectCancellable({
423
391
  message: "Select an option:",
424
392
  choices: choices
425
393
  });
394
+ // Handle escape (null) as Back
395
+ if (selectionResult === null) {
396
+ return;
397
+ }
398
+ const selection = selectionResult;
426
399
  if (selection === -5) {
427
400
  const settingsMenu = {
428
401
  title: "Settings & Profiles",
429
402
  items: [{
403
+ title: "View settings",
404
+ action: async () => {
405
+ console.log(JSON.stringify(this.getDisplaySettings(), bigIntReplacer, 2));
406
+ }
407
+ }, {
430
408
  title: "Edit settings",
431
409
  action: async () => {
432
410
  this.settings = await this.EditStruct(this.type, this.settings);
@@ -494,40 +472,13 @@ export class Sprinkle {
494
472
  return sprinkle;
495
473
  }
496
474
  static async GetProvider(network, settings) {
497
- switch (settings.type) {
498
- case "blockfrost":
499
- return new Blockfrost({
500
- network: `cardano-${network}`,
501
- projectId: settings.projectId
502
- });
503
- case "maestro":
504
- // Dynamic import - Maestro may or may not be exported depending on @blaze-cardano/query version
505
- const queryModule = await import("@blaze-cardano/query");
506
- if (!queryModule.Maestro) {
507
- throw new Error("Maestro is not available in the installed version of @blaze-cardano/query. Please install a version that includes Maestro support.");
508
- }
509
- return new queryModule.Maestro({
510
- network: network,
511
- apiKey: settings.apiKey
512
- });
513
- default:
514
- throw new Error("Invalid provider type");
515
- }
475
+ return GetProviderFn(network, settings);
516
476
  }
517
477
  static async GetWallet(settings, provider) {
518
- switch (settings.type) {
519
- case "hot":
520
- return HotWallet.fromMasterkey(Core.Bip32PrivateKeyHex(settings.privateKey), provider, provider.network);
521
- case "cold":
522
- return new ColdWallet(Core.Address.fromBech32(settings.address), provider.network, provider);
523
- default:
524
- throw new Error("Invalid wallet type");
525
- }
478
+ return GetWalletFn(settings, provider);
526
479
  }
527
480
  static async GetBlaze(network, providerSettings, walletSettings) {
528
- const provider = await Sprinkle.GetProvider(network, providerSettings);
529
- const wallet = await Sprinkle.GetWallet(walletSettings, provider);
530
- return Blaze.from(provider, wallet);
481
+ return GetBlazeFn(network, providerSettings, walletSettings);
531
482
  }
532
483
 
533
484
  /**
@@ -536,30 +487,10 @@ export class Sprinkle {
536
487
  * @returns The Bip32PrivateKey hex string for storage
537
488
  */
538
489
  static async generateWalletFromMnemonic() {
539
- const mnemonic = Core.generateMnemonic(wordlist, 256); // 24 words
540
- const words = mnemonic.split(" ");
541
- console.log("\n=== NEW WALLET GENERATED ===\n");
542
- console.log("IMPORTANT: Save these 24 words in a secure location.");
543
- console.log("This is the ONLY way to recover your wallet.\n");
544
-
545
- // Display in 4 columns
546
- for (let i = 0; i < 6; i++) {
547
- console.log(`${(i + 1).toString().padStart(2)}. ${words[i].padEnd(12)} ` + `${(i + 7).toString().padStart(2)}. ${words[i + 6].padEnd(12)} ` + `${(i + 13).toString().padStart(2)}. ${words[i + 12].padEnd(12)} ` + `${(i + 19).toString().padStart(2)}. ${words[i + 18]}`);
548
- }
549
- console.log("");
550
- const confirmed = await confirm({
551
- message: "Have you saved your recovery phrase?",
552
- default: false
553
- });
554
- if (!confirmed) {
555
- throw new Error("Wallet generation cancelled - recovery phrase not saved");
556
- }
557
- const entropy = Core.mnemonicToEntropy(mnemonic, wordlist);
558
- const masterKey = Core.Bip32PrivateKey.fromBip39Entropy(Buffer.from(entropy), "");
559
- return masterKey.hex();
490
+ return generateWalletFromMnemonic();
560
491
  }
561
492
  static async SearchSelect(opts) {
562
- return search(opts);
493
+ return searchCancellable(opts);
563
494
  }
564
495
  static SettingsPath(storagePath) {
565
496
  return `${storagePath}${path.sep}settings.json`;
@@ -583,155 +514,21 @@ export class Sprinkle {
583
514
  }
584
515
  }
585
516
  static bigIntReviver(key, value) {
586
- if (typeof value === "string" && /^\d+n$/.test(value)) {
587
- return BigInt(value.slice(0, -1));
588
- }
589
- return value;
590
- }
591
- static bigIntReplacer(_key, value) {
592
- return typeof value === "bigint" ? `${value.toString()}n` : value;
593
- }
594
- static collectSensitivePaths(type, prefix = "") {
595
- const paths = [];
596
- if (isObject(type)) {
597
- const fields = type["properties"];
598
- for (const [field, fieldType] of Object.entries(fields)) {
599
- const fieldPath = prefix ? `${prefix}.${field}` : field;
600
- if (isSensitive(fieldType)) {
601
- paths.push(fieldPath);
602
- }
603
- paths.push(...Sprinkle.collectSensitivePaths(fieldType, fieldPath));
604
- }
605
- }
606
- if (isUnion(type)) {
607
- for (const variant of type.anyOf) {
608
- paths.push(...Sprinkle.collectSensitivePaths(variant, prefix));
609
- }
610
- }
611
- return paths;
612
- }
613
- static getNestedValue(obj, path) {
614
- return path.split(".").reduce((o, k) => o?.[k], obj);
615
- }
616
- static setNestedValue(obj, path, value) {
617
- const keys = path.split(".");
618
- const last = keys.pop();
619
- const parent = keys.reduce((o, k) => o?.[k], obj);
620
- if (parent && typeof parent === "object") {
621
- parent[last] = value;
622
- }
517
+ return bigIntReviver(key, value);
623
518
  }
624
519
  encryptSettings(settings) {
625
520
  if (!this.options.encryption) return settings;
626
- const clone = JSON.parse(JSON.stringify(settings, Sprinkle.bigIntReplacer), Sprinkle.bigIntReviver);
627
- const sensitivePaths = Sprinkle.collectSensitivePaths(this.type);
628
- for (const p of sensitivePaths) {
629
- const value = Sprinkle.getNestedValue(clone, p);
630
- if (typeof value === "string" && value.length > 0) {
631
- Sprinkle.setNestedValue(clone, p, this.options.encryption.encrypt(value));
632
- }
633
- }
634
- return clone;
521
+ return encryptSensitiveFields(settings, this.type, this.options.encryption);
635
522
  }
636
523
  async decryptSettings(settings) {
637
524
  if (!this.options.encryption) return settings;
638
- const clone = JSON.parse(JSON.stringify(settings, Sprinkle.bigIntReplacer), Sprinkle.bigIntReviver);
639
- const sensitivePaths = Sprinkle.collectSensitivePaths(this.type);
640
- for (const p of sensitivePaths) {
641
- const value = Sprinkle.getNestedValue(clone, p);
642
- if (typeof value === "string" && value.length > 0) {
643
- Sprinkle.setNestedValue(clone, p, await this.options.encryption.decrypt(value));
644
- }
645
- }
646
- return clone;
525
+ return decryptSensitiveFields(settings, this.type, this.options.encryption);
647
526
  }
648
527
  saveSettings() {
649
528
  this.saveProfile();
650
529
  }
651
-
652
- // --- TxDialog Helpers ---
653
-
654
- /**
655
- * Get the payment key hash from a HotWallet's first address
656
- */
657
- async getWalletPaymentKeyHash(wallet) {
658
- try {
659
- const addresses = await wallet.getUsedAddresses();
660
- const address = addresses[0];
661
- if (!address) return null;
662
- const paymentCredential = address.asBase()?.getPaymentCredential();
663
- return paymentCredential?.hash?.toString() ?? null;
664
- } catch {
665
- return null;
666
- }
667
- }
668
-
669
- /**
670
- * Count the number of vkey signatures in a transaction's witness set
671
- */
672
- countSignatures(tx) {
673
- const vkeys = tx.witnessSet().vkeys();
674
- return vkeys ? vkeys.size() : 0;
675
- }
676
-
677
- /**
678
- * Check if a specific public key has already signed the transaction
679
- * Compares by vkey (public key bytes)
680
- */
681
- hasVkeySigned(tx, vkeyHex) {
682
- const vkeys = tx.witnessSet().vkeys();
683
- if (!vkeys) return false;
684
- const vkeyArray = vkeys.toCore();
685
- return vkeyArray.some(([vkey]) => vkey === vkeyHex);
686
- }
687
-
688
- /**
689
- * Get the list of required signer key hashes from the transaction body
690
- */
691
- getRequiredSigners(tx) {
692
- const requiredSigners = tx.body().requiredSigners();
693
- if (!requiredSigners) return [];
694
- return Array.from(requiredSigners.values()).map(s => s.toString());
695
- }
696
-
697
- /**
698
- * Compute the transaction body hash for display
699
- */
700
- getTxBodyHash(tx) {
701
- const bodyCbor = tx.body().toCbor();
702
- return blake2b_256(bodyCbor);
703
- }
704
-
705
- /**
706
- * Format a hash for display: first 8 chars + ... + last 8 chars
707
- */
708
- formatHash(hash) {
709
- if (hash.length <= 20) return hash;
710
- return `${hash.slice(0, 8)}...${hash.slice(-8)}`;
711
- }
712
-
713
- /**
714
- * Merge signatures from source transaction into target transaction.
715
- * Prevents duplicate signatures by comparing vkey (public key).
716
- * Returns the count of newly added signatures.
717
- */
718
- mergeSignatures(target, source) {
719
- const targetWs = target.witnessSet();
720
- const sourceWs = source.witnessSet();
721
- const targetVkeys = targetWs.vkeys()?.toCore() ?? [];
722
- const sourceVkeys = sourceWs.vkeys()?.toCore() ?? [];
723
-
724
- // Find vkeys in source that aren't in target (by comparing public key)
725
- const existingPubKeys = new Set(targetVkeys.map(([vkey]) => vkey));
726
- const newVkeys = sourceVkeys.filter(([vkey]) => !existingPubKeys.has(vkey));
727
- if (newVkeys.length === 0) {
728
- return 0;
729
- }
730
-
731
- // Merge the new vkeys into target
732
- targetWs.setVkeys(CborSet.fromCore([...targetVkeys, ...newVkeys], VkeyWitness.fromCore));
733
- target.setWitnessSet(targetWs);
734
- return newVkeys.length;
530
+ getDisplaySettings() {
531
+ return maskSensitiveFields(this.settings, this.type);
735
532
  }
736
533
  async TxDialog(blaze, tx, opts) {
737
534
  let currentTx = tx;
@@ -759,16 +556,16 @@ export class Sprinkle {
759
556
  }
760
557
  while (true) {
761
558
  // Display transaction status
762
- const txHash = this.getTxBodyHash(currentTx);
763
- const sigCount = this.countSignatures(currentTx);
764
- const requiredSigners = this.getRequiredSigners(currentTx);
559
+ const txHash = getTxBodyHash(currentTx);
560
+ const sigCount = countSignatures(currentTx);
561
+ const requiredSigners = getRequiredSigners(currentTx);
765
562
  console.log("");
766
- console.log(`Transaction: ${this.formatHash(txHash)}`);
563
+ console.log(`Transaction: ${formatHash(txHash)}`);
767
564
  if (requiredSigners.length > 0) {
768
565
  console.log(`Signatures: ${sigCount} of ${requiredSigners.length} required`);
769
566
  console.log("Required signers:");
770
567
  for (const signer of requiredSigners) {
771
- console.log(` - ${this.formatHash(signer)}`);
568
+ console.log(` - ${formatHash(signer)}`);
772
569
  }
773
570
  } else {
774
571
  console.log(`Signatures: ${sigCount}`);
@@ -818,11 +615,25 @@ export class Sprinkle {
818
615
  name: "Cancel",
819
616
  value: "cancel"
820
617
  });
821
- const selection = await select({
618
+ const selection = await selectCancellable({
822
619
  message: "Select an option:",
823
620
  choices
824
621
  });
825
622
 
623
+ // Handle escape/cancel as cancel action
624
+ if (selection === null) {
625
+ if (hasSignedThisSession) {
626
+ return {
627
+ action: "signed",
628
+ tx: currentTx
629
+ };
630
+ }
631
+ return {
632
+ action: "cancelled",
633
+ tx: currentTx
634
+ };
635
+ }
636
+
826
637
  // Handle selection
827
638
  if (selection === "sign") {
828
639
  if (opts?.beforeSign) {
@@ -888,7 +699,7 @@ export class Sprinkle {
888
699
  } else {
889
700
  const signedTx = await blaze.signTransaction(currentTx);
890
701
  // Merge signatures from signed tx into current tx
891
- const added = this.mergeSignatures(currentTx, signedTx);
702
+ const added = mergeSignatures(currentTx, signedTx);
892
703
  if (added > 0) {
893
704
  console.log(`Added ${added} signature(s).`);
894
705
  hasSignedThisSession = true;
@@ -920,10 +731,10 @@ export class Sprinkle {
920
731
  continue;
921
732
  }
922
733
  if (selection === "import") {
923
- const cborInput = await input({
734
+ const cborInput = await inputCancellable({
924
735
  message: "Paste transaction CBOR (hex):"
925
736
  });
926
- if (!cborInput || cborInput.trim() === "") {
737
+ if (cborInput === null || cborInput.trim() === "") {
927
738
  console.log("No CBOR provided.");
928
739
  continue;
929
740
  }
@@ -931,21 +742,21 @@ export class Sprinkle {
931
742
  const importedTx = Core.Transaction.fromCbor(TxCBOR(cborInput.trim()));
932
743
 
933
744
  // Validate body hash matches
934
- const currentHash = this.getTxBodyHash(currentTx);
935
- const importedHash = this.getTxBodyHash(importedTx);
745
+ const currentHash = getTxBodyHash(currentTx);
746
+ const importedHash = getTxBodyHash(importedTx);
936
747
  if (currentHash !== importedHash) {
937
- const proceed = await confirm({
938
- message: `Warning: Imported transaction has different body hash.\nCurrent: ${this.formatHash(currentHash)}\nImported: ${this.formatHash(importedHash)}\nProceed anyway?`,
748
+ const proceed = await confirmCancellable({
749
+ message: `Warning: Imported transaction has different body hash.\nCurrent: ${formatHash(currentHash)}\nImported: ${formatHash(importedHash)}\nProceed anyway?`,
939
750
  default: false
940
751
  });
941
- if (!proceed) {
752
+ if (proceed === null || !proceed) {
942
753
  console.log("Import cancelled.");
943
754
  continue;
944
755
  }
945
756
  }
946
757
 
947
758
  // Merge signatures
948
- const added = this.mergeSignatures(currentTx, importedTx);
759
+ const added = mergeSignatures(currentTx, importedTx);
949
760
  const sourceVkeys = importedTx.witnessSet().vkeys();
950
761
  const sourceCount = sourceVkeys ? sourceVkeys.size() : 0;
951
762
  const skipped = sourceCount - added;
@@ -964,13 +775,13 @@ export class Sprinkle {
964
775
  continue;
965
776
  }
966
777
  if (selection === "submit") {
967
- const sigCount = this.countSignatures(currentTx);
778
+ const sigCount = countSignatures(currentTx);
968
779
  if (sigCount === 0) {
969
- const proceed = await confirm({
780
+ const proceed = await confirmCancellable({
970
781
  message: "Warning: Transaction has no signatures. Submit anyway?",
971
782
  default: false
972
783
  });
973
- if (!proceed) {
784
+ if (proceed === null || !proceed) {
974
785
  continue;
975
786
  }
976
787
  }
@@ -1059,7 +870,7 @@ export class Sprinkle {
1059
870
  return this._fillInStruct(resolvedType, path, defs, def);
1060
871
  }
1061
872
  if (isOptional(type)) {
1062
- const shouldSet = await select({
873
+ const shouldSet = await selectCancellable({
1063
874
  message: Sprinkle.ExtractMessage(type, `Set value for ${path.join(".")}?`),
1064
875
  choices: [{
1065
876
  name: "Yes",
@@ -1070,6 +881,9 @@ export class Sprinkle {
1070
881
  }],
1071
882
  default: def !== undefined
1072
883
  });
884
+ if (shouldSet === null) {
885
+ throw new UserCancelledError();
886
+ }
1073
887
  if (!shouldSet) {
1074
888
  return undefined;
1075
889
  }
@@ -1089,17 +903,21 @@ export class Sprinkle {
1089
903
  value: variant
1090
904
  });
1091
905
  }
1092
- const selection = await select({
906
+ const selectionResult = await selectCancellable({
1093
907
  message: Sprinkle.ExtractMessage(resolved, `Enter a choice for ${path.join(".")}`),
1094
908
  choices: choices,
1095
909
  default: def ? `${def}` : undefined
1096
910
  });
911
+ if (selectionResult === null) {
912
+ throw new UserCancelledError();
913
+ }
914
+ const selection = selectionResult;
1097
915
  return this._fillInStruct(selection, path, defs);
1098
916
  }
1099
917
  if (isString(type)) {
1100
918
  // Special handling for hot wallet private key - offer generation option
1101
919
  if (type.title === "Hot Wallet Private Key") {
1102
- const choice = await select({
920
+ const choice = await selectCancellable({
1103
921
  message: "Hot wallet setup:",
1104
922
  choices: [{
1105
923
  name: "Enter existing private key",
@@ -1109,33 +927,44 @@ export class Sprinkle {
1109
927
  value: "generate"
1110
928
  }]
1111
929
  });
930
+ if (choice === null) {
931
+ throw new UserCancelledError();
932
+ }
1112
933
  if (choice === "generate") {
1113
934
  return Sprinkle.generateWalletFromMnemonic();
1114
935
  }
1115
936
  // Fall through to password prompt for "existing" choice
1116
- const answer = await password({
937
+ const answer = await passwordCancellable({
1117
938
  message: "Enter your private key:"
1118
939
  });
940
+ if (answer === null) {
941
+ throw new UserCancelledError();
942
+ }
1119
943
  return answer;
1120
944
  }
1121
945
  const defaultString = def ? def : this.defaults["string"];
1122
946
  const message = Sprinkle.ExtractMessage(type, `Enter a string for ${path.join(".")}`);
1123
947
  let answer;
1124
948
  if (isSensitive(type)) {
1125
- answer = await password({
949
+ answer = await passwordCancellable({
1126
950
  message
1127
951
  });
1128
952
  } else {
1129
- answer = await input({
953
+ answer = await inputCancellable({
1130
954
  message,
1131
955
  default: defaultString
1132
956
  });
1133
- this.defaults["string"] = answer;
957
+ if (answer !== null) {
958
+ this.defaults["string"] = answer;
959
+ }
960
+ }
961
+ if (answer === null) {
962
+ throw new UserCancelledError();
1134
963
  }
1135
964
  return answer;
1136
965
  }
1137
966
  if (isBigInt(type)) {
1138
- const answer = await input({
967
+ const answer = await inputCancellable({
1139
968
  message: Sprinkle.ExtractMessage(type, `Enter a bigint for ${path.join(".")}`),
1140
969
  default: def ? def.toString() : undefined,
1141
970
  validate: s => {
@@ -1147,6 +976,9 @@ export class Sprinkle {
1147
976
  }
1148
977
  }
1149
978
  });
979
+ if (answer === null) {
980
+ throw new UserCancelledError();
981
+ }
1150
982
  return BigInt(answer);
1151
983
  }
1152
984
  if (isLiteral(type)) {
@@ -1170,7 +1002,7 @@ export class Sprinkle {
1170
1002
  while (addMore) {
1171
1003
  const itemValue = await this._fillInStruct(itemType, path.concat([`[${arr.length}]`]), defs);
1172
1004
  arr.push(itemValue);
1173
- const continueAnswer = await select({
1005
+ const continueAnswer = await selectCancellable({
1174
1006
  message: `Add another item to ${path.join(".")}?`,
1175
1007
  choices: [{
1176
1008
  name: "Yes",
@@ -1180,6 +1012,9 @@ export class Sprinkle {
1180
1012
  value: false
1181
1013
  }]
1182
1014
  });
1015
+ if (continueAnswer === null) {
1016
+ throw new UserCancelledError();
1017
+ }
1183
1018
  addMore = continueAnswer;
1184
1019
  }
1185
1020
  return arr;