@1sat/sweep-ui 0.0.15 → 0.0.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"SweepApp.d.ts","sourceRoot":"","sources":["../../src/components/SweepApp.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAI5D,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,aAAa,2CA8QrG"}
1
+ {"version":3,"file":"SweepApp.d.ts","sourceRoot":"","sources":["../../src/components/SweepApp.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAI5D,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,SAAS,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,EAAE,aAAa,2CA0RrG"}
@@ -20,8 +20,10 @@ export declare function OrdinalsSection({ ordinals, selectedOrdinals, onToggle,
20
20
  onBurn?: () => void;
21
21
  walletConnected: boolean;
22
22
  }): import("react/jsx-runtime").JSX.Element | null;
23
- export declare function Bsv21Section({ tokens }: {
23
+ export declare function Bsv21Section({ tokens, onSweep, walletConnected }: {
24
24
  tokens: TokenBalance[];
25
+ onSweep?: (tokenId: string) => void;
26
+ walletConnected: boolean;
25
27
  }): import("react/jsx-runtime").JSX.Element | null;
26
28
  export declare function Bsv20Section({ tokens }: {
27
29
  tokens: IndexedOutput[];
@@ -1 +1 @@
1
- {"version":3,"file":"asset-preview.d.ts","sourceRoot":"","sources":["../../src/components/asset-preview.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA8CjD,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE;IACzH,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;CACpN,kDAqCA;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE;IAC/I,QAAQ,EAAE,eAAe,EAAE,CAAC;IAAC,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,IAAI,CAAC;IAAC,aAAa,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;CACjQ,kDAsDA;AA0BD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,YAAY,EAAE,CAAA;CAAE,kDAsBlE;AAED,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,kDAmBnE;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,kDAapE;AAED,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,aAAa,EAAE,CAAA;CAAE,kDAc3D"}
1
+ {"version":3,"file":"asset-preview.d.ts","sourceRoot":"","sources":["../../src/components/asset-preview.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA8CjD,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE;IACzH,OAAO,EAAE,aAAa,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;CACpN,kDAqCA;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,EAAE;IAC/I,QAAQ,EAAE,eAAe,EAAE,CAAC;IAAC,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,IAAI,CAAC;IAAC,aAAa,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAC;CACjQ,kDAsDA;AA+BD,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE;IAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,eAAe,EAAE,OAAO,CAAA;CAAE,kDAsB3J;AAED,wBAAgB,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,kDAmBnE;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,aAAa,EAAE,CAAA;CAAE,kDAapE;AAED,wBAAgB,UAAU,CAAC,EAAE,GAAG,EAAE,EAAE;IAAE,GAAG,EAAE,aAAa,EAAE,CAAA;CAAE,kDAc3D"}
package/dist/index.js CHANGED
@@ -341,7 +341,9 @@ import {
341
341
 
342
342
  // src/lib/scanner.ts
343
343
  import { PrivateKey } from "@bsv/sdk";
344
- import { parseOutpoint } from "@1sat/utils";
344
+ import {
345
+ scanAddresses as coreScanAddresses
346
+ } from "@1sat/actions";
345
347
 
346
348
  // src/lib/services.ts
347
349
  import { OneSatServices } from "@1sat/client";
@@ -362,12 +364,11 @@ function getServices() {
362
364
  }
363
365
 
364
366
  // src/lib/scanner.ts
365
- var RUN_PREFIX = Uint8Array.from([0, 106, 3, 114, 117, 110]);
366
367
  function deriveAddress(wif) {
367
368
  return PrivateKey.fromWif(wif.trim()).toPublicKey().toAddress();
368
369
  }
369
370
  function getEvent(events, prefix) {
370
- const e = events.find((e2) => e2.startsWith(prefix));
371
+ const e = events.find((ev) => ev.startsWith(prefix));
371
372
  return e ? e.slice(prefix.length) : undefined;
372
373
  }
373
374
  function getEvents(events, prefix) {
@@ -382,198 +383,44 @@ function enrichOrdinal(out) {
382
383
  const contentUrl = getServices().ordfs.getContentUrl(origin ?? out.outpoint, { raw: true });
383
384
  return { ...out, origin, contentType, name, contentUrl };
384
385
  }
385
- function resolveIconOutpoint(tokenId, icon) {
386
+ function resolveIconUrl(tokenId, icon) {
386
387
  if (!icon)
387
- return;
388
+ return "";
389
+ let outpoint = icon;
388
390
  if (icon.startsWith("_")) {
389
391
  const txid = tokenId.split("_")[0];
390
- return `${txid}${icon}`;
391
- }
392
- return icon;
393
- }
394
- async function groupBsv21Tokens(outputs) {
395
- const groups = new Map;
396
- for (const out of outputs) {
397
- const events = out.events ?? [];
398
- const tokenId = getEvent(events, "bsv21:");
399
- if (!tokenId)
400
- continue;
401
- let group = groups.get(tokenId);
402
- if (!group) {
403
- group = [];
404
- groups.set(tokenId, group);
405
- }
406
- group.push(out);
407
- }
408
- if (groups.size === 0)
409
- return [];
410
- const services = getServices();
411
- const tokenIds = [...groups.keys()];
412
- let details = [];
413
- try {
414
- details = await services.bsv21.lookupTokens(tokenIds);
415
- } catch {}
416
- const detailMap = new Map(details.map((d) => [d.tokenId, d]));
417
- const balances = [];
418
- for (const [tokenId, outs] of groups) {
419
- const detail = detailMap.get(tokenId);
420
- const isActive = detail?.status?.is_active ?? false;
421
- const iconOutpoint = resolveIconOutpoint(tokenId, detail?.token?.icon);
422
- let totalAmount = 0n;
423
- let validatedOutputs = outs;
424
- if (isActive) {
425
- try {
426
- const outpoints = outs.map((o) => o.outpoint);
427
- const validated = await services.bsv21.validateOutputs(tokenId, outpoints, { unspent: true });
428
- totalAmount = validated.reduce((sum, v) => {
429
- const bsv21 = v.data?.bsv21;
430
- return sum + (bsv21?.amt ? BigInt(bsv21.amt) : 0n);
431
- }, 0n);
432
- validatedOutputs = validated;
433
- } catch {}
434
- }
435
- balances.push({
436
- tokenId,
437
- symbol: detail?.token?.sym,
438
- icon: iconOutpoint ? services.ordfs.getContentUrl(iconOutpoint) : "",
439
- decimals: Number(detail?.token?.dec ?? 0),
440
- totalAmount,
441
- outputs: validatedOutputs,
442
- isActive
443
- });
444
- }
445
- return balances;
446
- }
447
- async function categorizeOutputs(outputs) {
448
- const funding = [];
449
- const rawOrdinals = [];
450
- const opnsRaw = [];
451
- const bsv21Raw = [];
452
- const bsv20Tokens = [];
453
- const locked = [];
454
- for (const out of outputs) {
455
- const events = out.events ?? [];
456
- const sats = out.satoshis ?? 0;
457
- if (events.some((e) => e.startsWith("bsv21:"))) {
458
- bsv21Raw.push(out);
459
- continue;
460
- }
461
- if (events.some((e) => e.startsWith("lock:"))) {
462
- locked.push(out);
463
- continue;
464
- }
465
- if (events.some((e) => e === "type:application/bsv-20" || e === "type:Token")) {
466
- bsv20Tokens.push(out);
467
- continue;
468
- }
469
- if (sats === 1) {
470
- if (events.some((e) => e === "type:application/op-ns")) {
471
- opnsRaw.push(out);
472
- } else {
473
- rawOrdinals.push(out);
474
- }
475
- continue;
476
- }
477
- if (sats > 1) {
478
- funding.push(out);
479
- }
480
- }
481
- const run = [];
482
- const cleanFunding = [];
483
- if (funding.length > 0) {
484
- const runTxids = await detectRunTransactions(funding);
485
- for (const f of funding) {
486
- const { txid } = parseOutpoint(f.outpoint);
487
- if (runTxids.has(txid)) {
488
- run.push(f);
489
- } else {
490
- cleanFunding.push(f);
491
- }
492
- }
392
+ outpoint = `${txid}${icon}`;
493
393
  }
494
- return {
495
- funding: cleanFunding,
496
- ordinals: rawOrdinals.map(enrichOrdinal),
497
- opnsNames: opnsRaw.map(enrichOrdinal),
498
- bsv21Tokens: await groupBsv21Tokens(bsv21Raw),
499
- bsv20Tokens,
500
- locked,
501
- run,
502
- totalBsv: cleanFunding.reduce((sum, o) => sum + (o.satoshis ?? 0), 0)
503
- };
394
+ return getServices().ordfs.getContentUrl(outpoint);
395
+ }
396
+ function enrichTokenBalances(tokens) {
397
+ return tokens.map((t) => ({
398
+ ...t,
399
+ icon: resolveIconUrl(t.tokenId, t.icon)
400
+ }));
504
401
  }
505
402
  async function scanAddress(address, onProgress) {
506
- const services = getServices();
507
- onProgress?.({ phase: "sync", detail: "Syncing address..." });
508
- for await (const event of services.owner.getTxos(address, { refresh: true, limit: 1 })) {
509
- if (event.type === "sync") {
510
- const p = event.data;
511
- onProgress?.({
512
- phase: "sync",
513
- detail: `${p.phase}: ${p.processed ?? 0}/${p.total ?? "?"}`
514
- });
515
- } else if (event.type === "done" || event.type === "error") {
516
- break;
517
- }
518
- }
519
- onProgress?.({ phase: "search", detail: "Searching for assets..." });
520
- const allOutputs = await services.txo.search(`own:${address}`, {
521
- unspent: true,
522
- events: true,
523
- sats: true,
524
- limit: 0
525
- });
526
- onProgress?.({ phase: "categorize", detail: "Loading token details..." });
527
- return await categorizeOutputs(allOutputs ?? []);
403
+ const result = await coreScanAddresses(getServices(), [address], onProgress);
404
+ return toScannedAssets(result);
528
405
  }
529
406
  async function scanAddresses(addresses, onProgress) {
530
407
  const unique = [...new Set(addresses)];
531
- const allResults = [];
532
- for (const addr of unique) {
533
- onProgress?.({ phase: "sync", detail: `Scanning ${addr.slice(0, 8)}...` });
534
- allResults.push(await scanAddress(addr, onProgress));
535
- }
408
+ const result = await coreScanAddresses(getServices(), unique, onProgress);
409
+ return toScannedAssets(result);
410
+ }
411
+ function toScannedAssets(result) {
536
412
  return {
537
- funding: allResults.flatMap((r) => r.funding),
538
- ordinals: allResults.flatMap((r) => r.ordinals),
539
- opnsNames: allResults.flatMap((r) => r.opnsNames),
540
- bsv21Tokens: allResults.flatMap((r) => r.bsv21Tokens),
541
- bsv20Tokens: allResults.flatMap((r) => r.bsv20Tokens),
542
- locked: allResults.flatMap((r) => r.locked),
543
- run: allResults.flatMap((r) => r.run),
544
- totalBsv: allResults.reduce((sum, r) => sum + r.totalBsv, 0)
413
+ funding: result.funding,
414
+ ordinals: result.ordinals.map(enrichOrdinal),
415
+ opnsNames: result.opnsNames.map(enrichOrdinal),
416
+ bsv21Tokens: enrichTokenBalances(result.bsv21Tokens),
417
+ bsv20Tokens: result.bsv20Tokens,
418
+ locked: result.locked,
419
+ run: result.run,
420
+ totalFundingSats: result.totalFundingSats,
421
+ totalBsv: result.totalFundingSats
545
422
  };
546
423
  }
547
- async function detectRunTransactions(funding) {
548
- const services = getServices();
549
- const txids = [...new Set(funding.map((f) => parseOutpoint(f.outpoint).txid))];
550
- const runTxids = new Set;
551
- for (const txid of txids) {
552
- try {
553
- const beef = await services.getBeefForTxid(txid);
554
- const beefTx = beef.findTxid(txid);
555
- if (!beefTx?.tx)
556
- continue;
557
- for (const output of beefTx.tx.outputs) {
558
- const script = output.lockingScript?.toBinary();
559
- if (script && hasRunPrefix(script)) {
560
- runTxids.add(txid);
561
- break;
562
- }
563
- }
564
- } catch {}
565
- }
566
- return runTxids;
567
- }
568
- function hasRunPrefix(script) {
569
- if (script.length < RUN_PREFIX.length)
570
- return false;
571
- for (let i = 0;i < RUN_PREFIX.length; i++) {
572
- if (script[i] !== RUN_PREFIX[i])
573
- return false;
574
- }
575
- return true;
576
- }
577
424
 
578
425
  // src/components/wif-input.tsx
579
426
  import { deriveIdentityKey } from "@1sat/utils";
@@ -1395,63 +1242,72 @@ function OrdinalsSection({ ordinals, selectedOrdinals, onToggle, onSelectAll, on
1395
1242
  ]
1396
1243
  });
1397
1244
  }
1398
- function TokenRow({ tb }) {
1399
- return /* @__PURE__ */ jsx8("div", {
1245
+ function TokenRow({ tb, onSweep, walletConnected }) {
1246
+ return /* @__PURE__ */ jsxs3("div", {
1400
1247
  className: `flex items-center justify-between p-3 rounded-lg border ${tb.isActive ? "bg-black/20 border-purple-500/10" : "bg-black/10 border-muted/20 opacity-60"}`,
1401
- children: /* @__PURE__ */ jsxs3("div", {
1402
- className: "flex items-center gap-3",
1403
- children: [
1404
- /* @__PURE__ */ jsx8("img", {
1405
- src: tb.icon,
1406
- alt: tb.symbol || "Token",
1407
- className: "w-8 h-8 rounded-full object-cover",
1408
- onError: (e) => {
1409
- e.target.style.display = "none";
1410
- }
1411
- }),
1412
- /* @__PURE__ */ jsxs3("div", {
1413
- children: [
1414
- /* @__PURE__ */ jsxs3("div", {
1415
- className: "flex items-center gap-2",
1416
- children: [
1417
- /* @__PURE__ */ jsx8("span", {
1418
- className: "font-medium text-foreground",
1419
- children: tb.symbol || tb.tokenId.slice(0, 8) + "..."
1420
- }),
1421
- tb.isActive ? /* @__PURE__ */ jsx8("span", {
1422
- className: "px-1.5 py-0.5 text-[9px] rounded bg-green-600/20 text-green-700 dark:text-green-400",
1423
- children: "active"
1424
- }) : /* @__PURE__ */ jsx8("span", {
1425
- className: "px-1.5 py-0.5 text-[9px] rounded bg-muted text-muted-foreground",
1426
- children: "inactive"
1427
- })
1428
- ]
1429
- }),
1430
- /* @__PURE__ */ jsxs3("div", {
1431
- className: "text-xs text-muted-foreground",
1432
- children: [
1433
- formatTokenAmount(tb.totalAmount.toString(), tb.decimals),
1434
- " ",
1435
- tb.symbol || "",
1436
- /* @__PURE__ */ jsxs3("span", {
1437
- className: "ml-2",
1438
- children: [
1439
- "(",
1440
- tb.outputs.length,
1441
- " output",
1442
- tb.outputs.length !== 1 ? "s" : "",
1443
- ")"
1444
- ]
1445
- })
1446
- ]
1447
- })
1448
- ]
1449
- })
1450
- ]
1451
- })
1248
+ children: [
1249
+ /* @__PURE__ */ jsxs3("div", {
1250
+ className: "flex items-center gap-3",
1251
+ children: [
1252
+ /* @__PURE__ */ jsx8("img", {
1253
+ src: tb.icon,
1254
+ alt: tb.symbol || "Token",
1255
+ className: "w-8 h-8 rounded-full object-cover",
1256
+ onError: (e) => {
1257
+ e.target.style.display = "none";
1258
+ }
1259
+ }),
1260
+ /* @__PURE__ */ jsxs3("div", {
1261
+ children: [
1262
+ /* @__PURE__ */ jsxs3("div", {
1263
+ className: "flex items-center gap-2",
1264
+ children: [
1265
+ /* @__PURE__ */ jsx8("span", {
1266
+ className: "font-medium text-foreground",
1267
+ children: tb.symbol || tb.tokenId.slice(0, 8) + "..."
1268
+ }),
1269
+ tb.isActive ? /* @__PURE__ */ jsx8("span", {
1270
+ className: "px-1.5 py-0.5 text-[9px] rounded bg-green-600/20 text-green-700 dark:text-green-400",
1271
+ children: "active"
1272
+ }) : /* @__PURE__ */ jsx8("span", {
1273
+ className: "px-1.5 py-0.5 text-[9px] rounded bg-muted text-muted-foreground",
1274
+ children: "inactive"
1275
+ })
1276
+ ]
1277
+ }),
1278
+ /* @__PURE__ */ jsxs3("div", {
1279
+ className: "text-xs text-muted-foreground",
1280
+ children: [
1281
+ formatTokenAmount(tb.totalAmount.toString(), tb.decimals),
1282
+ " ",
1283
+ tb.symbol || "",
1284
+ /* @__PURE__ */ jsxs3("span", {
1285
+ className: "ml-2",
1286
+ children: [
1287
+ "(",
1288
+ tb.outputs.length,
1289
+ " output",
1290
+ tb.outputs.length !== 1 ? "s" : "",
1291
+ ")"
1292
+ ]
1293
+ })
1294
+ ]
1295
+ })
1296
+ ]
1297
+ })
1298
+ ]
1299
+ }),
1300
+ tb.isActive && onSweep && /* @__PURE__ */ jsx8(Button, {
1301
+ size: "sm",
1302
+ onClick: () => onSweep(tb.tokenId),
1303
+ disabled: !walletConnected,
1304
+ title: walletConnected ? undefined : "Connect BRC-100 wallet to sweep",
1305
+ children: "Sweep to Wallet"
1306
+ })
1307
+ ]
1452
1308
  });
1453
1309
  }
1454
- function Bsv21Section({ tokens }) {
1310
+ function Bsv21Section({ tokens, onSweep, walletConnected }) {
1455
1311
  if (tokens.length === 0)
1456
1312
  return null;
1457
1313
  const active = tokens.filter((t) => t.isActive);
@@ -1475,7 +1331,9 @@ function Bsv21Section({ tokens }) {
1475
1331
  className: "space-y-3",
1476
1332
  children: [
1477
1333
  active.map((tb) => /* @__PURE__ */ jsx8(TokenRow, {
1478
- tb
1334
+ tb,
1335
+ onSweep,
1336
+ walletConnected
1479
1337
  }, tb.tokenId)),
1480
1338
  inactive.length > 0 && active.length > 0 && /* @__PURE__ */ jsx8("div", {
1481
1339
  className: "border-t border-purple-500/10 pt-3 mt-3",
@@ -1489,7 +1347,8 @@ function Bsv21Section({ tokens }) {
1489
1347
  })
1490
1348
  }),
1491
1349
  inactive.map((tb) => /* @__PURE__ */ jsx8(TokenRow, {
1492
- tb
1350
+ tb,
1351
+ walletConnected
1493
1352
  }, tb.tokenId))
1494
1353
  ]
1495
1354
  })
@@ -1904,7 +1763,7 @@ function buildKeys(outputs, keyMap) {
1904
1763
  });
1905
1764
  }
1906
1765
  async function executeSweep(params) {
1907
- const { wallet, keys, funding, ordinals, bsv21Tokens, amount, onProgress } = params;
1766
+ const { wallet, keys, funding, ordinals, amount, onProgress } = params;
1908
1767
  const ctx = createContext2(wallet, { services: getServices(), chain: "main" });
1909
1768
  const result = {
1910
1769
  ordinalTxids: [],
@@ -1937,42 +1796,31 @@ async function executeSweep(params) {
1937
1796
  result.errors.push(`Ordinals: ${e instanceof Error ? e.message : String(e)}`);
1938
1797
  }
1939
1798
  }
1940
- if (bsv21Tokens.length > 0) {
1941
- const groups = new Map;
1942
- for (const token of bsv21Tokens) {
1943
- const tokenEvent = token.events?.find((e) => e.startsWith("tokenId:"));
1944
- const tokenId = tokenEvent?.slice(8) ?? "unknown";
1945
- const group = groups.get(tokenId) ?? [];
1946
- group.push(token);
1947
- groups.set(tokenId, group);
1948
- }
1949
- for (const [tokenId, tokens] of groups) {
1950
- onProgress(`Sweeping ${tokens.length} tokens (${tokenId.slice(0, 8)}...)...`);
1951
- try {
1952
- const inputs = await prepareSweepInputs(ctx, tokens);
1953
- const tokenResult = await sweepBsv21.execute(ctx, {
1954
- inputs: inputs.map((inp) => ({
1955
- ...inp,
1956
- tokenId,
1957
- amount: "0"
1958
- })),
1959
- keys: buildKeys(tokens, keys)
1960
- });
1961
- if (tokenResult.error)
1962
- result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${tokenResult.error}`);
1963
- else if (tokenResult.txid)
1964
- result.bsv21Txids.push(tokenResult.txid);
1965
- } catch (e) {
1966
- result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${e instanceof Error ? e.message : String(e)}`);
1967
- }
1968
- }
1969
- }
1970
1799
  onProgress("Sweep complete");
1971
1800
  return result;
1972
1801
  }
1802
+ async function sweepBsv21Token(params) {
1803
+ const { wallet, keys, token, onProgress } = params;
1804
+ const ctx = createContext2(wallet, { services: getServices(), chain: "main" });
1805
+ onProgress(`Sweeping ${token.symbol ?? token.tokenId.slice(0, 8)}...`);
1806
+ try {
1807
+ const inputs = token.outputs.map((out) => ({
1808
+ outpoint: out.outpoint,
1809
+ tokenId: token.tokenId,
1810
+ amount: token.amounts.get(out.outpoint) ?? "0"
1811
+ }));
1812
+ const tokenKeys = buildKeys(token.outputs, keys);
1813
+ const result = await sweepBsv21.execute(ctx, { inputs, keys: tokenKeys });
1814
+ if (result.error)
1815
+ return { error: result.error };
1816
+ return { txid: result.txid };
1817
+ } catch (e) {
1818
+ return { error: e instanceof Error ? e.message : String(e) };
1819
+ }
1820
+ }
1973
1821
 
1974
1822
  // src/lib/legacy-send.ts
1975
- import { parseOutpoint as parseOutpoint2 } from "@1sat/utils";
1823
+ import { parseOutpoint } from "@1sat/utils";
1976
1824
  import { MAP_PREFIX } from "@1sat/types";
1977
1825
  import { OP, P2PKH, PrivateKey as PrivateKey2, Script, Transaction, Utils } from "@bsv/sdk";
1978
1826
  async function fetchSourceTx(txid) {
@@ -2019,7 +1867,7 @@ async function legacySendBsv(params) {
2019
1867
  const p2pkh = new P2PKH;
2020
1868
  const tx = new Transaction;
2021
1869
  for (const utxo of funding) {
2022
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1870
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2023
1871
  const key = keyForOutput(utxo, keyMap, payKey);
2024
1872
  tx.addInput({
2025
1873
  sourceTXID: txid,
@@ -2067,7 +1915,7 @@ async function legacySendOrdinals(params) {
2067
1915
  const p2pkh = new P2PKH;
2068
1916
  const tx = new Transaction;
2069
1917
  for (const ord of ordinals) {
2070
- const { txid, vout } = parseOutpoint2(ord.outpoint);
1918
+ const { txid, vout } = parseOutpoint(ord.outpoint);
2071
1919
  const key = keyForOutput(ord, keyMap, payKey);
2072
1920
  tx.addInput({
2073
1921
  sourceTXID: txid,
@@ -2084,7 +1932,7 @@ async function legacySendOrdinals(params) {
2084
1932
  });
2085
1933
  }
2086
1934
  for (const utxo of funding) {
2087
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1935
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2088
1936
  const key = keyForOutput(utxo, keyMap, payKey);
2089
1937
  tx.addInput({
2090
1938
  sourceTXID: txid,
@@ -2117,7 +1965,7 @@ async function legacyBurnOrdinals(params) {
2117
1965
  const p2pkh = new P2PKH;
2118
1966
  const tx = new Transaction;
2119
1967
  for (const ord of ordinals) {
2120
- const { txid, vout } = parseOutpoint2(ord.outpoint);
1968
+ const { txid, vout } = parseOutpoint(ord.outpoint);
2121
1969
  const key = keyForOutput(ord, keyMap, payKey);
2122
1970
  tx.addInput({
2123
1971
  sourceTXID: txid,
@@ -2128,7 +1976,7 @@ async function legacyBurnOrdinals(params) {
2128
1976
  });
2129
1977
  }
2130
1978
  for (const utxo of funding) {
2131
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1979
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2132
1980
  const key = keyForOutput(utxo, keyMap, payKey);
2133
1981
  tx.addInput({
2134
1982
  sourceTXID: txid,
@@ -2319,7 +2167,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2319
2167
  if (!wallet || !legacyKeys || !assets)
2320
2168
  return;
2321
2169
  await runOperation("Sweep BSV", async () => {
2322
- const result = await executeSweep({ wallet, keys: keyMap, funding: getSelectedFunding(), ordinals: [], bsv21Tokens: [], amount: sweepAmount ?? undefined, onProgress: setSweepProgress });
2170
+ const result = await executeSweep({ wallet, keys: keyMap, funding: getSelectedFunding(), ordinals: [], amount: sweepAmount ?? undefined, onProgress: setSweepProgress });
2323
2171
  if (result.errors.length > 0)
2324
2172
  throw new Error(result.errors[0]);
2325
2173
  return result.bsvTxid ?? "";
@@ -2341,7 +2189,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2341
2189
  if (selected.length === 0)
2342
2190
  return;
2343
2191
  await runOperation(`Sweep ${selected.length} ordinal${selected.length !== 1 ? "s" : ""}`, async () => {
2344
- const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, bsv21Tokens: [], onProgress: setSweepProgress });
2192
+ const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, onProgress: setSweepProgress });
2345
2193
  if (result.errors.length > 0)
2346
2194
  throw new Error(result.errors[0]);
2347
2195
  return result.ordinalTxids[0] ?? "";
@@ -2377,7 +2225,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2377
2225
  if (selected.length === 0)
2378
2226
  return;
2379
2227
  await runOperation(`Sweep ${selected.length} domain${selected.length !== 1 ? "s" : ""}`, async () => {
2380
- const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, bsv21Tokens: [], onProgress: setSweepProgress });
2228
+ const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, onProgress: setSweepProgress });
2381
2229
  if (result.errors.length > 0)
2382
2230
  throw new Error(result.errors[0]);
2383
2231
  return result.ordinalTxids[0] ?? "";
@@ -2405,6 +2253,20 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2405
2253
  return result.txid;
2406
2254
  });
2407
2255
  }, [legacyKeys, assets, selectedOpns, runOperation]);
2256
+ const handleSweepBsv21Token = useCallback2(async (tokenId) => {
2257
+ const wallet = resolveWallet();
2258
+ if (!wallet || !assets)
2259
+ return;
2260
+ const token = assets.bsv21Tokens.find((t) => t.tokenId === tokenId);
2261
+ if (!token)
2262
+ return;
2263
+ await runOperation(`Sweep ${token.symbol ?? tokenId.slice(0, 8)}`, async () => {
2264
+ const result = await sweepBsv21Token({ wallet, keys: keyMap, token, onProgress: setSweepProgress });
2265
+ if (result.error)
2266
+ throw new Error(result.error);
2267
+ return result.txid ?? "";
2268
+ });
2269
+ }, [resolveWallet, assets, keyMap, runOperation]);
2408
2270
  return /* @__PURE__ */ jsxs6("div", {
2409
2271
  className: "min-h-screen bg-background text-foreground",
2410
2272
  children: [
@@ -2503,7 +2365,9 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2503
2365
  /* @__PURE__ */ jsx11(TabsContent, {
2504
2366
  value: "bsv21",
2505
2367
  children: /* @__PURE__ */ jsx11(Bsv21Section, {
2506
- tokens: assets.bsv21Tokens
2368
+ tokens: assets.bsv21Tokens,
2369
+ onSweep: handleSweepBsv21Token,
2370
+ walletConnected
2507
2371
  })
2508
2372
  }),
2509
2373
  /* @__PURE__ */ jsx11(TabsContent, {
@@ -1,33 +1,18 @@
1
1
  import type { IndexedOutput } from "@1sat/types";
2
+ import { type ScanResult, type ScanProgress, type TokenBalance } from "@1sat/actions";
3
+ export type { TokenBalance, ScanProgress, ScanResult };
2
4
  export interface EnrichedOrdinal extends IndexedOutput {
3
5
  origin?: string;
4
6
  contentType?: string;
5
7
  name?: string;
6
8
  contentUrl: string;
7
9
  }
8
- export interface TokenBalance {
9
- tokenId: string;
10
- symbol?: string;
11
- icon: string;
12
- decimals: number;
13
- totalAmount: bigint;
14
- outputs: IndexedOutput[];
15
- isActive: boolean;
16
- }
17
- export interface ScannedAssets {
18
- funding: IndexedOutput[];
10
+ export interface ScannedAssets extends Omit<ScanResult, "ordinals" | "opnsNames"> {
19
11
  ordinals: EnrichedOrdinal[];
20
12
  opnsNames: EnrichedOrdinal[];
21
13
  bsv21Tokens: TokenBalance[];
22
- bsv20Tokens: IndexedOutput[];
23
- locked: IndexedOutput[];
24
- run: IndexedOutput[];
25
14
  totalBsv: number;
26
15
  }
27
- export interface ScanProgress {
28
- phase: string;
29
- detail?: string;
30
- }
31
16
  export declare function deriveAddress(wif: string): string;
32
17
  export declare function scanAddress(address: string, onProgress?: (p: ScanProgress) => void): Promise<ScannedAssets>;
33
18
  export declare function scanAddresses(addresses: string[], onProgress?: (p: ScanProgress) => void): Promise<ScannedAssets>;
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAOjD,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,GAAG,EAAE,aAAa,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAyKD,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GACpC,OAAO,CAAC,aAAa,CAAC,CA0BxB;AAED,wBAAsB,aAAa,CAClC,SAAS,EAAE,MAAM,EAAE,EACnB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GACpC,OAAO,CAAC,aAAa,CAAC,CAmBxB"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/lib/scanner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAEN,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,MAAM,eAAe,CAAC;AAGvB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAEvD,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAc,SAAQ,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,WAAW,CAAC;IAChF,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAuCD,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GACpC,OAAO,CAAC,aAAa,CAAC,CAGxB;AAED,wBAAsB,aAAa,CAClC,SAAS,EAAE,MAAM,EAAE,EACnB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GACpC,OAAO,CAAC,aAAa,CAAC,CAIxB"}
@@ -1,18 +1,34 @@
1
1
  import type { IndexedOutput } from "@1sat/types";
2
2
  import { PrivateKey, type WalletInterface } from "@bsv/sdk";
3
+ import type { TokenBalance } from "./scanner";
3
4
  export interface SweepResult {
4
5
  bsvTxid?: string;
5
6
  ordinalTxids: string[];
6
7
  bsv21Txids: string[];
7
8
  errors: string[];
8
9
  }
10
+ /**
11
+ * Sweep BSV funding and ordinals into the connected wallet.
12
+ */
9
13
  export declare function executeSweep(params: {
10
14
  wallet: WalletInterface;
11
15
  keys: Map<string, PrivateKey>;
12
16
  funding: IndexedOutput[];
13
17
  ordinals: IndexedOutput[];
14
- bsv21Tokens: IndexedOutput[];
15
18
  amount?: number;
16
19
  onProgress: (stage: string) => void;
17
20
  }): Promise<SweepResult>;
21
+ /**
22
+ * Sweep a single BSV-21 token into the connected wallet.
23
+ * Each token requires its own transaction since all inputs must share a tokenId.
24
+ */
25
+ export declare function sweepBsv21Token(params: {
26
+ wallet: WalletInterface;
27
+ keys: Map<string, PrivateKey>;
28
+ token: TokenBalance;
29
+ onProgress: (stage: string) => void;
30
+ }): Promise<{
31
+ txid?: string;
32
+ error?: string;
33
+ }>;
18
34
  //# sourceMappingURL=sweeper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../src/lib/sweeper.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAG5D,MAAM,WAAW,WAAW;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB;AAeD,wBAAsB,YAAY,CAAC,MAAM,EAAE;IAC1C,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,WAAW,EAAE,aAAa,EAAE,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,GAAG,OAAO,CAAC,WAAW,CAAC,CAkEvB"}
1
+ {"version":3,"file":"sweeper.d.ts","sourceRoot":"","sources":["../../src/lib/sweeper.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAG9C,MAAM,WAAW,WAAW;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB;AAeD;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE;IAC1C,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,GAAG,OAAO,CAAC,WAAW,CAAC,CAoCvB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC7C,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC,GAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqB7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/sweep-ui",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Sweep UI components for migrating legacy BSV assets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -19,10 +19,10 @@
19
19
  "keywords": ["1sat", "bsv", "ordinals", "sweep", "migration", "react"],
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@1sat/actions": "0.0.74",
22
+ "@1sat/actions": "0.0.79",
23
23
  "@1sat/client": "0.0.20",
24
24
  "@1sat/connect": "0.0.27",
25
- "@1sat/sweep-ui": "0.0.15",
25
+ "@1sat/sweep-ui": "0.0.17",
26
26
  "@1sat/types": "0.0.14",
27
27
  "@1sat/utils": "0.0.12",
28
28
  "bitcoin-backup": "^0.0.11",
@@ -8,7 +8,7 @@ import { FundingSection, OrdinalsSection, Bsv21Section, Bsv20Section, LockedSect
8
8
  import { OpnsSection } from "./opns-section";
9
9
  import { TxHistory, type TxRecord } from "./tx-history";
10
10
  import { deriveAddress, scanAddresses, type ScannedAssets } from "../lib/scanner";
11
- import { executeSweep } from "../lib/sweeper";
11
+ import { executeSweep, sweepBsv21Token } from "../lib/sweeper";
12
12
  import { legacySendBsv, legacySendOrdinals, legacyBurnOrdinals } from "../lib/legacy-send";
13
13
  import { getWallet } from "../lib/wallet";
14
14
  import type { LegacyKeys } from "../types";
@@ -160,7 +160,7 @@ export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, swee
160
160
  const wallet = resolveWallet();
161
161
  if (!wallet || !legacyKeys || !assets) return;
162
162
  await runOperation("Sweep BSV", async () => {
163
- const result = await executeSweep({ wallet, keys: keyMap, funding: getSelectedFunding(), ordinals: [], bsv21Tokens: [], amount: sweepAmount ?? undefined, onProgress: setSweepProgress });
163
+ const result = await executeSweep({ wallet, keys: keyMap, funding: getSelectedFunding(), ordinals: [], amount: sweepAmount ?? undefined, onProgress: setSweepProgress });
164
164
  if (result.errors.length > 0) throw new Error(result.errors[0]);
165
165
  return result.bsvTxid ?? "";
166
166
  });
@@ -180,7 +180,7 @@ export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, swee
180
180
  const selected = assets.ordinals.filter((o) => selectedOrdinals.has(o.outpoint));
181
181
  if (selected.length === 0) return;
182
182
  await runOperation(`Sweep ${selected.length} ordinal${selected.length !== 1 ? "s" : ""}`, async () => {
183
- const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, bsv21Tokens: [], onProgress: setSweepProgress });
183
+ const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, onProgress: setSweepProgress });
184
184
  if (result.errors.length > 0) throw new Error(result.errors[0]);
185
185
  return result.ordinalTxids[0] ?? "";
186
186
  });
@@ -212,7 +212,7 @@ export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, swee
212
212
  const selected = assets.opnsNames.filter((o) => selectedOpns.has(o.outpoint));
213
213
  if (selected.length === 0) return;
214
214
  await runOperation(`Sweep ${selected.length} domain${selected.length !== 1 ? "s" : ""}`, async () => {
215
- const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, bsv21Tokens: [], onProgress: setSweepProgress });
215
+ const result = await executeSweep({ wallet, keys: keyMap, funding: [], ordinals: selected, onProgress: setSweepProgress });
216
216
  if (result.errors.length > 0) throw new Error(result.errors[0]);
217
217
  return result.ordinalTxids[0] ?? "";
218
218
  });
@@ -238,6 +238,18 @@ export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, swee
238
238
  });
239
239
  }, [legacyKeys, assets, selectedOpns, runOperation]);
240
240
 
241
+ const handleSweepBsv21Token = useCallback(async (tokenId: string) => {
242
+ const wallet = resolveWallet();
243
+ if (!wallet || !assets) return;
244
+ const token = assets.bsv21Tokens.find((t) => t.tokenId === tokenId);
245
+ if (!token) return;
246
+ await runOperation(`Sweep ${token.symbol ?? tokenId.slice(0, 8)}`, async () => {
247
+ const result = await sweepBsv21Token({ wallet, keys: keyMap, token, onProgress: setSweepProgress });
248
+ if (result.error) throw new Error(result.error);
249
+ return result.txid ?? "";
250
+ });
251
+ }, [resolveWallet, assets, keyMap, runOperation]);
252
+
241
253
  return (
242
254
  <div className="min-h-screen bg-background text-foreground">
243
255
  <Toaster position="top-right" />
@@ -279,7 +291,7 @@ export function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, swee
279
291
  <TabsContent value="opns">
280
292
  <OpnsSection opnsNames={assets.opnsNames} selectedOpns={selectedOpns} onToggle={handleToggleOpns} onSelectAll={handleSelectAllOpns} onDeselectAll={handleDeselectAllOpns} onSweep={handleSweepOpns} onSend={sweepOnly ? undefined : handleSendOpns} onBurn={sweepOnly ? undefined : handleBurnOpns} walletConnected={walletConnected} />
281
293
  </TabsContent>
282
- <TabsContent value="bsv21"><Bsv21Section tokens={assets.bsv21Tokens} /></TabsContent>
294
+ <TabsContent value="bsv21"><Bsv21Section tokens={assets.bsv21Tokens} onSweep={handleSweepBsv21Token} walletConnected={walletConnected} /></TabsContent>
283
295
  <TabsContent value="bsv20"><Bsv20Section tokens={assets.bsv20Tokens} /></TabsContent>
284
296
  <TabsContent value="locks"><LockedSection locked={assets.locked} /></TabsContent>
285
297
  <TabsContent value="run"><RunSection run={assets.run} /></TabsContent>
@@ -149,7 +149,7 @@ export function OrdinalsSection({ ordinals, selectedOrdinals, onToggle, onSelect
149
149
  );
150
150
  }
151
151
 
152
- function TokenRow({ tb }: { tb: TokenBalance }) {
152
+ function TokenRow({ tb, onSweep, walletConnected }: { tb: TokenBalance; onSweep?: (tokenId: string) => void; walletConnected: boolean }) {
153
153
  return (
154
154
  <div className={`flex items-center justify-between p-3 rounded-lg border ${tb.isActive ? "bg-black/20 border-purple-500/10" : "bg-black/10 border-muted/20 opacity-60"}`}>
155
155
  <div className="flex items-center gap-3">
@@ -169,11 +169,16 @@ function TokenRow({ tb }: { tb: TokenBalance }) {
169
169
  </div>
170
170
  </div>
171
171
  </div>
172
+ {tb.isActive && onSweep && (
173
+ <Button size="sm" onClick={() => onSweep(tb.tokenId)} disabled={!walletConnected} title={walletConnected ? undefined : "Connect BRC-100 wallet to sweep"}>
174
+ Sweep to Wallet
175
+ </Button>
176
+ )}
172
177
  </div>
173
178
  );
174
179
  }
175
180
 
176
- export function Bsv21Section({ tokens }: { tokens: TokenBalance[] }) {
181
+ export function Bsv21Section({ tokens, onSweep, walletConnected }: { tokens: TokenBalance[]; onSweep?: (tokenId: string) => void; walletConnected: boolean }) {
177
182
  if (tokens.length === 0) return null;
178
183
  const active = tokens.filter((t) => t.isActive);
179
184
  const inactive = tokens.filter((t) => !t.isActive);
@@ -185,13 +190,13 @@ export function Bsv21Section({ tokens }: { tokens: TokenBalance[] }) {
185
190
  <span className="text-sm font-semibold text-purple-500">BSV-21 Tokens</span>
186
191
  </div>
187
192
  <div className="space-y-3">
188
- {active.map((tb) => (<TokenRow key={tb.tokenId} tb={tb} />))}
193
+ {active.map((tb) => (<TokenRow key={tb.tokenId} tb={tb} onSweep={onSweep} walletConnected={walletConnected} />))}
189
194
  {inactive.length > 0 && active.length > 0 && (
190
195
  <div className="border-t border-purple-500/10 pt-3 mt-3">
191
196
  <div className="text-xs text-muted-foreground mb-2">Inactive overlays ({inactive.length}) — cannot be swept</div>
192
197
  </div>
193
198
  )}
194
- {inactive.map((tb) => (<TokenRow key={tb.tokenId} tb={tb} />))}
199
+ {inactive.map((tb) => (<TokenRow key={tb.tokenId} tb={tb} walletConnected={walletConnected} />))}
195
200
  </div>
196
201
  </div>
197
202
  );
@@ -1,10 +1,14 @@
1
1
  import { PrivateKey } from "@bsv/sdk";
2
2
  import type { IndexedOutput } from "@1sat/types";
3
- import { parseOutpoint } from "@1sat/utils";
3
+ import {
4
+ scanAddresses as coreScanAddresses,
5
+ type ScanResult,
6
+ type ScanProgress,
7
+ type TokenBalance,
8
+ } from "@1sat/actions";
4
9
  import { getServices } from "./services";
5
10
 
6
- /** RUN protocol OP_RETURN prefix: OP_FALSE OP_RETURN OP_PUSH3 "run" */
7
- const RUN_PREFIX = Uint8Array.from([0x00, 0x6a, 0x03, 0x72, 0x75, 0x6e]);
11
+ export type { TokenBalance, ScanProgress, ScanResult };
8
12
 
9
13
  export interface EnrichedOrdinal extends IndexedOutput {
10
14
  origin?: string;
@@ -13,38 +17,19 @@ export interface EnrichedOrdinal extends IndexedOutput {
13
17
  contentUrl: string;
14
18
  }
15
19
 
16
- export interface TokenBalance {
17
- tokenId: string;
18
- symbol?: string;
19
- icon: string;
20
- decimals: number;
21
- totalAmount: bigint;
22
- outputs: IndexedOutput[];
23
- isActive: boolean;
24
- }
25
-
26
- export interface ScannedAssets {
27
- funding: IndexedOutput[];
20
+ export interface ScannedAssets extends Omit<ScanResult, "ordinals" | "opnsNames"> {
28
21
  ordinals: EnrichedOrdinal[];
29
22
  opnsNames: EnrichedOrdinal[];
30
23
  bsv21Tokens: TokenBalance[];
31
- bsv20Tokens: IndexedOutput[];
32
- locked: IndexedOutput[];
33
- run: IndexedOutput[];
34
24
  totalBsv: number;
35
25
  }
36
26
 
37
- export interface ScanProgress {
38
- phase: string;
39
- detail?: string;
40
- }
41
-
42
27
  export function deriveAddress(wif: string): string {
43
28
  return PrivateKey.fromWif(wif.trim()).toPublicKey().toAddress();
44
29
  }
45
30
 
46
31
  function getEvent(events: string[], prefix: string): string | undefined {
47
- const e = events.find((e) => e.startsWith(prefix));
32
+ const e = events.find((ev) => ev.startsWith(prefix));
48
33
  return e ? e.slice(prefix.length) : undefined;
49
34
  }
50
35
 
@@ -63,182 +48,29 @@ function enrichOrdinal(out: IndexedOutput): EnrichedOrdinal {
63
48
  return { ...out, origin, contentType, name, contentUrl };
64
49
  }
65
50
 
66
- function resolveIconOutpoint(tokenId: string, icon?: string): string | undefined {
67
- if (!icon) return undefined;
51
+ function resolveIconUrl(tokenId: string, icon?: string): string {
52
+ if (!icon) return "";
53
+ let outpoint = icon;
68
54
  if (icon.startsWith("_")) {
69
55
  const txid = tokenId.split("_")[0];
70
- return `${txid}${icon}`;
71
- }
72
- return icon;
73
- }
74
-
75
- async function groupBsv21Tokens(outputs: IndexedOutput[]): Promise<TokenBalance[]> {
76
- // Group outputs by token ID from general indexer events
77
- const groups = new Map<string, IndexedOutput[]>();
78
-
79
- for (const out of outputs) {
80
- const events = out.events ?? [];
81
- const tokenId = getEvent(events, "bsv21:");
82
- if (!tokenId) continue;
83
-
84
- let group = groups.get(tokenId);
85
- if (!group) {
86
- group = [];
87
- groups.set(tokenId, group);
88
- }
89
- group.push(out);
90
- }
91
-
92
- if (groups.size === 0) return [];
93
-
94
- const services = getServices();
95
- const tokenIds = [...groups.keys()];
96
-
97
- // Get token metadata and active status from overlay
98
- let details: Array<{ tokenId: string; token?: { sym?: string; dec?: string; icon?: string }; status?: { is_active?: boolean } }> = [];
99
- try {
100
- details = await services.bsv21.lookupTokens(tokenIds);
101
- } catch {
102
- // BSV21 service may not be available
103
- }
104
-
105
- const detailMap = new Map(details.map((d) => [d.tokenId, d]));
106
-
107
- const balances: TokenBalance[] = [];
108
- for (const [tokenId, outs] of groups) {
109
- const detail = detailMap.get(tokenId);
110
- const isActive = detail?.status?.is_active ?? false;
111
- const iconOutpoint = resolveIconOutpoint(tokenId, detail?.token?.icon);
112
-
113
- let totalAmount = 0n;
114
- let validatedOutputs = outs;
115
-
116
- // For active tokens, validate outputs against the overlay to get real amounts
117
- if (isActive) {
118
- try {
119
- const outpoints = outs.map((o) => o.outpoint);
120
- const validated = await services.bsv21.validateOutputs(tokenId, outpoints, { unspent: true });
121
- totalAmount = validated.reduce((sum, v) => {
122
- const bsv21 = v.data?.bsv21 as { amt?: string } | undefined;
123
- return sum + (bsv21?.amt ? BigInt(bsv21.amt) : 0n);
124
- }, 0n);
125
- validatedOutputs = validated;
126
- } catch {
127
- // Validation failed — show outputs without amounts
128
- }
129
- }
130
-
131
- balances.push({
132
- tokenId,
133
- symbol: detail?.token?.sym,
134
- icon: iconOutpoint ? services.ordfs.getContentUrl(iconOutpoint) : "",
135
- decimals: Number(detail?.token?.dec ?? 0),
136
- totalAmount,
137
- outputs: validatedOutputs,
138
- isActive,
139
- });
56
+ outpoint = `${txid}${icon}`;
140
57
  }
141
- return balances;
58
+ return getServices().ordfs.getContentUrl(outpoint);
142
59
  }
143
60
 
144
- async function categorizeOutputs(outputs: IndexedOutput[]): Promise<ScannedAssets> {
145
- const funding: IndexedOutput[] = [];
146
- const rawOrdinals: IndexedOutput[] = [];
147
- const opnsRaw: IndexedOutput[] = [];
148
- const bsv21Raw: IndexedOutput[] = [];
149
- const bsv20Tokens: IndexedOutput[] = [];
150
- const locked: IndexedOutput[] = [];
151
-
152
- for (const out of outputs) {
153
- const events = out.events ?? [];
154
- const sats = out.satoshis ?? 0;
155
-
156
- if (events.some((e) => e.startsWith("bsv21:"))) {
157
- bsv21Raw.push(out);
158
- continue;
159
- }
160
-
161
- if (events.some((e) => e.startsWith("lock:"))) {
162
- locked.push(out);
163
- continue;
164
- }
165
-
166
- if (events.some((e) => e === "type:application/bsv-20" || e === "type:Token")) {
167
- bsv20Tokens.push(out);
168
- continue;
169
- }
170
-
171
- if (sats === 1) {
172
- if (events.some((e) => e === "type:application/op-ns")) {
173
- opnsRaw.push(out);
174
- } else {
175
- rawOrdinals.push(out);
176
- }
177
- continue;
178
- }
179
-
180
- if (sats > 1) {
181
- funding.push(out);
182
- }
183
- }
184
-
185
- // Check funding outputs for RUN token transactions
186
- const run: IndexedOutput[] = [];
187
- const cleanFunding: IndexedOutput[] = [];
188
-
189
- if (funding.length > 0) {
190
- const runTxids = await detectRunTransactions(funding);
191
- for (const f of funding) {
192
- const { txid } = parseOutpoint(f.outpoint);
193
- if (runTxids.has(txid)) {
194
- run.push(f);
195
- } else {
196
- cleanFunding.push(f);
197
- }
198
- }
199
- }
200
-
201
- return {
202
- funding: cleanFunding,
203
- ordinals: rawOrdinals.map(enrichOrdinal),
204
- opnsNames: opnsRaw.map(enrichOrdinal),
205
- bsv21Tokens: await groupBsv21Tokens(bsv21Raw),
206
- bsv20Tokens,
207
- locked,
208
- run,
209
- totalBsv: cleanFunding.reduce((sum, o) => sum + (o.satoshis ?? 0), 0),
210
- };
61
+ function enrichTokenBalances(tokens: TokenBalance[]): TokenBalance[] {
62
+ return tokens.map((t) => ({
63
+ ...t,
64
+ icon: resolveIconUrl(t.tokenId, t.icon),
65
+ }));
211
66
  }
212
67
 
213
68
  export async function scanAddress(
214
69
  address: string,
215
70
  onProgress?: (p: ScanProgress) => void,
216
71
  ): Promise<ScannedAssets> {
217
- const services = getServices();
218
-
219
- onProgress?.({ phase: "sync", detail: "Syncing address..." });
220
- for await (const event of services.owner.getTxos(address, { refresh: true, limit: 1 })) {
221
- if (event.type === "sync") {
222
- const p = event.data;
223
- onProgress?.({
224
- phase: "sync",
225
- detail: `${p.phase}: ${p.processed ?? 0}/${p.total ?? "?"}`,
226
- });
227
- } else if (event.type === "done" || event.type === "error") {
228
- break;
229
- }
230
- }
231
-
232
- onProgress?.({ phase: "search", detail: "Searching for assets..." });
233
- const allOutputs = await services.txo.search(`own:${address}`, {
234
- unspent: true,
235
- events: true,
236
- sats: true,
237
- limit: 0,
238
- });
239
-
240
- onProgress?.({ phase: "categorize", detail: "Loading token details..." });
241
- return await categorizeOutputs(allOutputs ?? []);
72
+ const result = await coreScanAddresses(getServices(), [address], onProgress);
73
+ return toScannedAssets(result);
242
74
  }
243
75
 
244
76
  export async function scanAddresses(
@@ -246,59 +78,20 @@ export async function scanAddresses(
246
78
  onProgress?: (p: ScanProgress) => void,
247
79
  ): Promise<ScannedAssets> {
248
80
  const unique = [...new Set(addresses)];
249
- const allResults: ScannedAssets[] = [];
250
-
251
- for (const addr of unique) {
252
- onProgress?.({ phase: "sync", detail: `Scanning ${addr.slice(0, 8)}...` });
253
- allResults.push(await scanAddress(addr, onProgress));
254
- }
81
+ const result = await coreScanAddresses(getServices(), unique, onProgress);
82
+ return toScannedAssets(result);
83
+ }
255
84
 
85
+ function toScannedAssets(result: ScanResult): ScannedAssets {
256
86
  return {
257
- funding: allResults.flatMap((r) => r.funding),
258
- ordinals: allResults.flatMap((r) => r.ordinals),
259
- opnsNames: allResults.flatMap((r) => r.opnsNames),
260
- bsv21Tokens: allResults.flatMap((r) => r.bsv21Tokens),
261
- bsv20Tokens: allResults.flatMap((r) => r.bsv20Tokens),
262
- locked: allResults.flatMap((r) => r.locked),
263
- run: allResults.flatMap((r) => r.run),
264
- totalBsv: allResults.reduce((sum, r) => sum + r.totalBsv, 0),
87
+ funding: result.funding,
88
+ ordinals: result.ordinals.map(enrichOrdinal),
89
+ opnsNames: result.opnsNames.map(enrichOrdinal),
90
+ bsv21Tokens: enrichTokenBalances(result.bsv21Tokens),
91
+ bsv20Tokens: result.bsv20Tokens,
92
+ locked: result.locked,
93
+ run: result.run,
94
+ totalFundingSats: result.totalFundingSats,
95
+ totalBsv: result.totalFundingSats,
265
96
  };
266
97
  }
267
-
268
- /**
269
- * Check source transactions for the RUN protocol OP_RETURN pattern.
270
- * Returns the set of txids that contain a RUN OP_RETURN output.
271
- */
272
- async function detectRunTransactions(funding: IndexedOutput[]): Promise<Set<string>> {
273
- const services = getServices();
274
- const txids = [...new Set(funding.map((f) => parseOutpoint(f.outpoint).txid))];
275
- const runTxids = new Set<string>();
276
-
277
- for (const txid of txids) {
278
- try {
279
- const beef = await services.getBeefForTxid(txid);
280
- const beefTx = beef.findTxid(txid);
281
- if (!beefTx?.tx) continue;
282
-
283
- for (const output of beefTx.tx.outputs) {
284
- const script = output.lockingScript?.toBinary();
285
- if (script && hasRunPrefix(script)) {
286
- runTxids.add(txid);
287
- break;
288
- }
289
- }
290
- } catch {
291
- // If we can't fetch the tx, leave the output in funding
292
- }
293
- }
294
-
295
- return runTxids;
296
- }
297
-
298
- function hasRunPrefix(script: number[]): boolean {
299
- if (script.length < RUN_PREFIX.length) return false;
300
- for (let i = 0; i < RUN_PREFIX.length; i++) {
301
- if (script[i] !== RUN_PREFIX[i]) return false;
302
- }
303
- return true;
304
- }
@@ -7,6 +7,7 @@ import {
7
7
  } from "@1sat/actions";
8
8
  import type { IndexedOutput } from "@1sat/types";
9
9
  import { PrivateKey, type WalletInterface } from "@bsv/sdk";
10
+ import type { TokenBalance } from "./scanner";
10
11
  import { getServices } from "./services";
11
12
 
12
13
  export interface SweepResult {
@@ -29,16 +30,18 @@ function buildKeys(outputs: IndexedOutput[], keyMap: Map<string, PrivateKey>): P
29
30
  });
30
31
  }
31
32
 
33
+ /**
34
+ * Sweep BSV funding and ordinals into the connected wallet.
35
+ */
32
36
  export async function executeSweep(params: {
33
37
  wallet: WalletInterface;
34
38
  keys: Map<string, PrivateKey>;
35
39
  funding: IndexedOutput[];
36
40
  ordinals: IndexedOutput[];
37
- bsv21Tokens: IndexedOutput[];
38
41
  amount?: number;
39
42
  onProgress: (stage: string) => void;
40
43
  }): Promise<SweepResult> {
41
- const { wallet, keys, funding, ordinals, bsv21Tokens, amount, onProgress } = params;
44
+ const { wallet, keys, funding, ordinals, amount, onProgress } = params;
42
45
  const ctx = createContext(wallet, { services: getServices(), chain: "main" });
43
46
 
44
47
  const result: SweepResult = {
@@ -71,36 +74,38 @@ export async function executeSweep(params: {
71
74
  }
72
75
  }
73
76
 
74
- if (bsv21Tokens.length > 0) {
75
- const groups = new Map<string, IndexedOutput[]>();
76
- for (const token of bsv21Tokens) {
77
- const tokenEvent = token.events?.find((e) => e.startsWith("tokenId:"));
78
- const tokenId = tokenEvent?.slice(8) ?? "unknown";
79
- const group = groups.get(tokenId) ?? [];
80
- group.push(token);
81
- groups.set(tokenId, group);
82
- }
83
-
84
- for (const [tokenId, tokens] of groups) {
85
- onProgress(`Sweeping ${tokens.length} tokens (${tokenId.slice(0, 8)}...)...`);
86
- try {
87
- const inputs = await prepareSweepInputs(ctx, tokens);
88
- const tokenResult = await sweepBsv21.execute(ctx, {
89
- inputs: inputs.map((inp) => ({
90
- ...inp,
91
- tokenId,
92
- amount: "0",
93
- })),
94
- keys: buildKeys(tokens, keys),
95
- });
96
- if (tokenResult.error) result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${tokenResult.error}`);
97
- else if (tokenResult.txid) result.bsv21Txids.push(tokenResult.txid);
98
- } catch (e) {
99
- result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${e instanceof Error ? e.message : String(e)}`);
100
- }
101
- }
102
- }
103
-
104
77
  onProgress("Sweep complete");
105
78
  return result;
106
79
  }
80
+
81
+ /**
82
+ * Sweep a single BSV-21 token into the connected wallet.
83
+ * Each token requires its own transaction since all inputs must share a tokenId.
84
+ */
85
+ export async function sweepBsv21Token(params: {
86
+ wallet: WalletInterface;
87
+ keys: Map<string, PrivateKey>;
88
+ token: TokenBalance;
89
+ onProgress: (stage: string) => void;
90
+ }): Promise<{ txid?: string; error?: string }> {
91
+ const { wallet, keys, token, onProgress } = params;
92
+ const ctx = createContext(wallet, { services: getServices(), chain: "main" });
93
+
94
+ onProgress(`Sweeping ${token.symbol ?? token.tokenId.slice(0, 8)}...`);
95
+
96
+ try {
97
+ const inputs = token.outputs.map((out) => ({
98
+ outpoint: out.outpoint,
99
+ tokenId: token.tokenId,
100
+ amount: token.amounts.get(out.outpoint) ?? "0",
101
+ }));
102
+
103
+ const tokenKeys = buildKeys(token.outputs, keys);
104
+
105
+ const result = await sweepBsv21.execute(ctx, { inputs, keys: tokenKeys });
106
+ if (result.error) return { error: result.error };
107
+ return { txid: result.txid };
108
+ } catch (e) {
109
+ return { error: e instanceof Error ? e.message : String(e) };
110
+ }
111
+ }