@1sat/sweep-ui 0.0.14 → 0.0.16

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,187 +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
- const amtStr = getEvent(events, "amt:");
402
- const amount = amtStr ? BigInt(amtStr) : 0n;
403
- let group = groups.get(tokenId);
404
- if (!group) {
405
- group = { outputs: [], totalAmount: 0n };
406
- groups.set(tokenId, group);
407
- }
408
- group.outputs.push(out);
409
- group.totalAmount += amount;
410
- }
411
- if (groups.size === 0)
412
- return [];
413
- const services = getServices();
414
- const tokenIds = [...groups.keys()];
415
- let details = [];
416
- try {
417
- details = await services.bsv21.lookupTokens(tokenIds);
418
- } catch {}
419
- const detailMap = new Map(details.map((d) => [d.tokenId, d]));
420
- const balances = [];
421
- for (const [tokenId, group] of groups) {
422
- const detail = detailMap.get(tokenId);
423
- const iconOutpoint = resolveIconOutpoint(tokenId, detail?.token?.icon);
424
- balances.push({
425
- tokenId,
426
- symbol: detail?.token?.sym,
427
- icon: iconOutpoint ? services.ordfs.getContentUrl(iconOutpoint) : "",
428
- decimals: Number(detail?.token?.dec ?? 0),
429
- totalAmount: group.totalAmount,
430
- outputs: group.outputs,
431
- isActive: detail?.status?.is_active ?? false
432
- });
392
+ outpoint = `${txid}${icon}`;
433
393
  }
434
- return balances;
435
- }
436
- async function categorizeOutputs(outputs) {
437
- const funding = [];
438
- const rawOrdinals = [];
439
- const opnsRaw = [];
440
- const bsv21Raw = [];
441
- const bsv20Tokens = [];
442
- const locked = [];
443
- for (const out of outputs) {
444
- const events = out.events ?? [];
445
- const sats = out.satoshis ?? 0;
446
- if (events.some((e) => e.startsWith("bsv21:"))) {
447
- bsv21Raw.push(out);
448
- continue;
449
- }
450
- if (events.some((e) => e.startsWith("lock:"))) {
451
- locked.push(out);
452
- continue;
453
- }
454
- if (events.some((e) => e === "type:application/bsv-20" || e === "type:Token")) {
455
- bsv20Tokens.push(out);
456
- continue;
457
- }
458
- if (sats === 1) {
459
- if (events.some((e) => e === "type:application/op-ns")) {
460
- opnsRaw.push(out);
461
- } else {
462
- rawOrdinals.push(out);
463
- }
464
- continue;
465
- }
466
- if (sats > 1) {
467
- funding.push(out);
468
- }
469
- }
470
- const run = [];
471
- const cleanFunding = [];
472
- if (funding.length > 0) {
473
- const runTxids = await detectRunTransactions(funding);
474
- for (const f of funding) {
475
- const { txid } = parseOutpoint(f.outpoint);
476
- if (runTxids.has(txid)) {
477
- run.push(f);
478
- } else {
479
- cleanFunding.push(f);
480
- }
481
- }
482
- }
483
- return {
484
- funding: cleanFunding,
485
- ordinals: rawOrdinals.map(enrichOrdinal),
486
- opnsNames: opnsRaw.map(enrichOrdinal),
487
- bsv21Tokens: await groupBsv21Tokens(bsv21Raw),
488
- bsv20Tokens,
489
- locked,
490
- run,
491
- totalBsv: cleanFunding.reduce((sum, o) => sum + (o.satoshis ?? 0), 0)
492
- };
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
+ }));
493
401
  }
