@sundaeswap/sprinkles 0.2.1 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sundaeswap/sprinkles",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "A TypeScript library for building interactive CLI menus and TUI applications with TypeBox schema validation",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -1,13 +1,17 @@
1
- import { describe, expect, test, mock, beforeEach } from "bun:test";
2
- import { Sprinkle, Type } from "../index.js";
1
+ import { describe, expect, test, mock, beforeEach, spyOn } from "bun:test";
2
+ import { Sprinkle, Type, WalletSettingsSchema } from "../index.js";
3
3
 
4
4
  // Mock @inquirer/prompts
5
5
  const mockSelect = mock();
6
6
  const mockInput = mock();
7
+ const mockPassword = mock();
8
+ const mockConfirm = mock();
7
9
 
8
10
  mock.module("@inquirer/prompts", () => ({
9
11
  select: mockSelect,
10
12
  input: mockInput,
13
+ password: mockPassword,
14
+ confirm: mockConfirm,
11
15
  }));
12
16
 
13
17
  describe("FillInStruct", () => {
@@ -204,4 +208,53 @@ describe("FillInStruct", () => {
204
208
  const result = await sprinkle.FillInStruct(schema);
205
209
  expect(result).toEqual({ asset: ["policy123", "token456"] });
206
210
  });
211
+
212
+ test("hot wallet private key shows setup choice", async () => {
213
+ const schema = Type.String({ title: "Hot Wallet Private Key" });
214
+
215
+ // Select "existing" option
216
+ mockSelect.mockResolvedValueOnce("existing");
217
+ mockPassword.mockResolvedValueOnce("my-private-key");
218
+
219
+ const result = await sprinkle.FillInStruct(schema);
220
+
221
+ // Verify select was called with correct options
222
+ expect(mockSelect.mock.calls[0][0].message).toBe("Hot wallet setup:");
223
+ expect(mockSelect.mock.calls[0][0].choices).toEqual([
224
+ { name: "Enter existing private key", value: "existing" },
225
+ { name: "Generate new wallet", value: "generate" },
226
+ ]);
227
+ expect(result).toBe("my-private-key");
228
+ });
229
+
230
+ test("hot wallet existing key prompts for password", async () => {
231
+ const schema = Type.String({ title: "Hot Wallet Private Key" });
232
+
233
+ mockSelect.mockResolvedValueOnce("existing");
234
+ mockPassword.mockResolvedValueOnce("deadbeef1234");
235
+
236
+ const result = await sprinkle.FillInStruct(schema);
237
+
238
+ expect(mockPassword).toHaveBeenCalledWith({
239
+ message: "Enter your private key:",
240
+ });
241
+ expect(result).toBe("deadbeef1234");
242
+ });
243
+
244
+ test("full wallet settings schema with existing key", async () => {
245
+ // Select "hot" variant
246
+ mockSelect.mockImplementationOnce(async (opts: any) => {
247
+ return opts.choices[0].value; // hot wallet object
248
+ });
249
+ // Select "existing" key option
250
+ mockSelect.mockResolvedValueOnce("existing");
251
+ mockPassword.mockResolvedValueOnce("abc123privatekey");
252
+
253
+ const result = await sprinkle.FillInStruct(WalletSettingsSchema);
254
+
255
+ expect(result).toEqual({
256
+ type: "hot",
257
+ privateKey: "abc123privatekey",
258
+ });
259
+ });
207
260
  });
@@ -6,7 +6,13 @@ import {
6
6
  HotWallet,
7
7
  type Wallet,
8
8
  } from "@blaze-cardano/sdk";
9
- import { CborSet, VkeyWitness, blake2b_256, TxCBOR } from "@blaze-cardano/core";
9
+ import {
10
+ CborSet,
11
+ VkeyWitness,
12
+ blake2b_256,
13
+ TxCBOR,
14
+ wordlist,
15
+ } from "@blaze-cardano/core";
10
16
  import { confirm, input, password, search, select } from "@inquirer/prompts";
11
17
  import {
12
18
  Kind,
@@ -167,7 +173,11 @@ export const ProviderSettingsSchema = Type.Union([
167
173
  export const WalletSettingsSchema = Type.Union([
168
174
  Type.Object({
169
175
  type: Type.Literal("hot"),
170
- privateKey: Type.String({ minLength: 1, title: "Hot Wallet Private Key" }),
176
+ privateKey: Type.String({
177
+ minLength: 1,
178
+ title: "Hot Wallet Private Key",
179
+ sensitive: true,
180
+ }),
171
181
  }),
172
182
  Type.Object({
173
183
  type: Type.Literal("cold"),
@@ -686,6 +696,47 @@ export class Sprinkle<S extends TSchema> {
686
696
  return Blaze.from(provider, wallet);
687
697
  }
688
698
 
699
+ /**
700
+ * Generates a new wallet from a BIP39 mnemonic phrase.
701
+ * Displays the 24-word recovery phrase and requires user confirmation.
702
+ * @returns The Bip32PrivateKey hex string for storage
703
+ */
704
+ 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();
738
+ }
739
+
689
740
  static async SearchSelect<T>(opts: {
690
741
  message: string;
691
742
  source: (
@@ -1321,6 +1372,24 @@ export class Sprinkle<S extends TSchema> {
1321
1372
  }
1322
1373
 
1323
1374
  if (isString(type)) {
1375
+ // Special handling for hot wallet private key - offer generation option
1376
+ if (type.title === "Hot Wallet Private Key") {
1377
+ const choice = await select({
1378
+ message: "Hot wallet setup:",
1379
+ choices: [
1380
+ { name: "Enter existing private key", value: "existing" },
1381
+ { name: "Generate new wallet", value: "generate" },
1382
+ ],
1383
+ });
1384
+
1385
+ if (choice === "generate") {
1386
+ return Sprinkle.generateWalletFromMnemonic() as Promise<TExact<U>>;
1387
+ }
1388
+ // Fall through to password prompt for "existing" choice
1389
+ const answer = await password({ message: "Enter your private key:" });
1390
+ return answer as TExact<U>;
1391
+ }
1392
+
1324
1393
  const defaultString = (def ? def : this.defaults["string"]) as
1325
1394
  | string
1326
1395
  | undefined;