@1sat/sweep-ui 0.0.19 → 0.0.21
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 +3 -3
- package/dist/components/SweepApp.d.ts.map +1 -1
- package/dist/components/asset-preview.d.ts +5 -5
- package/dist/components/asset-preview.d.ts.map +1 -1
- package/dist/components/connect-wallet.d.ts +1 -1
- package/dist/components/connect-wallet.d.ts.map +1 -1
- package/dist/components/opns-section.d.ts +2 -2
- package/dist/components/opns-section.d.ts.map +1 -1
- package/dist/components/sweep-progress.d.ts +1 -1
- package/dist/components/sweep-progress.d.ts.map +1 -1
- package/dist/components/tx-history.d.ts.map +1 -1
- package/dist/components/ui/badge.d.ts +3 -3
- package/dist/components/ui/badge.d.ts.map +1 -1
- package/dist/components/ui/button.d.ts +3 -3
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/card.d.ts +9 -9
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/input.d.ts +2 -2
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/tabs.d.ts +5 -5
- package/dist/components/ui/tabs.d.ts.map +1 -1
- package/dist/components/wif-input.d.ts +1 -1
- package/dist/components/wif-input.d.ts.map +1 -1
- package/dist/index.d.ts +19 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1911 -1757
- package/dist/lib/legacy-send.d.ts +2 -2
- package/dist/lib/legacy-send.d.ts.map +1 -1
- package/dist/lib/scanner.d.ts +3 -3
- package/dist/lib/scanner.d.ts.map +1 -1
- package/dist/lib/services.d.ts +1 -1
- package/dist/lib/services.d.ts.map +1 -1
- package/dist/lib/sweeper.d.ts +3 -3
- package/dist/lib/sweeper.d.ts.map +1 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/wallet.d.ts +2 -2
- package/dist/lib/wallet.d.ts.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +53 -44
- package/src/components/SweepApp.tsx +480 -222
- package/src/components/asset-preview.tsx +380 -97
- package/src/components/connect-wallet.tsx +50 -25
- package/src/components/opns-section.tsx +167 -60
- package/src/components/sweep-progress.tsx +40 -17
- package/src/components/tx-history.tsx +30 -17
- package/src/components/ui/badge.tsx +17 -14
- package/src/components/ui/button.tsx +26 -22
- package/src/components/ui/card.tsx +76 -17
- package/src/components/ui/input.tsx +7 -7
- package/src/components/ui/tabs.tsx +51 -12
- package/src/components/wif-input.tsx +243 -135
- package/src/index.ts +54 -19
- package/src/lib/legacy-send.ts +110 -106
- package/src/lib/scanner.ts +45 -40
- package/src/lib/services.ts +11 -9
- package/src/lib/sweeper.ts +67 -54
- package/src/lib/utils.ts +11 -11
- package/src/lib/wallet.ts +16 -13
- package/src/types.ts +3 -3
|
@@ -1,105 +1,224 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
1
|
+
import type { IndexedOutput } from '@1sat/types'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import type { EnrichedOrdinal, TokenBalance } from '../lib/scanner'
|
|
4
|
+
import { formatSats, formatTokenAmount } from '../lib/utils'
|
|
5
|
+
import { Badge } from './ui/badge'
|
|
6
|
+
import { Button } from './ui/button'
|
|
7
|
+
import { Input } from './ui/input'
|
|
8
8
|
|
|
9
|
-
const ORDINALS_PER_PAGE = 20
|
|
9
|
+
const ORDINALS_PER_PAGE = 20
|
|
10
10
|
|
|
11
11
|
function isImageType(ct: string): boolean {
|
|
12
|
-
return ct.startsWith(
|
|
12
|
+
return ct.startsWith('image/') && ct !== 'image/svg+xml'
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function OrdinalCard({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
function OrdinalCard({
|
|
16
|
+
ordinal,
|
|
17
|
+
isSelected,
|
|
18
|
+
onToggle,
|
|
19
|
+
}: { ordinal: EnrichedOrdinal; isSelected: boolean; onToggle: () => void }) {
|
|
20
|
+
const ct = ordinal.contentType ?? ''
|
|
21
|
+
const isImage = isImageType(ct)
|
|
22
|
+
const subtype = ct.includes('/') ? ct.split('/')[1] : ct
|
|
19
23
|
|
|
20
24
|
return (
|
|
21
25
|
<div
|
|
22
|
-
className={`relative p-2 rounded-lg border cursor-pointer transition-all ${isSelected ?
|
|
26
|
+
className={`relative p-2 rounded-lg border cursor-pointer transition-all ${isSelected ? 'border-blue-500 bg-blue-500/10 ring-1 ring-blue-500/30' : 'border-border/50 hover:border-border bg-black/20'}`}
|
|
23
27
|
onClick={onToggle}
|
|
24
28
|
>
|
|
25
29
|
<div className="absolute top-1.5 right-1.5 z-10">
|
|
26
|
-
<div
|
|
27
|
-
{isSelected
|
|
30
|
+
<div
|
|
31
|
+
className={`w-4 h-4 rounded border-2 flex items-center justify-center text-[10px] ${isSelected ? 'bg-blue-500 border-blue-500 text-white' : 'border-muted-foreground/40'}`}
|
|
32
|
+
>
|
|
33
|
+
{isSelected && '\u2713'}
|
|
28
34
|
</div>
|
|
29
35
|
</div>
|
|
30
36
|
<div className="w-full aspect-square mb-1.5 rounded overflow-hidden bg-black/30 flex items-center justify-center">
|
|
31
37
|
{!ordinal.contentUrl ? (
|
|
32
|
-
<span className="text-muted-foreground text-lg">{
|
|
38
|
+
<span className="text-muted-foreground text-lg">{'\u25C6'}</span>
|
|
33
39
|
) : isImage ? (
|
|
34
|
-
<img
|
|
40
|
+
<img
|
|
41
|
+
src={ordinal.contentUrl}
|
|
42
|
+
alt={ordinal.name || 'Ordinal'}
|
|
43
|
+
className="w-full h-full object-cover"
|
|
44
|
+
loading="lazy"
|
|
45
|
+
/>
|
|
35
46
|
) : (
|
|
36
|
-
<iframe
|
|
47
|
+
<iframe
|
|
48
|
+
src={ordinal.contentUrl}
|
|
49
|
+
title={ordinal.name || 'Ordinal'}
|
|
50
|
+
className="w-full h-full border-0 pointer-events-none"
|
|
51
|
+
sandbox="allow-scripts"
|
|
52
|
+
loading="lazy"
|
|
53
|
+
/>
|
|
37
54
|
)}
|
|
38
55
|
</div>
|
|
39
56
|
{subtype && (
|
|
40
57
|
<div className="mb-1">
|
|
41
|
-
<span className="px-1 py-0.5 text-[9px] rounded bg-blue-500/20 text-blue-400 truncate">
|
|
58
|
+
<span className="px-1 py-0.5 text-[9px] rounded bg-blue-500/20 text-blue-400 truncate">
|
|
59
|
+
{subtype}
|
|
60
|
+
</span>
|
|
42
61
|
</div>
|
|
43
62
|
)}
|
|
44
63
|
{ordinal.name ? (
|
|
45
|
-
<a
|
|
64
|
+
<a
|
|
65
|
+
href={ordinal.contentUrl}
|
|
66
|
+
target="_blank"
|
|
67
|
+
rel="noopener noreferrer"
|
|
68
|
+
className="text-[10px] text-foreground truncate font-medium hover:text-blue-400"
|
|
69
|
+
title={ordinal.name}
|
|
70
|
+
>
|
|
71
|
+
{ordinal.name}
|
|
72
|
+
</a>
|
|
46
73
|
) : (
|
|
47
|
-
<a
|
|
74
|
+
<a
|
|
75
|
+
href={ordinal.contentUrl}
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener noreferrer"
|
|
78
|
+
className="text-[9px] text-muted-foreground truncate font-mono hover:text-blue-400"
|
|
79
|
+
>
|
|
80
|
+
{ordinal.outpoint.substring(0, 8)}...
|
|
81
|
+
</a>
|
|
48
82
|
)}
|
|
49
83
|
</div>
|
|
50
|
-
)
|
|
84
|
+
)
|
|
51
85
|
}
|
|
52
86
|
|
|
53
|
-
export function FundingSection({
|
|
54
|
-
funding
|
|
87
|
+
export function FundingSection({
|
|
88
|
+
funding,
|
|
89
|
+
totalBsv,
|
|
90
|
+
sweepAmount,
|
|
91
|
+
onSweepAmountChange,
|
|
92
|
+
onSweep,
|
|
93
|
+
onSend,
|
|
94
|
+
walletConnected,
|
|
95
|
+
}: {
|
|
96
|
+
funding: IndexedOutput[]
|
|
97
|
+
totalBsv: number
|
|
98
|
+
sweepAmount: number | null
|
|
99
|
+
onSweepAmountChange: (amount: number | null) => void
|
|
100
|
+
onSweep: () => void
|
|
101
|
+
onSend?: (destination: string) => void
|
|
102
|
+
walletConnected: boolean
|
|
55
103
|
}) {
|
|
56
|
-
const [address, setAddress] = useState(
|
|
57
|
-
if (funding.length === 0) return null
|
|
58
|
-
const isMax = sweepAmount === null
|
|
59
|
-
const displayAmount = isMax ? formatSats(totalBsv) : formatSats(sweepAmount!)
|
|
104
|
+
const [address, setAddress] = useState('')
|
|
105
|
+
if (funding.length === 0) return null
|
|
106
|
+
const isMax = sweepAmount === null
|
|
107
|
+
const displayAmount = isMax ? formatSats(totalBsv) : formatSats(sweepAmount!)
|
|
60
108
|
|
|
61
109
|
return (
|
|
62
110
|
<div className="border border-green-500/20 bg-green-500/5 p-4 rounded-lg">
|
|
63
111
|
<div className="flex items-center gap-2 mb-2">
|
|
64
112
|
<span className="h-2 w-2 rounded-full bg-green-500" />
|
|
65
|
-
<span className="text-sm font-semibold text-green-500">
|
|
113
|
+
<span className="text-sm font-semibold text-green-500">
|
|
114
|
+
BSV Funding
|
|
115
|
+
</span>
|
|
66
116
|
</div>
|
|
67
117
|
<div className="flex items-baseline justify-between mb-3">
|
|
68
118
|
<div>
|
|
69
|
-
<div className="text-2xl font-bold text-green-500">
|
|
70
|
-
|
|
119
|
+
<div className="text-2xl font-bold text-green-500">
|
|
120
|
+
{formatSats(totalBsv)} sats
|
|
121
|
+
</div>
|
|
122
|
+
<div className="text-xs text-muted-foreground">
|
|
123
|
+
{(totalBsv / 100_000_000).toFixed(8)} BSV
|
|
124
|
+
</div>
|
|
71
125
|
</div>
|
|
72
|
-
<Badge variant="secondary">
|
|
126
|
+
<Badge variant="secondary">
|
|
127
|
+
{funding.length} UTXO{funding.length !== 1 ? 's' : ''}
|
|
128
|
+
</Badge>
|
|
73
129
|
</div>
|
|
74
130
|
<div className="flex items-center gap-2">
|
|
75
|
-
<Input
|
|
131
|
+
<Input
|
|
132
|
+
type="number"
|
|
133
|
+
min={0}
|
|
134
|
+
max={totalBsv}
|
|
135
|
+
placeholder="Max"
|
|
136
|
+
value={isMax ? '' : sweepAmount}
|
|
137
|
+
onChange={(e) => {
|
|
138
|
+
const val = e.target.value
|
|
139
|
+
onSweepAmountChange(
|
|
140
|
+
val === '' ? null : Math.max(0, Math.min(totalBsv, Number(val))),
|
|
141
|
+
)
|
|
142
|
+
}}
|
|
143
|
+
className="flex-1 font-mono"
|
|
144
|
+
/>
|
|
76
145
|
<span className="text-xs text-muted-foreground">sats</span>
|
|
77
|
-
<Button
|
|
146
|
+
<Button
|
|
147
|
+
variant="outline"
|
|
148
|
+
size="sm"
|
|
149
|
+
className="h-9 text-xs"
|
|
150
|
+
onClick={() => onSweepAmountChange(null)}
|
|
151
|
+
disabled={isMax}
|
|
152
|
+
>
|
|
153
|
+
Max
|
|
154
|
+
</Button>
|
|
78
155
|
</div>
|
|
79
156
|
<div className="mt-3 space-y-2">
|
|
80
157
|
{onSend && (
|
|
81
|
-
<Input
|
|
158
|
+
<Input
|
|
159
|
+
type="text"
|
|
160
|
+
placeholder="Destination address..."
|
|
161
|
+
value={address}
|
|
162
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
163
|
+
className="font-mono text-xs"
|
|
164
|
+
/>
|
|
82
165
|
)}
|
|
83
166
|
<div className="flex gap-2">
|
|
84
167
|
{onSend && (
|
|
85
|
-
<Button
|
|
168
|
+
<Button
|
|
169
|
+
variant="outline"
|
|
170
|
+
size="sm"
|
|
171
|
+
className="flex-1"
|
|
172
|
+
disabled={!address.trim()}
|
|
173
|
+
onClick={() => onSend(address.trim())}
|
|
174
|
+
>
|
|
175
|
+
Send {displayAmount} sats
|
|
176
|
+
</Button>
|
|
86
177
|
)}
|
|
87
|
-
<Button
|
|
178
|
+
<Button
|
|
179
|
+
size="sm"
|
|
180
|
+
className="flex-1"
|
|
181
|
+
onClick={onSweep}
|
|
182
|
+
disabled={!walletConnected}
|
|
183
|
+
title={
|
|
184
|
+
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
|
|
185
|
+
}
|
|
186
|
+
>
|
|
187
|
+
Sweep to Wallet
|
|
188
|
+
</Button>
|
|
88
189
|
</div>
|
|
89
190
|
</div>
|
|
90
191
|
</div>
|
|
91
|
-
)
|
|
192
|
+
)
|
|
92
193
|
}
|
|
93
194
|
|
|
94
|
-
export function OrdinalsSection({
|
|
95
|
-
ordinals
|
|
195
|
+
export function OrdinalsSection({
|
|
196
|
+
ordinals,
|
|
197
|
+
selectedOrdinals,
|
|
198
|
+
onToggle,
|
|
199
|
+
onSelectAll,
|
|
200
|
+
onDeselectAll,
|
|
201
|
+
onSweep,
|
|
202
|
+
onSend,
|
|
203
|
+
onBurn,
|
|
204
|
+
walletConnected,
|
|
205
|
+
}: {
|
|
206
|
+
ordinals: EnrichedOrdinal[]
|
|
207
|
+
selectedOrdinals: Set<string>
|
|
208
|
+
onToggle: (outpoint: string) => void
|
|
209
|
+
onSelectAll: () => void
|
|
210
|
+
onDeselectAll: () => void
|
|
211
|
+
onSweep: () => void
|
|
212
|
+
onSend?: (destination: string) => void
|
|
213
|
+
onBurn?: () => void
|
|
214
|
+
walletConnected: boolean
|
|
96
215
|
}) {
|
|
97
|
-
const [page, setPage] = useState(0)
|
|
98
|
-
const [address, setAddress] = useState(
|
|
99
|
-
if (ordinals.length === 0) return null
|
|
100
|
-
const totalPages = Math.ceil(ordinals.length / ORDINALS_PER_PAGE)
|
|
101
|
-
const start = page * ORDINALS_PER_PAGE
|
|
102
|
-
const pageItems = ordinals.slice(start, start + ORDINALS_PER_PAGE)
|
|
216
|
+
const [page, setPage] = useState(0)
|
|
217
|
+
const [address, setAddress] = useState('')
|
|
218
|
+
if (ordinals.length === 0) return null
|
|
219
|
+
const totalPages = Math.ceil(ordinals.length / ORDINALS_PER_PAGE)
|
|
220
|
+
const start = page * ORDINALS_PER_PAGE
|
|
221
|
+
const pageItems = ordinals.slice(start, start + ORDINALS_PER_PAGE)
|
|
103
222
|
|
|
104
223
|
return (
|
|
105
224
|
<div className="border border-blue-500/20 bg-blue-500/5 p-4 rounded-lg">
|
|
@@ -107,149 +226,313 @@ export function OrdinalsSection({ ordinals, selectedOrdinals, onToggle, onSelect
|
|
|
107
226
|
<div>
|
|
108
227
|
<div className="flex items-center gap-2 mb-1">
|
|
109
228
|
<span className="h-2 w-2 rounded-full bg-blue-500" />
|
|
110
|
-
<span className="text-sm font-semibold text-blue-500">
|
|
229
|
+
<span className="text-sm font-semibold text-blue-500">
|
|
230
|
+
Ordinals
|
|
231
|
+
</span>
|
|
111
232
|
</div>
|
|
112
233
|
<div className="text-xs text-muted-foreground">
|
|
113
|
-
{ordinals.length} inscription{ordinals.length !== 1 ?
|
|
114
|
-
{selectedOrdinals.size > 0 &&
|
|
234
|
+
{ordinals.length} inscription{ordinals.length !== 1 ? 's' : ''}
|
|
235
|
+
{selectedOrdinals.size > 0 && (
|
|
236
|
+
<span className="text-blue-400 ml-1">
|
|
237
|
+
({selectedOrdinals.size} selected)
|
|
238
|
+
</span>
|
|
239
|
+
)}
|
|
115
240
|
</div>
|
|
116
241
|
</div>
|
|
117
242
|
<div className="flex gap-2">
|
|
118
|
-
<Button
|
|
119
|
-
|
|
243
|
+
<Button
|
|
244
|
+
variant="outline"
|
|
245
|
+
size="sm"
|
|
246
|
+
className="h-7 text-[11px]"
|
|
247
|
+
onClick={onSelectAll}
|
|
248
|
+
>
|
|
249
|
+
Select All
|
|
250
|
+
</Button>
|
|
251
|
+
<Button
|
|
252
|
+
variant="outline"
|
|
253
|
+
size="sm"
|
|
254
|
+
className="h-7 text-[11px]"
|
|
255
|
+
onClick={onDeselectAll}
|
|
256
|
+
disabled={selectedOrdinals.size === 0}
|
|
257
|
+
>
|
|
258
|
+
Deselect
|
|
259
|
+
</Button>
|
|
120
260
|
</div>
|
|
121
261
|
</div>
|
|
122
262
|
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-2 mb-3">
|
|
123
|
-
{pageItems.map((ord) => (
|
|
263
|
+
{pageItems.map((ord) => (
|
|
264
|
+
<OrdinalCard
|
|
265
|
+
key={ord.outpoint}
|
|
266
|
+
ordinal={ord}
|
|
267
|
+
isSelected={selectedOrdinals.has(ord.outpoint)}
|
|
268
|
+
onToggle={() => onToggle(ord.outpoint)}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
124
271
|
</div>
|
|
125
272
|
{totalPages > 1 && (
|
|
126
273
|
<div className="flex items-center justify-center gap-4">
|
|
127
|
-
<Button
|
|
128
|
-
|
|
129
|
-
|
|
274
|
+
<Button
|
|
275
|
+
variant="ghost"
|
|
276
|
+
size="sm"
|
|
277
|
+
className="text-xs"
|
|
278
|
+
onClick={() => setPage(page - 1)}
|
|
279
|
+
disabled={page === 0}
|
|
280
|
+
>
|
|
281
|
+
Prev
|
|
282
|
+
</Button>
|
|
283
|
+
<span className="text-xs text-muted-foreground">
|
|
284
|
+
Page {page + 1} of {totalPages}
|
|
285
|
+
</span>
|
|
286
|
+
<Button
|
|
287
|
+
variant="ghost"
|
|
288
|
+
size="sm"
|
|
289
|
+
className="text-xs"
|
|
290
|
+
onClick={() => setPage(page + 1)}
|
|
291
|
+
disabled={page >= totalPages - 1}
|
|
292
|
+
>
|
|
293
|
+
Next
|
|
294
|
+
</Button>
|
|
130
295
|
</div>
|
|
131
296
|
)}
|
|
132
297
|
{selectedOrdinals.size > 0 && (
|
|
133
298
|
<div className="mt-3 space-y-2">
|
|
134
299
|
{onSend && (
|
|
135
|
-
<Input
|
|
300
|
+
<Input
|
|
301
|
+
type="text"
|
|
302
|
+
placeholder="Destination address..."
|
|
303
|
+
value={address}
|
|
304
|
+
onChange={(e) => setAddress(e.target.value)}
|
|
305
|
+
className="font-mono text-xs"
|
|
306
|
+
/>
|
|
136
307
|
)}
|
|
137
308
|
<div className="flex gap-2">
|
|
138
309
|
{onSend && (
|
|
139
|
-
<Button
|
|
310
|
+
<Button
|
|
311
|
+
variant="outline"
|
|
312
|
+
size="sm"
|
|
313
|
+
className="flex-1"
|
|
314
|
+
disabled={!address.trim()}
|
|
315
|
+
onClick={() => onSend(address.trim())}
|
|
316
|
+
>
|
|
317
|
+
Send {selectedOrdinals.size} Ordinal
|
|
318
|
+
{selectedOrdinals.size !== 1 ? 's' : ''}
|
|
319
|
+
</Button>
|
|
140
320
|
)}
|
|
141
|
-
<Button
|
|
321
|
+
<Button
|
|
322
|
+
size="sm"
|
|
323
|
+
className="flex-1"
|
|
324
|
+
onClick={onSweep}
|
|
325
|
+
disabled={!walletConnected}
|
|
326
|
+
title={
|
|
327
|
+
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
|
|
328
|
+
}
|
|
329
|
+
>
|
|
330
|
+
Sweep to Wallet
|
|
331
|
+
</Button>
|
|
142
332
|
{onBurn && (
|
|
143
|
-
<Button
|
|
333
|
+
<Button
|
|
334
|
+
size="sm"
|
|
335
|
+
className="bg-red-600 hover:bg-red-700 text-white"
|
|
336
|
+
onClick={() => {
|
|
337
|
+
if (
|
|
338
|
+
window.confirm(
|
|
339
|
+
`Permanently burn ${selectedOrdinals.size} ordinal${selectedOrdinals.size !== 1 ? 's' : ''}? This cannot be undone.`,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
onBurn()
|
|
343
|
+
}}
|
|
344
|
+
>
|
|
345
|
+
Burn
|
|
346
|
+
</Button>
|
|
144
347
|
)}
|
|
145
348
|
</div>
|
|
146
349
|
</div>
|
|
147
350
|
)}
|
|
148
351
|
</div>
|
|
149
|
-
)
|
|
352
|
+
)
|
|
150
353
|
}
|
|
151
354
|
|
|
152
|
-
function TokenRow({
|
|
355
|
+
function TokenRow({
|
|
356
|
+
tb,
|
|
357
|
+
onSweep,
|
|
358
|
+
walletConnected,
|
|
359
|
+
}: {
|
|
360
|
+
tb: TokenBalance
|
|
361
|
+
onSweep?: (tokenId: string) => void
|
|
362
|
+
walletConnected: boolean
|
|
363
|
+
}) {
|
|
153
364
|
return (
|
|
154
|
-
<div
|
|
365
|
+
<div
|
|
366
|
+
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'}`}
|
|
367
|
+
>
|
|
155
368
|
<div className="flex items-center gap-3">
|
|
156
|
-
<img
|
|
369
|
+
<img
|
|
370
|
+
src={tb.icon}
|
|
371
|
+
alt={tb.symbol || 'Token'}
|
|
372
|
+
className="w-8 h-8 rounded-full object-cover"
|
|
373
|
+
onError={(e) => {
|
|
374
|
+
;(e.target as HTMLImageElement).style.display = 'none'
|
|
375
|
+
}}
|
|
376
|
+
/>
|
|
157
377
|
<div>
|
|
158
378
|
<div className="flex items-center gap-2">
|
|
159
|
-
<span className="font-medium text-foreground">
|
|
379
|
+
<span className="font-medium text-foreground">
|
|
380
|
+
{tb.symbol || tb.tokenId.slice(0, 8) + '...'}
|
|
381
|
+
</span>
|
|
160
382
|
{tb.isActive ? (
|
|
161
|
-
<span className="px-1.5 py-0.5 text-[9px] rounded bg-green-600/20 text-green-700 dark:text-green-400">
|
|
383
|
+
<span className="px-1.5 py-0.5 text-[9px] rounded bg-green-600/20 text-green-700 dark:text-green-400">
|
|
384
|
+
active
|
|
385
|
+
</span>
|
|
162
386
|
) : (
|
|
163
|
-
<span className="px-1.5 py-0.5 text-[9px] rounded bg-muted text-muted-foreground">
|
|
387
|
+
<span className="px-1.5 py-0.5 text-[9px] rounded bg-muted text-muted-foreground">
|
|
388
|
+
inactive
|
|
389
|
+
</span>
|
|
164
390
|
)}
|
|
165
391
|
</div>
|
|
166
392
|
<div className="text-xs text-muted-foreground">
|
|
167
|
-
{formatTokenAmount(tb.totalAmount.toString(), tb.decimals)}
|
|
168
|
-
|
|
393
|
+
{formatTokenAmount(tb.totalAmount.toString(), tb.decimals)}{' '}
|
|
394
|
+
{tb.symbol || ''}
|
|
395
|
+
<span className="ml-2">
|
|
396
|
+
({tb.outputs.length} output{tb.outputs.length !== 1 ? 's' : ''})
|
|
397
|
+
</span>
|
|
169
398
|
</div>
|
|
170
399
|
</div>
|
|
171
400
|
</div>
|
|
172
401
|
{tb.isActive && onSweep && (
|
|
173
|
-
<Button
|
|
402
|
+
<Button
|
|
403
|
+
size="sm"
|
|
404
|
+
onClick={() => onSweep(tb.tokenId)}
|
|
405
|
+
disabled={!walletConnected}
|
|
406
|
+
title={
|
|
407
|
+
walletConnected ? undefined : 'Connect BRC-100 wallet to sweep'
|
|
408
|
+
}
|
|
409
|
+
>
|
|
174
410
|
Sweep to Wallet
|
|
175
411
|
</Button>
|
|
176
412
|
)}
|
|
177
413
|
</div>
|
|
178
|
-
)
|
|
414
|
+
)
|
|
179
415
|
}
|
|
180
416
|
|
|
181
|
-
export function Bsv21Section({
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
417
|
+
export function Bsv21Section({
|
|
418
|
+
tokens,
|
|
419
|
+
onSweep,
|
|
420
|
+
walletConnected,
|
|
421
|
+
}: {
|
|
422
|
+
tokens: TokenBalance[]
|
|
423
|
+
onSweep?: (tokenId: string) => void
|
|
424
|
+
walletConnected: boolean
|
|
425
|
+
}) {
|
|
426
|
+
if (tokens.length === 0) return null
|
|
427
|
+
const active = tokens.filter((t) => t.isActive)
|
|
428
|
+
const inactive = tokens.filter((t) => !t.isActive)
|
|
185
429
|
|
|
186
430
|
return (
|
|
187
431
|
<div className="border border-purple-500/20 bg-purple-500/5 p-4 rounded-lg">
|
|
188
432
|
<div className="flex items-center gap-2 mb-3">
|
|
189
433
|
<span className="h-2 w-2 rounded-full bg-purple-500" />
|
|
190
|
-
<span className="text-sm font-semibold text-purple-500">
|
|
434
|
+
<span className="text-sm font-semibold text-purple-500">
|
|
435
|
+
BSV-21 Tokens
|
|
436
|
+
</span>
|
|
191
437
|
</div>
|
|
192
438
|
<div className="space-y-3">
|
|
193
|
-
{active.map((tb) => (
|
|
439
|
+
{active.map((tb) => (
|
|
440
|
+
<TokenRow
|
|
441
|
+
key={tb.tokenId}
|
|
442
|
+
tb={tb}
|
|
443
|
+
onSweep={onSweep}
|
|
444
|
+
walletConnected={walletConnected}
|
|
445
|
+
/>
|
|
446
|
+
))}
|
|
194
447
|
{inactive.length > 0 && active.length > 0 && (
|
|
195
448
|
<div className="border-t border-purple-500/10 pt-3 mt-3">
|
|
196
|
-
<div className="text-xs text-muted-foreground mb-2">
|
|
449
|
+
<div className="text-xs text-muted-foreground mb-2">
|
|
450
|
+
Inactive overlays ({inactive.length}) — cannot be swept
|
|
451
|
+
</div>
|
|
197
452
|
</div>
|
|
198
453
|
)}
|
|
199
|
-
{inactive.map((tb) => (
|
|
454
|
+
{inactive.map((tb) => (
|
|
455
|
+
<TokenRow
|
|
456
|
+
key={tb.tokenId}
|
|
457
|
+
tb={tb}
|
|
458
|
+
walletConnected={walletConnected}
|
|
459
|
+
/>
|
|
460
|
+
))}
|
|
200
461
|
</div>
|
|
201
462
|
</div>
|
|
202
|
-
)
|
|
463
|
+
)
|
|
203
464
|
}
|
|
204
465
|
|
|
205
466
|
export function Bsv20Section({ tokens }: { tokens: IndexedOutput[] }) {
|
|
206
|
-
if (tokens.length === 0) return null
|
|
467
|
+
if (tokens.length === 0) return null
|
|
207
468
|
return (
|
|
208
469
|
<div className="border border-muted/30 bg-muted/10 p-4 rounded-lg">
|
|
209
470
|
<div className="flex items-center gap-2 mb-2">
|
|
210
471
|
<span className="h-2 w-2 rounded-full bg-muted-foreground" />
|
|
211
|
-
<span className="text-sm font-semibold text-muted-foreground">
|
|
472
|
+
<span className="text-sm font-semibold text-muted-foreground">
|
|
473
|
+
BSV-20 Tokens
|
|
474
|
+
</span>
|
|
212
475
|
</div>
|
|
213
|
-
<p className="text-xs text-muted-foreground mb-2">
|
|
476
|
+
<p className="text-xs text-muted-foreground mb-2">
|
|
477
|
+
Cannot be swept automatically.
|
|
478
|
+
</p>
|
|
214
479
|
<div className="flex flex-wrap gap-2">
|
|
215
480
|
{tokens.slice(0, 10).map((o) => {
|
|
216
|
-
const tickEvent = o.events?.find((e) => e.startsWith(
|
|
217
|
-
const tick = tickEvent ? tickEvent.slice(5) :
|
|
218
|
-
return (
|
|
481
|
+
const tickEvent = o.events?.find((e) => e.startsWith('tick:'))
|
|
482
|
+
const tick = tickEvent ? tickEvent.slice(5) : 'Token'
|
|
483
|
+
return (
|
|
484
|
+
<span
|
|
485
|
+
key={o.outpoint}
|
|
486
|
+
className="px-2 py-1 text-xs rounded bg-muted/30 text-muted-foreground"
|
|
487
|
+
>
|
|
488
|
+
{tick}
|
|
489
|
+
</span>
|
|
490
|
+
)
|
|
219
491
|
})}
|
|
220
|
-
{tokens.length > 10 &&
|
|
492
|
+
{tokens.length > 10 && (
|
|
493
|
+
<span className="text-xs text-muted-foreground">
|
|
494
|
+
+{tokens.length - 10} more
|
|
495
|
+
</span>
|
|
496
|
+
)}
|
|
221
497
|
</div>
|
|
222
498
|
</div>
|
|
223
|
-
)
|
|
499
|
+
)
|
|
224
500
|
}
|
|
225
501
|
|
|
226
502
|
export function LockedSection({ locked }: { locked: IndexedOutput[] }) {
|
|
227
|
-
if (locked.length === 0) return null
|
|
503
|
+
if (locked.length === 0) return null
|
|
228
504
|
return (
|
|
229
505
|
<div className="border border-yellow-500/20 bg-yellow-500/5 p-4 rounded-lg">
|
|
230
506
|
<div className="flex items-center gap-2 mb-2">
|
|
231
507
|
<span className="h-2 w-2 rounded-full bg-yellow-500" />
|
|
232
|
-
<span className="text-sm font-semibold text-yellow-500">
|
|
508
|
+
<span className="text-sm font-semibold text-yellow-500">
|
|
509
|
+
Locked Outputs
|
|
510
|
+
</span>
|
|
233
511
|
</div>
|
|
234
512
|
<p className="text-xs text-muted-foreground">
|
|
235
|
-
{locked.length} locked output{locked.length !== 1 ?
|
|
513
|
+
{locked.length} locked output{locked.length !== 1 ? 's' : ''}. These are
|
|
514
|
+
in contracts and cannot be swept directly.
|
|
236
515
|
</p>
|
|
237
516
|
</div>
|
|
238
|
-
)
|
|
517
|
+
)
|
|
239
518
|
}
|
|
240
519
|
|
|
241
520
|
export function RunSection({ run }: { run: IndexedOutput[] }) {
|
|
242
|
-
if (run.length === 0) return null
|
|
243
|
-
const totalSats = run.reduce((sum, o) => sum + (o.satoshis ?? 0), 0)
|
|
521
|
+
if (run.length === 0) return null
|
|
522
|
+
const totalSats = run.reduce((sum, o) => sum + (o.satoshis ?? 0), 0)
|
|
244
523
|
return (
|
|
245
524
|
<div className="border border-orange-500/20 bg-orange-500/5 p-4 rounded-lg">
|
|
246
525
|
<div className="flex items-center gap-2 mb-2">
|
|
247
526
|
<span className="h-2 w-2 rounded-full bg-orange-500" />
|
|
248
|
-
<span className="text-sm font-semibold text-orange-500">
|
|
527
|
+
<span className="text-sm font-semibold text-orange-500">
|
|
528
|
+
RUN Protocol Tokens
|
|
529
|
+
</span>
|
|
249
530
|
</div>
|
|
250
531
|
<p className="text-xs text-muted-foreground">
|
|
251
|
-
{run.length} output{run.length !== 1 ?
|
|
532
|
+
{run.length} output{run.length !== 1 ? 's' : ''} (
|
|
533
|
+
{totalSats.toLocaleString()} sats). These are RUN protocol token outputs
|
|
534
|
+
and cannot be swept as BSV.
|
|
252
535
|
</p>
|
|
253
536
|
</div>
|
|
254
|
-
)
|
|
537
|
+
)
|
|
255
538
|
}
|