@1sat/sweep-ui 0.0.1

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.
Files changed (62) hide show
  1. package/dist/components/SweepApp.d.ts +6 -0
  2. package/dist/components/SweepApp.d.ts.map +1 -0
  3. package/dist/components/asset-preview.d.ts +32 -0
  4. package/dist/components/asset-preview.d.ts.map +1 -0
  5. package/dist/components/connect-wallet.d.ts +8 -0
  6. package/dist/components/connect-wallet.d.ts.map +1 -0
  7. package/dist/components/opns-section.d.ts +13 -0
  8. package/dist/components/opns-section.d.ts.map +1 -0
  9. package/dist/components/sweep-progress.d.ts +9 -0
  10. package/dist/components/sweep-progress.d.ts.map +1 -0
  11. package/dist/components/tx-history.d.ts +14 -0
  12. package/dist/components/tx-history.d.ts.map +1 -0
  13. package/dist/components/ui/badge.d.ts +8 -0
  14. package/dist/components/ui/badge.d.ts.map +1 -0
  15. package/dist/components/ui/button.d.ts +9 -0
  16. package/dist/components/ui/button.d.ts.map +1 -0
  17. package/dist/components/ui/card.d.ts +10 -0
  18. package/dist/components/ui/card.d.ts.map +1 -0
  19. package/dist/components/ui/input.d.ts +4 -0
  20. package/dist/components/ui/input.d.ts.map +1 -0
  21. package/dist/components/ui/tabs.d.ts +14 -0
  22. package/dist/components/ui/tabs.d.ts.map +1 -0
  23. package/dist/components/wif-input.d.ts +9 -0
  24. package/dist/components/wif-input.d.ts.map +1 -0
  25. package/dist/index.d.ts +20 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +2556 -0
  28. package/dist/lib/legacy-send.d.ts +24 -0
  29. package/dist/lib/legacy-send.d.ts.map +1 -0
  30. package/dist/lib/scanner.d.ts +33 -0
  31. package/dist/lib/scanner.d.ts.map +1 -0
  32. package/dist/lib/services.d.ts +4 -0
  33. package/dist/lib/services.d.ts.map +1 -0
  34. package/dist/lib/sweeper.d.ts +18 -0
  35. package/dist/lib/sweeper.d.ts.map +1 -0
  36. package/dist/lib/utils.d.ts +6 -0
  37. package/dist/lib/utils.d.ts.map +1 -0
  38. package/dist/lib/wallet.d.ts +9 -0
  39. package/dist/lib/wallet.d.ts.map +1 -0
  40. package/dist/types.d.ts +6 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/package.json +44 -0
  43. package/src/components/SweepApp.tsx +269 -0
  44. package/src/components/asset-preview.tsx +224 -0
  45. package/src/components/connect-wallet.tsx +59 -0
  46. package/src/components/opns-section.tsx +108 -0
  47. package/src/components/sweep-progress.tsx +69 -0
  48. package/src/components/tx-history.tsx +63 -0
  49. package/src/components/ui/badge.tsx +39 -0
  50. package/src/components/ui/button.tsx +52 -0
  51. package/src/components/ui/card.tsx +32 -0
  52. package/src/components/ui/input.tsx +20 -0
  53. package/src/components/ui/tabs.tsx +51 -0
  54. package/src/components/wif-input.tsx +332 -0
  55. package/src/index.ts +28 -0
  56. package/src/lib/legacy-send.ts +234 -0
  57. package/src/lib/scanner.ts +226 -0
  58. package/src/lib/services.ts +18 -0
  59. package/src/lib/sweeper.ts +93 -0
  60. package/src/lib/utils.ts +23 -0
  61. package/src/lib/wallet.ts +32 -0
  62. package/src/types.ts +5 -0
