javascript-solid-server 0.0.133 → 0.0.135

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.133",
3
+ "version": "0.0.135",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@ import crypto from 'crypto';
29
29
  import { getNostrPubkey, pubkeyToDidNostr } from '../auth/nostr.js';
30
30
  import { readLedger, writeLedger, getBalance, credit, debit } from '../webledger.js';
31
31
  import { verifyMrc20Deposit, verifyMrc20Anchor, jcs, btAddress } from '../mrc20.js';
32
- import { loadTrail, transferToken, buildTransaction, broadcastTx, p2trScript } from '../token.js';
32
+ import { loadTrail, transferToken, buildTransaction, broadcastTx, p2trScript, btDeriveChainedPrivkey } from '../token.js';
33
33
  import { secp256k1 } from '@noble/curves/secp256k1';
34
34
  import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
35
35
  import fs from 'fs-extra';
@@ -282,7 +282,7 @@ export function createPayHandler(options = {}) {
282
282
  return reply.send(info);
283
283
  }
284
284
 
285
- // --- GET /pay/.address — public deposit address ---
285
+ // --- GET /pay/.address — deposit address (optional per-user tweak) ---
286
286
  if (url === '/pay/.address' && request.method === 'GET') {
287
287
  const chain = request.query?.chain || (payChains ? payChains[0] : 'tbtc4');
288
288
  if (!CHAIN_REGISTRY[chain]) {
@@ -293,8 +293,15 @@ export function createPayHandler(options = {}) {
293
293
  }
294
294
  const kp = await loadOrCreateKeypair();
295
295
  const network = chain === 'btc' ? 'mainnet' : (chain === 'tbtc3' ? 'testnet' : 'testnet4');
296
- const address = btAddress(kp.pubkey, [], network);
297
- return reply.send({ address, chain, pubkey: kp.pubkey });
296
+ const user = request.query?.user?.trim().toLowerCase() || null;
297
+ if (user && !/^did:nostr:[0-9a-f]{64}$/.test(user)) {
298
+ return reply.code(400).send({ error: 'Invalid user DID. Expected: did:nostr:<64-hex>' });
299
+ }
300
+ const states = user ? [user] : [];
301
+ const address = btAddress(kp.pubkey, states, network);
302
+ const response = { address, chain, pubkey: kp.pubkey };
303
+ if (user) response.user = user;
304
+ return reply.send(response);
298
305
  }
299
306
 
300
307
  // --- GET /pay/.balance ---
@@ -304,6 +311,49 @@ export function createPayHandler(options = {}) {
304
311
  return reply.code(401).send({ error: 'NIP-98 authentication required' });
305
312
  }
306
313
  const didUri = pubkeyToDidNostr(pubkey);
314
+
315
+ // Auto-detect deposits to user's tweaked address (Phase 3)
316
+ if (payChains) {
317
+ try {
318
+ const kp = await loadOrCreateKeypair();
319
+ const utxos = await loadUtxos();
320
+ const ledger = await readLedger();
321
+ let credited = 0;
322
+
323
+ for (const chainId of payChains) {
324
+ const chain = CHAIN_REGISTRY[chainId];
325
+ const network = chainId === 'btc' ? 'mainnet' : (chainId === 'tbtc3' ? 'testnet' : 'testnet4');
326
+ const userAddr = btAddress(kp.pubkey, [didUri], network);
327
+
328
+ const resp = await fetch(`${chain.explorer}/address/${userAddr}/utxo`);
329
+ if (!resp.ok) continue;
330
+ const addrUtxos = await resp.json();
331
+
332
+ for (const u of addrUtxos) {
333
+ if (utxos.find(x => x.txid === u.txid && x.vout === u.vout)) continue;
334
+ // New UTXO — fetch tx for scriptpubkey, then auto-credit
335
+ let scriptpubkey = '';
336
+ try {
337
+ const txResp = await fetch(`${chain.explorer}/tx/${u.txid}`);
338
+ if (txResp.ok) {
339
+ const txData = await txResp.json();
340
+ scriptpubkey = txData.vout?.[u.vout]?.scriptpubkey || '';
341
+ }
342
+ } catch { /* best effort */ }
343
+ const currency = chain.unit;
344
+ credit(ledger, didUri, u.value, currency);
345
+ utxos.push({ txid: u.txid, vout: u.vout, amount: u.value, scriptpubkey, chain: chainId, tweak: didUri, spent: false });
346
+ credited += u.value;
347
+ }
348
+ }
349
+
350
+ if (credited > 0) {
351
+ await writeLedger(ledger);
352
+ await saveUtxos(utxos);
353
+ }
354
+ } catch { /* scan failure is non-fatal */ }
355
+ }
356
+
307
357
  const ledger = await readLedger();
308
358
  const response = {
309
359
  did: didUri,
@@ -435,8 +485,10 @@ export function createPayHandler(options = {}) {
435
485
  return reply.code(400).send({ error: `Unknown chain: ${chainId}` });
436
486
  }
437
487
 
438
- // Derive pod's address for this chain
488
+ // Derive address — try per-user tweaked address first, fall back to generic
439
489
  const network = chainId === 'btc' ? 'mainnet' : (chainId === 'tbtc3' ? 'testnet' : 'testnet4');
490
+ const didUri = pubkeyToDidNostr(pubkey);
491
+ const userAddress = btAddress(kp.pubkey, [didUri], network);
440
492
  const podAddress = btAddress(kp.pubkey, [], network);
441
493
 
442
494
  // Fetch transaction from mempool
@@ -455,9 +507,11 @@ export function createPayHandler(options = {}) {
455
507
  return reply.code(400).send({ error: `Output ${deposit.vout} not found` });
456
508
  }
457
509
 
458
- // Verify output pays our address
459
- if (output.scriptpubkey_address !== podAddress) {
460
- return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: podAddress });
510
+ // Verify output pays our address (per-user tweaked or generic pod address)
511
+ const outputAddr = output.scriptpubkey_address;
512
+ const tweak = outputAddr === userAddress ? didUri : null;
513
+ if (outputAddr !== userAddress && outputAddr !== podAddress) {
514
+ return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: { user: userAddress, pod: podAddress } });
461
515
  }
