@sundaeswap/sprinkles 0.2.1 → 0.4.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/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +52 -1
- package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js +120 -0
- package/dist/cjs/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
- package/dist/cjs/Sprinkle/index.js +73 -1
- package/dist/cjs/Sprinkle/index.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +53 -2
- package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
- package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js +120 -0
- package/dist/esm/Sprinkle/__tests__/settings-persistence.test.js.map +1 -1
- package/dist/esm/Sprinkle/index.js +74 -2
- package/dist/esm/Sprinkle/index.js.map +1 -1
- package/dist/types/Sprinkle/index.d.ts +11 -0
- package/dist/types/Sprinkle/index.d.ts.map +1 -1
- package/dist/types/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/Sprinkle/__tests__/fill-in-struct.test.ts +55 -2
- package/src/Sprinkle/__tests__/settings-persistence.test.ts +108 -0
- package/src/Sprinkle/index.ts +100 -2
package/package.json
CHANGED
|
@@ -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
|
});
|
|
@@ -178,4 +178,112 @@ describe("Settings Persistence", () => {
|
|
|
178
178
|
expect(sprinkle.defaults).toEqual({ string: "old" });
|
|
179
179
|
expect(sprinkle.profileMeta.name).toBe("Default");
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
describe("currentProfile", () => {
|
|
183
|
+
test("returns null when no profile loaded", () => {
|
|
184
|
+
const schema = Type.Object({ name: Type.String() });
|
|
185
|
+
const sprinkle = new Sprinkle(schema, tmpDir);
|
|
186
|
+
|
|
187
|
+
expect(sprinkle.currentProfile).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("returns profile info when loaded", () => {
|
|
191
|
+
const schema = Type.Object({ name: Type.String() });
|
|
192
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
193
|
+
|
|
194
|
+
const profile = sprinkle.currentProfile;
|
|
195
|
+
expect(profile).not.toBeNull();
|
|
196
|
+
expect(profile!.id).toBe("test");
|
|
197
|
+
expect(profile!.name).toBe("Test");
|
|
198
|
+
expect(profile!.createdAt).toBeDefined();
|
|
199
|
+
expect(profile!.updatedAt).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("includes description when present", () => {
|
|
203
|
+
const schema = Type.Object({ name: Type.String() });
|
|
204
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
205
|
+
sprinkle.profileMeta.description = "Test profile description";
|
|
206
|
+
|
|
207
|
+
const profile = sprinkle.currentProfile;
|
|
208
|
+
expect(profile!.description).toBe("Test profile description");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("has undefined description when not set", () => {
|
|
212
|
+
const schema = Type.Object({ name: Type.String() });
|
|
213
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
214
|
+
delete sprinkle.profileMeta.description;
|
|
215
|
+
|
|
216
|
+
const profile = sprinkle.currentProfile;
|
|
217
|
+
expect(profile!.description).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("getDisplaySettings", () => {
|
|
222
|
+
test("masks sensitive fields", () => {
|
|
223
|
+
const schema = Type.Object({
|
|
224
|
+
name: Type.String(),
|
|
225
|
+
secret: Type.String({ sensitive: true }),
|
|
226
|
+
});
|
|
227
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
228
|
+
sprinkle.settings = { name: "visible", secret: "hidden" } as any;
|
|
229
|
+
|
|
230
|
+
const display = sprinkle.getDisplaySettings();
|
|
231
|
+
expect(display.name).toBe("visible");
|
|
232
|
+
expect(display.secret).toBe("********");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("preserves non-sensitive fields", () => {
|
|
236
|
+
const schema = Type.Object({
|
|
237
|
+
name: Type.String(),
|
|
238
|
+
count: Type.Number(),
|
|
239
|
+
});
|
|
240
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
241
|
+
sprinkle.settings = { name: "test", count: 42 } as any;
|
|
242
|
+
|
|
243
|
+
const display = sprinkle.getDisplaySettings();
|
|
244
|
+
expect(display.name).toBe("test");
|
|
245
|
+
expect(display.count).toBe(42);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("preserves BigInt values", () => {
|
|
249
|
+
const schema = Type.Object({
|
|
250
|
+
amount: Type.BigInt(),
|
|
251
|
+
secret: Type.String({ sensitive: true }),
|
|
252
|
+
});
|
|
253
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
254
|
+
sprinkle.settings = { amount: 42n, secret: "hidden" } as any;
|
|
255
|
+
|
|
256
|
+
const display = sprinkle.getDisplaySettings();
|
|
257
|
+
expect(display.amount).toBe(42n);
|
|
258
|
+
expect(display.secret).toBe("********");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("masks nested sensitive fields", () => {
|
|
262
|
+
const schema = Type.Object({
|
|
263
|
+
wallet: Type.Object({
|
|
264
|
+
key: Type.String({ sensitive: true }),
|
|
265
|
+
address: Type.String(),
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
269
|
+
sprinkle.settings = {
|
|
270
|
+
wallet: { key: "secret", address: "addr1..." },
|
|
271
|
+
} as any;
|
|
272
|
+
|
|
273
|
+
const display = sprinkle.getDisplaySettings();
|
|
274
|
+
expect(display.wallet.key).toBe("********");
|
|
275
|
+
expect(display.wallet.address).toBe("addr1...");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("does not mask empty sensitive fields", () => {
|
|
279
|
+
const schema = Type.Object({
|
|
280
|
+
secret: Type.String({ sensitive: true }),
|
|
281
|
+
});
|
|
282
|
+
const sprinkle = withProfile(new Sprinkle(schema, tmpDir));
|
|
283
|
+
sprinkle.settings = { secret: "" } as any;
|
|
284
|
+
|
|
285
|
+
const display = sprinkle.getDisplaySettings();
|
|
286
|
+
expect(display.secret).toBe("");
|
|
287
|
+
});
|
|
288
|
+
});
|
|
181
289
|
});
|
package/src/Sprinkle/index.ts
CHANGED
|
@@ -6,7 +6,13 @@ import {
|
|
|
6
6
|
HotWallet,
|
|
7
7
|
type Wallet,
|
|
8
8
|
} from "@blaze-cardano/sdk";
|
|
9
|
-
import {
|
|
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,
|
|
@@ -68,6 +74,10 @@ export interface IProfileMeta {
|
|
|
68
74
|
updatedAt: string;
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
export interface ICurrentProfile extends IProfileMeta {
|
|
78
|
+
id: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
export interface IProfileEntry {
|
|
72
82
|
id: string;
|
|
73
83
|
meta: IProfileMeta;
|
|
@@ -167,7 +177,11 @@ export const ProviderSettingsSchema = Type.Union([
|
|
|
167
177
|
export const WalletSettingsSchema = Type.Union([
|
|
168
178
|
Type.Object({
|
|
169
179
|
type: Type.Literal("hot"),
|
|
170
|
-
privateKey: Type.String({
|
|
180
|
+
privateKey: Type.String({
|
|
181
|
+
minLength: 1,
|
|
182
|
+
title: "Hot Wallet Private Key",
|
|
183
|
+
sensitive: true,
|
|
184
|
+
}),
|
|
171
185
|
}),
|
|
172
186
|
Type.Object({
|
|
173
187
|
type: Type.Literal("cold"),
|
|
@@ -201,6 +215,16 @@ export class Sprinkle<S extends TSchema> {
|
|
|
201
215
|
this.options = options ?? {};
|
|
202
216
|
}
|
|
203
217
|
|
|
218
|
+
// --- Current Profile Accessor ---
|
|
219
|
+
|
|
220
|
+
get currentProfile(): ICurrentProfile | null {
|
|
221
|
+
if (!this.profileId) return null;
|
|
222
|
+
return {
|
|
223
|
+
id: this.profileId,
|
|
224
|
+
...this.profileMeta,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
// --- Profile path helpers ---
|
|
205
229
|
|
|
206
230
|
static sanitizeProfileId(name: string): string {
|
|
@@ -686,6 +710,47 @@ export class Sprinkle<S extends TSchema> {
|
|
|
686
710
|
return Blaze.from(provider, wallet);
|
|
687
711
|
}
|
|
688
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Generates a new wallet from a BIP39 mnemonic phrase.
|
|
715
|
+
* Displays the 24-word recovery phrase and requires user confirmation.
|
|
716
|
+
* @returns The Bip32PrivateKey hex string for storage
|
|
717
|
+
*/
|
|
718
|
+
private static async generateWalletFromMnemonic(): Promise<string> {
|
|
719
|
+
const mnemonic = Core.generateMnemonic(wordlist, 256); // 24 words
|
|
720
|
+
const words = mnemonic.split(" ");
|
|
721
|
+
|
|
722
|
+
console.log("\n=== NEW WALLET GENERATED ===\n");
|
|
723
|
+
console.log("IMPORTANT: Save these 24 words in a secure location.");
|
|
724
|
+
console.log("This is the ONLY way to recover your wallet.\n");
|
|
725
|
+
|
|
726
|
+
// Display in 4 columns
|
|
727
|
+
for (let i = 0; i < 6; i++) {
|
|
728
|
+
console.log(
|
|
729
|
+
`${(i + 1).toString().padStart(2)}. ${words[i]!.padEnd(12)} ` +
|
|
730
|
+
`${(i + 7).toString().padStart(2)}. ${words[i + 6]!.padEnd(12)} ` +
|
|
731
|
+
`${(i + 13).toString().padStart(2)}. ${words[i + 12]!.padEnd(12)} ` +
|
|
732
|
+
`${(i + 19).toString().padStart(2)}. ${words[i + 18]}`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
console.log("");
|
|
736
|
+
|
|
737
|
+
const confirmed = await confirm({
|
|
738
|
+
message: "Have you saved your recovery phrase?",
|
|
739
|
+
default: false,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
if (!confirmed) {
|
|
743
|
+
throw new Error("Wallet generation cancelled - recovery phrase not saved");
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const entropy = Core.mnemonicToEntropy(mnemonic, wordlist);
|
|
747
|
+
const masterKey = Core.Bip32PrivateKey.fromBip39Entropy(
|
|
748
|
+
Buffer.from(entropy),
|
|
749
|
+
"",
|
|
750
|
+
);
|
|
751
|
+
return masterKey.hex();
|
|
752
|
+
}
|
|
753
|
+
|
|
689
754
|
static async SearchSelect<T>(opts: {
|
|
690
755
|
message: string;
|
|
691
756
|
source: (
|
|
@@ -814,6 +879,21 @@ export class Sprinkle<S extends TSchema> {
|
|
|
814
879
|
this.saveProfile();
|
|
815
880
|
}
|
|
816
881
|
|
|
882
|
+
getDisplaySettings(): TExact<S> {
|
|
883
|
+
const clone = JSON.parse(
|
|
884
|
+
JSON.stringify(this.settings, Sprinkle.bigIntReplacer),
|
|
885
|
+
Sprinkle.bigIntReviver,
|
|
886
|
+
);
|
|
887
|
+
const sensitivePaths = Sprinkle.collectSensitivePaths(this.type);
|
|
888
|
+
for (const p of sensitivePaths) {
|
|
889
|
+
const value = Sprinkle.getNestedValue(clone, p);
|
|
890
|
+
if (typeof value === "string" && value.length > 0) {
|
|
891
|
+
Sprinkle.setNestedValue(clone, p, "********");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return clone;
|
|
895
|
+
}
|
|
896
|
+
|
|
817
897
|
// --- TxDialog Helpers ---
|
|
818
898
|
|
|
819
899
|
/**
|
|
@@ -1321,6 +1401,24 @@ export class Sprinkle<S extends TSchema> {
|
|
|
1321
1401
|
}
|
|
1322
1402
|
|
|
1323
1403
|
if (isString(type)) {
|
|
1404
|
+
// Special handling for hot wallet private key - offer generation option
|
|
1405
|
+
if (type.title === "Hot Wallet Private Key") {
|
|
1406
|
+
const choice = await select({
|
|
1407
|
+
message: "Hot wallet setup:",
|
|
1408
|
+
choices: [
|
|
1409
|
+
{ name: "Enter existing private key", value: "existing" },
|
|
1410
|
+
{ name: "Generate new wallet", value: "generate" },
|
|
1411
|
+
],
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
if (choice === "generate") {
|
|
1415
|
+
return Sprinkle.generateWalletFromMnemonic() as Promise<TExact<U>>;
|
|
1416
|
+
}
|
|
1417
|
+
// Fall through to password prompt for "existing" choice
|
|
1418
|
+
const answer = await password({ message: "Enter your private key:" });
|
|
1419
|
+
return answer as TExact<U>;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1324
1422
|
const defaultString = (def ? def : this.defaults["string"]) as
|
|
1325
1423
|
| string
|
|
1326
1424
|
| undefined;
|