@sundaeswap/sprinkles 0.8.1 → 0.9.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 (29) hide show
  1. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js +19 -7
  2. package/dist/cjs/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  3. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +114 -0
  4. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  5. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js +109 -98
  6. package/dist/cjs/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  7. package/dist/cjs/Sprinkle/index.js +480 -179
  8. package/dist/cjs/Sprinkle/index.js.map +1 -1
  9. package/dist/cjs/Sprinkle/types.js.map +1 -1
  10. package/dist/esm/Sprinkle/__tests__/enhancements.test.js +19 -7
  11. package/dist/esm/Sprinkle/__tests__/enhancements.test.js.map +1 -1
  12. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +114 -0
  13. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  14. package/dist/esm/Sprinkle/__tests__/show-menu.test.js +110 -99
  15. package/dist/esm/Sprinkle/__tests__/show-menu.test.js.map +1 -1
  16. package/dist/esm/Sprinkle/index.js +483 -182
  17. package/dist/esm/Sprinkle/index.js.map +1 -1
  18. package/dist/esm/Sprinkle/types.js.map +1 -1
  19. package/dist/types/Sprinkle/index.d.ts +29 -0
  20. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  21. package/dist/types/Sprinkle/types.d.ts +6 -0
  22. package/dist/types/Sprinkle/types.d.ts.map +1 -1
  23. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/src/Sprinkle/__tests__/enhancements.test.ts +20 -7
  26. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +112 -1
  27. package/src/Sprinkle/__tests__/show-menu.test.ts +132 -116
  28. package/src/Sprinkle/index.ts +552 -188
  29. package/src/Sprinkle/types.ts +6 -0
@@ -617,9 +617,133 @@ class Sprinkle {
617
617
  }
618
618
  }
619
619
  }