494
402
  async function scanAddress(address, onProgress) {
495
- const services = getServices();
496
- onProgress?.({ phase: "sync", detail: "Syncing address..." });
497
- for await (const event of services.owner.getTxos(address, { refresh: true, limit: 1 })) {
498
- if (event.type === "sync") {
499
- const p = event.data;
500
- onProgress?.({
501
- phase: "sync",
502
- detail: `${p.phase}: ${p.processed ?? 0}/${p.total ?? "?"}`
503
- });
504
- } else if (event.type === "done" || event.type === "error") {
505
- break;
506
- }
507
- }
508
- onProgress?.({ phase: "search", detail: "Searching for assets..." });
509
- const allOutputs = await services.txo.search(`own:${address}`, {
510
- unspent: true,
511
- events: true,
512
- sats: true,
513
- limit: 0
514
- });
515
- onProgress?.({ phase: "categorize", detail: "Loading token details..." });
516
- return await categorizeOutputs(allOutputs ?? []);
403
+ const result = await coreScanAddresses(getServices(), [address], onProgress);
404
+ return toScannedAssets(result);
517
405
  }
518
406
  async function scanAddresses(addresses, onProgress) {
519
407
  const unique = [...new Set(addresses)];
520
- const allResults = [];
521
- for (const addr of unique) {
522
- onProgress?.({ phase: "sync", detail: `Scanning ${addr.slice(0, 8)}...` });
523
- allResults.push(await scanAddress(addr, onProgress));
524
- }
408
+ const result = await coreScanAddresses(getServices(), unique, onProgress);
409
+ return toScannedAssets(result);
410
+ }
411
+ function toScannedAssets(result) {
525
412
  return {
526
- funding: allResults.flatMap((r) => r.funding),
527
- ordinals: allResults.flatMap((r) => r.ordinals),
528
- opnsNames: allResults.flatMap((r) => r.opnsNames),
529
- bsv21Tokens: allResults.flatMap((r) => r.bsv21Tokens),
530
- bsv20Tokens: allResults.flatMap((r) => r.bsv20Tokens),
531
- locked: allResults.flatMap((r) => r.locked),
532
- run: allResults.flatMap((r) => r.run),
533
- 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
534
422
  };
535
423
  }
536
- async function detectRunTransactions(funding) {
537
- const services = getServices();
538
- const txids = [...new Set(funding.map((f) => parseOutpoint(f.outpoint).txid))];
539
- const runTxids = new Set;
540
- for (const txid of txids) {
541
- try {
542
- const beef = await services.getBeefForTxid(txid);
543
- const beefTx = beef.findTxid(txid);
544
- if (!beefTx?.tx)
545
- continue;
546
- for (const output of beefTx.tx.outputs) {
547
- const script = output.lockingScript?.toBinary();
548
- if (script && hasRunPrefix(script)) {
549
- runTxids.add(txid);
550
- break;
551
- }
552
- }
553
- } catch {}
554
- }
555
- return runTxids;
556
- }
557
- function hasRunPrefix(script) {
558
- if (script.length < RUN_PREFIX.length)
559
- return false;
560
- for (let i = 0;i < RUN_PREFIX.length; i++) {
561
- if (script[i] !== RUN_PREFIX[i])
562
- return false;
563
- }
564
- return true;
565
- }
566
424
 
567
425
  // src/components/wif-input.tsx
568
426
  import { deriveIdentityKey } from "@1sat/utils";
@@ -1384,63 +1242,72 @@ function OrdinalsSection({ ordinals, selectedOrdinals, onToggle, onSelectAll, on
1384
1242
  ]
1385
1243
  });
1386
1244
  }