@@ -0,0 +1,20 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ return (
6
+ <input
7
+ type={type}
8
+ data-slot="input"
9
+ className={cn(
10
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
11
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
12
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Input }
@@ -0,0 +1,51 @@
1
+ import * as React from "react"
2
+ import { cn } from "../../lib/utils"
3
+
4
+ type TabsContextValue = {
5
+ value: string
6
+ onValueChange: (value: string) => void
7
+ }
8
+
9
+ const TabsContext = React.createContext<TabsContextValue | null>(null)
10
+
11
+ function useTabsContext() {
12
+ const context = React.useContext(TabsContext)
13
+ if (!context) {
14
+ throw new Error("Tabs components must be used within a <Tabs> provider")
15
+ }
16
+ return context
17
+ }
18
+
19
+ function Tabs({ value, onValueChange, className, ...props }: React.ComponentProps<"div"> & { value: string; onValueChange: (value: string) => void }) {
20
+ return (
21
+ <TabsContext.Provider value={{ value, onValueChange }}>
22
+ <div data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />
23
+ </TabsContext.Provider>
24
+ )
25
+ }
26
+
27
+ function TabsList({ className, ...props }: React.ComponentProps<"div">) {
28
+ return (<div data-slot="tabs-list" className={cn("inline-flex h-9 items-center justify-start gap-1 rounded-lg bg-muted p-1 text-muted-foreground", className)} {...props} />)
29
+ }
30
+
31
+ function TabsTrigger({ value, className, ...props }: React.ComponentProps<"button"> & { value: string }) {
32
+ const context = useTabsContext()
33
+ const isActive = context.value === value
34
+ return (
35
+ <button
36
+ data-slot="tabs-trigger"
37
+ data-active={isActive ? "" : undefined}
38
+ className={cn("inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50", isActive && "bg-background text-foreground shadow-sm", className)}
39
+ onClick={() => context.onValueChange(value)}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function TabsContent({ value, className, ...props }: React.ComponentProps<"div"> & { value: string }) {
46
+ const context = useTabsContext()
47
+ if (context.value !== value) return null
48
+ return (<div data-slot="tabs-content" className={cn("mt-2", className)} {...props} />)
49
+ }
50
+
51
+ export { Tabs, TabsList, TabsTrigger, TabsContent }
@@ -0,0 +1,332 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { KeyRound, Loader2, Search, Upload, X } from "lucide-react";
3
+ import { Button } from "./ui/button";
4
+ import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
5
+ import { Input } from "./ui/input";
6
+ import {
7
+ decryptBackup,
8
+ isOneSatBackup,
9
+ isYoursWalletBackup,
10
+ isWifBackup,
11
+ getBackupType,
12
+ type DecryptedBackup,
13
+ } from "bitcoin-backup";
14
+ import { deriveAddress } from "../lib/scanner";
15
+ import { deriveIdentityKey } from "@1sat/utils";
16
+ import type { LegacyKeys } from "../types";
17
+
18
+ interface Props {
19
+ onScan: (keys: LegacyKeys) => void;
20
+ scanning: boolean;
21
+ disabled: boolean;
22
+ }
23
+
24
+ type InputMode = "choose" | "backup" | "wif";
25
+
26
+ function withIdentityKey(keys: LegacyKeys): LegacyKeys {
27
+ if (keys.identityPk) return keys;
28
+ return { ...keys, identityPk: deriveIdentityKey(keys.payPk, keys.ordPk).toWif() };
29
+ }
30
+
31
+ function extractKeys(backup: DecryptedBackup): LegacyKeys | null {
32
+ if (isOneSatBackup(backup)) {
33
+ return withIdentityKey({
34
+ payPk: backup.payPk,
35
+ ordPk: backup.ordPk,
36
+ identityPk: backup.identityPk,
37
+ });
38
+ }
39
+ if (isYoursWalletBackup(backup)) {
40
+ return withIdentityKey({
41
+ payPk: backup.payPk,
42
+ ordPk: backup.ordPk,
43
+ identityPk: backup.identityPk,
44
+ });
45
+ }
46
+ if (isWifBackup(backup)) {
47
+ return withIdentityKey({
48
+ payPk: backup.wif,
49
+ ordPk: backup.wif,
50
+ });
51
+ }
52
+ return null;
53
+ }
54
+
55
+ function tryParseBackup(text: string): LegacyKeys | null {
56
+ try {
57
+ const parsed = JSON.parse(text);
58
+ if (parsed.payPk && parsed.ordPk) {
59
+ return withIdentityKey({
60
+ payPk: parsed.payPk,
61
+ ordPk: parsed.ordPk,
62
+ identityPk: parsed.identityPk,
63
+ });
64
+ }
65
+ if (parsed.accounts && parsed.selectedAccount) {
66
+ const account = parsed.accounts[parsed.selectedAccount];
67
+ if (account?.encryptedKeys) {
68
+ return null;
69
+ }
70
+ }
71
+ } catch {
72
+ // Not JSON
73
+ }
74
+ return null;
75
+ }
76
+
77
+ function looksEncrypted(text: string): boolean {
78
+ if (text.startsWith("{")) return false;
79
+ if (text.startsWith("5") || text.startsWith("K") || text.startsWith("L")) return false;
80
+ return text.length > 50;
81
+ }
82
+
83
+ export function WifInput({ onScan, scanning, disabled }: Props) {
84
+ const [mode, setMode] = useState<InputMode>("choose");
85
+ const [keys, setKeys] = useState<LegacyKeys | null>(null);
86
+ const [backupText, setBackupText] = useState("");
87
+ const [password, setPassword] = useState("");
88
+ const [needsPassword, setNeedsPassword] = useState(false);
89
+ const [decrypting, setDecrypting] = useState(false);
90
+ const [error, setError] = useState("");
91
+ const [backupType, setBackupType] = useState("");
92
+ const fileInputRef = useRef<HTMLInputElement>(null);
93
+ const [payWif, setPayWif] = useState("");
94
+ const [ordWif, setOrdWif] = useState("");
95
+ const [sameKey, setSameKey] = useState(true);
96
+
97
+ const handleReset = useCallback(() => {
98
+ setKeys(null);
99
+ setBackupText("");
100
+ setPassword("");
101
+ setNeedsPassword(false);
102
+ setError("");
103
+ setBackupType("");
104
+ setMode("choose");
105
+ setPayWif("");
106
+ setOrdWif("");
107
+ }, []);
108
+
109
+ const processBackupText = useCallback(async (text: string) => {
110
+ setError("");
111
+ setBackupText(text);
112
+
113
+ const directKeys = tryParseBackup(text);
114
+ if (directKeys) {
115
+ setKeys(directKeys);
116
+ setBackupType("JSON");
117
+ return;
118
+ }
119
+
120
+ try {
121
+ const parsed = JSON.parse(text);
122
+ if (parsed.encryptedBackup) {
123
+ setBackupText(parsed.encryptedBackup);
124
+ setNeedsPassword(true);
125
+ return;
126
+ }
127
+ if (parsed.encryptedKeys) {
128
+ setBackupText(parsed.encryptedKeys);
129
+ setNeedsPassword(true);
130
+ return;
131
+ }
132
+ if (parsed.accounts && parsed.selectedAccount) {
133
+ const account = parsed.accounts[parsed.selectedAccount];
134
+ if (account?.encryptedKeys) {
135
+ setBackupText(account.encryptedKeys);
136
+ setNeedsPassword(true);
137
+ return;
138
+ }
139
+ }
140
+ } catch {
141
+ // Not JSON
142
+ }
143
+
144
+ if (looksEncrypted(text)) {
145
+ setNeedsPassword(true);
146
+ return;
147
+ }
148
+
149
+ setError("Unrecognized backup format");
150
+ }, []);
151
+
152
+ const handleDecrypt = useCallback(async () => {
153
+ if (!backupText || !password) return;
154
+ setDecrypting(true);
155
+ setError("");
156
+
157
+ try {
158
+ const decrypted = await decryptBackup(backupText, password);
159
+ const extracted = extractKeys(decrypted);
160
+ if (!extracted) {
161
+ setError(`Unsupported backup type: ${getBackupType(decrypted)}`);
162
+ return;
163
+ }
164
+ setKeys(extracted);
165
+ setBackupType(getBackupType(decrypted));
166
+ setNeedsPassword(false);
167
+ } catch (e) {
168
+ setError(e instanceof Error ? e.message : "Decryption failed");
169
+ } finally {
170
+ setDecrypting(false);
171
+ }
172
+ }, [backupText, password]);
173
+
174
+ const handleFileUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
175
+ const file = e.target.files?.[0];
176
+ if (!file) return;
177
+ const reader = new FileReader();
178
+ reader.onload = () => {
179
+ processBackupText((reader.result as string).trim());
180
+ };
181
+ reader.readAsText(file);
182
+ e.target.value = "";
183
+ }, [processBackupText]);
184
+
185
+ const handleScan = useCallback(() => {
186
+ if (keys) {
187
+ onScan(keys);
188
+ } else if (mode === "wif") {
189
+ const pay = payWif.trim();
190
+ const ord = sameKey ? pay : ordWif.trim();
191
+ if (pay) onScan({ payPk: pay, ordPk: ord });
192
+ }
193
+ }, [keys, mode, payWif, ordWif, sameKey, onScan]);
194
+
195
+ if (keys) {
196
+ return (
197
+ <Card>
198
+ <CardHeader>
199
+ <div className="flex items-center justify-between">
200
+ <CardTitle className="flex items-center gap-2">
201
+ <KeyRound className="h-5 w-5" />
202
+ Legacy Keys
203
+ </CardTitle>
204
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleReset}>
205
+ <X className="h-3 w-3" />
206
+ </Button>
207
+ </div>
208
+ </CardHeader>
209
+ <CardContent className="space-y-3">
210
+ {backupType && (
211
+ <span className="text-xs px-2 py-0.5 rounded bg-blue-500/20 text-blue-400">{backupType}</span>
212
+ )}
213
+ <div className="space-y-1 text-xs text-muted-foreground font-mono">
214
+ <div>Pay: {deriveAddress(keys.payPk)}</div>
215
+ {keys.ordPk !== keys.payPk && <div>Ord: {deriveAddress(keys.ordPk)}</div>}
216
+ {keys.identityPk && keys.identityPk !== keys.payPk && <div>ID: {deriveAddress(keys.identityPk)}</div>}
217
+ </div>
218
+ <Button onClick={handleScan} disabled={disabled || scanning} className="w-full">
219
+ {scanning ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Search className="h-4 w-4 mr-2" />}
220
+ {scanning ? "Scanning..." : "Scan for Assets"}
221
+ </Button>
222
+ </CardContent>
223
+ </Card>
224
+ );
225
+ }
226
+
227
+ if (mode === "choose") {
228
+ return (
229
+ <Card>
230
+ <CardHeader>
231
+ <CardTitle className="flex items-center gap-2">
232
+ <KeyRound className="h-5 w-5" />
233
+ Legacy Keys
234
+ </CardTitle>
235
+ </CardHeader>
236
+ <CardContent className="space-y-3">
237
+ <input ref={fileInputRef} type="file" accept=".json,.bep,.txt" className="hidden" onChange={handleFileUpload} />
238
+ <Button variant="outline" className="w-full gap-2" onClick={() => fileInputRef.current?.click()}>
239
+ <Upload className="h-4 w-4" />
240
+ Import Backup File
241
+ </Button>
242
+ <Button variant="outline" className="w-full gap-2" onClick={() => setMode("backup")}>
243
+ Paste Backup Data
244
+ </Button>
245
+ <Button variant="ghost" className="w-full text-xs text-muted-foreground" onClick={() => setMode("wif")}>
246
+ Enter WIF keys manually
247
+ </Button>
248
+ </CardContent>
249
+ </Card>
250
+ );
251
+ }
252
+
253
+ if (mode === "backup") {
254
+ return (
255
+ <Card>
256
+ <CardHeader>
257
+ <div className="flex items-center justify-between">
258
+ <CardTitle className="flex items-center gap-2">
259
+ <KeyRound className="h-5 w-5" />
260
+ Import Backup
261
+ </CardTitle>
262
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleReset}>
263
+ <X className="h-3 w-3" />
264
+ </Button>
265
+ </div>
266
+ </CardHeader>
267
+ <CardContent className="space-y-3">
268
+ {!needsPassword ? (
269
+ <>
270
+ <textarea
271
+ placeholder="Paste backup JSON or encrypted data..."
272
+ className="w-full h-24 rounded-md border border-input bg-transparent px-3 py-2 text-sm font-mono resize-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
273
+ onChange={(e) => processBackupText(e.target.value.trim())}
274
+ />
275
+ <input ref={fileInputRef} type="file" accept=".json,.bep,.txt" className="hidden" onChange={handleFileUpload} />
276
+ <Button variant="outline" size="sm" className="w-full gap-2" onClick={() => fileInputRef.current?.click()}>
277
+ <Upload className="h-4 w-4" />
278
+ Or upload a file
279
+ </Button>
280
+ </>
281
+ ) : (
282
+ <>
283
+ <p className="text-sm text-muted-foreground">This backup is encrypted. Enter the password to decrypt.</p>
284
+ <Input type="password" placeholder="Backup password..." value={password} onChange={(e) => setPassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleDecrypt(); }} />
285
+ <Button onClick={handleDecrypt} disabled={!password || decrypting} className="w-full">
286
+ {decrypting ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
287
+ {decrypting ? "Decrypting..." : "Decrypt"}
288
+ </Button>
289
+ </>
290
+ )}
291
+ {error && <p className="text-sm text-destructive">{error}</p>}
292
+ </CardContent>
293
+ </Card>
294
+ );
295
+ }
296
+
297
+ return (
298
+ <Card>
299
+ <CardHeader>
300
+ <div className="flex items-center justify-between">
301
+ <CardTitle className="flex items-center gap-2">
302
+ <KeyRound className="h-5 w-5" />
303
+ Manual WIF Entry
304
+ </CardTitle>
305
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleReset}>
306
+ <X className="h-3 w-3" />
307
+ </Button>
308
+ </div>
309
+ </CardHeader>
310
+ <CardContent className="space-y-4">
311
+ <div className="space-y-2">
312
+ <label className="text-sm font-medium">{sameKey ? "Private Key (WIF)" : "Pay Key (WIF)"}</label>
313
+ <Input type="password" placeholder="Enter WIF private key..." value={payWif} onChange={(e) => setPayWif(e.target.value)} disabled={disabled || scanning} />
314
+ </div>
315
+ <label className="flex items-center gap-2 text-sm">
316
+ <input type="checkbox" checked={sameKey} onChange={(e) => setSameKey(e.target.checked)} disabled={disabled || scanning} />
317
+ Same key for pay and ordinals
318
+ </label>
319
+ {!sameKey && (
320
+ <div className="space-y-2">
321
+ <label className="text-sm font-medium">Ordinals Key (WIF)</label>
322
+ <Input type="password" placeholder="Enter ordinals WIF..." value={ordWif} onChange={(e) => setOrdWif(e.target.value)} disabled={disabled || scanning} />
323
+ </div>
324
+ )}
325
+ <Button onClick={handleScan} disabled={disabled || scanning || !payWif.trim()} className="w-full">
326
+ {scanning ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : <Search className="h-4 w-4 mr-2" />}
327
+ {scanning ? "Scanning..." : "Scan for Assets"}
328
+ </Button>
329
+ </CardContent>
330
+ </Card>
331
+ );
332
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Main app component
2
+ export { SweepApp, type SweepAppProps } from "./components/SweepApp";
3
+
4
+ // Feature components
5
+ export { ConnectWallet } from "./components/connect-wallet";
6
+ export { WifInput } from "./components/wif-input";
7
+ export { FundingSection, OrdinalsSection, Bsv21Section, Bsv20Section, LockedSection } from "./components/asset-preview";
8
+ export { OpnsSection } from "./components/opns-section";
9
+ export { TxHistory, type TxRecord } from "./components/tx-history";
10
+ export { SweepProgress } from "./components/sweep-progress";
11
+
12
+ // UI primitives
13
+ export { Badge, badgeVariants } from "./components/ui/badge";
14
+ export { Button, buttonVariants } from "./components/ui/button";
15
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent } from "./components/ui/card";
16
+ export { Input } from "./components/ui/input";
17
+ export { Tabs, TabsList, TabsTrigger, TabsContent } from "./components/ui/tabs";
18
+
19
+ // Lib
20
+ export { configureServices, getServices } from "./lib/services";
21
+ export { connectWallet, getWallet, getIdentityKey, getProvider, disconnectWallet, isConnected } from "./lib/wallet";
22
+ export { deriveAddress, scanAddress, scanAddresses, type ScannedAssets, type EnrichedOrdinal, type TokenBalance, type ScanProgress } from "./lib/scanner";
23
+ export { executeSweep, type SweepResult } from "./lib/sweeper";
24
+ export { legacySendBsv, legacySendOrdinals, legacyBurnOrdinals, type LegacySendResult } from "./lib/legacy-send";
25
+ export { cn, formatSats, formatTokenAmount, truncate } from "./lib/utils";
26
+
27
+ // Types
28
+ export type { LegacyKeys } from "./types";
@@ -0,0 +1,234 @@
1
+ import { parseOutpoint } from "@1sat/utils";
2
+ import type { IndexedOutput } from "@1sat/types";
3
+ import { MAP_PREFIX } from "@1sat/types";
4
+ import { OP, P2PKH, PrivateKey, Script, Transaction, Utils } from "@bsv/sdk";
5
+ import { deriveAddress } from "./scanner";
6
+ import { getServices } from "./services";
7
+ import type { LegacyKeys } from "../types";
8
+
9
+ export interface LegacySendResult {
10
+ txid: string;
11
+ rawtx: string;
12
+ }
13
+
14
+ async function fetchSourceTx(txid: string): Promise<Transaction> {
15
+ const services = getServices();
16
+ const beef = await services.getBeefForTxid(txid);
17
+ const found = beef.findTxid(txid);
18
+ if (!found?.tx) throw new Error(`Transaction ${txid} not found in BEEF`);
19
+ return found.tx;
20
+ }
21
+
22
+ function buildKeyMap(keys: LegacyKeys): Map<string, PrivateKey> {
23
+ const map = new Map<string, PrivateKey>();
24
+ const payKey = PrivateKey.fromWif(keys.payPk);
25
+ const ordKey = PrivateKey.fromWif(keys.ordPk);
26
+ map.set(deriveAddress(keys.payPk), payKey);
27
+ map.set(deriveAddress(keys.ordPk), ordKey);
28
+ if (keys.identityPk) {
29
+ map.set(deriveAddress(keys.identityPk), PrivateKey.fromWif(keys.identityPk));
30
+ }
31
+ return map;
32
+ }
33
+
34
+ function keyForOutput(output: IndexedOutput, keyMap: Map<string, PrivateKey>, fallback: PrivateKey): PrivateKey {
35
+ if (output.events) {
36
+ for (const event of output.events) {
37
+ if (event.startsWith("own:") || event.startsWith("p2pkh:")) {
38
+ const addr = event.split(":")[1];
39
+ const key = keyMap.get(addr);
40
+ if (key) return key;
41
+ }
42
+ }
43
+ }
44
+ return fallback;
45
+ }
46
+
47
+ export async function legacySendBsv(params: {
48
+ funding: IndexedOutput[];
49
+ keys: LegacyKeys;
50
+ destination: string;
51
+ amount?: number;
52
+ }): Promise<LegacySendResult> {
53
+ const { funding, keys, destination, amount } = params;
54
+ if (!funding.length) throw new Error("No funding UTXOs");
55
+ if (!destination) throw new Error("No destination address");
56
+
57
+ const keyMap = buildKeyMap(keys);
58
+ const payKey = PrivateKey.fromWif(keys.payPk);
59
+ const sourceAddress = payKey.toPublicKey().toAddress();
60
+ const p2pkh = new P2PKH();
61
+ const tx = new Transaction();
62
+
63
+ for (const utxo of funding) {
64
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
65
+ const key = keyForOutput(utxo, keyMap, payKey);
66
+ tx.addInput({
67
+ sourceTXID: txid,
68
+ sourceOutputIndex: vout,
69
+ sourceTransaction: await fetchSourceTx(txid),
70
+ unlockingScriptTemplate: p2pkh.unlock(key),
71
+ sequence: 0xffffffff,
72
+ });
73
+ }
74
+
75
+ if (amount) {
76
+ tx.addOutput({
77
+ lockingScript: p2pkh.lock(destination),
78
+ satoshis: amount,
79
+ });
80
+ tx.addOutput({
81
+ lockingScript: p2pkh.lock(sourceAddress),
82
+ change: true,
83
+ });
84
+ } else {
85
+ tx.addOutput({
86
+ lockingScript: p2pkh.lock(destination),
87
+ change: true,
88
+ });
89
+ }
90
+
91
+ await tx.fee();
92
+ await tx.sign();
93
+
94
+ const rawTx = tx.toBinary();
95
+ const result = await getServices().arcade.submitTransaction(rawTx);
96
+
97
+ return {
98
+ txid: result.txid,
99
+ rawtx: Utils.toHex(rawTx),
100
+ };
101
+ }
102
+
103
+ export async function legacySendOrdinals(params: {
104
+ ordinals: IndexedOutput[];
105
+ funding: IndexedOutput[];
106
+ keys: LegacyKeys;
107
+ destination: string;
108
+ }): Promise<LegacySendResult> {
109
+ const { ordinals, funding, keys, destination } = params;
110
+ if (!ordinals.length) throw new Error("No ordinals to send");
111
+ if (!funding.length) throw new Error("No funding UTXOs for fees");
112
+ if (!destination) throw new Error("No destination address");
113
+
114
+ const keyMap = buildKeyMap(keys);
115
+ const payKey = PrivateKey.fromWif(keys.payPk);
116
+ const sourceAddress = payKey.toPublicKey().toAddress();
117
+ const p2pkh = new P2PKH();
118
+ const tx = new Transaction();
119
+
120
+ for (const ord of ordinals) {
121
+ const { txid, vout } = parseOutpoint(ord.outpoint);
122
+ const key = keyForOutput(ord, keyMap, payKey);
123
+ tx.addInput({
124
+ sourceTXID: txid,
125
+ sourceOutputIndex: vout,
126
+ sourceTransaction: await fetchSourceTx(txid),
127
+ unlockingScriptTemplate: p2pkh.unlock(key),
128
+ sequence: 0xffffffff,
129
+ });
130
+ }
131
+
132
+ for (const _ord of ordinals) {
133
+ tx.addOutput({
134
+ lockingScript: p2pkh.lock(destination),
135
+ satoshis: 1,
136
+ });
137
+ }
138
+
139
+ for (const utxo of funding) {
140
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
141
+ const key = keyForOutput(utxo, keyMap, payKey);
142
+ tx.addInput({
143
+ sourceTXID: txid,
144
+ sourceOutputIndex: vout,
145
+ sourceTransaction: await fetchSourceTx(txid),
146
+ unlockingScriptTemplate: p2pkh.unlock(key),
147
+ sequence: 0xffffffff,
148
+ });
149
+ }
150
+
151
+ tx.addOutput({
152
+ lockingScript: p2pkh.lock(sourceAddress),
153
+ change: true,
154
+ });
155
+
156
+ await tx.fee();
157
+ await tx.sign();
158
+
159
+ const rawTx = tx.toBinary();
160
+ const result = await getServices().arcade.submitTransaction(rawTx);
161
+
162
+ return {
163
+ txid: result.txid,
164
+ rawtx: Utils.toHex(rawTx),
165
+ };
166
+ }
167
+
168
+ export async function legacyBurnOrdinals(params: {
169
+ ordinals: IndexedOutput[];
170
+ funding: IndexedOutput[];
171
+ keys: LegacyKeys;
172
+ }): Promise<LegacySendResult> {
173
+ const { ordinals, funding, keys } = params;
174
+ if (!ordinals.length) throw new Error("No ordinals to burn");
175
+
176
+ const keyMap = buildKeyMap(keys);
177
+ const payKey = PrivateKey.fromWif(keys.payPk);
178
+ const sourceAddress = payKey.toPublicKey().toAddress();
179
+ const p2pkh = new P2PKH();
180
+ const tx = new Transaction();
181
+
182
+ for (const ord of ordinals) {
183
+ const { txid, vout } = parseOutpoint(ord.outpoint);
184
+ const key = keyForOutput(ord, keyMap, payKey);
185
+ tx.addInput({
186
+ sourceTXID: txid,
187
+ sourceOutputIndex: vout,
188
+ sourceTransaction: await fetchSourceTx(txid),
189
+ unlockingScriptTemplate: p2pkh.unlock(key),
190
+ sequence: 0xffffffff,
191
+ });
192
+ }
193
+
194
+ for (const utxo of funding) {
195
+ const { txid, vout } = parseOutpoint(utxo.outpoint);
196
+ const key = keyForOutput(utxo, keyMap, payKey);
197
+ tx.addInput({
198
+ sourceTXID: txid,
199
+ sourceOutputIndex: vout,
200
+ sourceTransaction: await fetchSourceTx(txid),
201
+ unlockingScriptTemplate: p2pkh.unlock(key),
202
+ sequence: 0xffffffff,
203
+ });
204
+ }
205
+
206
+ const burnScript = new Script()
207
+ .writeOpCode(OP.OP_FALSE)
208
+ .writeOpCode(OP.OP_RETURN)
209
+ .writeBin(Utils.toArray(MAP_PREFIX))
210
+ .writeBin(Utils.toArray("SET"))
211
+ .writeBin(Utils.toArray("app"))
212
+ .writeBin(Utils.toArray("1sat-sweep"))
213
+ .writeBin(Utils.toArray("type"))
214
+ .writeBin(Utils.toArray("ord"))
215
+ .writeBin(Utils.toArray("op"))
216
+ .writeBin(Utils.toArray("burn"));
217
+ tx.addOutput({ satoshis: 0, lockingScript: burnScript });
218
+
219
+ tx.addOutput({
220
+ lockingScript: p2pkh.lock(sourceAddress),
221
+ change: true,
222
+ });
223
+
224
+ await tx.fee();
225
+ await tx.sign();
226
+
227
+ const rawTx = tx.toBinary();
228
+ const result = await getServices().arcade.submitTransaction(rawTx);
229
+
230
+ return {
231
+ txid: result.txid,
232
+ rawtx: Utils.toHex(rawTx),
233
+ };
234
+ }