javascript-solid-server 0.0.133 → 0.0.134

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.134",
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 ---
@@ -435,8 +442,10 @@ export function createPayHandler(options = {}) {
435
442
  return reply.code(400).send({ error: `Unknown chain: ${chainId}` });
436
443
  }
437
444
 
438
- // Derive pod's address for this chain
445
+ // Derive address — try per-user tweaked address first, fall back to generic
439
446
  const network = chainId === 'btc' ? 'mainnet' : (chainId === 'tbtc3' ? 'testnet' : 'testnet4');
447
+ const didUri = pubkeyToDidNostr(pubkey);
448
+ const userAddress = btAddress(kp.pubkey, [didUri], network);
440
449
  const podAddress = btAddress(kp.pubkey, [], network);
441
450
 
442
451
  // Fetch transaction from mempool
@@ -455,9 +464,11 @@ export function createPayHandler(options = {}) {
455
464
  return reply.code(400).send({ error: `Output ${deposit.vout} not found` });
456
465
  }
457
466
 
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 });
467
+ // Verify output pays our address (per-user tweaked or generic pod address)
468
+ const outputAddr = output.scriptpubkey_address;
469
+ const tweak = outputAddr === userAddress ? didUri : null;
470
+ if (outputAddr !== userAddress && outputAddr !== podAddress) {
471
+ return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: { user: userAddress, pod: podAddress } });
461
472
  }
462
473
 
463
474
  const amount = output.value;
@@ -465,14 +476,12 @@ export function createPayHandler(options = {}) {
465
476
 
466
477
  // Replay protection + UTXO tracking
467
478
  const utxos = await loadUtxos();
468
- const utxoKey = `${deposit.txid}:${deposit.vout}`;
469
479
  if (utxos.find(u => u.txid === deposit.txid && u.vout === deposit.vout)) {
470
480
  return reply.code(400).send({ error: 'This output has already been claimed' });
471
481
  }
472
- utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, spent: false });
482
+ utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, tweak, spent: false });
473
483
  await saveUtxos(utxos);
474
484
 
475
- const didUri = pubkeyToDidNostr(pubkey);
476
485
  const ledger = await readLedger();
477
486
  const newBalance = credit(ledger, didUri, amount, currency);
478
487
  await writeLedger(ledger);
@@ -752,23 +761,45 @@ export function createPayHandler(options = {}) {
752
761
  return reply.code(400).send({ error: 'No UTXOs available for withdrawal' });
753
762
  }
754
763
 
755
- // Select UTXOs (simple: pick first one that's big enough, or accumulate)
756
- let selected = [];
757
- let total = 0;
764
+ // Load pod keypair
765
+ const kp = await loadOrCreateKeypair();
766
+
767
+ // Select UTXOs — group by tweak so we can sign with one key
768
+ // Prefer untweaked UTXOs first, then tweaked ones
758
769
  const fee = 300;
759
770
  const needed = withdrawAmount + fee;
760
- for (const utxo of available) {
771
+ let selected = [];
772
+ let total = 0;
773
+ let selectedTweak = null;
774
+
775
+ // Try untweaked first
776
+ for (const utxo of available.filter(u => !u.tweak)) {
761
777
  selected.push(utxo);
762
778
  total += utxo.amount;
763
779
  if (total >= needed) break;
764
780
  }
781
+ // If not enough, try tweaked (same tweak group only)
782
+ if (total < needed) {
783
+ const tweaked = available.filter(u => u.tweak);
784
+ selected = [];
785
+ total = 0;
786
+ selectedTweak = null;
787
+ for (const utxo of tweaked) {
788
+ if (selectedTweak && utxo.tweak !== selectedTweak) continue;
789
+ selected.push(utxo);
790
+ selectedTweak = utxo.tweak;
791
+ total += utxo.amount;
792
+ if (total >= needed) break;
793
+ }
794
+ }
765
795
  if (total < needed) {
766
796
  return reply.code(400).send({ error: 'Not enough UTXO value for withdrawal + fee', available: total, needed });
767
797
  }
768
798
 
769
- // Load pod keypair
770
- const kp = await loadOrCreateKeypair();
771
- const privkeyBytes = hexToBytes(kp.privkey);
799
+ // Derive signing key (tweaked if UTXOs are tweaked)
800
+ const privkeyBytes = selectedTweak
801
+ ? btDeriveChainedPrivkey(hexToBytes(kp.privkey), [selectedTweak])
802
+ : hexToBytes(kp.privkey);
772
803
 
773
804
  // Generate a new keypair for the voucher recipient
774
805
  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) {