1387
- function TokenRow({ tb }) {
1388
- return /* @__PURE__ */ jsx8("div", {
1245
+ function TokenRow({ tb, onSweep, walletConnected }) {
1246
+ return /* @__PURE__ */ jsxs3("div", {
1389
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"}`,
1390
- children: /* @__PURE__ */ jsxs3("div", {
1391
- className: "flex items-center gap-3",
1392
- children: [
1393
- /* @__PURE__ */ jsx8("img", {
1394
- src: tb.icon,
1395
- alt: tb.symbol || "Token",
1396
- className: "w-8 h-8 rounded-full object-cover",
1397
- onError: (e) => {
1398
- e.target.style.display = "none";
1399
- }
1400
- }),
1401
- /* @__PURE__ */ jsxs3("div", {
1402
- children: [
1403
- /* @__PURE__ */ jsxs3("div", {
1404
- className: "flex items-center gap-2",
1405
- children: [
1406
- /* @__PURE__ */ jsx8("span", {
1407
- className: "font-medium text-foreground",
1408
- children: tb.symbol || tb.tokenId.slice(0, 8) + "..."
1409
- }),
1410
- tb.isActive ? /* @__PURE__ */ jsx8("span", {
1411
- className: "px-1.5 py-0.5 text-[9px] rounded bg-green-600/20 text-green-700 dark:text-green-400",
1412
- children: "active"
1413
- }) : /* @__PURE__ */ jsx8("span", {
1414
- className: "px-1.5 py-0.5 text-[9px] rounded bg-muted text-muted-foreground",
1415
- children: "inactive"
1416
- })
1417
- ]
1418
- }),
1419
- /* @__PURE__ */ jsxs3("div", {
1420
- className: "text-xs text-muted-foreground",
1421
- children: [
1422
- formatTokenAmount(tb.totalAmount.toString(), tb.decimals),
1423
- " ",
1424
- tb.symbol || "",
1425
- /* @__PURE__ */ jsxs3("span", {
1426
- className: "ml-2",
1427
- children: [
1428
- "(",
1429
- tb.outputs.length,
1430
- " output",
1431
- tb.outputs.length !== 1 ? "s" : "",
1432
- ")"
1433
- ]
1434
- })
1435
- ]
1436
- })
1437
- ]
1438
- })
1439
- ]
1440
- })
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
+ ]
1441
1308
  });
1442
1309
  }
1443
- function Bsv21Section({ tokens }) {
1310
+ function Bsv21Section({ tokens, onSweep, walletConnected }) {
1444
1311
  if (tokens.length === 0)
1445
1312
  return null;
1446
1313
  const active = tokens.filter((t) => t.isActive);
@@ -1464,7 +1331,9 @@ function Bsv21Section({ tokens }) {
1464
1331
  className: "space-y-3",
1465
1332
  children: [
1466
1333
  active.map((tb) => /* @__PURE__ */ jsx8(TokenRow, {
1467
- tb
1334
+ tb,
1335
+ onSweep,
1336
+ walletConnected
1468
1337
  }, tb.tokenId)),
1469
1338
  inactive.length > 0 && active.length > 0 && /* @__PURE__ */ jsx8("div", {
1470
1339
  className: "border-t border-purple-500/10 pt-3 mt-3",
@@ -1478,7 +1347,8 @@ function Bsv21Section({ tokens }) {
1478
1347
  })
1479
1348
  }),
1480
1349
  inactive.map((tb) => /* @__PURE__ */ jsx8(TokenRow, {
1481
- tb
1350
+ tb,
1351
+ walletConnected
1482
1352
  }, tb.tokenId))
1483
1353
  ]
1484
1354
  })
@@ -1893,7 +1763,7 @@ function buildKeys(outputs, keyMap) {
1893
1763
  });
1894
1764
  }
