feat(argus): loadWallet cross-repo fall-through to Deviant encrypted keystore
Extend Argus/src/lib/wallet.js so loadWallet(name) transparently reads from
D:/Dev/Deviant/Keys/chipnet-keystore.json when the alias isn't in the local
SilentMode wallets.json. Same master password (env DEVIANT_KEYSTORE_PASSWORD
or CHIPNET_KEYSTORE_PASSWORD, or interactive prompt) unlocks the Deviant
keystore once per process; decrypted state cached in memory.
- Adds DEVIANT_KEYSTORE_PATH in config.js (defaults to sibling repo, env
override for non-sibling layouts).
- loadWallet() tries local wallets.json first (fast path), then falls through
to the Deviant keystore. Existing "main" behaviour unchanged.
- Slug normalisation: sirius.x ↔ sirius-x, main ↔ i, beacon ↔ bns-beacon,
tld-beacon ↔ bns-tld-beacon — so scripts using either flavour resolve to
the same key.
- New unlockDeviantKeystore() for eager-unlock at script boot.
- New listWallets() returns a merged inventory of slug/cashaddr/label/
category/source. Never returns seeds or WIFs.
Verified against a live Deviant keystore with 15 entries: loadWallet('main')
resolves locally; loadWallet('sirius.x') falls through and produces the same
address as reading D:/Dev/SilentMode/Argus/wallets.json directly.
This commit is contained in:
parent
2d80e68ef8
commit
2847e98d83
2 changed files with 143 additions and 5 deletions
|
|
@ -9,4 +9,15 @@ export const TLD = ".bch";
|
|||
export const BEACON_DUST_SAT = 600;
|
||||
export const NAME_RE = /^[a-z0-9-]{1,32}$/;
|
||||
export const WALLET_FILE = new URL("../wallets.json", import.meta.url);
|
||||
|
||||
// Cross-repo fall-through: Deviant's encrypted keystore, sibling of this repo
|
||||
// under D:/Dev/. If loadWallet(name) can't find `name` in WALLET_FILE, it looks
|
||||
// here next. Encrypted with the same master password the user set via
|
||||
// scripts/chipnet-keystore-encrypt.mjs in the Deviant repo; unlock via env
|
||||
// DEVIANT_KEYSTORE_PASSWORD (== CHIPNET_KEYSTORE_PASSWORD) or interactive
|
||||
// prompt. Override with env DEVIANT_KEYSTORE_PATH if the repos aren't siblings.
|
||||
export const DEVIANT_KEYSTORE_PATH =
|
||||
process.env.DEVIANT_KEYSTORE_PATH ||
|
||||
new URL("../../../Deviant/Keys/chipnet-keystore.json", import.meta.url);
|
||||
|
||||
export const APP_PORT = 4626;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,137 @@
|
|||
import "./dns-patch.js"; // must load before mainnet-js touches the network
|
||||
import { TestNetWallet } from "mainnet-js";
|
||||
import { readFileSync } from "fs";
|
||||
import { WALLET_FILE } from "../config.js";
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { pbkdf2Sync, createDecipheriv } from "node:crypto";
|
||||
import { createInterface } from "node:readline";
|
||||
import { WALLET_FILE, DEVIANT_KEYSTORE_PATH } from "../config.js";
|
||||
|
||||
// In-process cache of the decrypted Deviant keystore. Populated on first
|
||||
// fall-through lookup, reused afterwards so the master-password prompt happens
|
||||
// at most once per script run.
|
||||
let _deviantCache = null;
|
||||
|
||||
async function promptMasterPassword(label) {
|
||||
if (process.env.DEVIANT_KEYSTORE_PASSWORD) return process.env.DEVIANT_KEYSTORE_PASSWORD;
|
||||
if (process.env.CHIPNET_KEYSTORE_PASSWORD) return process.env.CHIPNET_KEYSTORE_PASSWORD;
|
||||
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
||||
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write(label);
|
||||
process.stdout.write = (chunk, ...args) => {
|
||||
if (typeof chunk === "string" && chunk.includes("\n")) return originalWrite("\n", ...args);
|
||||
return true;
|
||||
};
|
||||
return new Promise((resolve) => rl.question("", (answer) => {
|
||||
process.stdout.write = originalWrite;
|
||||
rl.close();
|
||||
process.stdout.write("\n");
|
||||
resolve(answer);
|
||||
}));
|
||||
}
|
||||
|
||||
function decryptDeviantKeystore(encrypted, password) {
|
||||
if (encrypted.spec !== "chipnet-keystore/2-encrypted") return encrypted; // already plaintext
|
||||
const salt = Buffer.from(encrypted.kdf.salt, "hex");
|
||||
const iv = Buffer.from(encrypted.encryption.iv, "hex");
|
||||
const cipherAll = Buffer.from(encrypted.ciphertext, "hex");
|
||||
const ct = cipherAll.subarray(0, cipherAll.length - 16);
|
||||
const tag = cipherAll.subarray(cipherAll.length - 16);
|
||||
const key = pbkdf2Sync(password, salt, encrypted.kdf.iterations, 32, "sha256");
|
||||
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
||||
return JSON.parse(pt.toString("utf8"));
|
||||
}
|
||||
|
||||
async function loadDeviantKeystore() {
|
||||
if (_deviantCache) return _deviantCache;
|
||||
const p = DEVIANT_KEYSTORE_PATH;
|
||||
if (!existsSync(p)) return null;
|
||||
const parsed = JSON.parse(readFileSync(p, "utf8"));
|
||||
if (parsed.spec === "chipnet-keystore/2-encrypted") {
|
||||
const pw = await promptMasterPassword("🔒 Deviant keystore master password: ");
|
||||
try {
|
||||
_deviantCache = decryptDeviantKeystore(parsed, pw);
|
||||
} catch (e) {
|
||||
throw new Error(`Deviant keystore unlock failed: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
_deviantCache = parsed;
|
||||
}
|
||||
return _deviantCache;
|
||||
}
|
||||
|
||||
// SilentMode uses "sirius.x" as an alias; the Deviant keystore stores it as
|
||||
// "sirius-x" (dots collide with the covenant-address separator in cashaddr
|
||||
// tooling). Try both spellings + a few common variants so callers don't have
|
||||
// to memorise which flavour a given entry uses.
|
||||
function slugCandidates(name) {
|
||||
const set = new Set();
|
||||
set.add(name);
|
||||
set.add(name.replace(/\./g, "-"));
|
||||
set.add(name.replace(/-/g, "."));
|
||||
set.add(name.toLowerCase());
|
||||
// "main" in Argus/SilentMode = "i" in the Deviant keystore (same seed).
|
||||
if (name === "main") set.add("i");
|
||||
// "beacon" (SilentMode) ↔ "bns-beacon" (Deviant)
|
||||
if (name === "beacon") set.add("bns-beacon");
|
||||
if (name === "tld-beacon") set.add("bns-tld-beacon");
|
||||
return [...set];
|
||||
}
|
||||
|
||||
export async function loadWallet(which = "main") {
|
||||
const store = JSON.parse(readFileSync(WALLET_FILE, "utf8"));
|
||||
const { seed, path } = store[which];
|
||||
return TestNetWallet.fromSeed(seed, path);
|
||||
// Fast path: local SilentMode wallets.json.
|
||||
if (existsSync(WALLET_FILE)) {
|
||||
const store = JSON.parse(readFileSync(WALLET_FILE, "utf8"));
|
||||
if (store[which]?.seed) {
|
||||
const { seed, path } = store[which];
|
||||
return TestNetWallet.fromSeed(seed, path);
|
||||
}
|
||||
}
|
||||
// Fall-through: Deviant's cross-repo encrypted keystore.
|
||||
const dv = await loadDeviantKeystore();
|
||||
if (dv?.wallets) {
|
||||
for (const slug of slugCandidates(which)) {
|
||||
const entry = dv.wallets[slug];
|
||||
if (!entry) continue;
|
||||
if (entry.seed) return TestNetWallet.fromSeed(entry.seed, entry.path ?? "m/44'/145'/0'/0/0");
|
||||
if (entry.wif) return TestNetWallet.fromWIF(entry.wif);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`loadWallet('${which}') — not found in ${WALLET_FILE} nor in the Deviant keystore ` +
|
||||
`(tried aliases: ${slugCandidates(which).join(", ")}). Check the master password ` +
|
||||
`(env DEVIANT_KEYSTORE_PASSWORD) and DEVIANT_KEYSTORE_PATH.`
|
||||
);
|
||||
}
|
||||
|
||||
// Explicit unlock hook for scripts that want to prompt for the master
|
||||
// password at boot instead of lazily on first fall-through.
|
||||
export async function unlockDeviantKeystore() {
|
||||
return loadDeviantKeystore();
|
||||
}
|
||||
|
||||
// Enumerate every alias reachable via loadWallet (SilentMode + Deviant merged).
|
||||
// Never returns seeds or WIFs — only slug + cashaddr + source.
|
||||
export async function listWallets() {
|
||||
const out = [];
|
||||
if (existsSync(WALLET_FILE)) {
|
||||
const store = JSON.parse(readFileSync(WALLET_FILE, "utf8"));
|
||||
for (const [slug, w] of Object.entries(store)) {
|
||||
out.push({ slug, source: "silentmode", path: w.path ?? null });
|
||||
}
|
||||
}
|
||||
const dv = await loadDeviantKeystore();
|
||||
if (dv?.wallets) {
|
||||
for (const [slug, w] of Object.entries(dv.wallets)) {
|
||||
out.push({
|
||||
slug,
|
||||
source: "deviant",
|
||||
cashaddr: w.cashaddr,
|
||||
label: w.label ?? null,
|
||||
category: w.category ?? null,
|
||||
kind: w.seed ? "hd" : "wif",
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue