routstrd 0.2.21 → 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/dist/index.js CHANGED
@@ -2170,18 +2170,18 @@ var init_logger = __esm(() => {
2170
2170
  LOGS_DIR = join(LOG_DIR, "logs");
2171
2171
  logger = {
2172
2172
  log: (...args) => {
2173
- console.log(...args);
2174
2173
  writeLog("INFO", ...args);
2175
2174
  },
2176
2175
  debug: (...args) => {
2177
2176
  writeLog("DEBUG", ...args);
2178
2177
  },
2178
+ warn: (...args) => {
2179
+ writeLog("WARN", ...args);
2180
+ },
2179
2181
  error: (...args) => {
2180
- console.error(...args);
2181
2182
  writeLog("ERROR", ...args);
2182
2183
  },
2183
2184
  info: (...args) => {
2184
- console.log(...args);
2185
2185
  writeLog("INFO", ...args);
2186
2186
  }
2187
2187
  };
@@ -2320,7 +2320,7 @@ async function startDaemonUnlocked(options) {
2320
2320
  const pollIntervalMs = 250;
2321
2321
  const startupTimeoutMs = 10 * 60 * 1000;
2322
2322
  if (await isDaemonHealthy(port)) {
2323
- logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
2323
+ console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
2324
2324
  return;
2325
2325
  }
2326
2326
  if (options.port) {
@@ -2332,8 +2332,8 @@ async function startDaemonUnlocked(options) {
2332
2332
  const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname;
2333
2333
  const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`;
2334
2334
  const proc = Bun.spawn(["sh", "-c", shellCmd], {
2335
- stdout: "inherit",
2336
- stderr: "inherit",
2335
+ stdout: "ignore",
2336
+ stderr: "ignore",
2337
2337
  stdin: "ignore",
2338
2338
  detached: true
2339
2339
  });
@@ -2349,7 +2349,7 @@ async function startDaemonUnlocked(options) {
2349
2349
  throw new Error(`Daemon process exited early with code ${exitCode}. Check logs in ${LOGS_DIR2}`);
2350
2350
  }
2351
2351
  if (await isDaemonHealthy(port)) {
2352
- logger.log(`Routstr daemon started (PID: ${proc.pid}).`);
2352
+ console.log(`Routstr daemon started (PID: ${proc.pid}).`);
2353
2353
  return;
2354
2354
  }
2355
2355
  }
@@ -2359,7 +2359,7 @@ async function startDaemon(options = {}) {
2359
2359
  const port = options.port || "8008";
2360
2360
  const startupTimeoutMs = 10 * 60 * 1000;
2361
2361
  if (await isDaemonHealthy(port)) {
2362
- logger.log(`Routstr daemon already running on http://localhost:${port}/v1`);
2362
+ console.log(`Routstr daemon already running on http://localhost:${port}/v1`);
2363
2363
  return;
2364
2364
  }
2365
2365
  await withCrossProcessLock(DAEMON_STARTUP_LOCK_PATH, async () => {
@@ -7168,28 +7168,49 @@ function calcPaddedLen(len) {
7168
7168
  throw new Error("expected positive integer");
7169
7169
  if (len <= 32)
7170
7170
  return 32;
7171
- const nextPower = 1 << Math.floor(Math.log2(len - 1)) + 1;
7171
+ const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1);
7172
7172
  const chunk = nextPower <= 256 ? 32 : nextPower / 8;
7173
7173
  return chunk * (Math.floor((len - 1) / chunk) + 1);
7174
7174
  }
7175
7175
  function writeU16BE(num2) {
7176
- if (!Number.isSafeInteger(num2) || num2 < minPlaintextSize || num2 > maxPlaintextSize)
7176
+ if (!Number.isSafeInteger(num2) || num2 < minPlaintextSize || num2 > 65535)
7177
7177
  throw new Error("invalid plaintext size: must be between 1 and 65535 bytes");
7178
7178
  const arr = new Uint8Array(2);
7179
7179
  new DataView(arr.buffer).setUint16(0, num2, false);
7180
7180
  return arr;
7181
7181
  }
7182
+ function writeU32BE(num2) {
7183
+ if (!Number.isSafeInteger(num2) || num2 < extendedPrefixThreshold || num2 > maxPlaintextSize)
7184
+ throw new Error("invalid plaintext size: must be between 65536 and 4294967295 bytes");
7185
+ const arr = new Uint8Array(4);
7186
+ new DataView(arr.buffer).setUint32(0, num2, false);
7187
+ return arr;
7188
+ }
7182
7189
  function pad(plaintext) {
7183
7190
  const unpadded = utf8Encoder.encode(plaintext);
7184
7191
  const unpaddedLen = unpadded.length;
7185
- const prefix = writeU16BE(unpaddedLen);
7192
+ if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize)
7193
+ throw new Error("invalid plaintext size: must be between 1 and 4294967295 bytes");
7194
+ const prefix = unpaddedLen >= extendedPrefixThreshold ? concatBytes(new Uint8Array([0, 0]), writeU32BE(unpaddedLen)) : writeU16BE(unpaddedLen);
7186
7195
  const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen);
7187
7196
  return concatBytes(prefix, unpadded, suffix);
7188
7197
  }
7189
7198
  function unpad(padded) {
7190
- const unpaddedLen = new DataView(padded.buffer).getUint16(0);
7191
- const unpadded = padded.subarray(2, 2 + unpaddedLen);
7192
- if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize || unpadded.length !== unpaddedLen || padded.length !== 2 + calcPaddedLen(unpaddedLen))
7199
+ const dv = new DataView(padded.buffer, padded.byteOffset, padded.byteLength);
7200
+ const firstTwo = dv.getUint16(0);
7201
+ let unpaddedLen;
7202
+ let prefixLen;
7203
+ if (firstTwo === 0) {
7204
+ unpaddedLen = dv.getUint32(2);
7205
+ if (unpaddedLen < extendedPrefixThreshold)
7206
+ throw new Error("invalid padding");
7207
+ prefixLen = 6;
7208
+ } else {
7209
+ unpaddedLen = firstTwo;
7210
+ prefixLen = 2;
7211
+ }
7212
+ const unpadded = padded.subarray(prefixLen, prefixLen + unpaddedLen);
7213
+ if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize || unpadded.length !== unpaddedLen || padded.length !== prefixLen + calcPaddedLen(unpaddedLen))
7193
7214
  throw new Error("invalid padding");
7194
7215
  return utf8Decoder.decode(unpadded);
7195
7216
  }
@@ -7203,7 +7224,7 @@ function decodePayload(payload) {
7203
7224
  if (typeof payload !== "string")
7204
7225
  throw new Error("payload must be a valid string");
7205
7226
  const plen = payload.length;
7206
- if (plen < 132 || plen > 87472)
7227
+ if (plen < 132)
7207
7228
  throw new Error("invalid payload length: " + plen);
7208
7229
  if (payload[0] === "#")
7209
7230
  throw new Error("unknown encryption version");
@@ -7214,7 +7235,7 @@ function decodePayload(payload) {
7214
7235
  throw new Error("invalid base64: " + error.message);
7215
7236
  }
7216
7237
  const dlen = data.length;
7217
- if (dlen < 99 || dlen > 65603)
7238
+ if (dlen < 99)
7218
7239
  throw new Error("invalid data length: " + dlen);
7219
7240
  const vers = data[0];
7220
7241
  if (vers !== 2)
@@ -7400,6 +7421,135 @@ function parse2(uri) {
7400
7421
  decoded: decode(match[1])
7401
7422
  };
7402
7423
  }
7424
+ function parseKind(kind) {
7425
+ if (!kind)
7426
+ return;
7427
+ return /^\d+$/.test(kind) ? parseInt(kind, 10) : kind;
7428
+ }
7429
+ function parseAddressPointer(value, relayUrl) {
7430
+ const idx = value.indexOf(":");
7431
+ const idx2 = value.indexOf(":", idx + 1);
7432
+ if (idx === -1 || idx2 === -1)
7433
+ return;
7434
+ const kind = parseInt(value.slice(0, idx), 10);
7435
+ if (Number.isNaN(kind))
7436
+ return;
7437
+ return {
7438
+ kind,
7439
+ pubkey: value.slice(idx + 1, idx2),
7440
+ identifier: value.slice(idx2 + 1),
7441
+ relays: relayUrl ? [relayUrl] : []
7442
+ };
7443
+ }
7444
+ function parsePointer(tag) {
7445
+ switch (tag[0]) {
7446
+ case "E":
7447
+ case "e":
7448
+ if (!tag[1])
7449
+ return;
7450
+ return {
7451
+ id: tag[1],
7452
+ relays: tag[2] ? [tag[2]] : [],
7453
+ author: tag[3]
7454
+ };
7455
+ case "A":
7456
+ case "a":
7457
+ if (!tag[1])
7458
+ return;
7459
+ return parseAddressPointer(tag[1], tag[2]);
7460
+ case "I":
7461
+ case "i":
7462
+ if (!tag[1])
7463
+ return;
7464
+ return {
7465
+ value: tag[1],
7466
+ hint: tag[2]
7467
+ };
7468
+ }
7469
+ }
7470
+ function parseQuote(tag) {
7471
+ if (!tag[1])
7472
+ return;
7473
+ if (tag[1].includes(":")) {
7474
+ return parseAddressPointer(tag[1], tag[2]);
7475
+ }
7476
+ return {
7477
+ id: tag[1],
7478
+ relays: tag[2] ? [tag[2]] : [],
7479
+ author: tag[3]
7480
+ };
7481
+ }
7482
+ function choosePointer(candidates) {
7483
+ return candidates.findLast((candidate) => candidate.tagName === "A" || candidate.tagName === "a")?.pointer || candidates.findLast((candidate) => candidate.tagName === "I" || candidate.tagName === "i")?.pointer || candidates.findLast((candidate) => candidate.tagName === "E" || candidate.tagName === "e")?.pointer;
7484
+ }
7485
+ function inheritRelayHints(pointer, profiles) {
7486
+ if (!pointer || !("id" in pointer) || !pointer.author)
7487
+ return;
7488
+ const author = profiles.find((profile) => profile.pubkey === pointer.author);
7489
+ if (!author || !author.relays)
7490
+ return;
7491
+ if (!pointer.relays) {
7492
+ pointer.relays = [];
7493
+ }
7494
+ author.relays.forEach((url) => {
7495
+ if (pointer.relays.indexOf(url) === -1)
7496
+ pointer.relays.push(url);
7497
+ });
7498
+ author.relays = pointer.relays;
7499
+ }
7500
+ function parse3(event) {
7501
+ const result = {
7502
+ root: undefined,
7503
+ rootKind: undefined,
7504
+ reply: undefined,
7505
+ replyKind: undefined,
7506
+ mentions: [],
7507
+ quotes: [],
7508
+ profiles: []
7509
+ };
7510
+ const rootCandidates = [];
7511
+ const replyCandidates = [];
7512
+ for (const tag of event.tags) {
7513
+ if ((tag[0] === "E" || tag[0] === "A" || tag[0] === "I") && tag[1]) {
7514
+ const pointer = parsePointer(tag);
7515
+ if (pointer)
7516
+ rootCandidates.push({ tagName: tag[0], pointer });
7517
+ continue;
7518
+ }
7519
+ if ((tag[0] === "e" || tag[0] === "a" || tag[0] === "i") && tag[1]) {
7520
+ const pointer = parsePointer(tag);
7521
+ if (pointer)
7522
+ replyCandidates.push({ tagName: tag[0], pointer });
7523
+ continue;
7524
+ }
7525
+ if (tag[0] === "K") {
7526
+ result.rootKind = parseKind(tag[1]);
7527
+ continue;
7528
+ }
7529
+ if (tag[0] === "k") {
7530
+ result.replyKind = parseKind(tag[1]);
7531
+ continue;
7532
+ }
7533
+ if (tag[0] === "q") {
7534
+ const pointer = parseQuote(tag);
7535
+ if (pointer)
7536
+ result.quotes.push(pointer);
7537
+ continue;
7538
+ }
7539
+ if ((tag[0] === "P" || tag[0] === "p") && tag[1]) {
7540
+ result.profiles.push({
7541
+ pubkey: tag[1],
7542
+ relays: tag[2] ? [tag[2]] : []
7543
+ });
7544
+ }
7545
+ }
7546
+ result.root = choosePointer(rootCandidates);
7547
+ result.reply = choosePointer(replyCandidates);
7548
+ inheritRelayHints(result.root, result.profiles);
7549
+ inheritRelayHints(result.reply, result.profiles);
7550
+ result.quotes.forEach((pointer) => inheritRelayHints(pointer, result.profiles));
7551
+ return result;
7552
+ }
7403
7553
  function finishReactionEvent(t, reacted, privateKey) {
7404
7554
  const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
7405
7555
  return finalizeEvent({
@@ -7434,7 +7584,7 @@ function getReactedEventPointer(event) {
7434
7584
  author: lastPTag[1]
7435
7585
  };
7436
7586
  }
7437
- function* parse3(content) {
7587
+ function* parse4(content) {
7438
7588
  let emojis = [];
7439
7589
  if (typeof content !== "string") {
7440
7590
  for (let i2 = 0;i2 < content.tags.length; i2++) {
@@ -7613,12 +7763,12 @@ async function validateGithub(pubkey, username, proof) {
7613
7763
  function parseConnectionString(connectionString) {
7614
7764
  const { host, pathname, searchParams } = new URL(connectionString);
7615
7765
  const pubkey = pathname || host;
7616
- const relay = searchParams.get("relay");
7766
+ const relays = searchParams.getAll("relay");
7617
7767
  const secret = searchParams.get("secret");
7618
- if (!pubkey || !relay || !secret) {
7768
+ if (!pubkey || relays.length === 0 || !secret) {
7619
7769
  throw new Error("invalid connection string");
7620
7770
  }
7621
- return { pubkey, relay, secret };
7771
+ return { pubkey, relay: relays[0], relays, secret };
7622
7772
  }
7623
7773
  async function makeNwcRequestEvent(pubkey, secretKey, invoice) {
7624
7774
  const content = {
@@ -8333,7 +8483,11 @@ var __defProp2, __export2 = (target, all) => {
8333
8483
  case "AUTH": {
8334
8484
  this.challenge = data[1];
8335
8485
  if (this.onauth) {
8336
- this.auth(this.onauth);
8486
+ this.auth(this.onauth).catch((err) => {
8487
+ if (!(err instanceof SendingOnClosedConnection)) {
8488
+ throw err;
8489
+ }
8490
+ });
8337
8491
  }
8338
8492
  return;
8339
8493
  }
@@ -8413,7 +8567,7 @@ var __defProp2, __export2 = (target, all) => {
8413
8567
  this.relay.idleSince = Date.now();
8414
8568
  this.onclose?.(reason);
8415
8569
  }
8416
- }, _WebSocket, _WebSocket2, nip19_exports, NostrTypeGuard, Bech32MaxSize = 5000, BECH32_REGEX, nip04_exports, nip05_exports, NIP05_REGEX, isNip05 = (value) => NIP05_REGEX.test(value || ""), _fetch, nip10_exports, nip11_exports, _fetch2, nip13_exports, nip17_exports, nip59_exports, nip44_exports, minPlaintextSize = 1, maxPlaintextSize = 65535, v2, TWO_DAYS, now = () => Math.round(Date.now() / 1000), randomNow = () => Math.round(now() - Math.random() * TWO_DAYS), nip44ConversationKey = (privateKey, publicKey) => getConversationKey(privateKey, publicKey), nip44Encrypt = (data, privateKey, publicKey) => encrypt22(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey)), nip44Decrypt = (data, privateKey) => JSON.parse(decrypt22(data.content, nip44ConversationKey(privateKey, data.pubkey))), unwrapEvent2, unwrapManyEvents2, nip18_exports, nip21_exports, NOSTR_URI_REGEX, nip25_exports, nip27_exports, noCharacter, noURLCharacter, MAX_HASHTAG_LENGTH = 42, nip28_exports, channelCreateEvent = (t, privateKey) => {
8570
+ }, _WebSocket, _WebSocket2, nip19_exports, NostrTypeGuard, Bech32MaxSize = 5000, BECH32_REGEX, nip04_exports, nip05_exports, NIP05_REGEX, isNip05 = (value) => NIP05_REGEX.test(value || ""), _fetch, nip10_exports, nip11_exports, _fetch2, nip13_exports, nip17_exports, nip59_exports, nip44_exports, minPlaintextSize = 1, maxPlaintextSize = 4294967295, extendedPrefixThreshold = 65536, v2, TWO_DAYS, now = () => Math.round(Date.now() / 1000), randomNow = () => Math.round(now() - Math.random() * TWO_DAYS), nip44ConversationKey = (privateKey, publicKey) => getConversationKey(privateKey, publicKey), nip44Encrypt = (data, privateKey, publicKey) => encrypt22(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey)), nip44Decrypt = (data, privateKey) => JSON.parse(decrypt22(data.content, nip44ConversationKey(privateKey, data.pubkey))), unwrapEvent2, unwrapManyEvents2, nip18_exports, nip21_exports, NOSTR_URI_REGEX, nip22_exports, nip25_exports, nip27_exports, noCharacter, noURLCharacter, MAX_HASHTAG_LENGTH = 42, nip28_exports, channelCreateEvent = (t, privateKey) => {
8417
8571
  let content;
8418
8572
  if (typeof t.content === "object") {
8419
8573
  content = JSON.stringify(t.content);
@@ -9171,7 +9325,9 @@ var init_esm = __esm(() => {
9171
9325
  v2 = {
9172
9326
  utils: {
9173
9327
  getConversationKey,
9174
- calcPaddedLen
9328
+ calcPaddedLen,
9329
+ pad,
9330
+ unpad
9175
9331
  },
9176
9332
  encrypt: encrypt22,
9177
9333
  decrypt: decrypt22
@@ -9192,6 +9348,10 @@ var init_esm = __esm(() => {
9192
9348
  test: () => test
9193
9349
  });
9194
9350
  NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`);
9351
+ nip22_exports = {};
9352
+ __export2(nip22_exports, {
9353
+ parse: () => parse3
9354
+ });
9195
9355
  nip25_exports = {};
9196
9356
  __export2(nip25_exports, {
9197
9357
  finishReactionEvent: () => finishReactionEvent,
@@ -9199,7 +9359,7 @@ var init_esm = __esm(() => {
9199
9359
  });
9200
9360
  nip27_exports = {};
9201
9361
  __export2(nip27_exports, {
9202
- parse: () => parse3
9362
+ parse: () => parse4
9203
9363
  });
9204
9364
  noCharacter = /\W/m;
9205
9365
  noURLCharacter = /[^\w\/] |[^\w\/]$|$|,| /m;
@@ -13007,10 +13167,10 @@ var require_packer_sync = __commonJS((exports, module) => {
13007
13167
 
13008
13168
  // node_modules/pngjs/lib/png-sync.js
13009
13169
  var require_png_sync = __commonJS((exports) => {
13010
- var parse4 = require_parser_sync();
13170
+ var parse5 = require_parser_sync();
13011
13171
  var pack = require_packer_sync();
13012
13172
  exports.read = function(buffer, options) {
13013
- return parse4(buffer, options || {});
13173
+ return parse5(buffer, options || {});
13014
13174
  };
13015
13175
  exports.write = function(png, options) {
13016
13176
  return pack(png, options);
@@ -15610,7 +15770,7 @@ async function isCocodInstalled(cocodPath) {
15610
15770
  // package.json
15611
15771
  var package_default = {
15612
15772
  name: "routstrd",
15613
- version: "0.2.21",
15773
+ version: "0.3.0",
15614
15774
  module: "src/index.ts",
15615
15775
  type: "module",
15616
15776
  private: false,
@@ -15633,10 +15793,11 @@ var package_default = {
15633
15793
  typescript: "^5"
15634
15794
  },
15635
15795
  dependencies: {
15636
- "@cashu/cashu-ts": "^3.1.1",
15637
- "@routstr/sdk": "^0.3.6",
15796
+ "@cashu/cashu-ts": "^4.3.0",
15797
+ "@routstr/sdk": "^0.3.8",
15638
15798
  "applesauce-core": "^5.1.0",
15639
15799
  "applesauce-relay": "^5.1.0",
15800
+ "applesauce-wallet-connect": "^6.0.0",
15640
15801
  commander: "^14.0.2",
15641
15802
  "nostr-tools": "^2.12.0",
15642
15803
  qrcode: "^1.5.4",
@@ -15762,7 +15923,7 @@ Initialization complete!`);
15762
15923
  To ensure routstrd persists across system restarts, run: 'routstrd service install'`);
15763
15924
  }
15764
15925
  program.name("routstrd").description("Routstr daemon - Manage routstr processes").version(package_default.version, "--version", "output the version number");
15765
- program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
15926
+ program.command("refund").description("Refund pending tokens and API keys to a specified mint").option("-m, --mint-url <mintUrl>", "Mint URL to refund to (defaults to first mint in wallet)").option("-y, --yes", "Skip confirmation prompt", false).option("--xcashu", "Refund xcashu tokens only (uses refundXcashuTokens)", false).action(async (options) => {
15766
15927
  await ensureDaemonRunning();
15767
15928
  let mintUrl = options.mintUrl;
15768
15929
  if (!mintUrl) {
@@ -15780,6 +15941,27 @@ program.command("refund").description("Refund pending tokens and API keys to a s
15780
15941
  console.log(`Using mint URL: ${mintUrl}`);
15781
15942
  }
15782
15943
  try {
15944
+ if (options.xcashu) {
15945
+ const result2 = await callDaemon("/refund/xcashu", {
15946
+ method: "POST",
15947
+ body: { mintUrl }
15948
+ });
15949
+ if (result2.error) {
15950
+ console.log(result2.error);
15951
+ process.exit(1);
15952
+ }
15953
+ const output2 = result2.output;
15954
+ if (output2) {
15955
+ console.log(output2.message);
15956
+ console.log(`
15957
+ Results:`);
15958
+ for (const r of output2.results) {
15959
+ const status = r.success ? "success" : `failed: ${r.error || "unknown"}`;
15960
+ console.log(` - ${r.baseUrl}: ${status}`);
15961
+ }
15962
+ }
15963
+ return;
15964
+ }
15783
15965
  const result = await callDaemon("/refund", {
15784
15966
  method: "POST",
15785
15967
  body: { mintUrl }
@@ -16388,6 +16570,65 @@ walletMintsCmd.command("info <url>").description("Get wallet mint info").action(
16388
16570
  body: { url }
16389
16571
  });
16390
16572
  });
16573
+ var nwcCmd = program.command("nwc").description("Manage NWC (Nostr Wallet Connect) integration");
16574
+ nwcCmd.command("connect").description("Connect to a Lightning wallet via NWC").argument("[connection-string]", "NWC connection string (nostr+walletconnect://...)").action(async (connectionString) => {
16575
+ if (!connectionString) {
16576
+ const rl = __require("readline").createInterface({
16577
+ input: process.stdin,
16578
+ output: process.stdout
16579
+ });
16580
+ connectionString = await new Promise((resolve) => {
16581
+ rl.question("Paste your NWC connection string: ", (answer) => {
16582
+ rl.close();
16583
+ resolve(answer.trim());
16584
+ });
16585
+ });
16586
+ }
16587
+ if (!/^nostr\+walletconnect:\/\/[0-9a-fA-F]{64}\?relay=/.test(connectionString)) {
16588
+ console.error("Invalid NWC connection string: expected nostr+walletconnect://<64-char-hex>?relay=...");
16589
+ process.exit(1);
16590
+ }
16591
+ await handleDaemonCommand("/nwc/connect", {
16592
+ method: "POST",
16593
+ body: { connectionString }
16594
+ });
16595
+ });
16596
+ nwcCmd.command("disconnect").description("Disconnect from NWC wallet").action(async () => {
16597
+ await handleDaemonCommand("/nwc/disconnect", {
16598
+ method: "POST"
16599
+ });
16600
+ });
16601
+ nwcCmd.command("status").description("Show NWC connection status and wallet info").action(async () => {
16602
+ await handleDaemonCommand("/nwc/status");
16603
+ });
16604
+ nwcCmd.command("fund <amount>").description("Manually fund the Cashu wallet from the connected NWC wallet").action(async (amount) => {
16605
+ const parsedAmount = parsePositiveIntOrExit(amount, "amount");
16606
+ await handleDaemonCommand("/nwc/fund", {
16607
+ method: "POST",
16608
+ body: { amount: parsedAmount }
16609
+ });
16610
+ });
16611
+ var autoRefillCmd = nwcCmd.command("auto-refill").description("Manage automatic wallet refill from NWC");
16612
+ autoRefillCmd.command("on").description("Enable auto-refill").option("--threshold <sats>", "Refill when Cashu balance drops below this many sats", "500").option("--amount <sats>", "Refill this many sats at a time", "1000").option("--cooldown <seconds>", "Minimum time between refills in seconds", "300").action(async (options) => {
16613
+ const threshold = parsePositiveIntOrExit(options.threshold, "threshold");
16614
+ const amount = parsePositiveIntOrExit(options.amount, "amount");
16615
+ const cooldownSec = parsePositiveIntOrExit(options.cooldown, "cooldown");
16616
+ await handleDaemonCommand("/nwc/auto-refill", {
16617
+ method: "POST",
16618
+ body: {
16619
+ enabled: true,
16620
+ threshold,
16621
+ amount,
16622
+ cooldownMs: cooldownSec * 1000
16623
+ }
16624
+ });
16625
+ });
16626
+ autoRefillCmd.command("off").description("Disable auto-refill").action(async () => {
16627
+ await handleDaemonCommand("/nwc/auto-refill", {
16628
+ method: "POST",
16629
+ body: { enabled: false }
16630
+ });
16631
+ });
16391
16632
  program.command("stop").description("Stop the background daemon").action(async () => {
16392
16633
  await handleDaemonCommand("/stop", { method: "POST" });
16393
16634
  });
@@ -0,0 +1,15 @@
1
+ services:
2
+ routstrd:
3
+ build: .
4
+ container_name: routstrd
5
+ stdin_open: true
6
+ tty: true
7
+ volumes:
8
+ - routstrd_data:/data
9
+ ports:
10
+ - "8008:8008"
11
+ restart: unless-stopped
12
+ command: /bin/bash
13
+
14
+ volumes:
15
+ routstrd_data:
package/new-task.md ADDED
@@ -0,0 +1,60 @@
1
+ # Hot-reload NWC connection (remove restart requirement)
2
+
3
+ ## Problem
4
+
5
+ `nwc connect` and `nwc disconnect` require a daemon restart to take effect. The
6
+ CLI writes the new connection string to disk, but the daemon never re-reads it.
7
+
8
+ ## Current flow
9
+
10
+ 1. **Startup** — `createWalletAdapter` reads `options.nwcConnectionString` and
11
+ creates a `WalletConnect` (relay pool + NWC wallet service) once:
12
+ ```ts
13
+ // src/daemon/wallet/index.ts:70-78
14
+ if (options.nwcConnectionString) {
15
+ wallet = WalletConnect.fromConnectURI(options.nwcConnectionString, { pool });
16
+ }
17
+ ```
18
+
19
+ 2. **CLI `nwc connect`** — HTTP handler at `POST /nwc/connect` only calls
20
+ `saveDaemonConfig(config)`. The running `WalletConnect` instance is untouched.
21
+
22
+ 3. **Result** — the old connection (or lack thereof) stays active until restart.
23
+
24
+ ## What needs to happen
25
+
26
+ Provide a way for the daemon to tear down the old `WalletConnect` and create a
27
+ new one from the updated connection string, triggered by the HTTP handler,
28
+ **without restarting the process**.
29
+
30
+ ### Key pieces
31
+
32
+ - **`src/daemon/wallet/index.ts`** — `createWalletAdapter` needs to expose a
33
+ `reconnect(connectionString: string)` method that:
34
+ - Closes the old relay pool / WalletConnect
35
+ - Creates a new `WalletConnect` from the new connection string
36
+ - Updates the adapter's internal `wallet` reference
37
+ - If connection string is empty/undefined, disconnects and sets `wallet` to
38
+ `undefined`
39
+
40
+ - **`src/daemon/http/index.ts`** — `POST /nwc/connect` and `POST /nwc/disconnect`
41
+ handlers should call `reconnect()` after saving config, and drop the "Restart
42
+ daemon" message.
43
+
44
+ - **`src/daemon/index.ts`** — may need to store the `reconnect` function and pass
45
+ it through `DaemonDeps` to the HTTP handler.
46
+
47
+ - **`src/cli.ts`** — remove `console.log("\nRun 'routstrd restart' to connect...")`
48
+ from the connect/disconnect commands.
49
+
50
+ ### Considerations
51
+
52
+ - The `WalletConnect` instance is referenced in the auto-refill loop (passed to
53
+ `startAutoRefillLoop`). After a reconnect, the auto-refill loop's captured
54
+ `wallet` reference would be stale. The loop already reads config from disk
55
+ each cycle, so it should also reference the latest `wallet` instance.
56
+ - Could make the auto-refill loop accept a `getWallet: () => WalletConnect |
57
+ undefined` getter, same pattern as the config getter.
58
+ - Relay pool cleanup — `applesauce-wallet-connect` / `applesauce-relay` may need
59
+ explicit `pool.close()` or `pool.destroy()` to free connections.
60
+ - Edge case: a refill is in progress when reconnect is triggered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.2.21",
3
+ "version": "0.3.0",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
@@ -23,10 +23,11 @@
23
23
  "typescript": "^5"
24
24
  },
25
25
  "dependencies": {
26
- "@cashu/cashu-ts": "^3.1.1",
27
- "@routstr/sdk": "^0.3.6",
26
+ "@cashu/cashu-ts": "^4.3.0",
27
+ "@routstr/sdk": "^0.3.8",
28
28
  "applesauce-core": "^5.1.0",
29
29
  "applesauce-relay": "^5.1.0",
30
+ "applesauce-wallet-connect": "^6.0.0",
30
31
  "commander": "^14.0.2",
31
32
  "nostr-tools": "^2.12.0",
32
33
  "qrcode": "^1.5.4",