@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.
@@ -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
- onSelectAll,
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
- onSelectAll: () => void
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
- {selectedOpns.size > 0 && (
115
+ {selectedCount > 0 && (
104
116
  <span className="text-orange-400 ml-1">
105
- ({selectedOpns.size} selected)
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={onSelectAll}
130
+ onClick={() => onSelectPage(pageOutpoints)}
116
131
  >
117
- Select All
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={selectedOpns.size === 0}
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
- {opnsNames.map((item) => {
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
- {selectedOpns.size > 0 && (
166
- <div className="mt-3 space-y-2">
167
- {onSend && (
168
- <Input
169
- type="text"
170
- placeholder="Destination address..."
171
- value={address}
172
- onChange={(e) => setAddress(e.target.value)}
173
- className="font-mono text-xs"
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
- <div className="flex gap-2">
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 to Wallet
237
+ Sweep {selectedCount}
238
+ {selectedCount > SWEEP_BATCH_SIZE
239
+ ? ` (${Math.ceil(selectedCount / SWEEP_BATCH_SIZE)} batches)`
240
+ : ''}
199
241
  </Button>
200
- {onBurn && (
201
- <Button
202
- size="sm"
203
- className="bg-red-600 hover:bg-red-700 text-white"
204
- onClick={() => {
205
- if (
206
- window.confirm(
207
- `Permanently burn ${selectedOpns.size} domain${selectedOpns.size !== 1 ? 's' : ''}? This cannot be undone.`,
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
- onBurn()
211
- }}
212
- >
213
- Burn
214
- </Button>
215
- )}
216
- </div>
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 { executeSweep, type SweepResult } from './lib/sweeper'
53
+ export {
54
+ executeSweep,
55
+ SWEEP_BATCH_SIZE,
56
+ type SweepResult,
57
+ } from './lib/sweeper'
54
58
  export {
55
59
  legacySendBsv,
56
60
  legacySendOrdinals,
@@ -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
- onProgress(`Sweeping ${ordinals.length} ordinals...`)
75
- try {
76
- const inputs = await prepareSweepInputs(ctx, ordinals)
77
- const ordResult = await sweepOrdinals.execute(ctx, {
78
- inputs,
79
- keys: buildKeys(ordinals, keys),
80
- })
81
- if (ordResult.error) result.errors.push(`Ordinals: ${ordResult.error}`)
82
- else if (ordResult.txid) result.ordinalTxids.push(ordResult.txid)
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('Sweep complete')
122
+ onProgress(
123
+ result.errors.length > 0 ? 'Sweep stopped with errors' : 'Sweep complete',
124
+ )
91
125
  return result
92
126
  }
93
127