@1sat/cli 0.0.114 → 0.0.115
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/README.md +17 -4
- package/dist/cli.js +362 -122
- package/package.json +3 -3
- package/src/commands/sweep-classes.ts +64 -0
- package/src/commands/sweep-plan.ts +134 -0
- package/src/commands/sweep.ts +258 -152
- package/src/help.ts +33 -4
package/README.md
CHANGED
|
@@ -232,14 +232,27 @@ OpNS (Ordinals Name System) binds your BAP identity to an on-chain name inscript
|
|
|
232
232
|
|
|
233
233
|
### Sweep
|
|
234
234
|
|
|
235
|
-
Import assets from an external private key (
|
|
235
|
+
Import assets from an external private key (legacy P2PKH / Yours backup WIF) into the BRC-100 wallet.
|
|
236
236
|
|
|
237
237
|
```bash
|
|
238
|
-
1sat sweep scan --wif <key>
|
|
239
|
-
1sat sweep import --wif <key>
|
|
238
|
+
1sat sweep scan --wif <key>
|
|
239
|
+
1sat sweep import --wif <key> --dry-run
|
|
240
|
+
1sat sweep import --wif <key>
|
|
241
|
+
1sat sweep import --wif <key> --only opns,bsv20
|
|
242
|
+
1sat sweep import --wif <key> --skip bsv21
|
|
240
243
|
```
|
|
241
244
|
|
|
242
|
-
`scan` is
|
|
245
|
+
`scan` is the full inventory. `import --dry-run` is the same plan after `--only` / `--skip`. `import` with no class flags attempts every class that can move into BRC-100:
|
|
246
|
+
|
|
247
|
+
| Class | Destination |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `bsv` | Funding |
|
|
250
|
+
| `ordinals` | `1sat` basket |
|
|
251
|
+
| `opns` | `opns` basket |
|
|
252
|
+
| `bsv20` | `bsv20` basket (ticker identity) |
|
|
253
|
+
| `bsv21` | `bsv21` basket (deploy-outpoint identity) |
|
|
254
|
+
|
|
255
|
+
Listed OrdLocks cancel into the destination in the same transaction. Time-locks and RUN outputs stay as leftover. `--only` and `--skip` take `bsv,ordinals,opns,bsv20,bsv21`. Confirm when prompted, or pass `--yes`.
|
|
243
256
|
|
|
244
257
|
---
|
|
245
258
|
|
package/dist/cli.js
CHANGED
|
@@ -1328,13 +1328,41 @@ var init_help = __esm(() => {
|
|
|
1328
1328
|
subcommands: [
|
|
1329
1329
|
{
|
|
1330
1330
|
name: "scan",
|
|
1331
|
-
description: "
|
|
1332
|
-
args: [
|
|
1331
|
+
description: "Show every UTXO class at an address (BSV, ordinals, OpNS, BSV-20, BSV-21, leftover locks/RUN)",
|
|
1332
|
+
args: [
|
|
1333
|
+
{ flag: "--wif", values: "<key>", required: true },
|
|
1334
|
+
{
|
|
1335
|
+
flag: "--only",
|
|
1336
|
+
values: "<bsv,ordinals,opns,bsv20,bsv21>",
|
|
1337
|
+
description: "Limit the report to these classes"
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
flag: "--skip",
|
|
1341
|
+
values: "<class,...>",
|
|
1342
|
+
description: "Omit classes from the report"
|
|
1343
|
+
}
|
|
1344
|
+
]
|
|
1333
1345
|
},
|
|
1334
1346
|
{
|
|
1335
1347
|
name: "import",
|
|
1336
|
-
description: "Sweep UTXOs from a WIF into the wallet",
|
|
1337
|
-
args: [
|
|
1348
|
+
description: "Sweep transferable UTXOs from a WIF into the wallet. Default is all classes we can move to BRC-100 (including listed OrdLocks). Locks and RUN stay.",
|
|
1349
|
+
args: [
|
|
1350
|
+
{ flag: "--wif", values: "<key>", required: true },
|
|
1351
|
+
{
|
|
1352
|
+
flag: "--only",
|
|
1353
|
+
values: "<bsv,ordinals,opns,bsv20,bsv21>",
|
|
1354
|
+
description: "Sweep only these classes"
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
flag: "--skip",
|
|
1358
|
+
values: "<class,...>",
|
|
1359
|
+
description: "Skip these classes"
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
flag: "--dry-run",
|
|
1363
|
+
description: "Print the plan and do not broadcast"
|
|
1364
|
+
}
|
|
1365
|
+
]
|
|
1338
1366
|
}
|
|
1339
1367
|
]
|
|
1340
1368
|
},
|
|
@@ -3964,11 +3992,146 @@ var init_storage = __esm(() => {
|
|
|
3964
3992
|
init_output();
|
|
3965
3993
|
});
|
|
3966
3994
|
|
|
3995
|
+
// src/commands/sweep-classes.ts
|
|
3996
|
+
function parseSweepClasses(raw, flag) {
|
|
3997
|
+
if (!raw)
|
|
3998
|
+
return new Set;
|
|
3999
|
+
const selected = new Set;
|
|
4000
|
+
for (const token of raw.split(",").map((t) => t.trim().toLowerCase())) {
|
|
4001
|
+
if (!token)
|
|
4002
|
+
continue;
|
|
4003
|
+
const mapped = ALIASES[token];
|
|
4004
|
+
if (!mapped) {
|
|
4005
|
+
throw new Error(`Unknown ${flag} class '${token}'. Use: ${SWEEP_CLASSES.join(", ")}`);
|
|
4006
|
+
}
|
|
4007
|
+
selected.add(mapped);
|
|
4008
|
+
}
|
|
4009
|
+
return selected;
|
|
4010
|
+
}
|
|
4011
|
+
function selectedSweepClasses(only, skip) {
|
|
4012
|
+
if (only && skip) {
|
|
4013
|
+
throw new Error("--only and --skip are mutually exclusive");
|
|
4014
|
+
}
|
|
4015
|
+
if (only) {
|
|
4016
|
+
const selected2 = parseSweepClasses(only, "--only");
|
|
4017
|
+
if (selected2.size === 0) {
|
|
4018
|
+
throw new Error("--only requires at least one class");
|
|
4019
|
+
}
|
|
4020
|
+
return selected2;
|
|
4021
|
+
}
|
|
4022
|
+
const selected = new Set(SWEEP_CLASSES);
|
|
4023
|
+
if (skip) {
|
|
4024
|
+
for (const cls of parseSweepClasses(skip, "--skip"))
|
|
4025
|
+
selected.delete(cls);
|
|
4026
|
+
}
|
|
4027
|
+
return selected;
|
|
4028
|
+
}
|
|
4029
|
+
var SWEEP_CLASSES, ALIASES;
|
|
4030
|
+
var init_sweep_classes = __esm(() => {
|
|
4031
|
+
SWEEP_CLASSES = [
|
|
4032
|
+
"bsv",
|
|
4033
|
+
"ordinals",
|
|
4034
|
+
"opns",
|
|
4035
|
+
"bsv20",
|
|
4036
|
+
"bsv21"
|
|
4037
|
+
];
|
|
4038
|
+
ALIASES = {
|
|
4039
|
+
bsv: "bsv",
|
|
4040
|
+
funding: "bsv",
|
|
4041
|
+
ordinals: "ordinals",
|
|
4042
|
+
ord: "ordinals",
|
|
4043
|
+
opns: "opns",
|
|
4044
|
+
names: "opns",
|
|
4045
|
+
bsv20: "bsv20",
|
|
4046
|
+
bsv21: "bsv21"
|
|
4047
|
+
};
|
|
4048
|
+
});
|
|
4049
|
+
|
|
4050
|
+
// src/commands/sweep-plan.ts
|
|
4051
|
+
import {
|
|
4052
|
+
groupBsv20Tokens,
|
|
4053
|
+
isListedOutput
|
|
4054
|
+
} from "@1sat/actions";
|
|
4055
|
+
function listedCount(outputs) {
|
|
4056
|
+
return outputs.filter((o) => isListedOutput(o)).length;
|
|
4057
|
+
}
|
|
4058
|
+
function buildSweepPlan(scan, classes) {
|
|
4059
|
+
const want = (cls) => classes.has(cls);
|
|
4060
|
+
const groupedBsv20 = groupBsv20Tokens(scan.bsv20Tokens);
|
|
4061
|
+
const parsedOutpoints = new Set(groupedBsv20.flatMap((g) => g.outputs.map((o) => o.outpoint)));
|
|
4062
|
+
const transferableBsv21 = scan.bsv21Tokens.filter((t) => t.outputs.length > 0);
|
|
4063
|
+
const skippedBsv21 = scan.bsv21Tokens.filter((t) => t.outputs.length === 0);
|
|
4064
|
+
return {
|
|
4065
|
+
classes: SWEEP_CLASSES.filter((c) => classes.has(c)),
|
|
4066
|
+
bsv: want("bsv") ? { count: scan.funding.length, sats: scan.totalFundingSats } : { count: 0, sats: 0 },
|
|
4067
|
+
ordinals: want("ordinals") ? {
|
|
4068
|
+
count: scan.ordinals.length,
|
|
4069
|
+
listed: listedCount(scan.ordinals)
|
|
4070
|
+
} : { count: 0, listed: 0 },
|
|
4071
|
+
opns: want("opns") ? {
|
|
4072
|
+
count: scan.opnsNames.length,
|
|
4073
|
+
listed: listedCount(scan.opnsNames)
|
|
4074
|
+
} : { count: 0, listed: 0 },
|
|
4075
|
+
bsv20: want("bsv20") ? groupedBsv20.map((g) => ({
|
|
4076
|
+
tick: g.tick,
|
|
4077
|
+
amount: g.totalAmount.toString(),
|
|
4078
|
+
count: g.outputs.length,
|
|
4079
|
+
listed: listedCount(g.outputs)
|
|
4080
|
+
})) : [],
|
|
4081
|
+
bsv21: want("bsv21") ? transferableBsv21.map((t) => ({
|
|
4082
|
+
tokenId: t.tokenId,
|
|
4083
|
+
symbol: t.symbol,
|
|
4084
|
+
amount: t.totalAmount.toString(),
|
|
4085
|
+
count: t.outputs.length,
|
|
4086
|
+
listed: listedCount(t.outputs),
|
|
4087
|
+
active: t.isActive
|
|
4088
|
+
})) : [],
|
|
4089
|
+
leftover: {
|
|
4090
|
+
locked: scan.locked.length,
|
|
4091
|
+
run: scan.run.length,
|
|
4092
|
+
inactiveBsv21: skippedBsv21.reduce((n, t) => n + t.outputs.length, 0),
|
|
4093
|
+
unparsedBsv20: scan.bsv20Tokens.filter((o) => !parsedOutpoints.has(o.outpoint)).length
|
|
4094
|
+
}
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
function planHasWork(plan) {
|
|
4098
|
+
return plan.bsv.count > 0 || plan.ordinals.count > 0 || plan.opns.count > 0 || plan.bsv20.length > 0 || plan.bsv21.length > 0;
|
|
4099
|
+
}
|
|
4100
|
+
function describePlan(plan) {
|
|
4101
|
+
const parts = [];
|
|
4102
|
+
if (plan.bsv.count)
|
|
4103
|
+
parts.push(`${plan.bsv.sats} sats (${plan.bsv.count} UTXOs)`);
|
|
4104
|
+
if (plan.ordinals.count) {
|
|
4105
|
+
const listed = plan.ordinals.listed ? `, ${plan.ordinals.listed} listed` : "";
|
|
4106
|
+
parts.push(`${plan.ordinals.count} ordinal(s)${listed}`);
|
|
4107
|
+
}
|
|
4108
|
+
if (plan.opns.count) {
|
|
4109
|
+
const listed = plan.opns.listed ? `, ${plan.opns.listed} listed` : "";
|
|
4110
|
+
parts.push(`${plan.opns.count} OpNS name(s)${listed}`);
|
|
4111
|
+
}
|
|
4112
|
+
for (const t of plan.bsv20) {
|
|
4113
|
+
const listed = t.listed ? `, ${t.listed} listed` : "";
|
|
4114
|
+
parts.push(`${t.amount} ${t.tick} (${t.count} UTXOs${listed})`);
|
|
4115
|
+
}
|
|
4116
|
+
for (const t of plan.bsv21) {
|
|
4117
|
+
const listed = t.listed ? `, ${t.listed} listed` : "";
|
|
4118
|
+
parts.push(`${t.amount} ${t.symbol ?? t.tokenId.slice(0, 12)} (${t.count} UTXOs${listed})`);
|
|
4119
|
+
}
|
|
4120
|
+
return parts;
|
|
4121
|
+
}
|
|
4122
|
+
var init_sweep_plan = __esm(() => {
|
|
4123
|
+
init_sweep_classes();
|
|
4124
|
+
});
|
|
4125
|
+
|
|
3967
4126
|
// src/commands/sweep.ts
|
|
3968
4127
|
import {
|
|
4128
|
+
SWEEP_BATCH_SIZE,
|
|
4129
|
+
bsv21SweepBatches,
|
|
4130
|
+
groupBsv20Tokens as groupBsv20Tokens2,
|
|
3969
4131
|
prepareSweepInputs,
|
|
3970
4132
|
scanAddress,
|
|
3971
4133
|
sweepBsv,
|
|
4134
|
+
sweepBsv20,
|
|
3972
4135
|
sweepBsv21,
|
|
3973
4136
|
sweepOrdinals
|
|
3974
4137
|
} from "@1sat/actions";
|
|
@@ -3988,176 +4151,253 @@ async function handleSweepCommand(args, opts) {
|
|
|
3988
4151
|
}
|
|
3989
4152
|
}
|
|
3990
4153
|
}
|
|
3991
|
-
|
|
4154
|
+
function loadSweepKey(args) {
|
|
3992
4155
|
const wif = extractFlag(args, "--wif");
|
|
3993
4156
|
if (!wif)
|
|
3994
4157
|
fatal("Missing --wif <private-key>");
|
|
3995
|
-
let address;
|
|
3996
4158
|
try {
|
|
3997
4159
|
const pk = PrivateKey4.fromWif(wif);
|
|
3998
|
-
address
|
|
4160
|
+
return { wif, address: pk.toPublicKey().toAddress() };
|
|
3999
4161
|
} catch {
|
|
4000
4162
|
fatal("Invalid WIF private key");
|
|
4001
4163
|
}
|
|
4164
|
+
}
|
|
4165
|
+
function loadClasses(args) {
|
|
4166
|
+
try {
|
|
4167
|
+
return selectedSweepClasses(extractFlag(args, "--only"), extractFlag(args, "--skip"));
|
|
4168
|
+
} catch (error) {
|
|
4169
|
+
fatal(error instanceof Error ? error.message : String(error));
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
function leftoverLines(plan) {
|
|
4173
|
+
const lines = [];
|
|
4174
|
+
if (plan.leftover.locked)
|
|
4175
|
+
lines.push(`${plan.leftover.locked} locked output(s) (not swept)`);
|
|
4176
|
+
if (plan.leftover.run)
|
|
4177
|
+
lines.push(`${plan.leftover.run} RUN token output(s) (not sweepable)`);
|
|
4178
|
+
if (plan.leftover.inactiveBsv21)
|
|
4179
|
+
lines.push(`${plan.leftover.inactiveBsv21} BSV-21 UTXO(s) with no parseable amount`);
|
|
4180
|
+
if (plan.leftover.unparsedBsv20)
|
|
4181
|
+
lines.push(`${plan.leftover.unparsedBsv20} BSV-20 UTXO(s) with no parseable tick/amt`);
|
|
4182
|
+
return lines;
|
|
4183
|
+
}
|
|
4184
|
+
function printInventory(address, plan, opts, mode) {
|
|
4185
|
+
if (opts.json) {
|
|
4186
|
+
output({
|
|
4187
|
+
address,
|
|
4188
|
+
mode,
|
|
4189
|
+
classes: plan.classes,
|
|
4190
|
+
wouldSweep: {
|
|
4191
|
+
bsv: plan.bsv,
|
|
4192
|
+
ordinals: plan.ordinals,
|
|
4193
|
+
opns: plan.opns,
|
|
4194
|
+
bsv20: plan.bsv20,
|
|
4195
|
+
bsv21: plan.bsv21
|
|
4196
|
+
},
|
|
4197
|
+
leftover: plan.leftover
|
|
4198
|
+
}, opts);
|
|
4199
|
+
return;
|
|
4200
|
+
}
|
|
4201
|
+
const title = mode === "dry-run" ? "Dry run" : "Scan";
|
|
4202
|
+
console.log(`
|
|
4203
|
+
${title} for ${formatValue(address)}
|
|
4204
|
+
`);
|
|
4205
|
+
console.log(` ${formatLabel("Classes:")} ${formatValue(plan.classes.join(", ") || "(none)")}`);
|
|
4206
|
+
console.log(`
|
|
4207
|
+
${formatLabel("Funding UTXOs:")} ${plan.bsv.count}`);
|
|
4208
|
+
if (plan.bsv.count > 0) {
|
|
4209
|
+
console.log(` ${formatLabel("Total funding:")} ${formatValue(plan.bsv.sats)} satoshis`);
|
|
4210
|
+
}
|
|
4211
|
+
console.log(`
|
|
4212
|
+
${formatLabel("Ordinal UTXOs:")} ${plan.ordinals.count}`);
|
|
4213
|
+
if (plan.ordinals.listed)
|
|
4214
|
+
console.log(` ${formatLabel("Listed:")} ${plan.ordinals.listed}`);
|
|
4215
|
+
console.log(`
|
|
4216
|
+
${formatLabel("OpNS names:")} ${plan.opns.count}`);
|
|
4217
|
+
if (plan.opns.listed)
|
|
4218
|
+
console.log(` ${formatLabel("Listed:")} ${plan.opns.listed}`);
|
|
4219
|
+
console.log(`
|
|
4220
|
+
${formatLabel("BSV-20:")} ${plan.bsv20.length} ticker(s)`);
|
|
4221
|
+
for (const t of plan.bsv20) {
|
|
4222
|
+
const listed = t.listed ? ` ${formatLabel(`listed ${t.listed}`)}` : "";
|
|
4223
|
+
console.log(` ${formatValue(t.tick)} ${formatLabel("amount:")} ${formatValue(t.amount)} ${formatLabel("UTXOs:")} ${t.count}${listed}`);
|
|
4224
|
+
}
|
|
4225
|
+
console.log(`
|
|
4226
|
+
${formatLabel("BSV-21:")} ${plan.bsv21.length} token(s)`);
|
|
4227
|
+
for (const t of plan.bsv21) {
|
|
4228
|
+
const listed = t.listed ? ` ${formatLabel(`listed ${t.listed}`)}` : "";
|
|
4229
|
+
console.log(` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel("amount:")} ${formatValue(t.amount)} ${formatLabel("UTXOs:")} ${t.count}${listed}`);
|
|
4230
|
+
}
|
|
4231
|
+
const leftover = leftoverLines(plan);
|
|
4232
|
+
if (leftover.length) {
|
|
4233
|
+
console.log(`
|
|
4234
|
+
${formatLabel("Leftover:")}`);
|
|
4235
|
+
for (const line of leftover)
|
|
4236
|
+
console.log(` ${formatWarning(line)}`);
|
|
4237
|
+
}
|
|
4238
|
+
const transferable = plan.bsv.count + plan.ordinals.count + plan.opns.count + plan.bsv20.reduce((n, t) => n + t.count, 0) + plan.bsv21.reduce((n, t) => n + t.count, 0);
|
|
4239
|
+
console.log(`
|
|
4240
|
+
${transferable} transferable UTXO(s) in selected classes.`);
|
|
4241
|
+
}
|
|
4242
|
+
async function sweepScan(args, opts) {
|
|
4243
|
+
const { address } = loadSweepKey(args);
|
|
4244
|
+
const classes = loadClasses(args);
|
|
4002
4245
|
const privateKey = await loadKey();
|
|
4003
4246
|
const { ctx, destroy } = await loadContext(privateKey, {
|
|
4004
4247
|
chain: opts.chain
|
|
4005
4248
|
});
|
|
4006
4249
|
try {
|
|
4007
|
-
if (!ctx.services)
|
|
4250
|
+
if (!ctx.services)
|
|
4008
4251
|
fatal("Services required for sweep scan");
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
if (opts.json) {
|
|
4012
|
-
output(result, opts);
|
|
4013
|
-
return;
|
|
4014
|
-
}
|
|
4015
|
-
console.log(`
|
|
4016
|
-
Scan results for ${formatValue(address)}:
|
|
4017
|
-
`);
|
|
4018
|
-
console.log(` ${formatLabel("Funding UTXOs:")} ${result.funding.length}`);
|
|
4019
|
-
if (result.funding.length > 0) {
|
|
4020
|
-
console.log(` ${formatLabel("Total funding:")} ${formatValue(result.totalFundingSats)} satoshis`);
|
|
4021
|
-
for (const f of result.funding) {
|
|
4022
|
-
console.log(` ${formatValue(f.outpoint)} ${formatLabel(`${f.satoshis ?? 0} sats`)}`);
|
|
4023
|
-
}
|
|
4024
|
-
}
|
|
4025
|
-
console.log(`
|
|
4026
|
-
${formatLabel("Ordinal UTXOs:")} ${result.ordinals.length}`);
|
|
4027
|
-
if (result.ordinals.length > 0) {
|
|
4028
|
-
for (const o of result.ordinals) {
|
|
4029
|
-
console.log(` ${formatValue(o.outpoint)}`);
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
console.log(`
|
|
4033
|
-
${formatLabel("BSV-21 Tokens:")} ${result.bsv21Tokens.length}`);
|
|
4034
|
-
if (result.bsv21Tokens.length > 0) {
|
|
4035
|
-
for (const t of result.bsv21Tokens) {
|
|
4036
|
-
console.log(` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel("amount:")} ${formatValue(t.totalAmount.toString())} ${formatLabel("UTXOs:")} ${t.outputs.length} ${formatLabel("dec:")} ${t.decimals}`);
|
|
4037
|
-
}
|
|
4038
|
-
}
|
|
4039
|
-
console.log(`
|
|
4040
|
-
${formatLabel("RUN Tokens:")} ${result.run.length}`);
|
|
4041
|
-
if (result.run.length > 0) {
|
|
4042
|
-
const runSats = result.run.reduce((sum, r) => sum + (r.satoshis ?? 0), 0);
|
|
4043
|
-
console.log(` ${formatLabel("Total locked:")} ${formatValue(runSats)} satoshis (not sweepable)`);
|
|
4044
|
-
for (const r of result.run) {
|
|
4045
|
-
console.log(` ${formatValue(r.outpoint)} ${formatLabel(`${r.satoshis ?? 0} sats`)}`);
|
|
4046
|
-
}
|
|
4047
|
-
}
|
|
4048
|
-
const total = result.funding.length + result.ordinals.length + result.bsv21Tokens.reduce((n, t) => n + t.outputs.length, 0);
|
|
4049
|
-
console.log(`
|
|
4050
|
-
${total} sweepable UTXO(s) found.`);
|
|
4051
|
-
if (result.run.length > 0) {
|
|
4052
|
-
console.log(` ${result.run.length} RUN token output(s) excluded.`);
|
|
4053
|
-
}
|
|
4252
|
+
const scan = await scanAddress(ctx.services, address);
|
|
4253
|
+
printInventory(address, buildSweepPlan(scan, classes), opts, "scan");
|
|
4054
4254
|
} finally {
|
|
4055
4255
|
await destroy();
|
|
4056
4256
|
}
|
|
4057
4257
|
}
|
|
4058
4258
|
async function sweepImport(args, opts) {
|
|
4059
|
-
const wif =
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
let sweepKey;
|
|
4064
|
-
try {
|
|
4065
|
-
sweepKey = PrivateKey4.fromWif(wif);
|
|
4066
|
-
address = sweepKey.toPublicKey().toAddress();
|
|
4067
|
-
} catch {
|
|
4068
|
-
fatal("Invalid WIF private key");
|
|
4069
|
-
}
|
|
4259
|
+
const { wif, address } = loadSweepKey(args);
|
|
4260
|
+
const classes = loadClasses(args);
|
|
4261
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
4262
|
+
const sweepKey = PrivateKey4.fromWif(wif);
|
|
4070
4263
|
const privateKey = await loadKey();
|
|
4071
4264
|
const { ctx, destroy } = await loadContext(privateKey, {
|
|
4072
4265
|
chain: opts.chain
|
|
4073
4266
|
});
|
|
4267
|
+
let failed = false;
|
|
4074
4268
|
try {
|
|
4075
|
-
if (!ctx.services)
|
|
4269
|
+
if (!ctx.services)
|
|
4076
4270
|
fatal("Services required for sweep import");
|
|
4077
|
-
}
|
|
4078
4271
|
const scan = await scanAddress(ctx.services, address);
|
|
4079
|
-
const
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
if (scan.run.length > 0) {
|
|
4084
|
-
fatal(`No sweepable UTXOs found at ${address} (${scan.run.length} RUN token output(s) excluded)`);
|
|
4085
|
-
}
|
|
4086
|
-
fatal(`No UTXOs found at ${address}`);
|
|
4087
|
-
}
|
|
4088
|
-
const parts = [];
|
|
4089
|
-
if (hasFunding)
|
|
4090
|
-
parts.push(`${scan.totalFundingSats} sats (${scan.funding.length} UTXOs)`);
|
|
4091
|
-
if (hasOrdinals)
|
|
4092
|
-
parts.push(`${scan.ordinals.length} ordinal(s)`);
|
|
4093
|
-
if (hasTokens) {
|
|
4094
|
-
for (const t of scan.bsv21Tokens) {
|
|
4095
|
-
if (t.isActive && t.outputs.length > 0) {
|
|
4096
|
-
parts.push(`${t.totalAmount} ${t.symbol ?? t.tokenId.slice(0, 12)} token(s)`);
|
|
4097
|
-
}
|
|
4098
|
-
}
|
|
4272
|
+
const plan = buildSweepPlan(scan, classes);
|
|
4273
|
+
if (dryRun) {
|
|
4274
|
+
printInventory(address, plan, opts, "dry-run");
|
|
4275
|
+
return;
|
|
4099
4276
|
}
|
|
4100
|
-
if (
|
|
4101
|
-
|
|
4277
|
+
if (!planHasWork(plan)) {
|
|
4278
|
+
const leftover = leftoverLines(plan);
|
|
4279
|
+
const extra = leftover.length ? ` (${leftover.join("; ")})` : "";
|
|
4280
|
+
fatal(`No transferable UTXOs found at ${address}${extra}`);
|
|
4102
4281
|
}
|
|
4282
|
+
const parts = describePlan(plan);
|
|
4283
|
+
for (const line of leftoverLines(plan))
|
|
4284
|
+
parts.push(line);
|
|
4103
4285
|
if (!opts.yes) {
|
|
4104
4286
|
const ok = await confirm7({
|
|
4105
4287
|
message: `Sweep ${parts.join(", ")} from ${address}?`
|
|
4106
4288
|
});
|
|
4107
|
-
if (isCancel8(ok) || !ok)
|
|
4289
|
+
if (isCancel8(ok) || !ok)
|
|
4108
4290
|
fatal("Sweep cancelled.");
|
|
4109
|
-
}
|
|
4110
4291
|
}
|
|
4111
4292
|
const txids = [];
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4293
|
+
const errors = [];
|
|
4294
|
+
const run = async (label, fn) => {
|
|
4295
|
+
try {
|
|
4296
|
+
const result = await fn();
|
|
4297
|
+
if (result.error) {
|
|
4298
|
+
errors.push(`${label}: ${result.error}`);
|
|
4299
|
+
return false;
|
|
4300
|
+
}
|
|
4301
|
+
if (result.txid)
|
|
4302
|
+
txids.push(result.txid);
|
|
4303
|
+
return true;
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
4306
|
+
return false;
|
|
4307
|
+
}
|
|
4308
|
+
};
|
|
4309
|
+
const keysFor = (inputs) => inputs.map(() => sweepKey);
|
|
4310
|
+
const runOrdinalBatches = async (label, outputs) => {
|
|
4311
|
+
for (let offset = 0;offset < outputs.length; offset += SWEEP_BATCH_SIZE) {
|
|
4312
|
+
const batch = outputs.slice(offset, offset + SWEEP_BATCH_SIZE);
|
|
4313
|
+
const ok = await run(outputs.length <= SWEEP_BATCH_SIZE ? label : `${label} (${offset + 1}–${offset + batch.length} of ${outputs.length})`, async () => {
|
|
4314
|
+
const inputs = await prepareSweepInputs(ctx, batch);
|
|
4315
|
+
return sweepOrdinals.execute(ctx, {
|
|
4316
|
+
inputs,
|
|
4317
|
+
keys: keysFor(inputs)
|
|
4318
|
+
});
|
|
4319
|
+
});
|
|
4320
|
+
if (!ok)
|
|
4321
|
+
break;
|
|
4118
4322
|
}
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4323
|
+
};
|
|
4324
|
+
if (plan.bsv.count) {
|
|
4325
|
+
await run("BSV", async () => {
|
|
4326
|
+
const inputs = await prepareSweepInputs(ctx, scan.funding);
|
|
4327
|
+
return sweepBsv.execute(ctx, {
|
|
4328
|
+
inputs,
|
|
4329
|
+
keys: keysFor(inputs)
|
|
4330
|
+
});
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
if (plan.ordinals.count) {
|
|
4334
|
+
await runOrdinalBatches("Ordinals", scan.ordinals);
|
|
4335
|
+
}
|
|
4336
|
+
if (plan.opns.count) {
|
|
4337
|
+
await runOrdinalBatches("OpNS", scan.opnsNames);
|
|
4338
|
+
}
|
|
4339
|
+
if (plan.bsv20.length) {
|
|
4340
|
+
const grouped = groupBsv20Tokens2(scan.bsv20Tokens);
|
|
4341
|
+
for (const token of grouped) {
|
|
4342
|
+
await run(`BSV-20 ${token.tick}`, async () => {
|
|
4343
|
+
const base = await prepareSweepInputs(ctx, token.outputs);
|
|
4344
|
+
const inputs = base.map((b) => ({
|
|
4345
|
+
...b,
|
|
4346
|
+
tick: token.tick,
|
|
4347
|
+
amount: token.amounts.get(b.outpoint) ?? "0"
|
|
4348
|
+
}));
|
|
4349
|
+
return sweepBsv20.execute(ctx, {
|
|
4350
|
+
inputs,
|
|
4351
|
+
keys: inputs.map(() => sweepKey)
|
|
4352
|
+
});
|
|
4353
|
+
});
|
|
4128
4354
|
}
|
|
4129
|
-
if (result.txid)
|
|
4130
|
-
txids.push(result.txid);
|
|
4131
4355
|
}
|
|
4132
|
-
if (
|
|
4356
|
+
if (plan.bsv21.length) {
|
|
4133
4357
|
for (const token of scan.bsv21Tokens) {
|
|
4134
|
-
if (
|
|
4358
|
+
if (token.outputs.length === 0)
|
|
4135
4359
|
continue;
|
|
4136
|
-
const
|
|
4137
|
-
const
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4360
|
+
const name = token.symbol ?? token.tokenId.slice(0, 12);
|
|
4361
|
+
for (const batch of bsv21SweepBatches(token.outputs)) {
|
|
4362
|
+
await run(`BSV-21 ${name}`, async () => {
|
|
4363
|
+
const base = await prepareSweepInputs(ctx, batch);
|
|
4364
|
+
const inputs = base.map((b) => ({
|
|
4365
|
+
...b,
|
|
4366
|
+
tokenId: token.tokenId,
|
|
4367
|
+
amount: token.amounts.get(b.outpoint) ?? "0"
|
|
4368
|
+
}));
|
|
4369
|
+
return sweepBsv21.execute(ctx, {
|
|
4370
|
+
inputs,
|
|
4371
|
+
keys: inputs.map(() => sweepKey)
|
|
4372
|
+
});
|
|
4373
|
+
});
|
|
4146
4374
|
}
|
|
4147
|
-
if (result.txid)
|
|
4148
|
-
txids.push(result.txid);
|
|
4149
4375
|
}
|
|
4150
4376
|
}
|
|
4151
|
-
|
|
4377
|
+
if (opts.json) {
|
|
4378
|
+
output({ txids, errors, swept: parts }, opts);
|
|
4379
|
+
} else if (errors.length) {
|
|
4380
|
+
console.log(`Sweep finished with errors. ${txids.length} transaction(s): ${txids.join(", ") || "(none)"}`);
|
|
4381
|
+
for (const err of errors)
|
|
4382
|
+
console.error(formatWarning(err));
|
|
4383
|
+
} else {
|
|
4384
|
+
output(`Sweep complete. ${txids.length} transaction(s): ${txids.join(", ")}`, opts);
|
|
4385
|
+
}
|
|
4386
|
+
if (errors.length)
|
|
4387
|
+
failed = true;
|
|
4152
4388
|
} finally {
|
|
4153
4389
|
await destroy();
|
|
4154
4390
|
}
|
|
4391
|
+
if (failed)
|
|
4392
|
+
process.exit(1);
|
|
4155
4393
|
}
|
|
4156
4394
|
var init_sweep = __esm(() => {
|
|
4157
4395
|
init_context();
|
|
4158
4396
|
init_help();
|
|
4159
4397
|
init_keys();
|
|
4160
4398
|
init_output();
|
|
4399
|
+
init_sweep_classes();
|
|
4400
|
+
init_sweep_plan();
|
|
4161
4401
|
});
|
|
4162
4402
|
|
|
4163
4403
|
// src/commands/tokens.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.115",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cli.js",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@1sat/actions": "0.0.
|
|
30
|
+
"@1sat/actions": "0.0.217",
|
|
31
31
|
"@1sat/client": "0.0.53",
|
|
32
|
-
"@1sat/types": "0.0.
|
|
32
|
+
"@1sat/types": "0.0.46",
|
|
33
33
|
"@1sat/wallet-node": "0.0.75",
|
|
34
34
|
"@1sat/wallet-server": "0.0.51",
|
|
35
35
|
"@bopen-io/messagebox-server": "^1.3.2",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transferable sweep classes. Locked and RUN outputs are leftover, not classes.
|
|
3
|
+
*/
|
|
4
|
+
export const SWEEP_CLASSES = [
|
|
5
|
+
'bsv',
|
|
6
|
+
'ordinals',
|
|
7
|
+
'opns',
|
|
8
|
+
'bsv20',
|
|
9
|
+
'bsv21',
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export type SweepClass = (typeof SWEEP_CLASSES)[number]
|
|
13
|
+
|
|
14
|
+
const ALIASES: Record<string, SweepClass> = {
|
|
15
|
+
bsv: 'bsv',
|
|
16
|
+
funding: 'bsv',
|
|
17
|
+
ordinals: 'ordinals',
|
|
18
|
+
ord: 'ordinals',
|
|
19
|
+
opns: 'opns',
|
|
20
|
+
names: 'opns',
|
|
21
|
+
bsv20: 'bsv20',
|
|
22
|
+
bsv21: 'bsv21',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseSweepClasses(
|
|
26
|
+
raw: string | undefined,
|
|
27
|
+
flag: string,
|
|
28
|
+
): Set<SweepClass> {
|
|
29
|
+
if (!raw) return new Set()
|
|
30
|
+
const selected = new Set<SweepClass>()
|
|
31
|
+
for (const token of raw.split(',').map((t) => t.trim().toLowerCase())) {
|
|
32
|
+
if (!token) continue
|
|
33
|
+
const mapped = ALIASES[token]
|
|
34
|
+
if (!mapped) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Unknown ${flag} class '${token}'. Use: ${SWEEP_CLASSES.join(', ')}`,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
selected.add(mapped)
|
|
40
|
+
}
|
|
41
|
+
return selected
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Default is every transferable class. `--only` and `--skip` are exclusive. */
|
|
45
|
+
export function selectedSweepClasses(
|
|
46
|
+
only: string | undefined,
|
|
47
|
+
skip: string | undefined,
|
|
48
|
+
): Set<SweepClass> {
|
|
49
|
+
if (only && skip) {
|
|
50
|
+
throw new Error('--only and --skip are mutually exclusive')
|
|
51
|
+
}
|
|
52
|
+
if (only) {
|
|
53
|
+
const selected = parseSweepClasses(only, '--only')
|
|
54
|
+
if (selected.size === 0) {
|
|
55
|
+
throw new Error('--only requires at least one class')
|
|
56
|
+
}
|
|
57
|
+
return selected
|
|
58
|
+
}
|
|
59
|
+
const selected = new Set<SweepClass>(SWEEP_CLASSES)
|
|
60
|
+
if (skip) {
|
|
61
|
+
for (const cls of parseSweepClasses(skip, '--skip')) selected.delete(cls)
|
|
62
|
+
}
|
|
63
|
+
return selected
|
|
64
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ScanResult,
|
|
3
|
+
groupBsv20Tokens,
|
|
4
|
+
isListedOutput,
|
|
5
|
+
} from '@1sat/actions'
|
|
6
|
+
import { SWEEP_CLASSES, type SweepClass } from './sweep-classes.js'
|
|
7
|
+
|
|
8
|
+
function listedCount(
|
|
9
|
+
outputs: Array<{ events?: string[]; data?: Record<string, unknown> }>,
|
|
10
|
+
): number {
|
|
11
|
+
return outputs.filter((o) => isListedOutput(o)).length
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SweepPlan = {
|
|
15
|
+
classes: SweepClass[]
|
|
16
|
+
bsv: { count: number; sats: number }
|
|
17
|
+
ordinals: { count: number; listed: number }
|
|
18
|
+
opns: { count: number; listed: number }
|
|
19
|
+
bsv20: Array<{
|
|
20
|
+
tick: string
|
|
21
|
+
amount: string
|
|
22
|
+
count: number
|
|
23
|
+
listed: number
|
|
24
|
+
}>
|
|
25
|
+
bsv21: Array<{
|
|
26
|
+
tokenId: string
|
|
27
|
+
symbol?: string
|
|
28
|
+
amount: string
|
|
29
|
+
count: number
|
|
30
|
+
listed: number
|
|
31
|
+
active: boolean
|
|
32
|
+
}>
|
|
33
|
+
leftover: {
|
|
34
|
+
locked: number
|
|
35
|
+
run: number
|
|
36
|
+
inactiveBsv21: number
|
|
37
|
+
unparsedBsv20: number
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function buildSweepPlan(
|
|
42
|
+
scan: ScanResult,
|
|
43
|
+
classes: Set<SweepClass>,
|
|
44
|
+
): SweepPlan {
|
|
45
|
+
const want = (cls: SweepClass) => classes.has(cls)
|
|
46
|
+
const groupedBsv20 = groupBsv20Tokens(scan.bsv20Tokens)
|
|
47
|
+
const parsedOutpoints = new Set(
|
|
48
|
+
groupedBsv20.flatMap((g) => g.outputs.map((o) => o.outpoint)),
|
|
49
|
+
)
|
|
50
|
+
const transferableBsv21 = scan.bsv21Tokens.filter((t) => t.outputs.length > 0)
|
|
51
|
+
const skippedBsv21 = scan.bsv21Tokens.filter((t) => t.outputs.length === 0)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
classes: SWEEP_CLASSES.filter((c) => classes.has(c)),
|
|
55
|
+
bsv: want('bsv')
|
|
56
|
+
? { count: scan.funding.length, sats: scan.totalFundingSats }
|
|
57
|
+
: { count: 0, sats: 0 },
|
|
58
|
+
ordinals: want('ordinals')
|
|
59
|
+
? {
|
|
60
|
+
count: scan.ordinals.length,
|
|
61
|
+
listed: listedCount(scan.ordinals),
|
|
62
|
+
}
|
|
63
|
+
: { count: 0, listed: 0 },
|
|
64
|
+
opns: want('opns')
|
|
65
|
+
? {
|
|
66
|
+
count: scan.opnsNames.length,
|
|
67
|
+
listed: listedCount(scan.opnsNames),
|
|
68
|
+
}
|
|
69
|
+
: { count: 0, listed: 0 },
|
|
70
|
+
bsv20: want('bsv20')
|
|
71
|
+
? groupedBsv20.map((g) => ({
|
|
72
|
+
tick: g.tick,
|
|
73
|
+
amount: g.totalAmount.toString(),
|
|
74
|
+
count: g.outputs.length,
|
|
75
|
+
listed: listedCount(g.outputs),
|
|
76
|
+
}))
|
|
77
|
+
: [],
|
|
78
|
+
bsv21: want('bsv21')
|
|
79
|
+
? transferableBsv21.map((t) => ({
|
|
80
|
+
tokenId: t.tokenId,
|
|
81
|
+
symbol: t.symbol,
|
|
82
|
+
amount: t.totalAmount.toString(),
|
|
83
|
+
count: t.outputs.length,
|
|
84
|
+
listed: listedCount(t.outputs),
|
|
85
|
+
active: t.isActive,
|
|
86
|
+
}))
|
|
87
|
+
: [],
|
|
88
|
+
leftover: {
|
|
89
|
+
locked: scan.locked.length,
|
|
90
|
+
run: scan.run.length,
|
|
91
|
+
inactiveBsv21: skippedBsv21.reduce((n, t) => n + t.outputs.length, 0),
|
|
92
|
+
unparsedBsv20: scan.bsv20Tokens.filter(
|
|
93
|
+
(o) => !parsedOutpoints.has(o.outpoint),
|
|
94
|
+
).length,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function planHasWork(plan: SweepPlan): boolean {
|
|
100
|
+
return (
|
|
101
|
+
plan.bsv.count > 0 ||
|
|
102
|
+
plan.ordinals.count > 0 ||
|
|
103
|
+
plan.opns.count > 0 ||
|
|
104
|
+
plan.bsv20.length > 0 ||
|
|
105
|
+
plan.bsv21.length > 0
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function describePlan(plan: SweepPlan): string[] {
|
|
110
|
+
const parts: string[] = []
|
|
111
|
+
if (plan.bsv.count)
|
|
112
|
+
parts.push(`${plan.bsv.sats} sats (${plan.bsv.count} UTXOs)`)
|
|
113
|
+
if (plan.ordinals.count) {
|
|
114
|
+
const listed = plan.ordinals.listed
|
|
115
|
+
? `, ${plan.ordinals.listed} listed`
|
|
116
|
+
: ''
|
|
117
|
+
parts.push(`${plan.ordinals.count} ordinal(s)${listed}`)
|
|
118
|
+
}
|
|
119
|
+
if (plan.opns.count) {
|
|
120
|
+
const listed = plan.opns.listed ? `, ${plan.opns.listed} listed` : ''
|
|
121
|
+
parts.push(`${plan.opns.count} OpNS name(s)${listed}`)
|
|
122
|
+
}
|
|
123
|
+
for (const t of plan.bsv20) {
|
|
124
|
+
const listed = t.listed ? `, ${t.listed} listed` : ''
|
|
125
|
+
parts.push(`${t.amount} ${t.tick} (${t.count} UTXOs${listed})`)
|
|
126
|
+
}
|
|
127
|
+
for (const t of plan.bsv21) {
|
|
128
|
+
const listed = t.listed ? `, ${t.listed} listed` : ''
|
|
129
|
+
parts.push(
|
|
130
|
+
`${t.amount} ${t.symbol ?? t.tokenId.slice(0, 12)} (${t.count} UTXOs${listed})`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return parts
|
|
134
|
+
}
|
package/src/commands/sweep.ts
CHANGED
|
@@ -5,20 +5,37 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
SWEEP_BATCH_SIZE,
|
|
9
|
+
bsv21SweepBatches,
|
|
10
|
+
groupBsv20Tokens,
|
|
8
11
|
prepareSweepInputs,
|
|
9
12
|
scanAddress,
|
|
10
13
|
sweepBsv,
|
|
14
|
+
sweepBsv20,
|
|
11
15
|
sweepBsv21,
|
|
12
16
|
sweepOrdinals,
|
|
13
17
|
} from '@1sat/actions'
|
|
14
18
|
import { PrivateKey } from '@bsv/sdk'
|
|
15
19
|
import { confirm, isCancel } from '@clack/prompts'
|
|
16
20
|
import type { GlobalFlags } from '../args.js'
|
|
17
|
-
import { extractFlag } from '../args.js'
|
|
21
|
+
import { extractFlag, hasFlag } from '../args.js'
|
|
18
22
|
import { loadContext } from '../context.js'
|
|
19
23
|
import { printCommandHelp } from '../help.js'
|
|
20
24
|
import { loadKey } from '../keys.js'
|
|
21
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
fatal,
|
|
27
|
+
formatLabel,
|
|
28
|
+
formatValue,
|
|
29
|
+
formatWarning,
|
|
30
|
+
output,
|
|
31
|
+
} from '../output.js'
|
|
32
|
+
import { type SweepClass, selectedSweepClasses } from './sweep-classes.js'
|
|
33
|
+
import {
|
|
34
|
+
type SweepPlan,
|
|
35
|
+
buildSweepPlan,
|
|
36
|
+
describePlan,
|
|
37
|
+
planHasWork,
|
|
38
|
+
} from './sweep-plan.js'
|
|
22
39
|
|
|
23
40
|
export async function handleSweepCommand(
|
|
24
41
|
args: string[],
|
|
@@ -39,217 +56,306 @@ export async function handleSweepCommand(
|
|
|
39
56
|
}
|
|
40
57
|
}
|
|
41
58
|
|
|
42
|
-
|
|
59
|
+
function loadSweepKey(args: string[]): { wif: string; address: string } {
|
|
43
60
|
const wif = extractFlag(args, '--wif')
|
|
44
|
-
|
|
45
61
|
if (!wif) fatal('Missing --wif <private-key>')
|
|
46
|
-
|
|
47
|
-
let address: string
|
|
48
62
|
try {
|
|
49
63
|
const pk = PrivateKey.fromWif(wif)
|
|
50
|
-
address
|
|
64
|
+
return { wif, address: pk.toPublicKey().toAddress() }
|
|
51
65
|
} catch {
|
|
52
66
|
fatal('Invalid WIF private key')
|
|
53
67
|
}
|
|
68
|
+
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
const { ctx, destroy } = await loadContext(privateKey, {
|
|
57
|
-
chain: opts.chain,
|
|
58
|
-
})
|
|
59
|
-
|
|
70
|
+
function loadClasses(args: string[]): Set<SweepClass> {
|
|
60
71
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
return selectedSweepClasses(
|
|
73
|
+
extractFlag(args, '--only'),
|
|
74
|
+
extractFlag(args, '--skip'),
|
|
75
|
+
)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
fatal(error instanceof Error ? error.message : String(error))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
81
|
+
function leftoverLines(plan: SweepPlan): string[] {
|
|
82
|
+
const lines: string[] = []
|
|
83
|
+
if (plan.leftover.locked)
|
|
84
|
+
lines.push(`${plan.leftover.locked} locked output(s) (not swept)`)
|
|
85
|
+
if (plan.leftover.run)
|
|
86
|
+
lines.push(`${plan.leftover.run} RUN token output(s) (not sweepable)`)
|
|
87
|
+
if (plan.leftover.inactiveBsv21)
|
|
88
|
+
lines.push(
|
|
89
|
+
`${plan.leftover.inactiveBsv21} BSV-21 UTXO(s) with no parseable amount`,
|
|
90
|
+
)
|
|
91
|
+
if (plan.leftover.unparsedBsv20)
|
|
92
|
+
lines.push(
|
|
93
|
+
`${plan.leftover.unparsedBsv20} BSV-20 UTXO(s) with no parseable tick/amt`,
|
|
94
|
+
)
|
|
95
|
+
return lines
|
|
96
|
+
}
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
function printInventory(
|
|
99
|
+
address: string,
|
|
100
|
+
plan: SweepPlan,
|
|
101
|
+
opts: GlobalFlags,
|
|
102
|
+
mode: 'scan' | 'dry-run',
|
|
103
|
+
): void {
|
|
104
|
+
if (opts.json) {
|
|
105
|
+
output(
|
|
106
|
+
{
|
|
107
|
+
address,
|
|
108
|
+
mode,
|
|
109
|
+
classes: plan.classes,
|
|
110
|
+
wouldSweep: {
|
|
111
|
+
bsv: plan.bsv,
|
|
112
|
+
ordinals: plan.ordinals,
|
|
113
|
+
opns: plan.opns,
|
|
114
|
+
bsv20: plan.bsv20,
|
|
115
|
+
bsv21: plan.bsv21,
|
|
116
|
+
},
|
|
117
|
+
leftover: plan.leftover,
|
|
118
|
+
},
|
|
119
|
+
opts,
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
73
123
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const f of result.funding) {
|
|
80
|
-
console.log(
|
|
81
|
-
` ${formatValue(f.outpoint)} ${formatLabel(`${f.satoshis ?? 0} sats`)}`,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
124
|
+
const title = mode === 'dry-run' ? 'Dry run' : 'Scan'
|
|
125
|
+
console.log(`\n ${title} for ${formatValue(address)}\n`)
|
|
126
|
+
console.log(
|
|
127
|
+
` ${formatLabel('Classes:')} ${formatValue(plan.classes.join(', ') || '(none)')}`,
|
|
128
|
+
)
|
|
85
129
|
|
|
130
|
+
console.log(`\n ${formatLabel('Funding UTXOs:')} ${plan.bsv.count}`)
|
|
131
|
+
if (plan.bsv.count > 0) {
|
|
86
132
|
console.log(
|
|
87
|
-
|
|
133
|
+
` ${formatLabel('Total funding:')} ${formatValue(plan.bsv.sats)} satoshis`,
|
|
88
134
|
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`\n ${formatLabel('Ordinal UTXOs:')} ${plan.ordinals.count}`)
|
|
138
|
+
if (plan.ordinals.listed)
|
|
139
|
+
console.log(` ${formatLabel('Listed:')} ${plan.ordinals.listed}`)
|
|
94
140
|
|
|
141
|
+
console.log(`\n ${formatLabel('OpNS names:')} ${plan.opns.count}`)
|
|
142
|
+
if (plan.opns.listed)
|
|
143
|
+
console.log(` ${formatLabel('Listed:')} ${plan.opns.listed}`)
|
|
144
|
+
|
|
145
|
+
console.log(`\n ${formatLabel('BSV-20:')} ${plan.bsv20.length} ticker(s)`)
|
|
146
|
+
for (const t of plan.bsv20) {
|
|
147
|
+
const listed = t.listed ? ` ${formatLabel(`listed ${t.listed}`)}` : ''
|
|
95
148
|
console.log(
|
|
96
|
-
|
|
149
|
+
` ${formatValue(t.tick)} ${formatLabel('amount:')} ${formatValue(t.amount)} ${formatLabel('UTXOs:')} ${t.count}${listed}`,
|
|
97
150
|
)
|
|
98
|
-
|
|
99
|
-
for (const t of result.bsv21Tokens) {
|
|
100
|
-
console.log(
|
|
101
|
-
` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel('amount:')} ${formatValue(t.totalAmount.toString())} ${formatLabel('UTXOs:')} ${t.outputs.length} ${formatLabel('dec:')} ${t.decimals}`,
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
151
|
+
}
|
|
105
152
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.log(
|
|
114
|
-
` ${formatValue(r.outpoint)} ${formatLabel(`${r.satoshis ?? 0} sats`)}`,
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
153
|
+
console.log(`\n ${formatLabel('BSV-21:')} ${plan.bsv21.length} token(s)`)
|
|
154
|
+
for (const t of plan.bsv21) {
|
|
155
|
+
const listed = t.listed ? ` ${formatLabel(`listed ${t.listed}`)}` : ''
|
|
156
|
+
console.log(
|
|
157
|
+
` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel('amount:')} ${formatValue(t.amount)} ${formatLabel('UTXOs:')} ${t.count}${listed}`,
|
|
158
|
+
)
|
|
159
|
+
}
|
|
118
160
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.log(`\n ${total} sweepable UTXO(s) found.`)
|
|
124
|
-
if (result.run.length > 0) {
|
|
125
|
-
console.log(` ${result.run.length} RUN token output(s) excluded.`)
|
|
126
|
-
}
|
|
127
|
-
} finally {
|
|
128
|
-
await destroy()
|
|
161
|
+
const leftover = leftoverLines(plan)
|
|
162
|
+
if (leftover.length) {
|
|
163
|
+
console.log(`\n ${formatLabel('Leftover:')}`)
|
|
164
|
+
for (const line of leftover) console.log(` ${formatWarning(line)}`)
|
|
129
165
|
}
|
|
166
|
+
|
|
167
|
+
const transferable =
|
|
168
|
+
plan.bsv.count +
|
|
169
|
+
plan.ordinals.count +
|
|
170
|
+
plan.opns.count +
|
|
171
|
+
plan.bsv20.reduce((n, t) => n + t.count, 0) +
|
|
172
|
+
plan.bsv21.reduce((n, t) => n + t.count, 0)
|
|
173
|
+
console.log(`\n ${transferable} transferable UTXO(s) in selected classes.`)
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
async function
|
|
133
|
-
const
|
|
176
|
+
async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
177
|
+
const { address } = loadSweepKey(args)
|
|
178
|
+
const classes = loadClasses(args)
|
|
134
179
|
|
|
135
|
-
|
|
180
|
+
const privateKey = await loadKey()
|
|
181
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
182
|
+
chain: opts.chain,
|
|
183
|
+
})
|
|
136
184
|
|
|
137
|
-
let address: string
|
|
138
|
-
let sweepKey: PrivateKey
|
|
139
185
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
186
|
+
if (!ctx.services) fatal('Services required for sweep scan')
|
|
187
|
+
const scan = await scanAddress(ctx.services, address)
|
|
188
|
+
printInventory(address, buildSweepPlan(scan, classes), opts, 'scan')
|
|
189
|
+
} finally {
|
|
190
|
+
await destroy()
|
|
144
191
|
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
195
|
+
const { wif, address } = loadSweepKey(args)
|
|
196
|
+
const classes = loadClasses(args)
|
|
197
|
+
const dryRun = hasFlag(args, '--dry-run')
|
|
198
|
+
const sweepKey = PrivateKey.fromWif(wif)
|
|
145
199
|
|
|
146
200
|
const privateKey = await loadKey()
|
|
147
201
|
const { ctx, destroy } = await loadContext(privateKey, {
|
|
148
202
|
chain: opts.chain,
|
|
149
203
|
})
|
|
204
|
+
let failed = false
|
|
150
205
|
|
|
151
206
|
try {
|
|
152
|
-
if (!ctx.services)
|
|
153
|
-
fatal('Services required for sweep import')
|
|
154
|
-
}
|
|
207
|
+
if (!ctx.services) fatal('Services required for sweep import')
|
|
155
208
|
|
|
156
209
|
const scan = await scanAddress(ctx.services, address)
|
|
210
|
+
const plan = buildSweepPlan(scan, classes)
|
|
157
211
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(t) => t.isActive && t.outputs.length > 0,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if (!hasFunding && !hasOrdinals && !hasTokens) {
|
|
165
|
-
if (scan.run.length > 0) {
|
|
166
|
-
fatal(
|
|
167
|
-
`No sweepable UTXOs found at ${address} (${scan.run.length} RUN token output(s) excluded)`,
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
fatal(`No UTXOs found at ${address}`)
|
|
212
|
+
if (dryRun) {
|
|
213
|
+
printInventory(address, plan, opts, 'dry-run')
|
|
214
|
+
return
|
|
171
215
|
}
|
|
172
216
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (hasTokens) {
|
|
178
|
-
for (const t of scan.bsv21Tokens) {
|
|
179
|
-
if (t.isActive && t.outputs.length > 0) {
|
|
180
|
-
parts.push(
|
|
181
|
-
`${t.totalAmount} ${t.symbol ?? t.tokenId.slice(0, 12)} token(s)`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (scan.run.length > 0) {
|
|
187
|
-
parts.push(`${scan.run.length} RUN token output(s) excluded`)
|
|
217
|
+
if (!planHasWork(plan)) {
|
|
218
|
+
const leftover = leftoverLines(plan)
|
|
219
|
+
const extra = leftover.length ? ` (${leftover.join('; ')})` : ''
|
|
220
|
+
fatal(`No transferable UTXOs found at ${address}${extra}`)
|
|
188
221
|
}
|
|
189
222
|
|
|
223
|
+
const parts = describePlan(plan)
|
|
224
|
+
for (const line of leftoverLines(plan)) parts.push(line)
|
|
225
|
+
|
|
190
226
|
if (!opts.yes) {
|
|
191
227
|
const ok = await confirm({
|
|
192
228
|
message: `Sweep ${parts.join(', ')} from ${address}?`,
|
|
193
229
|
})
|
|
194
|
-
if (isCancel(ok) || !ok)
|
|
195
|
-
fatal('Sweep cancelled.')
|
|
196
|
-
}
|
|
230
|
+
if (isCancel(ok) || !ok) fatal('Sweep cancelled.')
|
|
197
231
|
}
|
|
198
232
|
|
|
199
233
|
const txids: string[] = []
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
234
|
+
const errors: string[] = []
|
|
235
|
+
|
|
236
|
+
const run = async (
|
|
237
|
+
label: string,
|
|
238
|
+
fn: () => Promise<{ txid?: string; error?: string }>,
|
|
239
|
+
): Promise<boolean> => {
|
|
240
|
+
try {
|
|
241
|
+
const result = await fn()
|
|
242
|
+
if (result.error) {
|
|
243
|
+
errors.push(`${label}: ${result.error}`)
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
if (result.txid) txids.push(result.txid)
|
|
247
|
+
return true
|
|
248
|
+
} catch (error) {
|
|
249
|
+
errors.push(
|
|
250
|
+
`${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
251
|
+
)
|
|
252
|
+
return false
|
|
208
253
|
}
|
|
209
|
-
if (result.txid) txids.push(result.txid)
|
|
210
254
|
}
|
|
211
255
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
const keysFor = (inputs: { outpoint: string }[]) =>
|
|
257
|
+
inputs.map(() => sweepKey)
|
|
258
|
+
|
|
259
|
+
const runOrdinalBatches = async (
|
|
260
|
+
label: string,
|
|
261
|
+
outputs: typeof scan.ordinals,
|
|
262
|
+
): Promise<void> => {
|
|
263
|
+
for (
|
|
264
|
+
let offset = 0;
|
|
265
|
+
offset < outputs.length;
|
|
266
|
+
offset += SWEEP_BATCH_SIZE
|
|
267
|
+
) {
|
|
268
|
+
const batch = outputs.slice(offset, offset + SWEEP_BATCH_SIZE)
|
|
269
|
+
const ok = await run(
|
|
270
|
+
outputs.length <= SWEEP_BATCH_SIZE
|
|
271
|
+
? label
|
|
272
|
+
: `${label} (${offset + 1}–${offset + batch.length} of ${outputs.length})`,
|
|
273
|
+
async () => {
|
|
274
|
+
const inputs = await prepareSweepInputs(ctx, batch)
|
|
275
|
+
return sweepOrdinals.execute(ctx, {
|
|
276
|
+
inputs,
|
|
277
|
+
keys: keysFor(inputs),
|
|
278
|
+
})
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
if (!ok) break
|
|
219
282
|
}
|
|
220
|
-
if (result.txid) txids.push(result.txid)
|
|
221
283
|
}
|
|
222
284
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
285
|
+
if (plan.bsv.count) {
|
|
286
|
+
await run('BSV', async () => {
|
|
287
|
+
const inputs = await prepareSweepInputs(ctx, scan.funding)
|
|
288
|
+
return sweepBsv.execute(ctx, {
|
|
289
|
+
inputs,
|
|
290
|
+
keys: keysFor(inputs),
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
}
|
|
227
294
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
tokenId: token.tokenId,
|
|
232
|
-
amount: token.amounts.get(b.outpoint) ?? '0',
|
|
233
|
-
}))
|
|
234
|
-
const keys = inputs.map(() => sweepKey)
|
|
295
|
+
if (plan.ordinals.count) {
|
|
296
|
+
await runOrdinalBatches('Ordinals', scan.ordinals)
|
|
297
|
+
}
|
|
235
298
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
299
|
+
if (plan.opns.count) {
|
|
300
|
+
await runOrdinalBatches('OpNS', scan.opnsNames)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (plan.bsv20.length) {
|
|
304
|
+
const grouped = groupBsv20Tokens(scan.bsv20Tokens)
|
|
305
|
+
for (const token of grouped) {
|
|
306
|
+
await run(`BSV-20 ${token.tick}`, async () => {
|
|
307
|
+
const base = await prepareSweepInputs(ctx, token.outputs)
|
|
308
|
+
const inputs = base.map((b) => ({
|
|
309
|
+
...b,
|
|
310
|
+
tick: token.tick,
|
|
311
|
+
amount: token.amounts.get(b.outpoint) ?? '0',
|
|
312
|
+
}))
|
|
313
|
+
return sweepBsv20.execute(ctx, {
|
|
314
|
+
inputs,
|
|
315
|
+
keys: inputs.map(() => sweepKey),
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (plan.bsv21.length) {
|
|
322
|
+
for (const token of scan.bsv21Tokens) {
|
|
323
|
+
if (token.outputs.length === 0) continue
|
|
324
|
+
const name = token.symbol ?? token.tokenId.slice(0, 12)
|
|
325
|
+
for (const batch of bsv21SweepBatches(token.outputs)) {
|
|
326
|
+
await run(`BSV-21 ${name}`, async () => {
|
|
327
|
+
const base = await prepareSweepInputs(ctx, batch)
|
|
328
|
+
const inputs = base.map((b) => ({
|
|
329
|
+
...b,
|
|
330
|
+
tokenId: token.tokenId,
|
|
331
|
+
amount: token.amounts.get(b.outpoint) ?? '0',
|
|
332
|
+
}))
|
|
333
|
+
return sweepBsv21.execute(ctx, {
|
|
334
|
+
inputs,
|
|
335
|
+
keys: inputs.map(() => sweepKey),
|
|
336
|
+
})
|
|
337
|
+
})
|
|
241
338
|
}
|
|
242
|
-
if (result.txid) txids.push(result.txid)
|
|
243
339
|
}
|
|
244
340
|
}
|
|
245
341
|
|
|
246
|
-
|
|
247
|
-
opts
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
342
|
+
if (opts.json) {
|
|
343
|
+
output({ txids, errors, swept: parts }, opts)
|
|
344
|
+
} else if (errors.length) {
|
|
345
|
+
console.log(
|
|
346
|
+
`Sweep finished with errors. ${txids.length} transaction(s): ${txids.join(', ') || '(none)'}`,
|
|
347
|
+
)
|
|
348
|
+
for (const err of errors) console.error(formatWarning(err))
|
|
349
|
+
} else {
|
|
350
|
+
output(
|
|
351
|
+
`Sweep complete. ${txids.length} transaction(s): ${txids.join(', ')}`,
|
|
352
|
+
opts,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (errors.length) failed = true
|
|
252
357
|
} finally {
|
|
253
358
|
await destroy()
|
|
254
359
|
}
|
|
360
|
+
if (failed) process.exit(1)
|
|
255
361
|
}
|
package/src/help.ts
CHANGED
|
@@ -698,13 +698,42 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
698
698
|
{
|
|
699
699
|
name: 'scan',
|
|
700
700
|
description:
|
|
701
|
-
'
|
|
702
|
-
args: [
|
|
701
|
+
'Show every UTXO class at an address (BSV, ordinals, OpNS, BSV-20, BSV-21, leftover locks/RUN)',
|
|
702
|
+
args: [
|
|
703
|
+
{ flag: '--wif', values: '<key>', required: true },
|
|
704
|
+
{
|
|
705
|
+
flag: '--only',
|
|
706
|
+
values: '<bsv,ordinals,opns,bsv20,bsv21>',
|
|
707
|
+
description: 'Limit the report to these classes',
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
flag: '--skip',
|
|
711
|
+
values: '<class,...>',
|
|
712
|
+
description: 'Omit classes from the report',
|
|
713
|
+
},
|
|
714
|
+
],
|
|
703
715
|
},
|
|
704
716
|
{
|
|
705
717
|
name: 'import',
|
|
706
|
-
description:
|
|
707
|
-
|
|
718
|
+
description:
|
|
719
|
+
'Sweep transferable UTXOs from a WIF into the wallet. Default is all classes we can move to BRC-100 (including listed OrdLocks). Locks and RUN stay.',
|
|
720
|
+
args: [
|
|
721
|
+
{ flag: '--wif', values: '<key>', required: true },
|
|
722
|
+
{
|
|
723
|
+
flag: '--only',
|
|
724
|
+
values: '<bsv,ordinals,opns,bsv20,bsv21>',
|
|
725
|
+
description: 'Sweep only these classes',
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
flag: '--skip',
|
|
729
|
+
values: '<class,...>',
|
|
730
|
+
description: 'Skip these classes',
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
flag: '--dry-run',
|
|
734
|
+
description: 'Print the plan and do not broadcast',
|
|
735
|
+
},
|
|
736
|
+
],
|
|
708
737
|
},
|
|
709
738
|
],
|
|
710
739
|
},
|