@1sat/sweep-ui 0.0.97 → 0.0.98

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,9 +1,11 @@
1
1
  import {
2
2
  type Bsv20Balance,
3
3
  SWEEP_BATCH_SIZE,
4
+ bsv20SweepBatches,
4
5
  bsv21SweepBatches,
5
6
  createContext,
6
7
  groupBsv20Tokens,
8
+ isListedOutput,
7
9
  prepareSweepInputs,
8
10
  sweepBsv,
9
11
  sweepBsv20,
@@ -11,12 +13,79 @@ import {
11
13
  sweepOrdinals,
12
14
  } from '@1sat/actions'
13
15
  import type { IndexedOutput } from '@1sat/types'
16
+ import { formatOutpoint, parseOutpoint } from '@1sat/utils'
14
17
  import type { PrivateKey, WalletInterface } from '@bsv/sdk'
15
18
  import type { ScannedAssets, TokenBalance } from './scanner'
16
19
  import { getServices } from './services'
17
20
 
18
21
  export { SWEEP_BATCH_SIZE }
19
22
 
23
+ /** Transferable sweep classes, same order and names as `1sat sweep import`. */
24
+ export type SweepClass = 'bsv' | 'ordinals' | 'opns' | 'bsv20' | 'bsv21'
25
+
26
+ /**
27
+ * Per-class sweep selection. Empty sets sweep nothing in that class;
28
+ * omit `selection` entirely to sweep every class (previous behavior).
29
+ */
30
+ export interface SweepClassSelection {
31
+ sweepBsv: boolean
32
+ /** Ordinals-class outpoints (`ordinals`, listed OrdLocks included). */
33
+ ordinalOutpoints: Set<string>
34
+ /** OpNS-class outpoints. */
35
+ opnsOutpoints: Set<string>
36
+ /** BSV-20 ticks. */
37
+ bsv20Ticks: Set<string>
38
+ /** BSV-21 tokenIds. */
39
+ bsv21TokenIds: Set<string>
40
+ }
41
+
42
+ /** Select every scanned asset, the default when no selection is passed. */
43
+ export function selectAllSweepClasses(
44
+ assets: ScannedAssets,
45
+ ): SweepClassSelection {
46
+ return {
47
+ sweepBsv: assets.funding.length > 0,
48
+ ordinalOutpoints: new Set(assets.ordinals.map((o) => o.outpoint)),
49
+ opnsOutpoints: new Set(assets.opnsNames.map((o) => o.outpoint)),
50
+ bsv20Ticks: new Set(
51
+ groupBsv20Tokens(assets.bsv20Tokens).map((t) => t.tick),
52
+ ),
53
+ bsv21TokenIds: new Set(
54
+ assets.bsv21Tokens
55
+ .filter((t) => t.outputs.length > 0)
56
+ .map((t) => t.tokenId),
57
+ ),
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Sweep-all is disabled only when every *currently available* class is
63
+ * skipped. Comparing counts instead strands the button on stale skip ids
64
+ * after a refresh drops a class.
65
+ */
66
+ export function isSweepAllDisabled(
67
+ available: SweepClass[],
68
+ skipped: Set<SweepClass>,
69
+ ): boolean {
70
+ return available.every((id) => skipped.has(id))
71
+ }
72
+
73
+ /** Show class skip chips when there is a choice, or a skip to clear. */
74
+ export function showClassSkips(
75
+ available: SweepClass[],
76
+ skipped: Set<SweepClass>,
77
+ ): boolean {
78
+ return available.length > 1 || skipped.size > 0
79
+ }
80
+
81
+ /** Per-step receipt, emitted via `onResult` as each class/batch completes. */
82
+ export interface SweepStepResult {
83
+ sweepClass: SweepClass
84
+ label: string
85
+ txid?: string
86
+ error?: string
87
+ }
88
+
20
89
  export interface SweepResult {
21
90
  bsvTxid?: string
22
91
  ordinalTxids: string[]
@@ -25,10 +94,17 @@ export interface SweepResult {
25
94
  errors: string[]
26
95
  /** Outpoints successfully swept (ordinals/OpNS, including listed OrdLocks). */
27
96
  sweptOutpoints: string[]
97
+ /** Swept outpoints that were marketplace listings canceled into the wallet. */
98
+ cancelledListings: string[]
28
99
  }
29
100
 
30
- function getOwner(output: IndexedOutput): string | undefined {
31
- return output.events?.find((e) => e.startsWith('own:'))?.slice(4)
101
+ /**
102
+ * Canonical `txid.vout` form. Indexer, overlay, and SDK inputs mix `.` and
103
+ * `_` separators; amount maps and retry sets must compare canonically.
104
+ */
105
+ function normalizeOutpoint(outpoint: string): string {
106
+ const { txid, vout } = parseOutpoint(outpoint)
107
+ return formatOutpoint(txid, vout)
32
108
  }
33
109
 
34
110
  function buildKeys(
@@ -36,14 +112,23 @@ function buildKeys(
36
112
  keyMap: Map<string, PrivateKey>,
37
113
  inputs: Pick<IndexedOutput, 'outpoint'>[] = outputs,
38
114
  ): PrivateKey[] {
39
- const owners = new Map(
40
- outputs.map((output) => [output.outpoint, getOwner(output)]),
115
+ const byOutpoint = new Map(
116
+ outputs.map((output) => [normalizeOutpoint(output.outpoint), output]),
41
117
  )
42
118
  return inputs.map(({ outpoint }) => {
43
- const owner = owners.get(outpoint)
44
- const key = owner ? keyMap.get(owner) : undefined
45
- if (!key) throw new Error(`No key for output ${outpoint} (owner: ${owner})`)
46
- return key
119
+ const output = byOutpoint.get(normalizeOutpoint(outpoint))
120
+ const owners = new Set(
121
+ (output?.events ?? [])
122
+ .filter((e) => e.startsWith('own:'))
123
+ .map((e) => e.slice(4)),
124
+ )
125
+ // Fail closed: zero or ambiguous owner matches must not sign.
126
+ const matches = [...owners].filter((owner) => keyMap.has(owner))
127
+ if (matches.length !== 1)
128
+ throw new Error(
129
+ `No key for output ${outpoint} (owner: ${[...owners].join(',') || 'unknown'})`,
130
+ )
131
+ return keyMap.get(matches[0])!
47
132
  })
48
133
  }
49
134
 
@@ -67,6 +152,10 @@ export async function executeSweep(params: {
67
152
  ordinals: IndexedOutput[]
68
153
  amount?: number
69
154
  onProgress: (stage: string) => void
155
+ /** Outpoints to skip; successes are added (canonical `txid.vout` form). */
156
+ completed?: Set<string>
157
+ signal?: AbortSignal
158
+ onResult?: (result: SweepStepResult) => void
70
159
  }): Promise<SweepResult> {
71
160
  const { wallet, keys, funding, ordinals, amount, onProgress } = params
72
161
  const ctx = createContext(wallet, { services: getServices(), chain: 'main' })
@@ -77,61 +166,170 @@ export async function executeSweep(params: {
77
166
  bsv21Txids: [],
78
167
  errors: [],
79
168
  sweptOutpoints: [],
169
+ cancelledListings: [],
80
170
  }
81
171
 
82
- if (funding.length > 0) {
83
- onProgress(`Sweeping ${funding.length} BSV UTXOs...`)
172
+ const fundingStep = await sweepFundingStep({
173
+ ctx,
174
+ keys,
175
+ funding,
176
+ amount,
177
+ completed: params.completed,
178
+ signal: params.signal,
179
+ onProgress,
180
+ onResult: params.onResult,
181
+ })
182
+ if (fundingStep.bsvTxid) result.bsvTxid = fundingStep.bsvTxid
183
+ if (fundingStep.error) result.errors.push(fundingStep.error)
184
+
185
+ const ordinalBatches = await sweepOrdinalBatches({
186
+ ctx,
187
+ keys,
188
+ ordinals,
189
+ sweepClass: 'ordinals',
190
+ completed: params.completed,
191
+ signal: params.signal,
192
+ onProgress,
193
+ onResult: params.onResult,
194
+ })
195
+ result.ordinalTxids.push(...ordinalBatches.txids)
196
+ result.sweptOutpoints.push(...ordinalBatches.swept)
197
+ result.errors.push(...ordinalBatches.errors)
198
+ result.cancelledListings.push(
199
+ ...cancelledIn(funding, fundingStep.swept),
200
+ ...cancelledIn(ordinals, ordinalBatches.swept),
201
+ )
202
+
203
+ onProgress(
204
+ result.errors.length > 0 ? 'Sweep stopped with errors' : 'Sweep complete',
205
+ )
206
+ return result
207
+ }
208
+
209
+ function isCompleted(completed: Set<string> | undefined, outpoint: string) {
210
+ if (!completed) return false
211
+ return completed.has(normalizeOutpoint(outpoint))
212
+ }
213
+
214
+ /** Swept outpoints that were listings: every one canceled into the wallet. */
215
+ function cancelledIn(outputs: IndexedOutput[], swept: string[]): string[] {
216
+ const done = new Set(swept.map(normalizeOutpoint))
217
+ return outputs
218
+ .filter((o) => done.has(normalizeOutpoint(o.outpoint)) && isListedOutput(o))
219
+ .map((o) => o.outpoint)
220
+ }
221
+
222
+ function markCompleted(
223
+ completed: Set<string> | undefined,
224
+ outputs: Pick<IndexedOutput, 'outpoint'>[],
225
+ ) {
226
+ if (!completed) return
227
+ for (const output of outputs)
228
+ completed.add(normalizeOutpoint(output.outpoint))
229
+ }
230
+
231
+ async function sweepFundingStep(options: {
232
+ ctx: ReturnType<typeof createContext>
233
+ keys: Map<string, PrivateKey>
234
+ funding: IndexedOutput[]
235
+ amount?: number
236
+ completed?: Set<string>
237
+ signal?: AbortSignal
238
+ onProgress: (stage: string) => void
239
+ onResult?: (result: SweepStepResult) => void
240
+ }): Promise<{ bsvTxid?: string; error?: string; swept: string[] }> {
241
+ const { ctx, keys, amount, completed, signal, onProgress, onResult } = options
242
+ const pending = options.funding.filter(
243
+ (o) => !isCompleted(completed, o.outpoint),
244
+ )
245
+ if (pending.length === 0) return { swept: [] }
246
+ signal?.throwIfAborted()
247
+ onProgress(`Sweeping ${pending.length} BSV UTXOs...`)
248
+ try {
249
+ const inputs = await prepareSweepInputs(ctx, pending)
250
+ const bsvResult = await sweepBsv.execute(ctx, {
251
+ inputs,
252
+ keys: buildKeys(pending, keys, inputs),
253
+ amount,
254
+ })
255
+ if (bsvResult.error) throw new Error(bsvResult.error)
256
+ const txid = bsvResult.txid?.trim()
257
+ if (!txid) throw new Error('Sweep returned no transaction ID')
258
+ markCompleted(completed, pending)
259
+ onResult?.({ sweepClass: 'bsv', label: 'BSV', txid })
260
+ return { bsvTxid: txid, swept: pending.map((o) => o.outpoint) }
261
+ } catch (e) {
262
+ if (signal?.aborted) throw e
263
+ const error = e instanceof Error ? e.message : String(e)
264
+ onResult?.({ sweepClass: 'bsv', label: 'BSV', error })
265
+ return { error: `BSV: ${error}`, swept: [] }
266
+ }
267
+ }
268
+
269
+ const ORDINAL_CLASS_LABEL: Record<'ordinals' | 'opns', string> = {
270
+ ordinals: 'Ordinals',
271
+ opns: 'OpNS',
272
+ }
273
+
274
+ async function sweepOrdinalBatches(options: {
275
+ ctx: ReturnType<typeof createContext>
276
+ keys: Map<string, PrivateKey>
277
+ ordinals: IndexedOutput[]
278
+ sweepClass: 'ordinals' | 'opns'
279
+ completed?: Set<string>
280
+ signal?: AbortSignal
281
+ onProgress: (stage: string) => void
282
+ onResult?: (result: SweepStepResult) => void
283
+ }): Promise<{ txids: string[]; swept: string[]; errors: string[] }> {
284
+ const { ctx, keys, sweepClass, completed, signal, onProgress, onResult } =
285
+ options
286
+ const label = ORDINAL_CLASS_LABEL[sweepClass]
287
+ const singular = sweepClass === 'opns' ? 'OpNS name' : 'ordinal'
288
+ const txids: string[] = []
289
+ const swept: string[] = []
290
+ const errors: string[] = []
291
+ const pending = options.ordinals.filter(
292
+ (o) => !isCompleted(completed, o.outpoint),
293
+ )
294
+ if (pending.length === 0) return { txids, swept, errors }
295
+
296
+ const batches = chunk(pending, SWEEP_BATCH_SIZE)
297
+ for (let b = 0; b < batches.length; b++) {
298
+ signal?.throwIfAborted()
299
+ const batch = batches[b]
300
+ const from = b * SWEEP_BATCH_SIZE + 1
301
+ const to = b * SWEEP_BATCH_SIZE + batch.length
302
+ const batchLabel =
303
+ batches.length === 1
304
+ ? label
305
+ : `${label} (${from}–${to} of ${pending.length})`
306
+ onProgress(
307
+ batches.length === 1
308
+ ? `Sweeping ${batch.length} ${singular}${batch.length !== 1 ? 's' : ''}...`
309
+ : `Sweeping ${singular}s ${from}–${to} of ${pending.length} (batch ${b + 1}/${batches.length})...`,
310
+ )
84
311
  try {
85
- const inputs = await prepareSweepInputs(ctx, funding)
86
- const bsvResult = await sweepBsv.execute(ctx, {
312
+ const inputs = await prepareSweepInputs(ctx, batch)
313
+ const ordResult = await sweepOrdinals.execute(ctx, {
87
314
  inputs,
88
- keys: buildKeys(funding, keys, inputs),
89
- amount,
315
+ keys: buildKeys(batch, keys, inputs),
90
316
  })
91
- if (bsvResult.error) throw new Error(bsvResult.error)
92
- const txid = bsvResult.txid?.trim()
317
+ if (ordResult.error) throw new Error(ordResult.error)
318
+ const txid = ordResult.txid?.trim()
93
319
  if (!txid) throw new Error('Sweep returned no transaction ID')
94
- result.bsvTxid = txid
320
+ txids.push(txid)
321
+ swept.push(...batch.map((o) => o.outpoint))
322
+ markCompleted(completed, batch)
323
+ onResult?.({ sweepClass, label: batchLabel, txid })
95
324
  } catch (e) {
96
- result.errors.push(`BSV: ${e instanceof Error ? e.message : String(e)}`)
97
- }
98
- }
99
-
100
- if (ordinals.length > 0) {
101
- const batches = chunk(ordinals, SWEEP_BATCH_SIZE)
102
- for (let b = 0; b < batches.length; b++) {
103
- const batch = batches[b]
104
- const from = b * SWEEP_BATCH_SIZE + 1
105
- const to = b * SWEEP_BATCH_SIZE + batch.length
106
- onProgress(
107
- batches.length === 1
108
- ? `Sweeping ${batch.length} ordinal${batch.length !== 1 ? 's' : ''}...`
109
- : `Sweeping ordinals ${from}–${to} of ${ordinals.length} (batch ${b + 1}/${batches.length})...`,
110
- )
111
- try {
112
- const inputs = await prepareSweepInputs(ctx, batch)
113
- const ordResult = await sweepOrdinals.execute(ctx, {
114
- inputs,
115
- keys: buildKeys(batch, keys, inputs),
116
- })
117
- if (ordResult.error) throw new Error(ordResult.error)
118
- const txid = ordResult.txid?.trim()
119
- if (!txid) throw new Error('Sweep returned no transaction ID')
120
- result.ordinalTxids.push(txid)
121
- result.sweptOutpoints.push(...batch.map((o) => o.outpoint))
122
- } catch (e) {
123
- result.errors.push(
124
- `Ordinals batch ${b + 1}/${batches.length}: ${e instanceof Error ? e.message : String(e)}`,
125
- )
126
- break
127
- }
325
+ if (signal?.aborted) throw e
326
+ const error = e instanceof Error ? e.message : String(e)
327
+ errors.push(`${label} batch ${b + 1}/${batches.length}: ${error}`)
328
+ onResult?.({ sweepClass, label: batchLabel, error })
329
+ break
128
330
  }
129
331
  }
130
-
131
- onProgress(
132
- result.errors.length > 0 ? 'Sweep stopped with errors' : 'Sweep complete',
133
- )
134
- return result
332
+ return { txids, swept, errors }
135
333
  }
136
334
 
137
335
  /**
@@ -143,14 +341,36 @@ export async function sweepBsv21Token(params: {
143
341
  keys: Map<string, PrivateKey>
144
342
  token: TokenBalance
145
343
  onProgress: (stage: string) => void
146
- }): Promise<{ txid?: string; txids: string[]; error?: string }> {
147
- const { wallet, keys, token, onProgress } = params
344
+ signal?: AbortSignal
345
+ /** Outpoints to skip; successes are added (canonical `txid.vout` form). */
346
+ completed?: Set<string>
347
+ onResult?: (result: SweepStepResult) => void
348
+ }): Promise<{
349
+ txid?: string
350
+ txids: string[]
351
+ sweptOutpoints: string[]
352
+ cancelledListings: string[]
353
+ error?: string
354
+ }> {
355
+ const { wallet, keys, token, onProgress, signal, completed, onResult } =
356
+ params
148
357
  const ctx = createContext(wallet, { services: getServices(), chain: 'main' })
149
358
  const txids: string[] = []
359
+ const sweptOutpoints: string[] = []
150
360
  const errors: string[] = []
151
361
  const name = token.symbol ?? token.tokenId.slice(0, 8)
362
+ const amounts = new Map(
363
+ [...token.amounts].map(([outpoint, amount]) => [
364
+ normalizeOutpoint(outpoint),
365
+ amount,
366
+ ]),
367
+ )
368
+ const outputs = token.outputs.filter(
369
+ (o) => !isCompleted(completed, o.outpoint),
370
+ )
152
371
 
153
- for (const batch of bsv21SweepBatches(token.outputs)) {
372
+ for (const batch of bsv21SweepBatches(outputs)) {
373
+ signal?.throwIfAborted()
154
374
  onProgress(`Sweeping ${name}...`)
155
375
  try {
156
376
  const sweepInputs = await prepareSweepInputs(ctx, batch)
@@ -161,7 +381,7 @@ export async function sweepBsv21Token(params: {
161
381
  return {
162
382
  ...base,
163
383
  tokenId: token.tokenId,
164
- amount: token.amounts.get(out.outpoint) ?? '0',
384
+ amount: amounts.get(normalizeOutpoint(out.outpoint)) ?? '0',
165
385
  }
166
386
  })
167
387
  const result = await sweepBsv21.execute(ctx, {
@@ -169,8 +389,14 @@ export async function sweepBsv21Token(params: {
169
389
  keys: buildKeys(batch, keys),
170
390
  })
171
391
  if (result.error) errors.push(result.error)
172
- else if (result.txid) txids.push(result.txid)
392
+ else if (result.txid) {
393
+ txids.push(result.txid)
394
+ sweptOutpoints.push(...batch.map((o) => o.outpoint))
395
+ markCompleted(completed, batch)
396
+ onResult?.({ sweepClass: 'bsv21', label: name, txid: result.txid })
397
+ }
173
398
  } catch (e) {
399
+ if (signal?.aborted) throw e
174
400
  errors.push(e instanceof Error ? e.message : String(e))
175
401
  }
176
402
  }
@@ -178,114 +404,238 @@ export async function sweepBsv21Token(params: {
178
404
  return {
179
405
  txid: txids.at(-1),
180
406
  txids,
407
+ sweptOutpoints,
408
+ cancelledListings: cancelledIn(outputs, sweptOutpoints),
181
409
  error: errors[0],
182
410
  }
183
411
  }
184
412
 
185
- /** Sweep one BSV-20 ticker into the connected wallet. */
413
+ /**
414
+ * Sweep one BSV-20 ticker into the connected wallet. BSV-20 needs no per-tx
415
+ * overlay funding, so splitting listed outputs 1-per-tx is cheap — pass
416
+ * `splitListed` so one invalid listing cannot sink the rest of the tick.
417
+ */
186
418
  export async function sweepBsv20Token(params: {
187
419
  wallet: WalletInterface
188
420
  keys: Map<string, PrivateKey>
189
421
  token: Bsv20Balance
190
422
  onProgress: (stage: string) => void
191
- }): Promise<{ txid?: string; error?: string }> {
192
- const { wallet, keys, token, onProgress } = params
423
+ signal?: AbortSignal
424
+ splitListed?: boolean
425
+ /** Outpoints to skip; successes are added (canonical `txid.vout` form). */
426
+ completed?: Set<string>
427
+ onResult?: (result: SweepStepResult) => void
428
+ }): Promise<{
429
+ txid?: string
430
+ txids: string[]
431
+ sweptOutpoints: string[]
432
+ cancelledListings: string[]
433
+ error?: string
434
+ }> {
435
+ const {
436
+ wallet,
437
+ keys,
438
+ token,
439
+ onProgress,
440
+ signal,
441
+ splitListed,
442
+ completed,
443
+ onResult,
444
+ } = params
193
445
  const ctx = createContext(wallet, { services: getServices(), chain: 'main' })
446
+ const txids: string[] = []
447
+ const sweptOutpoints: string[] = []
448
+ const errors: string[] = []
449
+ const amounts = new Map(
450
+ [...token.amounts].map(([outpoint, amount]) => [
451
+ normalizeOutpoint(outpoint),
452
+ amount,
453
+ ]),
454
+ )
455
+ const outputs = token.outputs.filter(
456
+ (o) => !isCompleted(completed, o.outpoint),
457
+ )
458
+ if (outputs.length === 0)
459
+ return { txids: [], sweptOutpoints: [], cancelledListings: [] }
460
+ const batches = splitListed ? bsv20SweepBatches(outputs) : [outputs]
194
461
 
195
- onProgress(`Sweeping ${token.tick}...`)
462
+ for (const batch of batches) {
463
+ signal?.throwIfAborted()
464
+ onProgress(`Sweeping ${token.tick}...`)
465
+ try {
466
+ const sweepInputs = await prepareSweepInputs(ctx, batch)
467
+ const sweepInputMap = new Map(sweepInputs.map((s) => [s.outpoint, s]))
196
468
 
197
- try {
198
- const sweepInputs = await prepareSweepInputs(ctx, token.outputs)
199
- const sweepInputMap = new Map(sweepInputs.map((s) => [s.outpoint, s]))
200
-
201
- const inputs = token.outputs.map((out) => {
202
- const base = sweepInputMap.get(out.outpoint)
203
- if (!base) throw new Error(`Missing sweep input for ${out.outpoint}`)
204
- return {
205
- ...base,
206
- tick: token.tick,
207
- amount: token.amounts.get(out.outpoint) ?? '0',
469
+ const inputs = batch.map((out) => {
470
+ const base = sweepInputMap.get(out.outpoint)
471
+ if (!base) throw new Error(`Missing sweep input for ${out.outpoint}`)
472
+ return {
473
+ ...base,
474
+ tick: token.tick,
475
+ amount: amounts.get(normalizeOutpoint(out.outpoint)) ?? '0',
476
+ }
477
+ })
478
+
479
+ const result = await sweepBsv20.execute(ctx, {
480
+ inputs,
481
+ keys: buildKeys(batch, keys),
482
+ })
483
+ if (result.error) errors.push(result.error)
484
+ else if (result.txid) {
485
+ txids.push(result.txid)
486
+ sweptOutpoints.push(...batch.map((o) => o.outpoint))
487
+ markCompleted(completed, batch)
488
+ onResult?.({
489
+ sweepClass: 'bsv20',
490
+ label: token.tick,
491
+ txid: result.txid,
492
+ })
208
493
  }
209
- })
494
+ } catch (e) {
495
+ if (signal?.aborted) throw e
496
+ errors.push(e instanceof Error ? e.message : String(e))
497
+ }
498
+ }
210
499
 
211
- const result = await sweepBsv20.execute(ctx, {
212
- inputs,
213
- keys: buildKeys(token.outputs, keys),
214
- })
215
- if (result.error) return { error: result.error }
216
- return { txid: result.txid }
217
- } catch (e) {
218
- return { error: e instanceof Error ? e.message : String(e) }
500
+ return {
501
+ txid: txids.at(-1),
502
+ txids,
503
+ sweptOutpoints,
504
+ cancelledListings: cancelledIn(outputs, sweptOutpoints),
505
+ error: errors[0],
219
506
  }
220
507
  }
221
508
 
222
- /** Same class order as `1sat sweep import`. Continues later classes if one fails. */
509
+ /**
510
+ * Same class order as `1sat sweep import`: BSV, ordinals, OpNS, BSV-20,
511
+ * BSV-21. Continues later classes if one fails. Pass `selection` to sweep
512
+ * only chosen classes/items (like `1sat sweep --only/--skip`); pass
513
+ * `completed` to skip already-swept outpoints and record new successes for
514
+ * retry; pass `signal` to abort between steps. Listed OrdLocks stay in their
515
+ * class and cancel in that spend.
516
+ */
223
517
  export async function sweepAllClasses(params: {
224
518
  wallet: WalletInterface
225
519
  keys: Map<string, PrivateKey>
226
520
  assets: ScannedAssets
227
521
  amount?: number
228
522
  onProgress: (stage: string) => void
523
+ selection?: SweepClassSelection
524
+ completed?: Set<string>
525
+ signal?: AbortSignal
526
+ onResult?: (result: SweepStepResult) => void
527
+ /** Split listed BSV-20 outputs 1-per-tx so one bad listing cannot sink the tick. */
528
+ splitListedBsv20?: boolean
229
529
  }): Promise<SweepResult> {
230
530
  const { wallet, keys, assets, amount, onProgress } = params
531
+ const selection = params.selection ?? selectAllSweepClasses(assets)
532
+ const completed = params.completed
533
+ const signal = params.signal
534
+ const onResult = params.onResult
535
+ const ctx = createContext(wallet, { services: getServices(), chain: 'main' })
231
536
  const result: SweepResult = {
232
537
  ordinalTxids: [],
233
538
  bsv20Txids: [],
234
539
  bsv21Txids: [],
235
540
  errors: [],
236
541
  sweptOutpoints: [],
542
+ cancelledListings: [],
237
543
  }
238
544
 
239
- const first = await executeSweep({
240
- wallet,
241
- keys,
242
- funding: assets.funding,
243
- ordinals: assets.ordinals,
244
- amount,
245
- onProgress,
246
- })
247
- result.bsvTxid = first.bsvTxid
248
- result.ordinalTxids.push(...first.ordinalTxids)
249
- result.sweptOutpoints.push(...first.sweptOutpoints)
250
- result.errors.push(...first.errors)
545
+ signal?.throwIfAborted()
546
+ if (selection.sweepBsv) {
547
+ const fundingStep = await sweepFundingStep({
548
+ ctx,
549
+ keys,
550
+ funding: assets.funding,
551
+ amount,
552
+ completed,
553
+ signal,
554
+ onProgress,
555
+ onResult,
556
+ })
557
+ if (fundingStep.bsvTxid) result.bsvTxid = fundingStep.bsvTxid
558
+ if (fundingStep.error) result.errors.push(fundingStep.error)
559
+ result.cancelledListings.push(
560
+ ...cancelledIn(assets.funding, fundingStep.swept),
561
+ )
562
+ }
251
563
 
252
- if (assets.opnsNames.length > 0) {
253
- const opns = await executeSweep({
254
- wallet,
564
+ const inSelection = (outpoints: Set<string>) => (o: IndexedOutput) =>
565
+ [...outpoints].some(
566
+ (selected) =>
567
+ normalizeOutpoint(selected) === normalizeOutpoint(o.outpoint),
568
+ )
569
+
570
+ for (const [sweepClass, outputs] of [
571
+ [
572
+ 'ordinals',
573
+ assets.ordinals.filter(inSelection(selection.ordinalOutpoints)),
574
+ ],
575
+ ['opns', assets.opnsNames.filter(inSelection(selection.opnsOutpoints))],
576
+ ] as const) {
577
+ signal?.throwIfAborted()
578
+ if (outputs.length === 0) continue
579
+ const batches = await sweepOrdinalBatches({
580
+ ctx,
255
581
  keys,
256
- funding: [],
257
- ordinals: assets.opnsNames,
582
+ ordinals: outputs,
583
+ sweepClass,
584
+ completed,
585
+ signal,
258
586
  onProgress,
587
+ onResult,
259
588
  })
260
- result.ordinalTxids.push(...opns.ordinalTxids)
261
- result.sweptOutpoints.push(...opns.sweptOutpoints)
262
- result.errors.push(...opns.errors)
589
+ result.ordinalTxids.push(...batches.txids)
590
+ result.sweptOutpoints.push(...batches.swept)
591
+ result.errors.push(...batches.errors)
592
+ result.cancelledListings.push(...cancelledIn(outputs, batches.swept))
263
593
  }
264
594
 
265
595
  for (const token of groupBsv20Tokens(assets.bsv20Tokens)) {
596
+ signal?.throwIfAborted()
597
+ if (!selection.bsv20Ticks.has(token.tick)) continue
266
598
  const swept = await sweepBsv20Token({
267
599
  wallet,
268
600
  keys,
269
601
  token,
270
602
  onProgress,
603
+ signal,
604
+ splitListed: params.splitListedBsv20,
605
+ completed,
606
+ onResult,
271
607
  })
272
- if (swept.error) result.errors.push(`BSV-20 ${token.tick}: ${swept.error}`)
273
- else if (swept.txid) result.bsv20Txids.push(swept.txid)
608
+ result.bsv20Txids.push(...swept.txids)
609
+ result.cancelledListings.push(...swept.cancelledListings)
610
+ if (swept.error) {
611
+ result.errors.push(`BSV-20 ${token.tick}: ${swept.error}`)
612
+ onResult?.({ sweepClass: 'bsv20', label: token.tick, error: swept.error })
613
+ }
274
614
  }
275
615
 
276
616
  for (const token of assets.bsv21Tokens) {
277
- if (token.outputs.length === 0) continue
617
+ signal?.throwIfAborted()
618
+ if (!selection.bsv21TokenIds.has(token.tokenId)) continue
278
619
  const swept = await sweepBsv21Token({
279
620
  wallet,
280
621
  keys,
281
622
  token,
282
623
  onProgress,
624
+ signal,
625
+ completed,
626
+ onResult,
283
627
  })
284
628
  result.bsv21Txids.push(...swept.txids)
629
+ result.cancelledListings.push(...swept.cancelledListings)
285
630
  if (swept.error) {
286
631
  result.errors.push(
287
632
  `BSV-21 ${token.symbol ?? token.tokenId.slice(0, 8)}: ${swept.error}`,
288
633
  )
634
+ onResult?.({
635
+ sweepClass: 'bsv21',
636
+ label: token.symbol ?? token.tokenId,
637
+ error: swept.error,
638
+ })
289
639
  }
290
640
  }
291
641