-
Notifications
You must be signed in to change notification settings - Fork 9
feat: per-user tweaked deposit addresses (Phase 2 of #267) #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,7 +29,7 @@ import crypto from 'crypto'; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getNostrPubkey, pubkeyToDidNostr } from '../auth/nostr.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { readLedger, writeLedger, getBalance, credit, debit } from '../webledger.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { verifyMrc20Deposit, verifyMrc20Anchor, jcs, btAddress } from '../mrc20.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loadTrail, transferToken, buildTransaction, broadcastTx, p2trScript } from '../token.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loadTrail, transferToken, buildTransaction, broadcastTx, p2trScript, btDeriveChainedPrivkey } from '../token.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { secp256k1 } from '@noble/curves/secp256k1'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from 'fs-extra'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -282,7 +282,7 @@ export function createPayHandler(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.send(info); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --- GET /pay/.address — public deposit address --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --- GET /pay/.address — deposit address (optional per-user tweak) --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (url === '/pay/.address' && request.method === 'GET') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chain = request.query?.chain || (payChains ? payChains[0] : 'tbtc4'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!CHAIN_REGISTRY[chain]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -293,8 +293,15 @@ export function createPayHandler(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const kp = await loadOrCreateKeypair(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const network = chain === 'btc' ? 'mainnet' : (chain === 'tbtc3' ? 'testnet' : 'testnet4'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const address = btAddress(kp.pubkey, [], network); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.send({ address, chain, pubkey: kp.pubkey }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = request.query?.user?.trim().toLowerCase() || null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (user && !/^did:nostr:[0-9a-f]{64}$/.test(user)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: 'Invalid user DID. Expected: did:nostr:<64-hex>' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const states = user ? [user] : []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const address = btAddress(kp.pubkey, states, network); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = { address, chain, pubkey: kp.pubkey }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (user) response.user = user; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.send(response); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --- GET /pay/.balance --- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -435,8 +442,10 @@ export function createPayHandler(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: `Unknown chain: ${chainId}` }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Derive pod's address for this chain | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Derive address — try per-user tweaked address first, fall back to generic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const network = chainId === 'btc' ? 'mainnet' : (chainId === 'tbtc3' ? 'testnet' : 'testnet4'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const didUri = pubkeyToDidNostr(pubkey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userAddress = btAddress(kp.pubkey, [didUri], network); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const podAddress = btAddress(kp.pubkey, [], network); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Fetch transaction from mempool | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -455,24 +464,24 @@ export function createPayHandler(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: `Output ${deposit.vout} not found` }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Verify output pays our address | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (output.scriptpubkey_address !== podAddress) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: podAddress }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Verify output pays our address (per-user tweaked or generic pod address) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const outputAddr = output.scriptpubkey_address; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tweak = outputAddr === userAddress ? didUri : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputAddr !== userAddress && outputAddr !== podAddress) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: { user: userAddress, pod: podAddress } }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+467
to
472
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const amount = output.value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currency = chain.unit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replay protection + UTXO tracking | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const utxos = await loadUtxos(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const utxoKey = `${deposit.txid}:${deposit.vout}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (utxos.find(u => u.txid === deposit.txid && u.vout === deposit.vout)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: 'This output has already been claimed' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, spent: false }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utxos.push({ txid: deposit.txid, vout: deposit.vout, amount, scriptpubkey: output.scriptpubkey, chain: chainId, tweak, spent: false }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await saveUtxos(utxos); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const didUri = pubkeyToDidNostr(pubkey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ledger = await readLedger(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newBalance = credit(ledger, didUri, amount, currency); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await writeLedger(ledger); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -752,23 +761,45 @@ export function createPayHandler(options = {}) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return reply.code(400).send({ error: 'No UTXOs available for withdrawal' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Select UTXOs (simple: pick first one that's big enough, or accumulate) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let selected = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let total = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Load pod keypair | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const kp = await loadOrCreateKeypair(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Select UTXOs — group by tweak so we can sign with one key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Prefer untweaked UTXOs first, then tweaked ones | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fee = 300; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const needed = withdrawAmount + fee; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const utxo of available) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let selected = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let total = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let selectedTweak = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Try untweaked first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const utxo of available.filter(u => !u.tweak)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selected.push(utxo); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total += utxo.amount; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (total >= needed) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If not enough, try tweaked (same tweak group only) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (total < needed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tweaked = available.filter(u => u.tweak); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selected = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedTweak = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const utxo of tweaked) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (selectedTweak && utxo.tweak !== selectedTweak) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selected.push(utxo); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedTweak = utxo.tweak; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total += utxo.amount; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (total >= needed) break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+783
to
+792
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tweaked = available.filter(u => u.tweak); | |
| selected = []; | |
| total = 0; | |
| selectedTweak = null; | |
| for (const utxo of tweaked) { | |
| if (selectedTweak && utxo.tweak !== selectedTweak) continue; | |
| selected.push(utxo); | |
| selectedTweak = utxo.tweak; | |
| total += utxo.amount; | |
| if (total >= needed) break; | |
| const tweakedGroups = new Map(); | |
| for (const utxo of available.filter(u => u.tweak)) { | |
| if (!tweakedGroups.has(utxo.tweak)) tweakedGroups.set(utxo.tweak, []); | |
| tweakedGroups.get(utxo.tweak).push(utxo); | |
| } | |
| const candidateGroups = Array.from(tweakedGroups.entries()) | |
| .map(([tweak, utxos]) => ({ | |
| tweak, | |
| utxos, | |
| total: utxos.reduce((sum, u) => sum + u.amount, 0), | |
| })) | |
| .filter(group => group.total >= needed) | |
| .sort((a, b) => a.total - b.total); | |
| selected = []; | |
| total = 0; | |
| selectedTweak = null; | |
| if (candidateGroups.length > 0) { | |
| const bestGroup = candidateGroups[0]; | |
| selectedTweak = bestGroup.tweak; | |
| for (const utxo of bestGroup.utxos) { | |
| selected.push(utxo); | |
| total += utxo.amount; | |
| if (total >= needed) break; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The claim handler now accepts both
userAddressandpodAddress, but the success response doesn’t indicate which address was actually matched. For troubleshooting/client correctness (especially with per-user addresses), consider including the matchedoutputAddr(and/or the resolvedtweak) in the response payload.