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 +1 -1
- package/src/handlers/pay.js +92 -18
- package/src/token.js +1 -1
package/package.json
CHANGED
package/src/handlers/pay.js
CHANGED
|
@@ -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 —
|
|
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
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
770
|
-
const
|
|
771
|
-
|
|
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) {
|