620
+
621
+ // --- Importing profiles from external files ---
622
+
623
+ static globToRegex(pattern) {
624
+ const escaped = pattern.split("*").map(p => p.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
625
+ return new RegExp(`^${escaped}$`);
626
+ }
627
+
628
+ /**
629
+ * Scan the current working directory for files matching `importPattern`.
630
+ * Returns absolute paths. Designed to surface state files committed to a
631
+ * project repo (e.g. `state.preview.json`) that the consumer might want
632
+ * to restore from.
633
+ */
634
+ findImportableProfiles(cwd = process.cwd()) {
635
+ if (this.options.importPattern === null) return [];
636
+ const pattern = this.options.importPattern ?? "state.*.json";
637
+ const regex = Sprinkle.globToRegex(pattern);
638
+ try {
639
+ return fs.readdirSync(cwd).filter(f => regex.test(f)).map(f => path.join(cwd, f));
640
+ } catch {
641
+ return [];
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Read a profile or raw-settings JSON file and save it as a new local
647
+ * profile. Prompts for missing sensitive fields before writing.
648
+ */
649
+ async importProfileFromFile(filePath) {
650
+ const content = fs.readFileSync(filePath, "utf-8");
651
+ const parsed = JSON.parse(content, Sprinkle.bigIntReviver);
652
+ let importedSettings;
653
+ let importedMeta;
654
+ let importedDefaults = {};
655
+ if (parsed && typeof parsed === "object" && "settings" in parsed && "meta" in parsed) {
656
+ const obj = parsed;
657
+ importedMeta = obj.meta;
658
+ importedSettings = obj.settings;
659
+ importedDefaults = obj.defaults ?? {};
660
+ } else {
661
+ importedSettings = parsed;
662
+ }
663
+
664
+ // If the file came from this same Sprinkle install (same encryption),
665
+ // decrypt sensitive fields. Otherwise we'll re-prompt for missing ones below.
666
+ try {
667
+ importedSettings = await this.decryptSettings(importedSettings);
668
+ } catch {
669
+ // ignore - encrypted with a different key, will re-prompt
670
+ }
671
+ const baseName = path.basename(filePath).replace(/\.json$/, "");
672
+ const meta = await this.promptProfileMeta(importedMeta?.name ?? baseName, importedMeta?.description);
673
+ if (meta === null) return; // user cancelled
674
+ const {
675
+ name,
676
+ description
677
+ } = meta;
678
+
679
+ // Re-prompt any sensitive fields that are missing or look encrypted
680
+ const sensitivePaths = (0, _encryption.collectSensitivePaths)(this.type);
681
+ for (const p of sensitivePaths) {
682
+ const value = (0, _encryption.getNestedValue)(importedSettings, p);
683
+ if (typeof value !== "string" || value.length === 0) {
684
+ const entered = await (0, _prompts.passwordCancellable)({
685
+ message: `Enter value for sensitive field "${p}":`
686
+ });
687
+ if (entered === null) return; // user cancelled
688
+ (0, _encryption.setNestedValue)(importedSettings, p, entered);
689
+ }
690
+ }
691
+ const profilesDir = Sprinkle.profilesDir(this.storagePath);
692
+ if (!fs.existsSync(profilesDir)) {
693
+ fs.mkdirSync(profilesDir, {
694
+ recursive: true
695
+ });
696
+ }
697
+ const id = Sprinkle.findAvailableId(profilesDir, Sprinkle.sanitizeProfileId(name));
698
+ const now = new Date().toISOString();
699
+ this.profileId = id;
700
+ this.profileMeta = {
701
+ name,
702
+ description,
703
+ createdAt: now,
704
+ updatedAt: now
705
+ };
706
+ this.settings = importedSettings;
707
+ this.defaults = importedDefaults;
708
+ this.saveProfile();
709
+ fs.writeFileSync(Sprinkle.activeProfilePath(this.storagePath), id, "utf-8");
710
+ }
711
+ async promptImportFromCwd() {
712
+ const importable = this.findImportableProfiles();
713
+ if (importable.length === 0) return false;
714
+ const cancelToken = "__cancel__";
715
+ const choices = [...importable.map(f => ({
716
+ name: `Import ${path.basename(f)}`,
717
+ value: f
718
+ })), {
719
+ name: "Enter a different path...",
720
+ value: "__custom__"
721
+ }, {
722
+ name: "Skip import",
723
+ value: cancelToken
724
+ }];
725
+ const choice = await (0, _prompts.select)({
726
+ message: "Found importable state file(s) in this directory. Restore from one?",
727
+ choices
728
+ });
729
+ if (choice === null || choice === cancelToken) return false;
730
+ let filePath = choice;
731
+ if (choice === "__custom__") {
732
+ const entered = await (0, _prompts.inputCancellable)({
733
+ message: "Path to state file:",
734
+ validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
735
+ });
736
+ if (entered === null) return false;
737
+ filePath = entered;
738
+ }
739
+ await this.importProfileFromFile(filePath);
740
+ return true;
741
+ }
620
742
  async selectOrCreateProfile() {
621
743
  const profiles = this.scanProfiles();
622
744
  if (profiles.length === 0) {
745
+ const imported = await this.promptImportFromCwd();
746
+ if (imported) return;
623
747
  await this.createProfile();
624
748
  return;
625
749
  }
@@ -680,6 +804,7 @@ class Sprinkle {
680
804
  addressbook: this.addressbook
681
805
  }, _encryption.bigIntReplacer, 2);
682
806
  fs.writeFileSync(path.join(profilesDir, `${id}.json`), jsonContent, "utf-8");
807
+ await this.loadProfile(id);
683
808
  console.log(`Profile "${name}" created as a copy.`);
684
809
  }
685
810
  async renameProfile() {
@@ -741,8 +866,318 @@ class Sprinkle {
741
866
 
742
867
  // --- Menu ---
743
868
 
869
+ async importProfileInteractive() {
870
+ const importable = this.findImportableProfiles();
871
+ const customToken = "__custom__";
872
+ let filePath;
873
+ if (importable.length > 0) {
874
+ const choice = await (0, _prompts.select)({
875
+ message: "Select a file to import:",
876
+ choices: [...importable.map(f => ({
877
+ name: path.basename(f),
878
+ value: f
879
+ })), {
880
+ name: "Enter a different path...",
881
+ value: customToken
882
+ }]
883
+ });
884
+ if (choice === null) return; // user cancelled
885
+ if (choice === customToken) {
886
+ const entered = await (0, _prompts.inputCancellable)({
887
+ message: "Path to state file:",
888
+ validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
889
+ });
890
+ if (entered === null) return;
891
+ filePath = entered;
892
+ } else {
893
+ filePath = choice;
894
+ }
895
+ } else {
896
+ const entered = await (0, _prompts.inputCancellable)({
897
+ message: "Path to state file:",
898
+ validate: v => v.trim().length > 0 ? true : "Path cannot be empty"
899
+ });
900
+ if (entered === null) return;
901
+ filePath = entered;
902
+ }
903
+ try {
904
+ await this.importProfileFromFile(filePath);
905
+ console.log(`Imported profile "${this.profileMeta.name}".`);
906
+ } catch (error) {
907
+ console.error(`Import failed: ${error.message}`);
908
+ }
909
+ }
910
+ buildSettingsMenu() {
911
+ return {
912
+ title: "Settings & Profiles",
913
+ items: [{
914
+ title: "View settings",
915
+ action: async () => {
916
+ const jsonStr = JSON.stringify(this.getDisplaySettings(), _encryption.bigIntReplacer, 2);
917
+ const jsonLines = jsonStr.split("\n").length;
918
+ console.log(jsonStr);
919
+
920
+ // Wait for user to press Enter
921
+ await (0, _prompts.selectWithClear)({
922
+ message: "Press Enter to continue...",
923
+ choices: [{
924
+ name: "Continue",
925
+ value: "continue"
926
+ }]
927
+ });
928
+
929
+ // Clear the JSON output
930
+ process.stdout.write("\x1b[1A\x1b[2K".repeat(jsonLines) + "\x1b[G");
931
+ }
932
+ }, {
933
+ title: "Edit settings",
934
+ action: async () => {
935
+ try {
936
+ this.settings = await this.EditStruct(this.type, this.settings);
937
+ this.saveSettings();
938
+ } catch (e) {
939
+ if (e instanceof _types.UserCancelledError) {
940
+ return; // User cancelled, return to menu
941
+ }
942
+ throw e;
943
+ }
944
+ }
945
+ }, {
946
+ title: "Switch profile",
947
+ action: async () => {
948
+ this.saveSettings();
949
+ const profiles = this.scanProfiles();
950
+ if (profiles.length <= 1) {
951
+ console.log("No other profiles to switch to. Create a new one first.");
952
+ } else {
953
+ await this.selectProfile(profiles);
954
+ }
955
+ }
956
+ }, {
957
+ title: "Create new profile",
958
+ action: async () => {
959
+ await this.createProfile();
960
+ }
961
+ }, {
962
+ title: "Duplicate current profile",
963
+ action: async () => {
964
+ await this.duplicateProfile();
965
+ }
966
+ }, {
967
+ title: "Rename current profile",
968
+ action: async () => {
969
+ await this.renameProfile();
970
+ }
971
+ }, {
972
+ title: "Delete a profile",
973
+ action: async () => {
974
+ await this.deleteProfile();
975
+ }
976
+ }, {
977
+ title: "Import profile from file",
978
+ action: async () => {
979
+ await this.importProfileInteractive();
980
+ }
981
+ }, {
982
+ title: "Addressbook",
983
+ items: [{
984
+ title: "View entries",
985
+ action: async () => {
986
+ const entries = Object.entries(this.addressbook);
987
+ if (entries.length === 0) {
988
+ console.log("Addressbook is empty.");
989
+ } else {
990
+ for (const [name, ms] of entries) {
991
+ const json = JSON.stringify(ms, _encryption.bigIntReplacer, 2);
992
+ let hashStr = "unknown";
993
+ try {
994
+ hashStr = (0, _index2.toNativeScript)(ms).hash();
995
+ } catch {/* skip */}
996
+ console.log(_yoctocolorsCjs.default.bold(name) + " " + _yoctocolorsCjs.default.dim(hashStr));
997
+ console.log(json);
998
+ console.log();
999
+ }
1000
+ }
1001
+ await (0, _prompts.selectWithClear)({
1002
+ message: "Press Enter to continue...",
1003
+ choices: [{
1004
+ name: "Continue",
1005
+ value: "continue"
1006
+ }]
1007
+ });
1008
+ }
1009
+ }, {
1010
+ title: "Add entry",
1011
+ action: async () => {
1012
+ const name = await (0, _prompts.inputCancellable)({
1013
+ message: "Entry name:",
1014
+ validate: v => v.trim().length > 0 ? true : "Name cannot be empty"
1015
+ });
1016
+ if (name === null) return;
1017
+ if (this.addressbook[name]) {
1018
+ const overwrite = await (0, _prompts.confirmCancellable)({
1019
+ message: `Entry "${name}" already exists. Overwrite?`,
1020
+ default: false
1021
+ });
1022
+ if (!overwrite) return;
1023
+ }
1024
+ try {
1025
+ const script = await this.FillInStruct(_schemas.MultisigScript);
1026
+ this.addressbook[name] = script;
1027
+ this.saveSettings();
1028
+ console.log(`Added "${name}" to addressbook.`);
1029
+ } catch (e) {
1030
+ if (e instanceof _types.UserCancelledError) return;
1031
+ throw e;
1032
+ }
1033
+ }
1034
+ }, {
1035
+ title: "Edit entry",
1036
+ action: async () => {
1037
+ const entries = Object.keys(this.addressbook);
1038
+ if (entries.length === 0) {
1039
+ console.log("Addressbook is empty.");
1040
+ return;
1041
+ }
1042
+ const selected = await (0, _prompts.selectCancellable)({
1043
+ message: "Select entry to edit:",
1044
+ choices: entries.map(n => ({
1045
+ name: n,
1046
+ value: n
1047
+ }))
1048
+ });
1049
+ if (selected === null) return;
1050
+ const editName = selected;
1051
+ try {
1052
+ const updated = await this.EditStruct(_schemas.MultisigScript, this.addressbook[editName]);
1053
+ this.addressbook[editName] = updated;
1054
+ this.saveSettings();
1055
+ console.log(`Updated "${editName}".`);
1056
+ } catch (e) {
1057
+ if (e instanceof _types.UserCancelledError) return;
1058
+ throw e;
1059
+ }
1060
+ }
1061
+ }, {
1062
+ title: "Delete entry",
1063
+ action: async () => {
1064
+ const entries = Object.keys(this.addressbook);
1065
+ if (entries.length === 0) {
1066
+ console.log("Addressbook is empty.");
1067
+ return;
1068
+ }
1069
+ const delSelected = await (0, _prompts.selectCancellable)({
1070
+ message: "Select entry to delete:",
1071
+ choices: entries.map(n => ({
1072
+ name: n,
1073
+ value: n
1074
+ }))
1075
+ });
1076
+ if (delSelected === null) return;
1077
+ const delName = delSelected;
1078
+ const confirmed = await (0, _prompts.confirmCancellable)({
1079
+ message: `Delete "${delName}"?`,
1080
+ default: false
1081
+ });
1082
+ if (!confirmed) return;
1083
+ delete this.addressbook[delName];
1084
+ this.saveSettings();
1085
+ console.log(`Deleted "${delName}".`);
1086
+ }
1087
+ }]
1088
+ }]
1089
+ };
1090
+ }
744
1091
  async showMenu(menu) {
745
- return this._showMenu(menu, true, [menu.title]);
1092
+ return this._searchMenu(menu);
1093
+ }
1094
+
1095
+ // --- Flat search-based top-level menu ---
1096
+
1097
+ _flattenLeaves(menu, prefix, beforeShowChain, isRoot) {
1098
+ // Root's title is omitted from breadcrumbs (no "Main Menu › " prefix on
1099
+ // every leaf). Root's beforeShow is fired by the search loop, not as part
1100
+ // of the leaf chain (otherwise it would fire twice).
1101
+ const chain = !isRoot && menu.beforeShow ? [...beforeShowChain, menu.beforeShow] : beforeShowChain;
1102
+ const breadcrumbPrefix = isRoot ? prefix : [...prefix, menu.title];
1103
+ const leaves = [];
1104
+ for (const item of menu.items) {
1105
+ if ("action" in item) {
1106
+ leaves.push({
1107
+ breadcrumb: [...breadcrumbPrefix, item.title].join(" › "),
1108
+ leafTitle: item.title,
1109
+ action: item.action,
1110
+ beforeShowChain: chain
1111
+ });
1112
+ } else {
1113
+ leaves.push(...this._flattenLeaves(item, breadcrumbPrefix, chain, false));
1114
+ }
1115
+ }
1116
+ return leaves;
1117
+ }
1118
+ static _scoreLeaf(leaf, tokens) {
1119
+ if (tokens.length === 0) return 1;
1120
+ const crumbLower = leaf.breadcrumb.toLowerCase();
1121
+ const titleLower = leaf.leafTitle.toLowerCase();
1122
+ let score = 0;
1123
+ for (const t of tokens) {
1124
+ if (!crumbLower.includes(t)) return 0; // require every token
1125
+ if (titleLower.includes(t)) {
1126
+ score += 3; // matches in the leaf title outrank ancestor matches
1127
+ } else {
1128
+ score += 1;
1129
+ }
1130
+ }
1131
+ return score;
1132
+ }
1133
+ _filterLeaves(leaves, term) {
1134
+ const trimmed = term.trim();
1135
+ if (!trimmed) return leaves;
1136
+ const tokens = trimmed.toLowerCase().split(/\s+/);
1137
+ return leaves.map(leaf => ({
1138
+ leaf,
1139
+ score: Sprinkle._scoreLeaf(leaf, tokens)
1140
+ })).filter(x => x.score > 0).sort((a, b) => b.score - a.score).map(x => x.leaf);
1141
+ }
1142
+ async _searchMenu(menu) {
1143
+ const EXIT_TITLE = "Exit";
1144
+ while (true) {
1145
+ // Root beforeShow fires once per iteration so consumers can refresh any
1146
+ // dynamic state before the search prompt is built.
1147
+ if (menu.beforeShow) {
1148
+ await menu.beforeShow(this);
1149
+ }
1150
+ const userLeaves = this._flattenLeaves(menu, [], [], true);
1151
+ const settingsLeaves = this._flattenLeaves(this.buildSettingsMenu(), [], [], false);
1152
+ const exitLeaf = {
1153
+ breadcrumb: EXIT_TITLE,
1154
+ leafTitle: EXIT_TITLE,
1155
+ action: async () => {},
1156
+ beforeShowChain: [],
1157
+ isExit: true
1158
+ };
1159
+ const allLeaves = [...userLeaves, ...settingsLeaves, exitLeaf];
1160
+ const selected = await (0, _prompts.searchCancellable)({
1161
+ message: "What would you like to do?",
1162
+ source: async term => {
1163
+ return this._filterLeaves(allLeaves, term ?? "").map(leaf => ({
1164
+ name: leaf.breadcrumb,
1165
+ value: leaf
1166
+ }));
1167
+ }
1168
+ });
1169
+ if (selected === null || selected.isExit) return;
1170
+
1171
+ // Fire submenu beforeShow hooks along the path to the selected leaf
1172
+ for (const beforeShow of selected.beforeShowChain) {
1173
+ await beforeShow(this);
1174
+ }
1175
+ const result = await selected.action(this);
1176
+ if (result instanceof Sprinkle) {
1177
+ this.settings = result.settings;
1178
+ this.saveSettings();
1179
+ }
1180
+ }
746
1181
  }
747
1182
  async _showMenu(menu, main, path, clearPrevious = false) {
748
1183
  // Clear previous breadcrumb if coming back from action/submenu
@@ -851,181 +1286,7 @@ class Sprinkle {
851
1286
  return;
852
1287
  }
853
1288
  if (selection === -5) {
854
- const settingsMenu = {
855
- title: "Settings & Profiles",
856
- items: [{
857
- title: "View settings",
858
- action: async () => {
859
- const jsonStr = JSON.stringify(this.getDisplaySettings(), _encryption.bigIntReplacer, 2);
860
- const jsonLines = jsonStr.split("\n").length;
861
- console.log(jsonStr);
862
-
863
- // Wait for user to press Enter
864
- await (0, _prompts.selectWithClear)({
865
- message: "Press Enter to continue...",
866
- choices: [{
867
- name: "Continue",
868
- value: "continue"
869
- }]
870
- });
871
-
872
- // Clear the JSON output
873
- process.stdout.write("\x1b[1A\x1b[2K".repeat(jsonLines) + "\x1b[G");
874
- }
875
- }, {
876
- title: "Edit settings",
877
- action: async () => {
878
- try {
879
- this.settings = await this.EditStruct(this.type, this.settings);
880
- this.saveSettings();
881
- } catch (e) {
882
- if (e instanceof _types.UserCancelledError) {
883
- return; // User cancelled, return to menu
884
- }
885
- throw e;
886
- }
887
- }
888
- }, {
889
- title: "Switch profile",
890
- action: async () => {
891
- this.saveSettings();
892
- const profiles = this.scanProfiles();
893
- if (profiles.length <= 1) {
894
- console.log("No other profiles to switch to. Create a new one first.");
895
- } else {
896
- await this.selectProfile(profiles);
897
- }
898
- }
899
- }, {
900
- title: "Create new profile",
901
- action: async () => {
902
- await this.createProfile();
903
- }
904
- }, {
905
- title: "Duplicate current profile",
906
- action: async () => {
907
- await this.duplicateProfile();
908
- }
909
- }, {
910
- title: "Rename current profile",
911
- action: async () => {
912
- await this.renameProfile();
913
- }
914
- }, {
915
- title: "Delete a profile",
916
- action: async () => {
917
- await this.deleteProfile();
918
- }
919
- }, {
920
- title: "Addressbook",
921
- items: [{
922
- title: "View entries",
923
- action: async () => {
924
- const entries = Object.entries(this.addressbook);
925
- if (entries.length === 0) {
926
- console.log("Addressbook is empty.");
927
- } else {
928
- for (const [name, ms] of entries) {
929
- const json = JSON.stringify(ms, _encryption.bigIntReplacer, 2);
930
- let hashStr = "unknown";
931
- try {
932
- hashStr = (0, _index2.toNativeScript)(ms).hash();
933
- } catch {/* skip */}
934
- console.log(_yoctocolorsCjs.default.bold(name) + " " + _yoctocolorsCjs.default.dim(hashStr));
935
- console.log(json);
936
- console.log();
937
- }
938
- }
939
- await (0, _prompts.selectWithClear)({
940
- message: "Press Enter to continue...",
941
- choices: [{
942
- name: "Continue",
943
- value: "continue"
944
- }]
945
- });
946
- }
947
- }, {
948
- title: "Add entry",
949
- action: async () => {
950
- const name = await (0, _prompts.inputCancellable)({
951
- message: "Entry name:",
952
- validate: v => v.trim().length > 0 ? true : "Name cannot be empty"
953
- });
954
- if (name === null) return;
955
- if (this.addressbook[name]) {
956
- const overwrite = await (0, _prompts.confirmCancellable)({
957
- message: `Entry "${name}" already exists. Overwrite?`,
958
- default: false
959
- });
960
- if (!overwrite) return;
961
- }
962
- try {
963
- const script = await this.FillInStruct(_schemas.MultisigScript);
964
- this.addressbook[name] = script;
965
- this.saveSettings();
966
- console.log(`Added "${name}" to addressbook.`);
967
- } catch (e) {
968
- if (e instanceof _types.UserCancelledError) return;
969
- throw e;
970
- }
971
- }
972
- }, {
973
- title: "Edit entry",
974
- action: async () => {
975
- const entries = Object.keys(this.addressbook);
976
- if (entries.length === 0) {
977
- console.log("Addressbook is empty.");
978
- return;
979
- }
980
- const selected = await (0, _prompts.selectCancellable)({
981
- message: "Select entry to edit:",
982
- choices: entries.map(n => ({
983
- name: n,
984
- value: n
985
- }))
986
- });
987
- if (selected === null) return;
988
- const editName = selected;
989
- try {
990
- const updated = await this.EditStruct(_schemas.MultisigScript, this.addressbook[editName]);
991
- this.addressbook[editName] = updated;
992
- this.saveSettings();
993
- console.log(`Updated "${editName}".`);
994
- } catch (e) {
995
- if (e instanceof _types.UserCancelledError) return;
996
- throw e;
997
- }
998
- }
999
- }, {
1000
- title: "Delete entry",
1001
- action: async () => {
1002
- const entries = Object.keys(this.addressbook);
1003
- if (entries.length === 0) {
1004
- console.log("Addressbook is empty.");
1005
- return;
1006
- }
1007
- const delSelected = await (0, _prompts.selectCancellable)({
1008
- message: "Select entry to delete:",
1009
- choices: entries.map(n => ({
1010
- name: n,
1011
- value: n
1012
- }))
1013
- });
1014
- if (delSelected === null) return;
1015
- const delName = delSelected;
1016
- const confirmed = await (0, _prompts.confirmCancellable)({
1017
- message: `Delete "${delName}"?`,
1018
- default: false
1019
- });
1020
- if (!confirmed) return;
1021
- delete this.addressbook[delName];
1022
- this.saveSettings();
1023
- console.log(`Deleted "${delName}".`);
1024
- }
1025
- }]
1026
- }]
1027
- };
1028
- await this._showMenu(settingsMenu, false, [...path, "Settings & Profiles"], true);
1289
+ await this._showMenu(this.buildSettingsMenu(), false, [...path, "Settings & Profiles"], true);
1029
1290
  await this._showMenu(menu, main, path, true);
1030
1291
  return;
1031
1292
  }
@@ -1067,6 +1328,37 @@ class Sprinkle {
1067
1328
  return (0, _wallet.GetBlaze)(network, providerSettings, walletSettings);
1068
1329
  }
1069
1330
 
1331
+ /**
1332
+ * Derive a Bip32 private key hex from a BIP39 mnemonic phrase.
1333
+ * Accepts 12, 15, or 24 word phrases.
1334
+ */
1335
+ static privateKeyFromMnemonic(phrase) {
1336
+ const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
1337
+ const words = normalized.split(" ");
1338
+ if (![12, 15, 24].includes(words.length)) {
1339
+ throw new Error(`Expected 12, 15, or 24 words but got ${words.length}.`);
1340
+ }
1341
+ const entropy = _sdk.Core.mnemonicToEntropy(normalized, _core.wordlist);
1342
+ const masterKey = _sdk.Core.Bip32PrivateKey.fromBip39Entropy(Buffer.from(entropy), "");
1343
+ return masterKey.hex();
1344
+ }
1345
+
1346
+ /**
1347
+ * Prompt the user for a recovery phrase and derive the Bip32 private key hex.
1348
+ */
1349
+ static async importWalletFromMnemonic() {
1350
+ const phrase = await (0, _prompts.passwordCancellable)({
1351
+ message: "Enter your recovery phrase (12, 15, or 24 words):"
1352
+ });
1353
+ if (phrase === null) {
1354
+ throw new _types.UserCancelledError();
1355
+ }
1356
+ if (phrase.trim().length === 0) {
1357
+ throw new Error("Recovery phrase cannot be empty");
1358
+ }
1359
+ return Sprinkle.privateKeyFromMnemonic(phrase);
1360
+ }
1361
+
1070
1362
  /**
1071
1363
  * Generates a new wallet from a BIP39 mnemonic phrase.
1072
1364
  * Displays the 24-word recovery phrase and requires user confirmation.
@@ -1618,6 +1910,9 @@ class Sprinkle {
1618
1910
  // if the literal field values in the selected variant match those in `def`.
1619
1911
  // For non-discriminated unions, fall back to structural matching with Value.Check.
1620
1912
  let matchedDef = undefined;
1913
+ // Build references array for Value.Check — schemas with $id from defs
1914
+ // so TypeBox can resolve $ref inside variant schemas (e.g. MultisigScript).
1915
+ const references = Object.values(defs).filter(s => s.$id);
1621
1916
  if (def !== undefined) {
1622
1917
  if ((0, _typeGuards.isObject)(selection)) {
1623
1918
  // Check if all literal fields in the selected variant match def
@@ -1630,13 +1925,13 @@ class Sprinkle {
1630
1925
  }
1631
1926
  } else {
1632
1927
  // No literal discriminators - use structural check
1633
- if (_value.Value.Check(selection, def)) {
1928
+ if (_value.Value.Check(selection, references, def)) {
1634
1929
  matchedDef = def;
1635
1930
  }
1636
1931
  }
1637
1932
  } else {
1638
1933
  // Non-object variant - use structural check
1639
- if (_value.Value.Check(selection, def)) {
1934
+ if (_value.Value.Check(selection, references, def)) {
1640
1935
  matchedDef = def;
1641
1936
  }
1642
1937
  }
@@ -1644,13 +1939,16 @@ class Sprinkle {
1644
1939
  return this._fillInStruct(selection, path, defs, matchedDef);
1645
1940
  }
1646
1941
  if ((0, _typeGuards.isString)(type)) {
1647
- // Special handling for hot wallet private key - offer generation option
1942
+ // Special handling for hot wallet private key - offer multiple sources
1648
1943
  if (type.title === "Hot Wallet Private Key") {
1649
1944
  const choice = await (0, _prompts.selectWithClear)({
1650
1945
  message: "Hot wallet setup:",
1651
1946
  choices: [{
1652
1947
  name: "Enter existing private key",
1653
1948
  value: "existing"
1949
+ }, {
1950
+ name: "Import from recovery phrase",
1951
+ value: "mnemonic"
1654
1952
  }, {
1655
1953
  name: "Generate new wallet",
1656
1954
  value: "generate"
@@ -1662,6 +1960,9 @@ class Sprinkle {
1662
1960
  if (choice === "generate") {
1663
1961
  return Sprinkle.generateWalletFromMnemonic();
1664
1962
  }
1963
+ if (choice === "mnemonic") {
1964
+ return Sprinkle.importWalletFromMnemonic();
1965
+ }
1665
1966
  // Fall through to password prompt for "existing" choice
1666
1967
  const answer = await (0, _prompts.passwordWithClear)({
1667
1968
  message: "Enter your private key:"