1895
1765
  async function executeSweep(params) {
1896
- const { wallet, keys, funding, ordinals, bsv21Tokens, amount, onProgress } = params;
1766
+ const { wallet, keys, funding, ordinals, amount, onProgress } = params;
1897
1767
  const ctx = createContext2(wallet, { services: getServices(), chain: "main" });
1898
1768
  const result = {
1899
1769
  ordinalTxids: [],
@@ -1926,42 +1796,31 @@ async function executeSweep(params) {
1926
1796
  result.errors.push(`Ordinals: ${e instanceof Error ? e.message : String(e)}`);
1927
1797
  }
1928
1798
  }
1929
- if (bsv21Tokens.length > 0) {
1930
- const groups = new Map;
1931
- for (const token of bsv21Tokens) {
1932
- const tokenEvent = token.events?.find((e) => e.startsWith("tokenId:"));
1933
- const tokenId = tokenEvent?.slice(8) ?? "unknown";
1934
- const group = groups.get(tokenId) ?? [];
1935
- group.push(token);
1936
- groups.set(tokenId, group);
1937
- }
1938
- for (const [tokenId, tokens] of groups) {
1939
- onProgress(`Sweeping ${tokens.length} tokens (${tokenId.slice(0, 8)}...)...`);
1940
- try {
1941
- const inputs = await prepareSweepInputs(ctx, tokens);
1942
- const tokenResult = await sweepBsv21.execute(ctx, {
1943
- inputs: inputs.map((inp) => ({
1944
- ...inp,
1945
- tokenId,
1946
- amount: "0"
1947
- })),
1948
- keys: buildKeys(tokens, keys)
1949
- });
1950
- if (tokenResult.error)
1951
- result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${tokenResult.error}`);
1952
- else if (tokenResult.txid)
1953
- result.bsv21Txids.push(tokenResult.txid);
1954
- } catch (e) {
1955
- result.errors.push(`BSV-21 (${tokenId.slice(0, 8)}): ${e instanceof Error ? e.message : String(e)}`);
1956
- }
1957
- }
1958
- }
1959
1799
  onProgress("Sweep complete");
1960
1800
  return result;
1961
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
+ }
1962
1821
 
1963
1822
  // src/lib/legacy-send.ts
1964
- import { parseOutpoint as parseOutpoint2 } from "@1sat/utils";
1823
+ import { parseOutpoint } from "@1sat/utils";
1965
1824
  import { MAP_PREFIX } from "@1sat/types";
1966
1825
  import { OP, P2PKH, PrivateKey as PrivateKey2, Script, Transaction, Utils } from "@bsv/sdk";
1967
1826
  async function fetchSourceTx(txid) {
@@ -2008,7 +1867,7 @@ async function legacySendBsv(params) {
2008
1867
  const p2pkh = new P2PKH;
2009
1868
  const tx = new Transaction;
2010
1869
  for (const utxo of funding) {
2011
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1870
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2012
1871
  const key = keyForOutput(utxo, keyMap, payKey);
2013
1872
  tx.addInput({
2014
1873
  sourceTXID: txid,
@@ -2056,7 +1915,7 @@ async function legacySendOrdinals(params) {
2056
1915
  const p2pkh = new P2PKH;
2057
1916
  const tx = new Transaction;
2058
1917
  for (const ord of ordinals) {
2059
- const { txid, vout } = parseOutpoint2(ord.outpoint);
1918
+ const { txid, vout } = parseOutpoint(ord.outpoint);
2060
1919
  const key = keyForOutput(ord, keyMap, payKey);
2061
1920
  tx.addInput({
2062
1921
  sourceTXID: txid,
@@ -2073,7 +1932,7 @@ async function legacySendOrdinals(params) {
2073
1932
  });
2074
1933
  }
2075
1934
  for (const utxo of funding) {
2076
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1935
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2077
1936
  const key = keyForOutput(utxo, keyMap, payKey);
2078
1937
  tx.addInput({
2079
1938
  sourceTXID: txid,
@@ -2106,7 +1965,7 @@ async function legacyBurnOrdinals(params) {
2106
1965
  const p2pkh = new P2PKH;
2107
1966
  const tx = new Transaction;
2108
1967
  for (const ord of ordinals) {
2109
- const { txid, vout } = parseOutpoint2(ord.outpoint);
1968
+ const { txid, vout } = parseOutpoint(ord.outpoint);
2110
1969
  const key = keyForOutput(ord, keyMap, payKey);
2111
1970
  tx.addInput({
2112
1971
  sourceTXID: txid,
@@ -2117,7 +1976,7 @@ async function legacyBurnOrdinals(params) {
2117
1976
  });
2118
1977
  }
2119
1978
  for (const utxo of funding) {
2120
- const { txid, vout } = parseOutpoint2(utxo.outpoint);
1979
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
2121
1980
  const key = keyForOutput(utxo, keyMap, payKey);
2122
1981
  tx.addInput({
2123
1982
  sourceTXID: txid,
@@ -2308,7 +2167,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2308
2167
  if (!wallet || !legacyKeys || !assets)
2309
2168
  return;
2310
2169
  await runOperation("Sweep BSV", async () => {
2311
- 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 });
2312
2171
  if (result.errors.length > 0)
2313
2172
  throw new Error(result.errors[0]);
2314
2173
  return result.bsvTxid ?? "";
@@ -2330,7 +2189,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2330
2189
  if (selected.length === 0)
2331
2190
  return;
2332
2191
  await runOperation(`Sweep ${selected.length} ordinal${selected.length !== 1 ? "s" : ""}`, async () => {
2333
- 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 });
2334
2193
  if (result.errors.length > 0)
2335
2194
  throw new Error(result.errors[0]);
2336
2195
  return result.ordinalTxids[0] ?? "";
@@ -2366,7 +2225,7 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2366
2225
  if (selected.length === 0)
2367
2226
  return;
2368
2227
  await runOperation(`Sweep ${selected.length} domain${selected.length !== 1 ? "s" : ""}`, async () => {
2369
- 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 });
2370
2229
  if (result.errors.length > 0)
2371
2230
  throw new Error(result.errors[0]);
2372
2231
  return result.ordinalTxids[0] ?? "";
@@ -2394,6 +2253,20 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2394
2253
  return result.txid;
2395
2254
  });
2396
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]);
2397
2270
  return /* @__PURE__ */ jsxs6("div", {
2398
2271
  className: "min-h-screen bg-background text-foreground",
2399
2272
  children: [
@@ -2492,7 +2365,9 @@ function SweepApp({ legacyKeys: initialKeys, wallet: externalWallet, sweepOnly }
2492
2365
  /* @__PURE__ */ jsx11(TabsContent, {
2493
2366
  value: "bsv21",
2494
2367
  children: /* @__PURE__ */ jsx11(Bsv21Section, {
2495
- tokens: assets.bsv21Tokens
2368
+ tokens: assets.bsv21Tokens,
2369
+ onSweep: handleSweepBsv21Token,
2370
+ walletConnected
2496
2371
  })
2497
2372
  }),
2498
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;AAwJD,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.14",
3
+ "version": "0.0.16",
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.75",
23
23
  "@1sat/client": "0.0.20",
24
24
  "@1sat/connect": "0.0.27",
25
- "@1sat/sweep-ui": "0.0.14",
25
+ "@1sat/sweep-ui": "0.0.16",
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,165 +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}`;
56
+ outpoint = `${txid}${icon}`;
71
57
  }
72
- return icon;
58
+ return getServices().ordfs.getContentUrl(outpoint);
73
59
  }
74
60
 
75
- async function groupBsv21Tokens(outputs: IndexedOutput[]): Promise<TokenBalance[]> {
76
- const groups = new Map<string, { outputs: IndexedOutput[]; totalAmount: bigint }>();
77
-
78
- for (const out of outputs) {
79
- const events = out.events ?? [];
80
- const tokenId = getEvent(events, "bsv21:");
81
- if (!tokenId) continue;
82
-
83
- const amtStr = getEvent(events, "amt:");
84
- const amount = amtStr ? BigInt(amtStr) : 0n;
85
-
86
- let group = groups.get(tokenId);
87
- if (!group) {
88
- group = { outputs: [], totalAmount: 0n };
89
- groups.set(tokenId, group);
90
- }
91
- group.outputs.push(out);
92
- group.totalAmount += amount;
93
- }
94
-
95
- if (groups.size === 0) return [];
96
-
97
- const services = getServices();
98
- const tokenIds = [...groups.keys()];
99
-
100
- let details: Array<{ tokenId: string; token?: { sym?: string; dec?: string; icon?: string }; status?: { is_active?: boolean } }> = [];
101
- try {
102
- details = await services.bsv21.lookupTokens(tokenIds);
103
- } catch {
104
- // BSV21 service may not be available
105
- }
106
-
107
- const detailMap = new Map(details.map((d) => [d.tokenId, d]));
108
-
109
- const balances: TokenBalance[] = [];
110
- for (const [tokenId, group] of groups) {
111
- const detail = detailMap.get(tokenId);
112
- const iconOutpoint = resolveIconOutpoint(tokenId, detail?.token?.icon);
113
-
114
- balances.push({
115
- tokenId,
116
- symbol: detail?.token?.sym,
117
- icon: iconOutpoint ? services.ordfs.getContentUrl(iconOutpoint) : "",
118
- decimals: Number(detail?.token?.dec ?? 0),
119
- totalAmount: group.totalAmount,
120
- outputs: group.outputs,
121
- isActive: detail?.status?.is_active ?? false,
122
- });
123
- }
124
- return balances;
125
- }
126
-
127
- async function categorizeOutputs(outputs: IndexedOutput[]): Promise<ScannedAssets> {
128
- const funding: IndexedOutput[] = [];
129
- const rawOrdinals: IndexedOutput[] = [];
130
- const opnsRaw: IndexedOutput[] = [];
131
- const bsv21Raw: IndexedOutput[] = [];
132
- const bsv20Tokens: IndexedOutput[] = [];
133
- const locked: IndexedOutput[] = [];
134
-
135
- for (const out of outputs) {
136
- const events = out.events ?? [];
137
- const sats = out.satoshis ?? 0;
138
-
139
- if (events.some((e) => e.startsWith("bsv21:"))) {
140
- bsv21Raw.push(out);
141
- continue;
142
- }
143
-
144
- if (events.some((e) => e.startsWith("lock:"))) {
145
- locked.push(out);
146
- continue;
147
- }
148
-
149
- if (events.some((e) => e === "type:application/bsv-20" || e === "type:Token")) {
150
- bsv20Tokens.push(out);
151
- continue;
152
- }
153
-
154
- if (sats === 1) {
155
- if (events.some((e) => e === "type:application/op-ns")) {
156
- opnsRaw.push(out);
157
- } else {
158
- rawOrdinals.push(out);
159
- }
160
- continue;
161
- }
162
-
163
- if (sats > 1) {
164
- funding.push(out);
165
- }
166
- }
167
-
168
- // Check funding outputs for RUN token transactions
169
- const run: IndexedOutput[] = [];
170
- const cleanFunding: IndexedOutput[] = [];
171
-
172
- if (funding.length > 0) {
173
- const runTxids = await detectRunTransactions(funding);
174
- for (const f of funding) {
175
- const { txid } = parseOutpoint(f.outpoint);
176
- if (runTxids.has(txid)) {
177
- run.push(f);
178
- } else {
179
- cleanFunding.push(f);
180
- }
181
- }
182
- }
183
-
184
- return {
185
- funding: cleanFunding,
186
- ordinals: rawOrdinals.map(enrichOrdinal),
187
- opnsNames: opnsRaw.map(enrichOrdinal),
188
- bsv21Tokens: await groupBsv21Tokens(bsv21Raw),
189
- bsv20Tokens,
190
- locked,
191
- run,
192
- totalBsv: cleanFunding.reduce((sum, o) => sum + (o.satoshis ?? 0), 0),
193
- };
61
+ function enrichTokenBalances(tokens: TokenBalance[]): TokenBalance[] {
62
+ return tokens.map((t) => ({
63
+ ...t,
64
+ icon: resolveIconUrl(t.tokenId, t.icon),
65
+ }));
194
66
  }
195
67
 
196
68
  export async function scanAddress(
197
69
  address: string,
198
70
  onProgress?: (p: ScanProgress) => void,
199
71
  ): Promise<ScannedAssets> {
200
- const services = getServices();
201
-
202
- onProgress?.({ phase: "sync", detail: "Syncing address..." });
203
- for await (const event of services.owner.getTxos(address, { refresh: true, limit: 1 })) {
204
- if (event.type === "sync") {
205
- const p = event.data;
206
- onProgress?.({
207
- phase: "sync",
208
- detail: `${p.phase}: ${p.processed ?? 0}/${p.total ?? "?"}`,
209
- });
210
- } else if (event.type === "done" || event.type === "error") {
211
- break;
212
- }
213
- }
214
-
215
- onProgress?.({ phase: "search", detail: "Searching for assets..." });
216
- const allOutputs = await services.txo.search(`own:${address}`, {
217
- unspent: true,
218
- events: true,
219
- sats: true,
220
- limit: 0,
221
- });
222
-
223
- onProgress?.({ phase: "categorize", detail: "Loading token details..." });
224
- return await categorizeOutputs(allOutputs ?? []);
72
+ const result = await coreScanAddresses(getServices(), [address], onProgress);
73
+ return toScannedAssets(result);
225
74
  }
226
75
 
227
76
  export async function scanAddresses(
@@ -229,59 +78,20 @@ export async function scanAddresses(
229
78
  onProgress?: (p: ScanProgress) => void,
230
79
  ): Promise<ScannedAssets> {
231
80
  const unique = [...new Set(addresses)];
232
- const allResults: ScannedAssets[] = [];
233
-
234
- for (const addr of unique) {
235
- onProgress?.({ phase: "sync", detail: `Scanning ${addr.slice(0, 8)}...` });
236
- allResults.push(await scanAddress(addr, onProgress));
237
- }
81
+ const result = await coreScanAddresses(getServices(), unique, onProgress);
82
+ return toScannedAssets(result);
83
+ }
238
84
 
85
+ function toScannedAssets(result: ScanResult): ScannedAssets {
239
86
  return {
240
- funding: allResults.flatMap((r) => r.funding),
241
- ordinals: allResults.flatMap((r) => r.ordinals),
242
- opnsNames: allResults.flatMap((r) => r.opnsNames),
243
- bsv21Tokens: allResults.flatMap((r) => r.bsv21Tokens),
244
- bsv20Tokens: allResults.flatMap((r) => r.bsv20Tokens),
245
- locked: allResults.flatMap((r) => r.locked),
246
- run: allResults.flatMap((r) => r.run),
247
- 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,
248
96
  };
249
97
  }
250
-
251
- /**
252
- * Check source transactions for the RUN protocol OP_RETURN pattern.
253
- * Returns the set of txids that contain a RUN OP_RETURN output.
254
- */
255
- async function detectRunTransactions(funding: IndexedOutput[]): Promise<Set<string>> {
256
- const services = getServices();
257
- const txids = [...new Set(funding.map((f) => parseOutpoint(f.outpoint).txid))];
258
- const runTxids = new Set<string>();
259
-
260
- for (const txid of txids) {
261
- try {
262
- const beef = await services.getBeefForTxid(txid);
263
- const beefTx = beef.findTxid(txid);
264
- if (!beefTx?.tx) continue;
265
-
266
- for (const output of beefTx.tx.outputs) {
267
- const script = output.lockingScript?.toBinary();
268
- if (script && hasRunPrefix(script)) {
269
- runTxids.add(txid);
270
- break;
271
- }
272
- }
273
- } catch {
274
- // If we can't fetch the tx, leave the output in funding
275
- }
276
- }
277
-
278
- return runTxids;
279
- }
280
-
281
- function hasRunPrefix(script: number[]): boolean {
282
- if (script.length < RUN_PREFIX.length) return false;
283
- for (let i = 0; i < RUN_PREFIX.length; i++) {
284
- if (script[i] !== RUN_PREFIX[i]) return false;
285
- }
286
- return true;
287
- }
@@ -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
+ }