Skip to content

Commit 42d3ac7

Browse files
feat: pod deposit address and claim-style deposits
- GET /pay/.address returns the pod's taproot deposit address - Keypair generated on first use, persisted in .well-known/webledgers/ - POST /pay/.deposit now accepts {"txid", "vout", "chain"} to claim sats sent to the pod's address (verifies via mempool API) - Export buildTransaction, broadcastTx, p2trScript from token.js Phase 1 of JavaScriptSolidServer#267
1 parent a3c3c1e commit 42d3ac7

2 files changed

Lines changed: 60 additions & 5 deletions

File tree

src/handlers/pay.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ function classifyDepositObject(obj) {
195195
if (obj.state?.profile === 'mono.mrc20.v0.1' && obj.prevState) {
196196
return { type: 'mrc20', state: obj.state, prevState: obj.prevState, anchor: obj.anchor };
197197
}
198+
// Claim deposit: user sent sats to pod's address, claiming with txid
199+
if (obj.txid && obj.vout !== undefined) {
200+
return { type: 'claim', txid: obj.txid, vout: parseInt(obj.vout, 10), chain: obj.chain };
201+
}
198202
// Fall back to TXO URI in .txo field
199203
if (obj.txo) {
200204
return { type: 'sats', txo: obj.txo };
@@ -398,11 +402,62 @@ export function createPayHandler(options = {}) {
398402
});
399403
}
400404

405+
// --- Claim deposit: user sent sats to pod's address ---
406+
if (deposit.type === 'claim') {
407+
const kp = await loadOrCreateKeypair();
408+
const chainId = deposit.chain || (payChains ? payChains[0] : 'tbtc4');
409+
if (payChains && !payChains.includes(chainId)) {
410+
return reply.code(400).send({ error: `Chain '${chainId}' not enabled`, enabledChains: payChains });
411+
}
412+
const chain = CHAIN_REGISTRY[chainId];
413+
if (!chain) {
414+
return reply.code(400).send({ error: `Unknown chain: ${chainId}` });
415+
}
416+
417+
// Derive pod's address for this chain
418+
const network = chainId === 'btc' ? 'mainnet' : (chainId === 'tbtc3' ? 'testnet' : 'testnet4');
419+
const podAddress = btAddress(kp.pubkey, [], network);
420+
421+
// Fetch transaction from mempool
422+
const txResp = await fetch(`${chain.explorer}/tx/${deposit.txid}`);
423+
if (!txResp.ok) {
424+
return reply.code(400).send({ error: 'Transaction not found' });
425+
}
426+
const txData = await txResp.json();
427+
const output = txData.vout?.[deposit.vout];
428+
if (!output) {
429+
return reply.code(400).send({ error: `Output ${deposit.vout} not found` });
430+
}
431+
432+
// Verify output pays our address
433+
if (output.scriptpubkey_address !== podAddress) {
434+
return reply.code(400).send({ error: 'Output does not pay this pod\'s address', expected: podAddress });
435+
}
436+
437+
const amount = output.value;
438+
const currency = chain.unit;
439+
const didUri = pubkeyToDidNostr(pubkey);
440+
const ledger = await readLedger();
441+
const newBalance = credit(ledger, didUri, amount, currency);
442+
await writeLedger(ledger);
443+
444+
return reply.send({
445+
did: didUri,
446+
deposited: amount,
447+
balance: newBalance,
448+
unit: currency,
449+
chain: chainId,
450+
txid: deposit.txid,
451+
address: podAddress
452+
});
453+
}
454+
401455
return reply.code(400).send({
402-
error: 'Invalid deposit format. Send a TXO URI string or MRC20 state proof.',
456+
error: 'Invalid deposit format. Send a TXO URI string, MRC20 state proof, or claim {txid, vout}.',
403457
formats: {
404458
sats: 'POST body: "<txid>:<vout>" or {"txo": "<txid>:<vout>"}',
405-
mrc20: 'POST body: {"type": "mrc20", "state": {...}, "prevState": {...}}'
459+
mrc20: 'POST body: {"type": "mrc20", "state": {...}, "prevState": {...}}',
460+
claim: 'POST body: {"txid": "...", "vout": 0, "chain": "tbtc4"}'
406461
}
407462
});
408463
}

src/token.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ function btDeriveChainedPrivkey(privkeyBytes, states) {
109109
return bigIntToBytes(d);
110110
}
111111

112-
function p2trScript(xonly) {
112+
export function p2trScript(xonly) {
113113
return concatBytes(new Uint8Array([0x51, 0x20]), xonly);
114114
}
115115

116116
// --- Bitcoin transaction building ---
117-
function buildTransaction(inputs, outputs, privkeyBytes) {
117+
export function buildTransaction(inputs, outputs, privkeyBytes) {
118118
const internalXOnly = new Uint8Array(secp256k1.getPublicKey(privkeyBytes, true)).slice(1);
119119
const untweakedHex = '5120' + bytesToHex(internalXOnly);
120120
const needsTweak = bytesToHex(inputs[0].scriptPubKey) !== untweakedHex;
@@ -173,7 +173,7 @@ function buildTransaction(inputs, outputs, privkeyBytes) {
173173
return bytesToHex(concatBytes(...parts));
174174
}
175175

176-
async function broadcastTx(rawTxHex, mempoolUrl) {
176+
export async function broadcastTx(rawTxHex, mempoolUrl) {
177177
const res = await fetch(`${mempoolUrl}/api/tx`, {
178178
method: 'POST',
179179
headers: { 'Content-Type': 'text/plain' },

0 commit comments

Comments
 (0)