@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.
- package/dist/components/SweepApp.d.ts.map +1 -1
- package/dist/components/tx-history.d.ts +3 -1
- package/dist/components/tx-history.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -123
- package/dist/lib/sweeper.d.ts +73 -2
- package/dist/lib/sweeper.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/components/SweepApp.tsx +150 -7
- package/src/components/tx-history.tsx +15 -1
- package/src/index.ts +8 -0
- package/src/lib/sweeper.ts +457 -107
package/src/lib/sweeper.ts
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
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
|
|
40
|
-
outputs.map((output) => [output.outpoint,
|
|
115
|
+
const byOutpoint = new Map(
|
|
116
|
+
outputs.map((output) => [normalizeOutpoint(output.outpoint), output]),
|
|
41
117
|
)
|
|
42
118
|
return inputs.map(({ outpoint }) => {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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,
|
|
86
|
-
const
|
|
312
|
+
const inputs = await prepareSweepInputs(ctx, batch)
|
|
313
|
+
const ordResult = await sweepOrdinals.execute(ctx, {
|
|
87
314
|
inputs,
|
|
88
|
-
keys: buildKeys(
|
|
89
|
-
amount,
|
|
315
|
+
keys: buildKeys(batch, keys, inputs),
|
|
90
316
|
})
|
|
91
|
-
if (
|
|
92
|
-
const txid =
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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(
|
|
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:
|
|
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)
|
|
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
|
-
/**
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
582
|
+
ordinals: outputs,
|
|
583
|
+
sweepClass,
|
|
584
|
+
completed,
|
|
585
|
+
signal,
|
|
258
586
|
onProgress,
|
|
587
|
+
onResult,
|
|
259
588
|
})
|
|
260
|
-
result.ordinalTxids.push(...
|
|
261
|
-
result.sweptOutpoints.push(...
|
|
262
|
-
result.errors.push(...
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
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
|
|