462
516
 
463
517
  const amount = output.value;
@@ -465,14 +519,12 @@ export function createPayHandler(options = {}) {
465
519
 
466
520
  // Replay protection + UTXO tracking
467
521
  const utxos = await loadUtxos();
468
- const utxoKey = `${deposit.txid}:${deposit.vout}`;
469
522
  if (utxos.find(u => u.txid === deposit.txid && u.vout === deposit.vout)) {
470
523
  return reply.code(400).send({ error: 'This output has already been claimed' });
471
524
  }
472
- utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, spent: false });
525
+ utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, tweak, spent: false });
473
526
  await saveUtxos(utxos);
474
527
 
475
- const didUri = pubkeyToDidNostr(pubkey);
476
528
  const ledger = await readLedger();
477
529
  const newBalance = credit(ledger, didUri, amount, currency);
478
530
  await writeLedger(ledger);
@@ -752,23 +804,45 @@ export function createPayHandler(options = {}) {
752
804
  return reply.code(400).send({ error: 'No UTXOs available for withdrawal' });
753
805
  }
754
806
 
755
- // Select UTXOs (simple: pick first one that's big enough, or accumulate)
756
- let selected = [];
757
- let total = 0;
807
+ // Load pod keypair
808
+ const kp = await loadOrCreateKeypair();
809
+
810
+ // Select UTXOs — group by tweak so we can sign with one key
811
+ // Prefer untweaked UTXOs first, then tweaked ones
758
812
  const fee = 300;
759
813
  const needed = withdrawAmount + fee;
760
- for (const utxo of available) {
814
+ let selected = [];
815
+ let total = 0;
816
+ let selectedTweak = null;
817
+
818
+ // Try untweaked first
819
+ for (const utxo of available.filter(u => !u.tweak)) {
761
820
  selected.push(utxo);
762
821
  total += utxo.amount;
763
822
  if (total >= needed) break;
764
823
  }
824
+ // If not enough, try tweaked (same tweak group only)
825
+ if (total < needed) {
826
+ const tweaked = available.filter(u => u.tweak);
827
+ selected = [];
828
+ total = 0;
829
+ selectedTweak = null;
830
+ for (const utxo of tweaked) {
831
+ if (selectedTweak && utxo.tweak !== selectedTweak) continue;
832
+ selected.push(utxo);
833
+ selectedTweak = utxo.tweak;
834
+ total += utxo.amount;
835
+ if (total >= needed) break;
836
+ }
837
+ }
765
838
  if (total < needed) {
766
839
  return reply.code(400).send({ error: 'Not enough UTXO value for withdrawal + fee', available: total, needed });
767
840
  }
768
841
 
769
- // Load pod keypair
770
- const kp = await loadOrCreateKeypair();
771
- const privkeyBytes = hexToBytes(kp.privkey);
842
+ // Derive signing key (tweaked if UTXOs are tweaked)
843
+ const privkeyBytes = selectedTweak
844
+ ? btDeriveChainedPrivkey(hexToBytes(kp.privkey), [selectedTweak])
845
+ : hexToBytes(kp.privkey);
772
846
 
773
847
  // Generate a new keypair for the voucher recipient
774
848
  const voucherPrivkey = secp256k1.utils.randomPrivateKey();
package/src/token.js CHANGED
@@ -98,7 +98,7 @@ function btDeriveChainedPubkey(pubkeyBase, states) {
98
98
  return cur;
99
99
  }
100
100
 
101
- function btDeriveChainedPrivkey(privkeyBytes, states) {
101
+ export function btDeriveChainedPrivkey(privkeyBytes, states) {
102
102
  let d = bytesToBigInt(privkeyBytes);
103
103
  let cur = new Uint8Array(secp256k1.getPublicKey(privkeyBytes, true));
104
104
  for (const s of states) {