@@ -29,7 +29,7 @@ import crypto from 'crypto';
2929import { getNostrPubkey , pubkeyToDidNostr } from '../auth/nostr.js' ;
3030import { readLedger , writeLedger , getBalance , credit , debit } from '../webledger.js' ;
3131import { 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' ;
3333import { secp256k1 } from '@noble/curves/secp256k1' ;
3434import { bytesToHex , hexToBytes } from '@noble/hashes/utils' ;
3535import fs from 'fs-extra' ;
@@ -282,7 +282,7 @@ export function createPayHandler(options = {}) {
282282 return reply . send ( info ) ;
283283 }
284284
285- // --- GET /pay/.address — public deposit address ---
285+ // --- GET /pay/.address — deposit address (optional per-user tweak) ---
286286 if ( url === '/pay/.address' && request . method === 'GET' ) {
287287 const chain = request . query ?. chain || ( payChains ? payChains [ 0 ] : 'tbtc4' ) ;
288288 if ( ! CHAIN_REGISTRY [ chain ] ) {
@@ -293,8 +293,15 @@ export function createPayHandler(options = {}) {
293293 }
294294 const kp = await loadOrCreateKeypair ( ) ;
295295 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 && ! / ^ d i d : n o s t r : [ 0 - 9 a - 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 ) ;
298305 }
299306
300307 // --- GET /pay/.balance ---
@@ -435,8 +442,10 @@ export function createPayHandler(options = {}) {
435442 return reply . code ( 400 ) . send ( { error : `Unknown chain: ${ chainId } ` } ) ;
436443 }
437444
438- // Derive pod's address for this chain
445+ // Derive address — try per-user tweaked address first, fall back to generic
439446 const network = chainId === 'btc' ? 'mainnet' : ( chainId === 'tbtc3' ? 'testnet' : 'testnet4' ) ;
447+ const didUri = pubkeyToDidNostr ( pubkey ) ;
448+ const userAddress = btAddress ( kp . pubkey , [ didUri ] , network ) ;
440449 const podAddress = btAddress ( kp . pubkey , [ ] , network ) ;
441450
442451 // Fetch transaction from mempool
@@ -455,24 +464,24 @@ export function createPayHandler(options = {}) {
455464 return reply . code ( 400 ) . send ( { error : `Output ${ deposit . vout } not found` } ) ;
456465 }
457466
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 } } ) ;
461472 }
462473
463474 const amount = output . value ;
464475 const currency = chain . unit ;
465476
466477 // Replay protection + UTXO tracking
467478 const utxos = await loadUtxos ( ) ;
468- const utxoKey = `${ deposit . txid } :${ deposit . vout } ` ;
469479 if ( utxos . find ( u => u . txid === deposit . txid && u . vout === deposit . vout ) ) {
470480 return reply . code ( 400 ) . send ( { error : 'This output has already been claimed' } ) ;
471481 }
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 } ) ;
473483 await saveUtxos ( utxos ) ;
474484
475- const didUri = pubkeyToDidNostr ( pubkey ) ;
476485 const ledger = await readLedger ( ) ;
477486 const newBalance = credit ( ledger , didUri , amount , currency ) ;
478487 await writeLedger ( ledger ) ;
@@ -752,23 +761,45 @@ export function createPayHandler(options = {}) {
752761 return reply . code ( 400 ) . send ( { error : 'No UTXOs available for withdrawal' } ) ;
753762 }
754763
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
758769 const fee = 300 ;
759770 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 ) ) {
761777 selected . push ( utxo ) ;
762778 total += utxo . amount ;
763779 if ( total >= needed ) break ;
764780 }
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+ }
765795 if ( total < needed ) {
766796 return reply . code ( 400 ) . send ( { error : 'Not enough UTXO value for withdrawal + fee' , available : total , needed } ) ;
767797 }
768798
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 ) ;
772803
773804 // Generate a new keypair for the voucher recipient
774805 const voucherPrivkey = secp256k1 . utils . randomPrivateKey ( ) ;
0 commit comments