@1sat/wallet-toolbox 0.0.74 → 0.0.76
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/dist/api/balance/index.js +1 -5
- package/dist/api/broadcast/index.js +2 -1
- package/dist/api/inscriptions/index.js +1 -0
- package/dist/api/locks/index.js +5 -0
- package/dist/api/ordinals/index.js +12 -5
- package/dist/api/payments/index.js +2 -0
- package/dist/api/signing/index.js +1 -0
- package/dist/api/sweep/index.js +53 -29
- package/dist/api/tokens/index.js +90 -63
- package/dist/indexers/Bsv21Indexer.js +1 -2
- package/dist/services/OneSatServices.js +4 -6
- package/dist/services/client/Bsv21Client.d.ts +36 -0
- package/dist/services/client/Bsv21Client.js +45 -0
- package/dist/services/client/index.d.ts +1 -1
- package/dist/sync/SyncManager.d.ts +3 -3
- package/dist/sync/SyncManager.js +22 -18
- package/dist/wallet/factory.d.ts +5 -12
- package/dist/wallet/factory.js +15 -19
- package/package.json +1 -1
|
@@ -107,8 +107,4 @@ export const getExchangeRate = {
|
|
|
107
107
|
// Module exports
|
|
108
108
|
// ============================================================================
|
|
109
109
|
/** All balance skills for registry */
|
|
110
|
-
export const balanceSkills = [
|
|
111
|
-
getBalance,
|
|
112
|
-
getPaymentUtxos,
|
|
113
|
-
getExchangeRate,
|
|
114
|
-
];
|
|
110
|
+
export const balanceSkills = [getBalance, getPaymentUtxos, getExchangeRate];
|
|
@@ -65,8 +65,9 @@ export const broadcast = {
|
|
|
65
65
|
return { txid };
|
|
66
66
|
}
|
|
67
67
|
catch (error) {
|
|
68
|
+
console.error("[broadcast]", error);
|
|
68
69
|
return {
|
|
69
|
-
error: error instanceof Error ? error.message : "
|
|
70
|
+
error: error instanceof Error ? error.message : "unknown-error",
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
},
|
package/dist/api/locks/index.js
CHANGED
|
@@ -143,6 +143,7 @@ export const lockBsv = {
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
catch (error) {
|
|
146
|
+
console.error("[lockBsv]", error);
|
|
146
147
|
return {
|
|
147
148
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
148
149
|
};
|
|
@@ -228,6 +229,9 @@ export const unlockBsv = {
|
|
|
228
229
|
for (let i = 0; i < maturedLocks.length; i++) {
|
|
229
230
|
const lock = maturedLocks[i];
|
|
230
231
|
const input = tx.inputs[i];
|
|
232
|
+
if (!lock.output.lockingScript || !input.sourceTXID) {
|
|
233
|
+
return { error: "missing-lock-data" };
|
|
234
|
+
}
|
|
231
235
|
const lockingScript = Script.fromHex(lock.output.lockingScript);
|
|
232
236
|
const preimage = TransactionSignature.format({
|
|
233
237
|
sourceTXID: input.sourceTXID,
|
|
@@ -277,6 +281,7 @@ export const unlockBsv = {
|
|
|
277
281
|
};
|
|
278
282
|
}
|
|
279
283
|
catch (error) {
|
|
284
|
+
console.error("[unlockBsv]", error);
|
|
280
285
|
return {
|
|
281
286
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
282
287
|
};
|
|
@@ -175,9 +175,12 @@ export async function buildTransferOrdinals(ctx, request) {
|
|
|
175
175
|
});
|
|
176
176
|
recipientAddress = PublicKey.fromString(publicKey).toAddress();
|
|
177
177
|
}
|
|
178
|
-
else {
|
|
178
|
+
else if (address) {
|
|
179
179
|
recipientAddress = address;
|
|
180
180
|
}
|
|
181
|
+
else {
|
|
182
|
+
return { error: "must-provide-counterparty-or-address" };
|
|
183
|
+
}
|
|
181
184
|
// Preserve important tags from source output
|
|
182
185
|
const tags = [];
|
|
183
186
|
for (const tag of ordinal.tags ?? []) {
|
|
@@ -188,7 +191,7 @@ export async function buildTransferOrdinals(ctx, request) {
|
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
const sourceName = extractName(ordinal.customInstructions);
|
|
191
|
-
inputs
|
|
194
|
+
inputs?.push({
|
|
192
195
|
outpoint,
|
|
193
196
|
inputDescription: "Ordinal to transfer",
|
|
194
197
|
unlockingScriptLength: 108,
|
|
@@ -196,7 +199,7 @@ export async function buildTransferOrdinals(ctx, request) {
|
|
|
196
199
|
// Only track output in wallet when transferring to a counterparty (wallet can derive keys to spend it)
|
|
197
200
|
// External address transfers are NOT tracked since the wallet cannot spend them
|
|
198
201
|
if (counterparty) {
|
|
199
|
-
outputs
|
|
202
|
+
outputs?.push({
|
|
200
203
|
lockingScript: new P2PKH().lock(recipientAddress).toHex(),
|
|
201
204
|
satoshis: 1,
|
|
202
205
|
outputDescription: "Ordinal transfer",
|
|
@@ -211,7 +214,7 @@ export async function buildTransferOrdinals(ctx, request) {
|
|
|
211
214
|
}
|
|
212
215
|
else {
|
|
213
216
|
// External address - output is not tracked in wallet
|
|
214
|
-
outputs
|
|
217
|
+
outputs?.push({
|
|
215
218
|
lockingScript: new P2PKH().lock(recipientAddress).toHex(),
|
|
216
219
|
satoshis: 1,
|
|
217
220
|
outputDescription: "Ordinal transfer to external address",
|
|
@@ -396,7 +399,7 @@ export const transferOrdinals = {
|
|
|
396
399
|
inputs: params.inputs,
|
|
397
400
|
outputs: params.outputs?.map((o) => ({
|
|
398
401
|
...o,
|
|
399
|
-
lockingScript: o.lockingScript?.slice(0, 20)
|
|
402
|
+
lockingScript: `${o.lockingScript?.slice(0, 20)}...`,
|
|
400
403
|
})),
|
|
401
404
|
}, null, 2));
|
|
402
405
|
// Debug: Check if BEEF contains the source transactions
|
|
@@ -451,6 +454,7 @@ export const transferOrdinals = {
|
|
|
451
454
|
};
|
|
452
455
|
}
|
|
453
456
|
catch (error) {
|
|
457
|
+
console.error("[transferOrdinals]", error);
|
|
454
458
|
return {
|
|
455
459
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
456
460
|
};
|
|
@@ -520,6 +524,7 @@ export const listOrdinal = {
|
|
|
520
524
|
};
|
|
521
525
|
}
|
|
522
526
|
catch (error) {
|
|
527
|
+
console.error("[listOrdinal]", error);
|
|
523
528
|
return {
|
|
524
529
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
525
530
|
};
|
|
@@ -658,6 +663,7 @@ export const cancelListing = {
|
|
|
658
663
|
};
|
|
659
664
|
}
|
|
660
665
|
catch (error) {
|
|
666
|
+
console.error("[cancelListing]", error);
|
|
661
667
|
return {
|
|
662
668
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
663
669
|
};
|
|
@@ -824,6 +830,7 @@ export const purchaseOrdinal = {
|
|
|
824
830
|
};
|
|
825
831
|
}
|
|
826
832
|
catch (error) {
|
|
833
|
+
console.error("[purchaseOrdinal]", error);
|
|
827
834
|
return {
|
|
828
835
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
829
836
|
};
|
|
@@ -126,6 +126,7 @@ export const sendBsv = {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
catch (error) {
|
|
129
|
+
console.error("[sendBsv]", error);
|
|
129
130
|
return {
|
|
130
131
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
131
132
|
};
|
|
@@ -178,6 +179,7 @@ export const sendAllBsv = {
|
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
181
|
catch (error) {
|
|
182
|
+
console.error("[sendAllBsv]", error);
|
|
181
183
|
return {
|
|
182
184
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
183
185
|
};
|
package/dist/api/sweep/index.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { BSV21 } from "@bopen-io/templates";
|
|
7
7
|
import { P2PKH, PrivateKey, PublicKey, Transaction, Utils, } from "@bsv/sdk";
|
|
8
|
-
import {
|
|
9
|
-
import { BSV21_BASKET, BSV21_FEE_SATS, BSV21_PROTOCOL, ONESAT_PROTOCOL, } from "../constants";
|
|
8
|
+
import { BSV21_BASKET, BSV21_PROTOCOL, ONESAT_PROTOCOL } from "../constants";
|
|
10
9
|
export * from "./types";
|
|
11
10
|
/**
|
|
12
11
|
* Prepare sweep inputs from IndexedOutput objects by fetching locking scripts.
|
|
@@ -217,10 +216,11 @@ export const sweepBsv = {
|
|
|
217
216
|
};
|
|
218
217
|
}
|
|
219
218
|
catch (error) {
|
|
219
|
+
console.error("[sweepBsv]", error);
|
|
220
220
|
// Log detailed error info for WERR_REVIEW_ACTIONS
|
|
221
221
|
if (error && typeof error === "object" && "sendWithResults" in error) {
|
|
222
222
|
const werr = error;
|
|
223
|
-
console.error("[
|
|
223
|
+
console.error("[sweepBsv] WERR_REVIEW_ACTIONS details:", {
|
|
224
224
|
message: werr.message,
|
|
225
225
|
txid: werr.txid,
|
|
226
226
|
sendWithResults: JSON.stringify(werr.sendWithResults, null, 2),
|
|
@@ -359,19 +359,19 @@ export const sweepOrdinals = {
|
|
|
359
359
|
outputs,
|
|
360
360
|
options: { signAndProcess: false, randomizeOutputs: false },
|
|
361
361
|
};
|
|
362
|
-
console.log(
|
|
362
|
+
console.log("[sweepOrdinals] === CREATE ACTION ARGS ===");
|
|
363
363
|
console.log(`[sweepOrdinals] description: ${createActionArgs.description}`);
|
|
364
364
|
console.log(`[sweepOrdinals] inputBEEF length: ${beefData.length} bytes`);
|
|
365
365
|
console.log(`[sweepOrdinals] inputs count: ${inputDescriptors.length}`);
|
|
366
366
|
console.log(`[sweepOrdinals] outputs count: ${outputs.length}`);
|
|
367
|
-
console.log(
|
|
368
|
-
console.log(
|
|
369
|
-
console.log(
|
|
370
|
-
console.log(
|
|
367
|
+
console.log("[sweepOrdinals] inputs:", JSON.stringify(inputDescriptors, null, 2));
|
|
368
|
+
console.log("[sweepOrdinals] outputs:", JSON.stringify(outputs, null, 2));
|
|
369
|
+
console.log("[sweepOrdinals] options:", JSON.stringify(createActionArgs.options));
|
|
370
|
+
console.log("[sweepOrdinals] Calling createAction...");
|
|
371
371
|
let createResult;
|
|
372
372
|
try {
|
|
373
373
|
createResult = await ctx.wallet.createAction(createActionArgs);
|
|
374
|
-
console.log(
|
|
374
|
+
console.log("[sweepOrdinals] createAction returned:", JSON.stringify(createResult, (key, value) => {
|
|
375
375
|
// Don't stringify large binary data
|
|
376
376
|
if (key === "tx" && value instanceof Uint8Array)
|
|
377
377
|
return `<Uint8Array ${value.length} bytes>`;
|
|
@@ -381,12 +381,12 @@ export const sweepOrdinals = {
|
|
|
381
381
|
}, 2));
|
|
382
382
|
}
|
|
383
383
|
catch (createError) {
|
|
384
|
-
console.error(
|
|
384
|
+
console.error("[sweepOrdinals] createAction threw:", createError);
|
|
385
385
|
const errorMsg = createError instanceof Error
|
|
386
386
|
? createError.message
|
|
387
387
|
: String(createError);
|
|
388
388
|
const errorStack = createError instanceof Error ? createError.stack : undefined;
|
|
389
|
-
console.error(
|
|
389
|
+
console.error("[sweepOrdinals] Stack:", errorStack);
|
|
390
390
|
return { error: `createAction failed: ${errorMsg}` };
|
|
391
391
|
}
|
|
392
392
|
if ("error" in createResult && createResult.error) {
|
|
@@ -398,7 +398,7 @@ export const sweepOrdinals = {
|
|
|
398
398
|
// Sign each input with our external key
|
|
399
399
|
const tx = Transaction.fromBEEF(createResult.signableTransaction.tx);
|
|
400
400
|
// Log transaction structure for debugging
|
|
401
|
-
console.log(
|
|
401
|
+
console.log("[sweepOrdinals] === Transaction Structure ===");
|
|
402
402
|
console.log(`[sweepOrdinals] Inputs (${tx.inputs.length}):`);
|
|
403
403
|
let totalInputSats = 0;
|
|
404
404
|
for (let i = 0; i < tx.inputs.length; i++) {
|
|
@@ -417,7 +417,7 @@ export const sweepOrdinals = {
|
|
|
417
417
|
}
|
|
418
418
|
console.log(`[sweepOrdinals] Total in: ${totalInputSats}, Total out: ${totalOutputSats}, Fee: ${totalInputSats - totalOutputSats}`);
|
|
419
419
|
console.log(`[sweepOrdinals] Signable tx hex: ${Utils.toHex(createResult.signableTransaction.tx)}`);
|
|
420
|
-
console.log(
|
|
420
|
+
console.log("[sweepOrdinals] ==============================");
|
|
421
421
|
// Build a set of outpoints we control
|
|
422
422
|
const ourOutpoints = new Set(inputs.map((input) => {
|
|
423
423
|
const [txid, vout] = input.outpoint.split("_");
|
|
@@ -435,12 +435,12 @@ export const sweepOrdinals = {
|
|
|
435
435
|
await tx.sign();
|
|
436
436
|
// Log signed transaction details for debugging
|
|
437
437
|
const localTxid = tx.id("hex");
|
|
438
|
-
console.log(
|
|
438
|
+
console.log("[sweepOrdinals] === LOCAL SIGNED TX ===");
|
|
439
439
|
console.log(`[sweepOrdinals] Local txid: ${localTxid}`);
|
|
440
440
|
console.log(`[sweepOrdinals] Signed tx hex: ${tx.toHex()}`);
|
|
441
441
|
// Extract unlocking scripts for signAction
|
|
442
442
|
const spends = {};
|
|
443
|
-
console.log(
|
|
443
|
+
console.log("[sweepOrdinals] === UNLOCKING SCRIPTS FOR SIGNACTION ===");
|
|
444
444
|
for (let i = 0; i < tx.inputs.length; i++) {
|
|
445
445
|
const txInput = tx.inputs[i];
|
|
446
446
|
const inputOutpoint = `${txInput.sourceTXID}.${txInput.sourceOutputIndex}`;
|
|
@@ -463,18 +463,18 @@ export const sweepOrdinals = {
|
|
|
463
463
|
return { error: String(signResult.error) };
|
|
464
464
|
}
|
|
465
465
|
// Debug: compare local vs signAction result
|
|
466
|
-
console.log(
|
|
466
|
+
console.log("[sweepOrdinals] === SIGN ACTION RESULT ===");
|
|
467
467
|
console.log(`[sweepOrdinals] signAction txid: ${signResult.txid}`);
|
|
468
468
|
// Log broadcast results if available
|
|
469
469
|
if ("sendWithResults" in signResult) {
|
|
470
|
-
console.log(
|
|
470
|
+
console.log("[sweepOrdinals] sendWithResults:", JSON.stringify(signResult.sendWithResults));
|
|
471
471
|
}
|
|
472
472
|
console.log(`[sweepOrdinals] Local txid (partial): ${localTxid}`);
|
|
473
|
-
console.log(
|
|
473
|
+
console.log("[sweepOrdinals] Note: TXIDs differ because local is partial (wallet input unsigned)");
|
|
474
474
|
if (signResult.tx) {
|
|
475
475
|
// Parse returned BEEF to show final transaction structure
|
|
476
476
|
const returnedTx = Transaction.fromBEEF(signResult.tx);
|
|
477
|
-
console.log(
|
|
477
|
+
console.log("[sweepOrdinals] === FINAL TX STRUCTURE (broadcast) ===");
|
|
478
478
|
console.log(`[sweepOrdinals] Final inputs (${returnedTx.inputs.length}):`);
|
|
479
479
|
let returnedInputSats = 0;
|
|
480
480
|
for (let i = 0; i < returnedTx.inputs.length; i++) {
|
|
@@ -501,7 +501,7 @@ export const sweepOrdinals = {
|
|
|
501
501
|
const satPerByte = finalFee / txSize;
|
|
502
502
|
console.log(`[sweepOrdinals] Tx size: ${txSize} bytes, Fee rate: ${satPerByte.toFixed(2)} sat/byte`);
|
|
503
503
|
if (satPerByte < 0.5) {
|
|
504
|
-
console.warn(
|
|
504
|
+
console.warn("[sweepOrdinals] WARNING: Fee rate seems very low!");
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
return {
|
|
@@ -510,6 +510,7 @@ export const sweepOrdinals = {
|
|
|
510
510
|
};
|
|
511
511
|
}
|
|
512
512
|
catch (error) {
|
|
513
|
+
console.error("[sweepOrdinals]", error);
|
|
513
514
|
return {
|
|
514
515
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
515
516
|
};
|
|
@@ -583,6 +584,22 @@ export const sweepBsv21 = {
|
|
|
583
584
|
if (!inputs.every((i) => i.tokenId === tokenId)) {
|
|
584
585
|
return { error: "mixed-token-ids" };
|
|
585
586
|
}
|
|
587
|
+
// Lookup token details to verify it's active and get fee info
|
|
588
|
+
const tokenDetails = await ctx.services.bsv21.getTokenDetails(tokenId);
|
|
589
|
+
if (!tokenDetails.status.is_active) {
|
|
590
|
+
return { error: "token-not-active" };
|
|
591
|
+
}
|
|
592
|
+
const { fee_address, fee_per_output } = tokenDetails.status;
|
|
593
|
+
// Validate all input outpoints exist in the overlay
|
|
594
|
+
const candidateOutpoints = inputs.map((i) => i.outpoint);
|
|
595
|
+
const validated = await ctx.services.bsv21.validateOutputs(tokenId, candidateOutpoints, { unspent: true });
|
|
596
|
+
const validSet = new Set(validated.map((v) => v.outpoint));
|
|
597
|
+
const invalidInputs = inputs.filter((i) => !validSet.has(i.outpoint));
|
|
598
|
+
if (invalidInputs.length > 0) {
|
|
599
|
+
return {
|
|
600
|
+
error: `unvalidated-inputs: ${invalidInputs.map((i) => i.outpoint).join(", ")}`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
586
603
|
// Parse WIF
|
|
587
604
|
const privateKey = PrivateKey.fromWif(wif);
|
|
588
605
|
// Sum all input amounts
|
|
@@ -630,17 +647,24 @@ export const sweepBsv21 = {
|
|
|
630
647
|
satoshis: 1,
|
|
631
648
|
outputDescription: `Sweep ${totalAmount} tokens`,
|
|
632
649
|
basket: BSV21_BASKET,
|
|
633
|
-
tags: [
|
|
650
|
+
tags: [
|
|
651
|
+
`id:${tokenId}`,
|
|
652
|
+
`amt:${totalAmount}`,
|
|
653
|
+
`dec:${tokenDetails.token.dec}`,
|
|
654
|
+
...(tokenDetails.token.sym ? [`sym:${tokenDetails.token.sym}`] : []),
|
|
655
|
+
...(tokenDetails.token.icon
|
|
656
|
+
? [`icon:${tokenDetails.token.icon}`]
|
|
657
|
+
: []),
|
|
658
|
+
],
|
|
634
659
|
customInstructions: JSON.stringify({
|
|
635
660
|
protocolID: BSV21_PROTOCOL,
|
|
636
661
|
keyID,
|
|
637
662
|
}),
|
|
638
663
|
});
|
|
639
|
-
// 2. Fee output
|
|
640
|
-
const fundAddress = deriveFundAddress(tokenId);
|
|
664
|
+
// 2. Fee output to overlay fund address
|
|
641
665
|
outputs.push({
|
|
642
|
-
lockingScript: p2pkh.lock(
|
|
643
|
-
satoshis:
|
|
666
|
+
lockingScript: p2pkh.lock(fee_address).toHex(),
|
|
667
|
+
satoshis: fee_per_output,
|
|
644
668
|
outputDescription: "Overlay processing fee",
|
|
645
669
|
tags: [],
|
|
646
670
|
});
|
|
@@ -698,13 +722,12 @@ export const sweepBsv21 = {
|
|
|
698
722
|
// Submit to overlay service for indexing
|
|
699
723
|
if (signResult.tx) {
|
|
700
724
|
try {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
console.log(`[sweepBsv21] Overlay submission result:`, overlayResult);
|
|
725
|
+
const overlayResult = await ctx.services.overlay.submitBsv21(signResult.tx, tokenId);
|
|
726
|
+
console.log("[sweepBsv21] Overlay submission result:", overlayResult);
|
|
704
727
|
}
|
|
705
728
|
catch (overlayError) {
|
|
706
729
|
// Log but don't fail the sweep - tx is already broadcast
|
|
707
|
-
console.warn(
|
|
730
|
+
console.warn("[sweepBsv21] Overlay submission failed:", overlayError);
|
|
708
731
|
}
|
|
709
732
|
}
|
|
710
733
|
return {
|
|
@@ -713,6 +736,7 @@ export const sweepBsv21 = {
|
|
|
713
736
|
};
|
|
714
737
|
}
|
|
715
738
|
catch (error) {
|
|
739
|
+
console.error("[sweepBsv21]", error);
|
|
716
740
|
return {
|
|
717
741
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
718
742
|
};
|
package/dist/api/tokens/index.js
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { BSV21, OrdLock } from "@bopen-io/templates";
|
|
7
7
|
import { BigNumber, LockingScript, OP, P2PKH, PublicKey, Transaction, TransactionSignature, UnlockingScript, Utils, } from "@bsv/sdk";
|
|
8
|
-
import {
|
|
9
|
-
import { BSV21_BASKET, BSV21_FEE_SATS, BSV21_PROTOCOL } from "../constants";
|
|
8
|
+
import { BSV21_BASKET, BSV21_PROTOCOL } from "../constants";
|
|
10
9
|
// ============================================================================
|
|
11
10
|
// Internal helpers
|
|
12
11
|
// ============================================================================
|
|
@@ -105,31 +104,19 @@ export const getBsv21Balances = {
|
|
|
105
104
|
const amtTag = o.tags?.find((t) => t.startsWith("amt:"))?.slice(4);
|
|
106
105
|
if (!idTag || !amtTag)
|
|
107
106
|
continue;
|
|
108
|
-
const
|
|
109
|
-
const lastColonIdx = idContent.lastIndexOf(":");
|
|
110
|
-
if (lastColonIdx === -1)
|
|
111
|
-
continue;
|
|
112
|
-
const tokenId = idContent.slice(0, lastColonIdx);
|
|
113
|
-
const status = idContent.slice(lastColonIdx + 1);
|
|
114
|
-
if (status === "invalid")
|
|
115
|
-
continue;
|
|
116
|
-
const isConfirmed = status === "valid";
|
|
107
|
+
const tokenId = idTag.slice(3);
|
|
117
108
|
const amt = BigInt(amtTag);
|
|
118
109
|
const dec = Number.parseInt(o.tags?.find((t) => t.startsWith("dec:"))?.slice(4) || "0", 10);
|
|
119
110
|
const symTag = o.tags?.find((t) => t.startsWith("sym:"))?.slice(4);
|
|
120
111
|
const iconTag = o.tags?.find((t) => t.startsWith("icon:"))?.slice(5);
|
|
121
112
|
const existing = balanceMap.get(tokenId);
|
|
122
113
|
if (existing) {
|
|
123
|
-
|
|
124
|
-
existing.confirmed += amt;
|
|
125
|
-
else
|
|
126
|
-
existing.pending += amt;
|
|
114
|
+
existing.amt += amt;
|
|
127
115
|
}
|
|
128
116
|
else {
|
|
129
117
|
balanceMap.set(tokenId, {
|
|
130
118
|
id: tokenId,
|
|
131
|
-
|
|
132
|
-
pending: isConfirmed ? 0n : amt,
|
|
119
|
+
amt,
|
|
133
120
|
sym: symTag,
|
|
134
121
|
icon: iconTag,
|
|
135
122
|
dec,
|
|
@@ -140,11 +127,11 @@ export const getBsv21Balances = {
|
|
|
140
127
|
p: "bsv-20",
|
|
141
128
|
op: "transfer",
|
|
142
129
|
dec: b.dec,
|
|
143
|
-
amt:
|
|
130
|
+
amt: b.amt.toString(),
|
|
144
131
|
id: b.id,
|
|
145
132
|
sym: b.sym,
|
|
146
133
|
icon: b.icon,
|
|
147
|
-
all: { confirmed: b.
|
|
134
|
+
all: { confirmed: b.amt, pending: 0n },
|
|
148
135
|
listed: { confirmed: 0n, pending: 0n },
|
|
149
136
|
}));
|
|
150
137
|
},
|
|
@@ -191,6 +178,14 @@ export const sendBsv21 = {
|
|
|
191
178
|
!/^\d+$/.test(parts[1])) {
|
|
192
179
|
return { error: "invalid-token-id-format" };
|
|
193
180
|
}
|
|
181
|
+
if (!ctx.services) {
|
|
182
|
+
return { error: "services-required" };
|
|
183
|
+
}
|
|
184
|
+
const tokenDetails = await ctx.services.bsv21.getTokenDetails(tokenId);
|
|
185
|
+
if (!tokenDetails.status.is_active) {
|
|
186
|
+
return { error: "token-not-active" };
|
|
187
|
+
}
|
|
188
|
+
const { fee_address, fee_per_output } = tokenDetails.status;
|
|
194
189
|
const result = await ctx.wallet.listOutputs({
|
|
195
190
|
basket: BSV21_BASKET,
|
|
196
191
|
includeTags: true,
|
|
@@ -201,16 +196,24 @@ export const sendBsv21 = {
|
|
|
201
196
|
const idTag = o.tags?.find((t) => t.startsWith("id:"));
|
|
202
197
|
if (!idTag)
|
|
203
198
|
return false;
|
|
204
|
-
|
|
205
|
-
const lastColonIdx = idContent.lastIndexOf(":");
|
|
206
|
-
if (lastColonIdx === -1)
|
|
207
|
-
return false;
|
|
208
|
-
const id = idContent.slice(0, lastColonIdx);
|
|
209
|
-
const status = idContent.slice(lastColonIdx + 1);
|
|
210
|
-
return id === tokenId && status !== "invalid";
|
|
199
|
+
return idTag.slice(3) === tokenId;
|
|
211
200
|
});
|
|
212
|
-
|
|
213
|
-
|
|
201
|
+
// Batch-validate all candidate outpoints against the overlay
|
|
202
|
+
const validOutpoints = new Set();
|
|
203
|
+
let overlayValidated = false;
|
|
204
|
+
if (ctx.services?.bsv21) {
|
|
205
|
+
const candidateOutpoints = tokenUtxos.map((o) => o.outpoint);
|
|
206
|
+
try {
|
|
207
|
+
const validated = await ctx.services.bsv21.validateOutputs(tokenId, candidateOutpoints, { unspent: true });
|
|
208
|
+
overlayValidated = true;
|
|
209
|
+
for (const v of validated) {
|
|
210
|
+
validOutpoints.add(v.outpoint);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
console.error("[sendBsv21] overlay validation error:", e);
|
|
215
|
+
return { error: "overlay-validation-failed" };
|
|
216
|
+
}
|
|
214
217
|
}
|
|
215
218
|
const selected = [];
|
|
216
219
|
let totalIn = 0n;
|
|
@@ -221,23 +224,15 @@ export const sendBsv21 = {
|
|
|
221
224
|
if (!amtTag)
|
|
222
225
|
continue;
|
|
223
226
|
const utxoAmount = BigInt(amtTag.slice(4));
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const validation = await ctx.services.bsv21.getTokenByTxid(tokenId, txid);
|
|
228
|
-
const outputData = validation.outputs.find((o) => `${validation.txid}_${o.vout}` === utxo.outpoint);
|
|
229
|
-
if (!outputData)
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
227
|
+
// Skip UTXOs not confirmed in the overlay
|
|
228
|
+
if (overlayValidated && !validOutpoints.has(utxo.outpoint)) {
|
|
229
|
+
continue;
|
|
235
230
|
}
|
|
236
231
|
selected.push(utxo);
|
|
237
232
|
totalIn += utxoAmount;
|
|
238
233
|
}
|
|
239
234
|
if (totalIn < amount) {
|
|
240
|
-
return { error: "insufficient-
|
|
235
|
+
return { error: "insufficient-tokens" };
|
|
241
236
|
}
|
|
242
237
|
let recipientAddress;
|
|
243
238
|
if (counterparty) {
|
|
@@ -252,9 +247,12 @@ export const sendBsv21 = {
|
|
|
252
247
|
else if (paymail) {
|
|
253
248
|
return { error: "paymail-not-yet-implemented" };
|
|
254
249
|
}
|
|
255
|
-
else {
|
|
250
|
+
else if (address) {
|
|
256
251
|
recipientAddress = address;
|
|
257
252
|
}
|
|
253
|
+
else {
|
|
254
|
+
return { error: "must-provide-counterparty-or-address" };
|
|
255
|
+
}
|
|
258
256
|
const outputs = [];
|
|
259
257
|
const p2pkh = new P2PKH();
|
|
260
258
|
const destinationLockingScript = p2pkh.lock(recipientAddress);
|
|
@@ -264,16 +262,10 @@ export const sendBsv21 = {
|
|
|
264
262
|
satoshis: 1,
|
|
265
263
|
outputDescription: `Send ${amount} tokens`,
|
|
266
264
|
});
|
|
267
|
-
// Fee output to overlay fund address
|
|
268
|
-
const fundAddress = deriveFundAddress(tokenId);
|
|
269
|
-
outputs.push({
|
|
270
|
-
lockingScript: p2pkh.lock(fundAddress).toHex(),
|
|
271
|
-
satoshis: BSV21_FEE_SATS,
|
|
272
|
-
outputDescription: "Overlay processing fee",
|
|
273
|
-
tags: [],
|
|
274
|
-
});
|
|
275
265
|
const change = totalIn - amount;
|
|
266
|
+
let tokenOutputCount = 1;
|
|
276
267
|
if (change > 0n) {
|
|
268
|
+
tokenOutputCount = 2;
|
|
277
269
|
const changeKeyID = `${tokenId}-${Date.now()}`;
|
|
278
270
|
const { publicKey } = await ctx.wallet.getPublicKey({
|
|
279
271
|
protocolID: BSV21_PROTOCOL,
|
|
@@ -289,15 +281,31 @@ export const sendBsv21 = {
|
|
|
289
281
|
satoshis: 1,
|
|
290
282
|
outputDescription: "Token change",
|
|
291
283
|
basket: BSV21_BASKET,
|
|
292
|
-
tags: [
|
|
284
|
+
tags: [
|
|
285
|
+
`id:${tokenId}`,
|
|
286
|
+
`amt:${change}`,
|
|
287
|
+
`dec:${tokenDetails.token.dec}`,
|
|
288
|
+
...(tokenDetails.token.sym
|
|
289
|
+
? [`sym:${tokenDetails.token.sym}`]
|
|
290
|
+
: []),
|
|
291
|
+
...(tokenDetails.token.icon
|
|
292
|
+
? [`icon:${tokenDetails.token.icon}`]
|
|
293
|
+
: []),
|
|
294
|
+
],
|
|
293
295
|
customInstructions: JSON.stringify({
|
|
294
296
|
protocolID: BSV21_PROTOCOL,
|
|
295
297
|
keyID: changeKeyID,
|
|
296
298
|
}),
|
|
297
299
|
});
|
|
298
300
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
// Fee output to overlay fund address (per token output)
|
|
302
|
+
outputs.push({
|
|
303
|
+
lockingScript: p2pkh.lock(fee_address).toHex(),
|
|
304
|
+
satoshis: fee_per_output * tokenOutputCount,
|
|
305
|
+
outputDescription: "Overlay processing fee",
|
|
306
|
+
tags: [],
|
|
307
|
+
});
|
|
308
|
+
const symbol = tokenDetails.token.sym || tokenId.slice(0, 8);
|
|
301
309
|
const createResult = await ctx.wallet.createAction({
|
|
302
310
|
description: `Send ${amount} ${symbol}`,
|
|
303
311
|
inputs: selected.map((o) => ({
|
|
@@ -324,12 +332,11 @@ export const sendBsv21 = {
|
|
|
324
332
|
// Submit to overlay service for indexing
|
|
325
333
|
if (signResult.tx && ctx.services) {
|
|
326
334
|
try {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
console.log(`[sendBsv21] Overlay submission result:`, overlayResult);
|
|
335
|
+
const overlayResult = await ctx.services.overlay.submitBsv21(signResult.tx, tokenId);
|
|
336
|
+
console.log("[sendBsv21] Overlay submission result:", overlayResult);
|
|
330
337
|
}
|
|
331
338
|
catch (overlayError) {
|
|
332
|
-
console.warn(
|
|
339
|
+
console.warn("[sendBsv21] Overlay submission failed:", overlayError);
|
|
333
340
|
}
|
|
334
341
|
}
|
|
335
342
|
return {
|
|
@@ -338,6 +345,7 @@ export const sendBsv21 = {
|
|
|
338
345
|
};
|
|
339
346
|
}
|
|
340
347
|
catch (error) {
|
|
348
|
+
console.error("[sendBsv21]", error);
|
|
341
349
|
return {
|
|
342
350
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
343
351
|
};
|
|
@@ -391,11 +399,13 @@ export const purchaseBsv21 = {
|
|
|
391
399
|
const [txid, voutStr] = parts;
|
|
392
400
|
const vout = Number.parseInt(voutStr, 10);
|
|
393
401
|
try {
|
|
394
|
-
await ctx.services.bsv21.
|
|
402
|
+
await ctx.services.bsv21.validateOutput(tokenId, outpoint);
|
|
395
403
|
}
|
|
396
|
-
catch {
|
|
404
|
+
catch (e) {
|
|
405
|
+
console.error("[purchaseBsv21] overlay validation error:", e);
|
|
397
406
|
return { error: "listing-not-found-in-overlay" };
|
|
398
407
|
}
|
|
408
|
+
const tokenDetails = await ctx.services.bsv21.getTokenDetails(tokenId);
|
|
399
409
|
const beef = await ctx.services.getBeefForTxid(txid);
|
|
400
410
|
const listingBeefTx = beef.findTxid(txid);
|
|
401
411
|
if (!listingBeefTx?.tx) {
|
|
@@ -426,7 +436,15 @@ export const purchaseBsv21 = {
|
|
|
426
436
|
satoshis: 1,
|
|
427
437
|
outputDescription: "Purchased tokens",
|
|
428
438
|
basket: BSV21_BASKET,
|
|
429
|
-
tags: [
|
|
439
|
+
tags: [
|
|
440
|
+
`id:${tokenId}`,
|
|
441
|
+
`amt:${tokenAmount}`,
|
|
442
|
+
`dec:${tokenDetails.token.dec}`,
|
|
443
|
+
...(tokenDetails.token.sym ? [`sym:${tokenDetails.token.sym}`] : []),
|
|
444
|
+
...(tokenDetails.token.icon
|
|
445
|
+
? [`icon:${tokenDetails.token.icon}`]
|
|
446
|
+
: []),
|
|
447
|
+
],
|
|
430
448
|
customInstructions: JSON.stringify({
|
|
431
449
|
protocolID: BSV21_PROTOCOL,
|
|
432
450
|
keyID: bsv21KeyID,
|
|
@@ -454,6 +472,15 @@ export const purchaseBsv21 = {
|
|
|
454
472
|
});
|
|
455
473
|
}
|
|
456
474
|
}
|
|
475
|
+
// Fee output to overlay fund address
|
|
476
|
+
if (tokenDetails.status.is_active) {
|
|
477
|
+
outputs.push({
|
|
478
|
+
lockingScript: p2pkh.lock(tokenDetails.status.fee_address).toHex(),
|
|
479
|
+
satoshis: tokenDetails.status.fee_per_output,
|
|
480
|
+
outputDescription: "Overlay processing fee",
|
|
481
|
+
tags: [],
|
|
482
|
+
});
|
|
483
|
+
}
|
|
457
484
|
const createResult = await ctx.wallet.createAction({
|
|
458
485
|
description: `Purchase ${tokenAmount} tokens for ${payoutSatoshis} sats`,
|
|
459
486
|
inputBEEF: beef.toBinary(),
|
|
@@ -488,12 +515,11 @@ export const purchaseBsv21 = {
|
|
|
488
515
|
// Submit to overlay service for indexing
|
|
489
516
|
if (signResult.tx && ctx.services) {
|
|
490
517
|
try {
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
console.log(`[purchaseBsv21] Overlay submission result:`, overlayResult);
|
|
518
|
+
const overlayResult = await ctx.services.overlay.submitBsv21(signResult.tx, tokenId);
|
|
519
|
+
console.log("[purchaseBsv21] Overlay submission result:", overlayResult);
|
|
494
520
|
}
|
|
495
521
|
catch (overlayError) {
|
|
496
|
-
console.warn(
|
|
522
|
+
console.warn("[purchaseBsv21] Overlay submission failed:", overlayError);
|
|
497
523
|
}
|
|
498
524
|
}
|
|
499
525
|
return {
|
|
@@ -502,6 +528,7 @@ export const purchaseBsv21 = {
|
|
|
502
528
|
};
|
|
503
529
|
}
|
|
504
530
|
catch (error) {
|
|
531
|
+
console.error("[purchaseBsv21]", error);
|
|
505
532
|
return {
|
|
506
533
|
error: error instanceof Error ? error.message : "unknown-error",
|
|
507
534
|
};
|
|
@@ -77,8 +77,7 @@ export class Bsv21Indexer extends Indexer {
|
|
|
77
77
|
return;
|
|
78
78
|
const tags = [];
|
|
79
79
|
if (txo.owner && this.owners.has(txo.owner)) {
|
|
80
|
-
|
|
81
|
-
tags.push(`id:${bsv21.id}:${bsv21.status}`);
|
|
80
|
+
tags.push(`id:${bsv21.id}`);
|
|
82
81
|
tags.push(`amt:${bsv21.amt.toString()}`);
|
|
83
82
|
// Add metadata tags for efficient querying
|
|
84
83
|
if (bsv21.sym)
|
|
@@ -139,7 +139,7 @@ export class OneSatServices {
|
|
|
139
139
|
}
|
|
140
140
|
async postBeef(beef, txids) {
|
|
141
141
|
console.log("[OneSatServices] postBeef called with txids:", txids);
|
|
142
|
-
console.log(
|
|
142
|
+
console.log(`[OneSatServices] BEEF structure:\n${beef.toLogString()}`);
|
|
143
143
|
const results = [];
|
|
144
144
|
for (const txid of txids) {
|
|
145
145
|
try {
|
|
@@ -149,7 +149,7 @@ export class OneSatServices {
|
|
|
149
149
|
console.log("[OneSatServices] AtomicBEEF length:", atomicBeef.length, "bytes");
|
|
150
150
|
// Parse back to verify structure
|
|
151
151
|
const verifyBeef = Beef.fromBinary(atomicBeef);
|
|
152
|
-
console.log(
|
|
152
|
+
console.log(`[OneSatServices] AtomicBEEF parsed back:\n${verifyBeef.toLogString()}`);
|
|
153
153
|
// TODO: Remove hardcoded callback headers after server testing
|
|
154
154
|
const status = await this.arcade.submitTransaction(atomicBeef, {
|
|
155
155
|
callbackUrl: `${this.baseUrl}/1sat/arc/callback`,
|
|
@@ -292,10 +292,8 @@ export class OneSatServices {
|
|
|
292
292
|
const depth = currentHeight - tx.merklePath.blockHeight + 1;
|
|
293
293
|
return { txid, status: "mined", depth };
|
|
294
294
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return { txid, status: "known", depth: 0 };
|
|
298
|
-
}
|
|
295
|
+
// No merkle path = known but not yet mined
|
|
296
|
+
return { txid, status: "known", depth: 0 };
|
|
299
297
|
}
|
|
300
298
|
catch {
|
|
301
299
|
// 404 or error from Beef = unknown
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { Bsv21TransactionData, ClientOptions, IndexedOutput, TokenDetailResponse } from "../types";
|
|
2
2
|
import { BaseClient } from "./BaseClient";
|
|
3
|
+
/**
|
|
4
|
+
* Query options for /outputs validation endpoints.
|
|
5
|
+
* All flags default to false on the server.
|
|
6
|
+
*/
|
|
7
|
+
export interface OutputQueryOptions {
|
|
8
|
+
/** Filter for unspent outputs only */
|
|
9
|
+
unspent?: boolean;
|
|
10
|
+
/** Include spend txid */
|
|
11
|
+
spend?: boolean;
|
|
12
|
+
/** Include satoshis */
|
|
13
|
+
sats?: boolean;
|
|
14
|
+
/** Include events array */
|
|
15
|
+
events?: boolean;
|
|
16
|
+
/** Include block info */
|
|
17
|
+
block?: boolean;
|
|
18
|
+
/** Comma-separated data tags to include (e.g. 'bsv21') */
|
|
19
|
+
tags?: string;
|
|
20
|
+
}
|
|
3
21
|
/**
|
|
4
22
|
* Client for /1sat/bsv21/* routes.
|
|
5
23
|
* Provides BSV21 token queries.
|
|
@@ -66,6 +84,24 @@ export declare class Bsv21Client extends BaseClient {
|
|
|
66
84
|
* @param addresses - Array of addresses (max 100)
|
|
67
85
|
*/
|
|
68
86
|
getHistoryMulti(tokenId: string, lockType: string, addresses: string[]): Promise<IndexedOutput[]>;
|
|
87
|
+
/**
|
|
88
|
+
* Validate specific outpoints against the token's overlay topic.
|
|
89
|
+
* Returns only those found in the overlay. By default returns minimal data
|
|
90
|
+
* (outpoint + score). Use opts to include additional fields.
|
|
91
|
+
* @param tokenId - Token ID (txid_vout format)
|
|
92
|
+
* @param outpoints - Array of outpoints to validate (max 1000)
|
|
93
|
+
* @param opts - Optional query flags for additional data
|
|
94
|
+
*/
|
|
95
|
+
validateOutputs(tokenId: string, outpoints: string[], opts?: OutputQueryOptions): Promise<IndexedOutput[]>;
|
|
96
|
+
/**
|
|
97
|
+
* Validate a single outpoint against the token's overlay topic.
|
|
98
|
+
* Returns 404 if not found. By default returns minimal data (outpoint + score).
|
|
99
|
+
* @param tokenId - Token ID (txid_vout format)
|
|
100
|
+
* @param outpoint - Outpoint to validate (txid_vout or txid:vout)
|
|
101
|
+
* @param opts - Optional query flags for additional data
|
|
102
|
+
*/
|
|
103
|
+
validateOutput(tokenId: string, outpoint: string, opts?: OutputQueryOptions): Promise<IndexedOutput>;
|
|
104
|
+
private buildOutputQuery;
|
|
69
105
|
/**
|
|
70
106
|
* Clear the token details cache
|
|
71
107
|
*/
|
|
@@ -100,6 +100,51 @@ export class Bsv21Client extends BaseClient {
|
|
|
100
100
|
body: JSON.stringify(addresses),
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Validate specific outpoints against the token's overlay topic.
|
|
105
|
+
* Returns only those found in the overlay. By default returns minimal data
|
|
106
|
+
* (outpoint + score). Use opts to include additional fields.
|
|
107
|
+
* @param tokenId - Token ID (txid_vout format)
|
|
108
|
+
* @param outpoints - Array of outpoints to validate (max 1000)
|
|
109
|
+
* @param opts - Optional query flags for additional data
|
|
110
|
+
*/
|
|
111
|
+
async validateOutputs(tokenId, outpoints, opts) {
|
|
112
|
+
const params = this.buildOutputQuery(opts);
|
|
113
|
+
return this.request(`/${tokenId}/outputs${params}`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify(outpoints),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate a single outpoint against the token's overlay topic.
|
|
121
|
+
* Returns 404 if not found. By default returns minimal data (outpoint + score).
|
|
122
|
+
* @param tokenId - Token ID (txid_vout format)
|
|
123
|
+
* @param outpoint - Outpoint to validate (txid_vout or txid:vout)
|
|
124
|
+
* @param opts - Optional query flags for additional data
|
|
125
|
+
*/
|
|
126
|
+
async validateOutput(tokenId, outpoint, opts) {
|
|
127
|
+
const params = this.buildOutputQuery(opts);
|
|
128
|
+
return this.request(`/${tokenId}/outputs/${outpoint}${params}`);
|
|
129
|
+
}
|
|
130
|
+
buildOutputQuery(opts) {
|
|
131
|
+
if (!opts)
|
|
132
|
+
return "";
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (opts.unspent)
|
|
135
|
+
parts.push("unspent=true");
|
|
136
|
+
if (opts.spend)
|
|
137
|
+
parts.push("spend=true");
|
|
138
|
+
if (opts.sats)
|
|
139
|
+
parts.push("sats=true");
|
|
140
|
+
if (opts.events)
|
|
141
|
+
parts.push("events=true");
|
|
142
|
+
if (opts.block)
|
|
143
|
+
parts.push("block=true");
|
|
144
|
+
if (opts.tags)
|
|
145
|
+
parts.push(`tags=${encodeURIComponent(opts.tags)}`);
|
|
146
|
+
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
147
|
+
}
|
|
103
148
|
/**
|
|
104
149
|
* Clear the token details cache
|
|
105
150
|
*/
|
|
@@ -5,5 +5,5 @@ export { ArcadeClient } from "./ArcadeClient";
|
|
|
5
5
|
export { TxoClient } from "./TxoClient";
|
|
6
6
|
export { OwnerClient } from "./OwnerClient";
|
|
7
7
|
export { OrdfsClient } from "./OrdfsClient";
|
|
8
|
-
export { Bsv21Client } from "./Bsv21Client";
|
|
8
|
+
export { Bsv21Client, type OutputQueryOptions } from "./Bsv21Client";
|
|
9
9
|
export { OverlayClient } from "./OverlayClient";
|
|
@@ -81,13 +81,13 @@ export interface SyncProcessorOptions {
|
|
|
81
81
|
indexers?: Indexer[];
|
|
82
82
|
}
|
|
83
83
|
export interface SyncProcessorEvents {
|
|
84
|
-
"process:start":
|
|
84
|
+
"process:start": Record<string, never>;
|
|
85
85
|
"process:progress": {
|
|
86
86
|
pending: number;
|
|
87
87
|
done: number;
|
|
88
88
|
failed: number;
|
|
89
89
|
};
|
|
90
|
-
"process:complete":
|
|
90
|
+
"process:complete": Record<string, never>;
|
|
91
91
|
"process:error": {
|
|
92
92
|
message: string;
|
|
93
93
|
};
|
|
@@ -161,7 +161,7 @@ export interface SyncEvents {
|
|
|
161
161
|
done: number;
|
|
162
162
|
failed: number;
|
|
163
163
|
};
|
|
164
|
-
"sync:complete":
|
|
164
|
+
"sync:complete": Record<string, never>;
|
|
165
165
|
"sync:error": {
|
|
166
166
|
message: string;
|
|
167
167
|
};
|
package/dist/sync/SyncManager.js
CHANGED
|
@@ -44,7 +44,7 @@ export class SyncFetcher {
|
|
|
44
44
|
}
|
|
45
45
|
this.listeners
|
|
46
46
|
.get(event)
|
|
47
|
-
|
|
47
|
+
?.add(listener);
|
|
48
48
|
}
|
|
49
49
|
off(event, listener) {
|
|
50
50
|
this.listeners
|
|
@@ -52,7 +52,9 @@ export class SyncFetcher {
|
|
|
52
52
|
?.delete(listener);
|
|
53
53
|
}
|
|
54
54
|
emit(event, data) {
|
|
55
|
-
this.listeners.get(event)
|
|
55
|
+
for (const listener of this.listeners.get(event) ?? []) {
|
|
56
|
+
listener(data);
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Fetch new outputs via SSE and enqueue them.
|
|
@@ -161,7 +163,7 @@ export class SyncProcessor {
|
|
|
161
163
|
}
|
|
162
164
|
this.listeners
|
|
163
165
|
.get(event)
|
|
164
|
-
|
|
166
|
+
?.add(listener);
|
|
165
167
|
}
|
|
166
168
|
off(event, listener) {
|
|
167
169
|
this.listeners
|
|
@@ -169,7 +171,9 @@ export class SyncProcessor {
|
|
|
169
171
|
?.delete(listener);
|
|
170
172
|
}
|
|
171
173
|
emit(event, data) {
|
|
172
|
-
this.listeners.get(event)
|
|
174
|
+
for (const listener of this.listeners.get(event) ?? []) {
|
|
175
|
+
listener(data);
|
|
176
|
+
}
|
|
173
177
|
}
|
|
174
178
|
/**
|
|
175
179
|
* Start processing the queue.
|
|
@@ -379,18 +383,16 @@ export class SyncProcessor {
|
|
|
379
383
|
},
|
|
380
384
|
};
|
|
381
385
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
393
|
-
}
|
|
386
|
+
// P2PKH-based output - use wallet payment for auto-signing
|
|
387
|
+
return {
|
|
388
|
+
outputIndex: vout,
|
|
389
|
+
protocol: "wallet payment",
|
|
390
|
+
paymentRemittance: {
|
|
391
|
+
derivationPrefix: derivation.derivationPrefix,
|
|
392
|
+
derivationSuffix: derivation.derivationSuffix,
|
|
393
|
+
senderIdentityKey: derivation.senderIdentityKey,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
394
396
|
}
|
|
395
397
|
collectTags(txo) {
|
|
396
398
|
const tags = [];
|
|
@@ -467,7 +469,7 @@ export class SyncManager {
|
|
|
467
469
|
}
|
|
468
470
|
this.listeners
|
|
469
471
|
.get(event)
|
|
470
|
-
|
|
472
|
+
?.add(listener);
|
|
471
473
|
}
|
|
472
474
|
off(event, listener) {
|
|
473
475
|
this.listeners
|
|
@@ -475,7 +477,9 @@ export class SyncManager {
|
|
|
475
477
|
?.delete(listener);
|
|
476
478
|
}
|
|
477
479
|
emit(event, data) {
|
|
478
|
-
this.listeners.get(event)
|
|
480
|
+
for (const listener of this.listeners.get(event) ?? []) {
|
|
481
|
+
listener(data);
|
|
482
|
+
}
|
|
479
483
|
}
|
|
480
484
|
// ===== Sync Control =====
|
|
481
485
|
/**
|
package/dist/wallet/factory.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (browser extension) and 1sat-website (React app).
|
|
6
6
|
*/
|
|
7
7
|
import { PrivateKey } from "@bsv/sdk";
|
|
8
|
-
import { Monitor,
|
|
8
|
+
import { Monitor, StorageClient, Wallet, WalletStorageManager } from "@bsv/wallet-toolbox-mobile/out/src/index.client.js";
|
|
9
9
|
import { OneSatServices } from "../services/OneSatServices";
|
|
10
10
|
import { type FullSyncResult, type FullSyncStage } from "./fullSync";
|
|
11
11
|
type Chain = "main" | "test";
|
|
@@ -17,10 +17,6 @@ export interface WebWalletConfig {
|
|
|
17
17
|
privateKey: PrivateKey | string;
|
|
18
18
|
/** Network: 'main' or 'test' */
|
|
19
19
|
chain: Chain;
|
|
20
|
-
/** Admin originator that bypasses permission checks (e.g., chrome-extension://id or https://wallet.example.com) */
|
|
21
|
-
adminOriginator: string;
|
|
22
|
-
/** Permission configuration for WalletPermissionsManager */
|
|
23
|
-
permissionsConfig: PermissionsManagerConfig;
|
|
24
20
|
/** Fee model. Default: { model: 'sat/kb', value: 100 } */
|
|
25
21
|
feeModel?: {
|
|
26
22
|
model: "sat/kb";
|
|
@@ -39,10 +35,8 @@ export interface WebWalletConfig {
|
|
|
39
35
|
* Result of wallet creation.
|
|
40
36
|
*/
|
|
41
37
|
export interface WebWalletResult {
|
|
42
|
-
/** Wallet instance
|
|
43
|
-
wallet:
|
|
44
|
-
/** Underlying wallet without permission checks (for trusted contexts like sweep-ui) */
|
|
45
|
-
rawWallet: Wallet;
|
|
38
|
+
/** Wallet instance */
|
|
39
|
+
wallet: Wallet;
|
|
46
40
|
/** 1Sat services for API access */
|
|
47
41
|
services: OneSatServices;
|
|
48
42
|
/** Monitor for transaction lifecycle (not started - call monitor.startTasks() when ready) */
|
|
@@ -57,15 +51,14 @@ export interface WebWalletResult {
|
|
|
57
51
|
remoteStorage?: StorageClient;
|
|
58
52
|
}
|
|
59
53
|
/**
|
|
60
|
-
* Create a web wallet with storage, services,
|
|
54
|
+
* Create a web wallet with storage, services, and monitor.
|
|
61
55
|
*
|
|
62
56
|
* @example
|
|
63
57
|
* ```typescript
|
|
64
58
|
* const { wallet, services, monitor, destroy } = await createWebWallet({
|
|
65
59
|
* privateKey: identityWif,
|
|
66
60
|
* chain: 'main',
|
|
67
|
-
*
|
|
68
|
-
* permissionsConfig: DEFAULT_PERMISSIONS_CONFIG,
|
|
61
|
+
* storageIdentityKey: 'device-unique-id',
|
|
69
62
|
* });
|
|
70
63
|
*
|
|
71
64
|
* // Wire up monitor callbacks
|
package/dist/wallet/factory.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (browser extension) and 1sat-website (React app).
|
|
6
6
|
*/
|
|
7
7
|
import { KeyDeriver, PrivateKey } from "@bsv/sdk";
|
|
8
|
-
import { Monitor, Services, StorageClient, StorageIdb, StorageProvider, Wallet,
|
|
8
|
+
import { Monitor, Services, StorageClient, StorageIdb, StorageProvider, Wallet, WalletStorageManager, } from "@bsv/wallet-toolbox-mobile/out/src/index.client.js";
|
|
9
9
|
import { OneSatServices } from "../services/OneSatServices";
|
|
10
10
|
import { fullSync } from "./fullSync";
|
|
11
11
|
// Default database name for IndexedDB storage
|
|
@@ -39,15 +39,14 @@ function parsePrivateKey(input) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Create a web wallet with storage, services,
|
|
42
|
+
* Create a web wallet with storage, services, and monitor.
|
|
43
43
|
*
|
|
44
44
|
* @example
|
|
45
45
|
* ```typescript
|
|
46
46
|
* const { wallet, services, monitor, destroy } = await createWebWallet({
|
|
47
47
|
* privateKey: identityWif,
|
|
48
48
|
* chain: 'main',
|
|
49
|
-
*
|
|
50
|
-
* permissionsConfig: DEFAULT_PERMISSIONS_CONFIG,
|
|
49
|
+
* storageIdentityKey: 'device-unique-id',
|
|
51
50
|
* });
|
|
52
51
|
*
|
|
53
52
|
* // Wire up monitor callbacks
|
|
@@ -58,7 +57,7 @@ function parsePrivateKey(input) {
|
|
|
58
57
|
* ```
|
|
59
58
|
*/
|
|
60
59
|
export async function createWebWallet(config) {
|
|
61
|
-
const { chain
|
|
60
|
+
const { chain } = config;
|
|
62
61
|
const feeModel = config.feeModel ?? DEFAULT_FEE_MODEL;
|
|
63
62
|
// 1. Parse private key and create KeyDeriver
|
|
64
63
|
const privateKey = parsePrivateKey(config.privateKey);
|
|
@@ -73,10 +72,10 @@ export async function createWebWallet(config) {
|
|
|
73
72
|
const localStorage = new StorageIdb(storageOptions);
|
|
74
73
|
await localStorage.migrate(DEFAULT_DATABASE_NAME, config.storageIdentityKey);
|
|
75
74
|
// 4. Create storage manager with local-only storage initially (empty backups)
|
|
76
|
-
|
|
75
|
+
const storage = new WalletStorageManager(identityPubKey, localStorage, []);
|
|
77
76
|
await storage.makeAvailable();
|
|
78
|
-
// 5. Create
|
|
79
|
-
const
|
|
77
|
+
// 5. Create wallet (needed before StorageClient for signing)
|
|
78
|
+
const wallet = new Wallet({
|
|
80
79
|
chain,
|
|
81
80
|
keyDeriver,
|
|
82
81
|
storage,
|
|
@@ -89,7 +88,7 @@ export async function createWebWallet(config) {
|
|
|
89
88
|
if (config.remoteStorageUrl) {
|
|
90
89
|
console.log(`[createWebWallet] Attempting remote storage connection to ${config.remoteStorageUrl}`);
|
|
91
90
|
try {
|
|
92
|
-
remoteClient = new StorageClient(
|
|
91
|
+
remoteClient = new StorageClient(wallet, config.remoteStorageUrl);
|
|
93
92
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Remote storage connection timeout")), DEFAULT_REMOTE_STORAGE_TIMEOUT));
|
|
94
93
|
await Promise.race([remoteClient.makeAvailable(), timeoutPromise]);
|
|
95
94
|
// Add remote to storage manager - it will partition as conflicting if another device is active
|
|
@@ -137,8 +136,8 @@ export async function createWebWallet(config) {
|
|
|
137
136
|
// bypass the monitor's onTransactionBroadcasted callback. We detect broadcasts
|
|
138
137
|
// by checking for txid in the result and sync to backup immediately.
|
|
139
138
|
if (remoteClient) {
|
|
140
|
-
const originalCreateAction =
|
|
141
|
-
|
|
139
|
+
const originalCreateAction = wallet.createAction.bind(wallet);
|
|
140
|
+
wallet.createAction = async (args) => {
|
|
142
141
|
const result = await originalCreateAction(args);
|
|
143
142
|
if (result.txid) {
|
|
144
143
|
console.log("[createWebWallet] Broadcast detected in createAction:", result.txid);
|
|
@@ -148,8 +147,8 @@ export async function createWebWallet(config) {
|
|
|
148
147
|
}
|
|
149
148
|
return result;
|
|
150
149
|
};
|
|
151
|
-
const originalSignAction =
|
|
152
|
-
|
|
150
|
+
const originalSignAction = wallet.signAction.bind(wallet);
|
|
151
|
+
wallet.signAction = async (args) => {
|
|
153
152
|
const result = await originalSignAction(args);
|
|
154
153
|
if (result.txid) {
|
|
155
154
|
console.log("[createWebWallet] Broadcast detected in signAction:", result.txid);
|
|
@@ -160,9 +159,7 @@ export async function createWebWallet(config) {
|
|
|
160
159
|
return result;
|
|
161
160
|
};
|
|
162
161
|
}
|
|
163
|
-
// 9.
|
|
164
|
-
const wallet = new WalletPermissionsManager(underlyingWallet, adminOriginator, permissionsConfig);
|
|
165
|
-
// 10. Create monitor (not started - consumer calls startTasks() when ready)
|
|
162
|
+
// 9. Create monitor (not started - consumer calls startTasks() when ready)
|
|
166
163
|
const monitor = new Monitor({
|
|
167
164
|
chain,
|
|
168
165
|
services: oneSatServices,
|
|
@@ -175,7 +172,7 @@ export async function createWebWallet(config) {
|
|
|
175
172
|
unprovenAttemptsLimitMain: 144,
|
|
176
173
|
});
|
|
177
174
|
monitor.addDefaultTasks();
|
|
178
|
-
console.log("[createWebWallet] Monitor created with tasks:", monitor
|
|
175
|
+
console.log("[createWebWallet] Monitor created with tasks:", monitor._tasks.map((t) => t.name));
|
|
179
176
|
// 11. Wire up monitor callbacks - sync to remote first, then call user callbacks
|
|
180
177
|
// Note: For delayed broadcasts, the monitor triggers these. For immediate broadcasts,
|
|
181
178
|
// the interception in step 8 handles the sync, but these still fire for the user callback.
|
|
@@ -228,7 +225,7 @@ export async function createWebWallet(config) {
|
|
|
228
225
|
const destroy = async () => {
|
|
229
226
|
monitor.stopTasks();
|
|
230
227
|
await monitor.destroy();
|
|
231
|
-
await
|
|
228
|
+
await wallet.destroy();
|
|
232
229
|
};
|
|
233
230
|
// 13. Create fullSync function if remote storage is connected
|
|
234
231
|
const fullSyncFn = remoteClient
|
|
@@ -243,7 +240,6 @@ export async function createWebWallet(config) {
|
|
|
243
240
|
: undefined;
|
|
244
241
|
return {
|
|
245
242
|
wallet,
|
|
246
|
-
rawWallet: underlyingWallet,
|
|
247
243
|
services: oneSatServices,
|
|
248
244
|
monitor,
|
|
249
245
|
destroy,
|