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/.dockerignore +12 -0
- package/Dockerfile +11 -0
- package/bun.lock +79 -8
- package/dist/daemon/index.js +32564 -18466
- package/dist/index.js +271 -30
- package/docker-compose.yml +15 -0
- package/new-task.md +60 -0
- package/package.json +4 -3
- package/src/cli.ts +140 -1
- package/src/daemon/config-store.ts +13 -1
- package/src/daemon/wallet/auto-refill.ts +192 -0
- package/src/start-daemon.ts +5 -5
- package/src/utils/config.ts +24 -0
- package/src/utils/logger.ts +3 -3
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
|
-
|
|
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: "
|
|
2336
|
-
stderr: "
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 >
|
|
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
|
-
|
|
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
|
|
7191
|
-
const
|
|
7192
|
-
|
|
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
|
|
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
|
|
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*
|
|
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
|
|
7766
|
+
const relays = searchParams.getAll("relay");
|
|
7617
7767
|
const secret = searchParams.get("secret");
|
|
7618
|
-
if (!pubkey ||
|
|
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 =
|
|
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: () =>
|
|
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
|
|
13170
|
+
var parse5 = require_parser_sync();
|
|
13011
13171
|
var pack = require_packer_sync();
|
|
13012
13172
|
exports.read = function(buffer, options) {
|
|
13013
|
-
return
|
|
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.
|
|
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.
|
|
15637
|
-
"@routstr/sdk": "^0.3.
|
|
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
|
});
|
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.
|
|
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.
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
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",
|