@1sat/sweep-ui 0.0.75 → 0.0.77
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/asset-preview.d.ts +3 -2
- package/dist/components/asset-preview.d.ts.map +1 -1
- package/dist/components/opns-section.d.ts +3 -2
- package/dist/components/opns-section.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -93
- package/dist/lib/sweeper.d.ts +5 -0
- package/dist/lib/sweeper.d.ts.map +1 -1
- package/package.json +52 -52
- package/src/components/SweepApp.tsx +101 -40
- package/src/components/asset-preview.tsx +82 -59
- package/src/components/opns-section.tsx +105 -50
- package/src/index.ts +5 -1
- package/src/lib/sweeper.ts +47 -13
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { EnrichedOrdinal } from '../lib/scanner'
|
|
3
3
|
import { getServices } from '../lib/services'
|
|
4
|
+
import { SWEEP_BATCH_SIZE } from '../lib/sweeper'
|
|
4
5
|
import { Button } from './ui/button'
|
|
5
6
|
import { Input } from './ui/input'
|
|
6
7
|
|
|
@@ -8,9 +9,10 @@ export function OpnsSection({
|
|
|
8
9
|
opnsNames,
|
|
9
10
|
selectedOpns,
|
|
10
11
|
onToggle,
|
|
11
|
-
|
|
12
|
+
onSelectPage,
|
|
12
13
|
onDeselectAll,
|
|
13
14
|
onSweep,
|
|
15
|
+
onSweepAll,
|
|
14
16
|
onSend,
|
|
15
17
|
onBurn,
|
|
16
18
|
walletConnected,
|
|
@@ -18,13 +20,15 @@ export function OpnsSection({
|
|
|
18
20
|
opnsNames: EnrichedOrdinal[]
|
|
19
21
|
selectedOpns: Set<string>
|
|
20
22
|
onToggle: (outpoint: string) => void
|
|
21
|
-
|
|
23
|
+
onSelectPage: (outpoints: string[]) => void
|
|
22
24
|
onDeselectAll: () => void
|
|
23
25
|
onSweep: () => void
|
|
26
|
+
onSweepAll: () => void
|
|
24
27
|
onSend?: (destination: string) => void
|
|
25
28
|
onBurn?: () => void
|
|
26
29
|
walletConnected: boolean
|
|
27
30
|
}) {
|
|
31
|
+
const [page, setPage] = useState(0)
|
|
28
32
|
const [address, setAddress] = useState('')
|
|
29
33
|
const [resolvedNames, setResolvedNames] = useState<Map<string, string>>(
|
|
30
34
|
new Map(),
|
|
@@ -88,6 +92,14 @@ export function OpnsSection({
|
|
|
88
92
|
|
|
89
93
|
if (opnsNames.length === 0) return null
|
|
90
94
|
|
|
95
|
+
const totalPages = Math.ceil(opnsNames.length / SWEEP_BATCH_SIZE)
|
|
96
|
+
const safePage = Math.min(page, totalPages - 1)
|
|
97
|
+
const start = safePage * SWEEP_BATCH_SIZE
|
|
98
|
+
const pageItems = opnsNames.slice(start, start + SWEEP_BATCH_SIZE)
|
|
99
|
+
const pageOutpoints = pageItems.map((o) => o.outpoint)
|
|
100
|
+
const selectedCount = selectedOpns.size
|
|
101
|
+
const batchCount = Math.ceil(opnsNames.length / SWEEP_BATCH_SIZE)
|
|
102
|
+
|
|
91
103
|
return (
|
|
92
104
|
<div className="border border-orange-500/20 bg-orange-500/5 p-4 rounded-lg">
|
|
93
105
|
<div className="flex items-start justify-between mb-3">
|
|
@@ -100,11 +112,14 @@ export function OpnsSection({
|
|
|
100
112
|
</div>
|
|
101
113
|
<div className="text-xs text-muted-foreground">
|
|
102
114
|
{opnsNames.length} domain{opnsNames.length !== 1 ? 's' : ''}
|
|
103
|
-
{
|
|
115
|
+
{selectedCount > 0 && (
|
|
104
116
|
<span className="text-orange-400 ml-1">
|
|
105
|
-
({
|
|
117
|
+
({selectedCount} selected)
|
|
106
118
|
</span>
|
|
107
119
|
)}
|
|
120
|
+
<span className="ml-1 text-muted-foreground/80">
|
|
121
|
+
· {SWEEP_BATCH_SIZE}/page
|
|
122
|
+
</span>
|
|
108
123
|
</div>
|
|
109
124
|
</div>
|
|
110
125
|
<div className="flex gap-2">
|
|
@@ -112,23 +127,23 @@ export function OpnsSection({
|
|
|
112
127
|
variant="outline"
|
|
113
128
|
size="sm"
|
|
114
129
|
className="h-7 text-[11px]"
|
|
115
|
-
onClick={
|
|
130
|
+
onClick={() => onSelectPage(pageOutpoints)}
|
|
116
131
|
>
|
|
117
|
-
Select
|
|
132
|
+
Select page
|
|
118
133
|
</Button>
|
|
119
134
|
<Button
|
|
120
135
|
variant="outline"
|
|
121
136
|
size="sm"
|
|
122
137
|
className="h-7 text-[11px]"
|
|
123
138
|
onClick={onDeselectAll}
|
|
124
|
-
disabled={
|
|
139
|
+
disabled={selectedCount === 0}
|
|
125
140
|
>
|
|
126
141
|
Deselect
|
|
127
142
|
</Button>
|
|
128
143
|
</div>
|
|
129
144
|
</div>
|
|
130
145
|
<div className="space-y-1">
|
|
131
|
-
{
|
|
146
|
+
{pageItems.map((item) => {
|
|
132
147
|
const isSelected = selectedOpns.has(item.outpoint)
|
|
133
148
|
const displayName =
|
|
134
149
|
resolvedNames.get(item.outpoint) ??
|
|
@@ -162,60 +177,100 @@ export function OpnsSection({
|
|
|
162
177
|
)
|
|
163
178
|
})}
|
|
164
179
|
</div>
|
|
165
|
-
{
|
|
166
|
-
<div className="
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
{totalPages > 1 && (
|
|
181
|
+
<div className="flex items-center justify-center gap-4 mt-2">
|
|
182
|
+
<Button
|
|
183
|
+
variant="ghost"
|
|
184
|
+
size="sm"
|
|
185
|
+
className="text-xs"
|
|
186
|
+
onClick={() => setPage(safePage - 1)}
|
|
187
|
+
disabled={safePage === 0}
|
|
188
|
+
>
|
|
189
|
+
Prev
|
|
190
|
+
</Button>
|
|
191
|
+
<span className="text-xs text-muted-foreground">
|
|
192
|
+
Page {safePage + 1} of {totalPages}
|
|
193
|
+
</span>
|
|
194
|
+
<Button
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="sm"
|
|
197
|
+
className="text-xs"
|
|
198
|
+
onClick={() => setPage(safePage + 1)}
|
|
199
|
+
disabled={safePage >= totalPages - 1}
|
|
200
|
+
>
|
|
201
|
+
Next
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
<div className="mt-3 space-y-2">
|
|
206
|
+
{selectedCount > 0 && onSend && (
|
|
207
|
+
<Input
|
|
208
|
+
type="text"
|
|
209
|
+
placeholder="Destination address..."
|
|
210
|
+
value={address}
|
|
211
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
212
|
+
className="font-mono text-xs"
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
<div className="flex flex-wrap gap-2">
|
|
216
|
+
{selectedCount > 0 && onSend && (
|
|
217
|
+
<Button
|
|
218
|
+
variant="outline"
|
|
219
|
+
size="sm"
|
|
220
|
+
className="flex-1 min-w-[6rem]"
|
|
221
|
+
disabled={!address.trim()}
|
|
222
|
+
onClick={() => onSend(address.trim())}
|
|
223
|
+
>
|
|
224
|
+
Send {selectedCount}
|
|
225
|
+
</Button>
|
|
175
226
|
)}
|
|
176
|
-
|
|
177
|
-
{onSend && (
|
|
178
|
-
<Button
|
|
179
|
-
variant="outline"
|
|
180
|
-
size="sm"
|
|
181
|
-
className="flex-1"
|
|
182
|
-
disabled={!address.trim()}
|
|
183
|
-
onClick={() => onSend(address.trim())}
|
|
184
|
-
>
|
|
185
|
-
Send {selectedOpns.size} Domain
|
|
186
|
-
{selectedOpns.size !== 1 ? 's' : ''}
|
|
187
|
-
</Button>
|
|
188
|
-
)}
|
|
227
|
+
{selectedCount > 0 && (
|
|
189
228
|
<Button
|
|
190
229
|
size="sm"
|
|
191
|
-
className="flex-1"
|
|
230
|
+
className="flex-1 min-w-[6rem]"
|
|
192
231
|
onClick={onSweep}
|
|
193
232
|
disabled={!walletConnected}
|
|
194
233
|
title={
|
|
195
234
|
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
|
|
196
235
|
}
|
|
197
236
|
>
|
|
198
|
-
Sweep
|
|
237
|
+
Sweep {selectedCount}
|
|
238
|
+
{selectedCount > SWEEP_BATCH_SIZE
|
|
239
|
+
? ` (${Math.ceil(selectedCount / SWEEP_BATCH_SIZE)} batches)`
|
|
240
|
+
: ''}
|
|
199
241
|
</Button>
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
)}
|
|
243
|
+
<Button
|
|
244
|
+
variant={selectedCount > 0 ? 'outline' : 'default'}
|
|
245
|
+
size="sm"
|
|
246
|
+
className="flex-1 min-w-[6rem]"
|
|
247
|
+
onClick={onSweepAll}
|
|
248
|
+
disabled={!walletConnected}
|
|
249
|
+
title={
|
|
250
|
+
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
|
|
251
|
+
}
|
|
252
|
+
>
|
|
253
|
+
Sweep all
|
|
254
|
+
{batchCount > 1 ? ` (${batchCount} batches)` : ''}
|
|
255
|
+
</Button>
|
|
256
|
+
{selectedCount > 0 && onBurn && (
|
|
257
|
+
<Button
|
|
258
|
+
size="sm"
|
|
259
|
+
className="bg-red-600 hover:bg-red-700 text-white"
|
|
260
|
+
onClick={() => {
|
|
261
|
+
if (
|
|
262
|
+
window.confirm(
|
|
263
|
+
`Permanently burn ${selectedCount} domain${selectedCount !== 1 ? 's' : ''}? This cannot be undone.`,
|
|
209
264
|
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
265
|
+
)
|
|
266
|
+
onBurn()
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
Burn
|
|
270
|
+
</Button>
|
|
271
|
+
)}
|
|
217
272
|
</div>
|
|
218
|
-
|
|
273
|
+
</div>
|
|
219
274
|
</div>
|
|
220
275
|
)
|
|
221
276
|
}
|
package/src/index.ts
CHANGED
|
@@ -50,7 +50,11 @@ export {
|
|
|
50
50
|
type TokenBalance,
|
|
51
51
|
type ScanProgress,
|
|
52
52
|
} from './lib/scanner'
|
|
53
|
-
export {
|
|
53
|
+
export {
|
|
54
|
+
executeSweep,
|
|
55
|
+
SWEEP_BATCH_SIZE,
|
|
56
|
+
type SweepResult,
|
|
57
|
+
} from './lib/sweeper'
|
|
54
58
|
export {
|
|
55
59
|
legacySendBsv,
|
|
56
60
|
legacySendOrdinals,
|
package/src/lib/sweeper.ts
CHANGED
|
@@ -10,11 +10,16 @@ import type { PrivateKey, WalletInterface } from '@bsv/sdk'
|
|
|
10
10
|
import type { TokenBalance } from './scanner'
|
|
11
11
|
import { getServices } from './services'
|
|
12
12
|
|
|
13
|
+
/** Page size, select-page size, and createAction batch size for ordinal/OpNS sweeps. */
|
|
14
|
+
export const SWEEP_BATCH_SIZE = 25
|
|
15
|
+
|
|
13
16
|
export interface SweepResult {
|
|
14
17
|
bsvTxid?: string
|
|
15
18
|
ordinalTxids: string[]
|
|
16
19
|
bsv21Txids: string[]
|
|
17
20
|
errors: string[]
|
|
21
|
+
/** Outpoints successfully swept (ordinals/OpNS). */
|
|
22
|
+
sweptOutpoints: string[]
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
function getOwner(output: IndexedOutput): string | undefined {
|
|
@@ -34,8 +39,17 @@ function buildKeys(
|
|
|
34
39
|
})
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
function chunk<T>(items: T[], size: number): T[][] {
|
|
43
|
+
const batches: T[][] = []
|
|
44
|
+
for (let i = 0; i < items.length; i += size) {
|
|
45
|
+
batches.push(items.slice(i, i + size))
|
|
46
|
+
}
|
|
47
|
+
return batches
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
/**
|
|
38
51
|
* Sweep BSV funding and ordinals into the connected wallet.
|
|
52
|
+
* Ordinals are processed in batches of {@link SWEEP_BATCH_SIZE}; stops on first batch error.
|
|
39
53
|
*/
|
|
40
54
|
export async function executeSweep(params: {
|
|
41
55
|
wallet: WalletInterface
|
|
@@ -52,6 +66,7 @@ export async function executeSweep(params: {
|
|
|
52
66
|
ordinalTxids: [],
|
|
53
67
|
bsv21Txids: [],
|
|
54
68
|
errors: [],
|
|
69
|
+
sweptOutpoints: [],
|
|
55
70
|
}
|
|
56
71
|
|
|
57
72
|
if (funding.length > 0) {
|
|
@@ -71,23 +86,42 @@ export async function executeSweep(params: {
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
if (ordinals.length > 0) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} catch (e) {
|
|
84
|
-
result.errors.push(
|
|
85
|
-
`Ordinals: ${e instanceof Error ? e.message : String(e)}`,
|
|
89
|
+
const batches = chunk(ordinals, SWEEP_BATCH_SIZE)
|
|
90
|
+
for (let b = 0; b < batches.length; b++) {
|
|
91
|
+
const batch = batches[b]
|
|
92
|
+
const from = b * SWEEP_BATCH_SIZE + 1
|
|
93
|
+
const to = b * SWEEP_BATCH_SIZE + batch.length
|
|
94
|
+
onProgress(
|
|
95
|
+
batches.length === 1
|
|
96
|
+
? `Sweeping ${batch.length} ordinal${batch.length !== 1 ? 's' : ''}...`
|
|
97
|
+
: `Sweeping ordinals ${from}–${to} of ${ordinals.length} (batch ${b + 1}/${batches.length})...`,
|
|
86
98
|
)
|
|
99
|
+
try {
|
|
100
|
+
const inputs = await prepareSweepInputs(ctx, batch)
|
|
101
|
+
const ordResult = await sweepOrdinals.execute(ctx, {
|
|
102
|
+
inputs,
|
|
103
|
+
keys: buildKeys(batch, keys),
|
|
104
|
+
})
|
|
105
|
+
if (ordResult.error) {
|
|
106
|
+
result.errors.push(
|
|
107
|
+
`Ordinals batch ${b + 1}/${batches.length}: ${ordResult.error}`,
|
|
108
|
+
)
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
if (ordResult.txid) result.ordinalTxids.push(ordResult.txid)
|
|
112
|
+
result.sweptOutpoints.push(...batch.map((o) => o.outpoint))
|
|
113
|
+
} catch (e) {
|
|
114
|
+
result.errors.push(
|
|
115
|
+
`Ordinals batch ${b + 1}/${batches.length}: ${e instanceof Error ? e.message : String(e)}`,
|
|
116
|
+
)
|
|
117
|
+
break
|
|
118
|
+
}
|
|
87
119
|
}
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
onProgress(
|
|
122
|
+
onProgress(
|
|
123
|
+
result.errors.length > 0 ? 'Sweep stopped with errors' : 'Sweep complete',
|
|
124
|
+
)
|
|
91
125
|
return result
|
|
92
126
|
}
|
|
93
127
|
|