2026-06-11 10:48:07 +02:00
|
|
|
using NBitcoin;
|
|
|
|
|
using PalladiumWallet.Core.Chain;
|
|
|
|
|
using PalladiumWallet.Core.Crypto;
|
|
|
|
|
using PalladiumWallet.Core.Net;
|
|
|
|
|
using PalladiumWallet.Core.Spv;
|
|
|
|
|
using PalladiumWallet.Core.Storage;
|
|
|
|
|
using PalladiumWallet.Core.Wallet;
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
// Wallet CLI (blueprint §13): Core use cases exposed via command line,
|
|
|
|
|
// for scripting, testing and comparison against the reference wallet.
|
2026-06-11 10:48:07 +02:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return args switch
|
|
|
|
|
{
|
|
|
|
|
["newseed", .. var rest] => NewSeed(rest),
|
|
|
|
|
["addresses", var words, .. var rest] => Addresses(words, rest),
|
|
|
|
|
["create", .. var rest] => Create(rest),
|
|
|
|
|
["restore", var words, .. var rest] => Restore(words, rest),
|
|
|
|
|
["restore-xpub", var xpub, .. var rest] => RestoreXpub(xpub, rest),
|
|
|
|
|
["info", .. var rest] => Info(rest),
|
|
|
|
|
["sync", .. var rest] => await Sync(rest),
|
|
|
|
|
["send", .. var rest] => await Send(rest),
|
|
|
|
|
["reset-certs", .. var rest] => ResetCerts(rest),
|
2026-06-11 11:27:52 +02:00
|
|
|
["servers", .. var rest] => await Servers(rest),
|
2026-06-11 10:48:07 +02:00
|
|
|
_ => Usage(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex) when (ex is WalletSpendException or WrongPasswordException
|
|
|
|
|
or CertificatePinMismatchException or ElectrumServerException or SpvVerificationException)
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.Error.WriteLine($"Error: {ex.Message}");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int NewSeed(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var length = Opt(o, "--words") == "24" ? MnemonicLength.TwentyFour : MnemonicLength.Twelve;
|
|
|
|
|
Console.WriteLine(Bip39.Generate(length));
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int Addresses(string words, string[] o)
|
|
|
|
|
{
|
|
|
|
|
var account = AccountFromWords(words, o);
|
|
|
|
|
if (account is null)
|
|
|
|
|
return 1;
|
|
|
|
|
var count = int.TryParse(Opt(o, "--count"), out var n) ? n : 5;
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"network: {account.Profile.NetName} kind: {account.Kind} path: m/{account.AccountPath}");
|
2026-06-11 10:48:07 +02:00
|
|
|
Console.WriteLine($"account: {account.ToSlip132()}");
|
|
|
|
|
for (var i = 0; i < count; i++)
|
|
|
|
|
Console.WriteLine($" receiving/{i}: {account.GetReceiveAddress(i)}");
|
|
|
|
|
for (var i = 0; i < Math.Min(count, 2); i++)
|
|
|
|
|
Console.WriteLine($" change/{i}: {account.GetChangeAddress(i)}");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int Create(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var mnemonic = Bip39.Generate(Opt(o, "--words") == "24" ? MnemonicLength.TwentyFour : MnemonicLength.Twelve);
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine("New mnemonic (write it on paper, it is NOT shown again):");
|
2026-06-11 10:48:07 +02:00
|
|
|
Console.WriteLine($" {mnemonic}");
|
|
|
|
|
return SaveWallet(mnemonic.ToString(), o);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int Restore(string words, string[] o)
|
|
|
|
|
{
|
|
|
|
|
if (!Bip39.TryParse(words, out _))
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.Error.WriteLine("Invalid mnemonic (wrong words or checksum).");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
return SaveWallet(words.Trim(), o);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int RestoreXpub(string xpubText, string[] o)
|
|
|
|
|
{
|
|
|
|
|
var profile = Profile(o);
|
|
|
|
|
if (!Slip132.TryDecodePublic(xpubText, profile, out var xpub, out var kind))
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.Error.WriteLine("Extended key not recognised for this network.");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
var account = HdAccount.FromAccountXpub(xpub!, kind, profile);
|
|
|
|
|
var doc = new WalletDocument
|
|
|
|
|
{
|
|
|
|
|
Network = profile.NetName,
|
|
|
|
|
ScriptKind = kind.ToString(),
|
|
|
|
|
AccountPath = account.AccountPath.ToString(),
|
|
|
|
|
AccountXpub = account.ToSlip132(),
|
|
|
|
|
};
|
|
|
|
|
var path = WalletPath(o, profile);
|
|
|
|
|
WalletStore.Save(doc, path, Opt(o, "--password"));
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Watch-only wallet saved to {path}");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int Info(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var (doc, account, path) = OpenWallet(o);
|
|
|
|
|
Console.WriteLine($"file: {path}");
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"network: {doc.Network} kind: {doc.ScriptKind} watch-only: {doc.IsWatchOnly}");
|
2026-06-11 10:48:07 +02:00
|
|
|
Console.WriteLine($"path: m/{doc.AccountPath}");
|
|
|
|
|
Console.WriteLine($"xpub: {doc.AccountXpub}");
|
|
|
|
|
if (doc.Cache is { } cache)
|
|
|
|
|
{
|
2026-07-17 19:41:31 +02:00
|
|
|
Console.WriteLine($"balance: {CoinAmount.Format(cache.SpendableSats, account.Profile.CoinUnit)} spendable"
|
2026-07-07 14:46:03 +02:00
|
|
|
+ (cache.ImmatureSats != 0 ? $" + {CoinAmount.Format(cache.ImmatureSats)} maturing (not spendable)" : "")
|
2026-07-17 19:41:31 +02:00
|
|
|
+ (cache.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(cache.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
|
2026-07-07 14:46:03 +02:00
|
|
|
+ (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} pending confirmation (not spendable)." : ""));
|
|
|
|
|
Console.WriteLine($"sync: height {cache.TipHeight}, {cache.History.Count} transactions");
|
|
|
|
|
Console.WriteLine($"receive: {account.GetReceiveAddress(cache.NextReceiveIndex)}");
|
2026-06-11 10:48:07 +02:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"receive: {account.GetReceiveAddress(0)} (never synchronized)");
|
2026-06-11 10:48:07 +02:00
|
|
|
}
|
2026-06-11 11:27:52 +02:00
|
|
|
|
|
|
|
|
if (o.Contains("--addresses") && doc.Cache is { } c)
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine("addresses:");
|
2026-06-11 11:27:52 +02:00
|
|
|
foreach (var a in c.Addresses)
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($" {(a.IsChange ? "change " : "receive")}{a.Index,3} {a.Address} " +
|
2026-06-11 11:27:52 +02:00
|
|
|
$"{CoinAmount.Format(a.BalanceSats),18} ({a.TxCount} tx)");
|
|
|
|
|
}
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<int> Sync(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var (doc, account, path) = OpenWallet(o);
|
|
|
|
|
await using var client = await Connect(o, account.Profile);
|
|
|
|
|
|
|
|
|
|
var sync = new WalletSynchronizer(account, client, doc.GapLimit);
|
|
|
|
|
sync.Progress += msg => Console.WriteLine($" {msg}");
|
2026-06-16 09:27:40 +02:00
|
|
|
var net = PalladiumNetworks.For(account.Profile.Kind);
|
|
|
|
|
sync.PreloadCaches(
|
|
|
|
|
doc.Cache?.RawTxHex ?? [],
|
|
|
|
|
doc.Cache?.VerifiedAt ?? [],
|
|
|
|
|
doc.Cache?.BlockHeaders,
|
|
|
|
|
doc.Cache?.NextReceiveIndex ?? 0,
|
|
|
|
|
doc.Cache?.NextChangeIndex ?? 0,
|
|
|
|
|
net);
|
2026-06-11 10:48:07 +02:00
|
|
|
var result = await sync.SyncOnceAsync();
|
|
|
|
|
|
2026-06-16 09:27:40 +02:00
|
|
|
var (rawHex, verifiedAt, blockHeaders) = sync.ExportCaches(net);
|
2026-06-11 10:48:07 +02:00
|
|
|
doc.Cache = new SyncCache
|
|
|
|
|
{
|
|
|
|
|
TipHeight = result.TipHeight,
|
|
|
|
|
ConfirmedSats = result.ConfirmedSats,
|
|
|
|
|
UnconfirmedSats = result.UnconfirmedSats,
|
2026-07-02 18:24:26 +02:00
|
|
|
ImmatureSats = result.ImmatureSats,
|
2026-07-17 19:41:31 +02:00
|
|
|
PendingVerificationSats = result.PendingVerificationSats,
|
|
|
|
|
SpendableSats = result.SpendableSats,
|
2026-06-11 10:48:07 +02:00
|
|
|
NextReceiveIndex = result.NextReceiveIndex,
|
|
|
|
|
NextChangeIndex = result.NextChangeIndex,
|
|
|
|
|
History = [.. result.History],
|
|
|
|
|
Utxos = [.. result.Utxos],
|
2026-06-11 11:27:52 +02:00
|
|
|
Addresses = [.. result.AddressRows],
|
2026-06-16 09:27:40 +02:00
|
|
|
RawTxHex = rawHex,
|
|
|
|
|
VerifiedAt = verifiedAt,
|
|
|
|
|
BlockHeaders = blockHeaders,
|
2026-06-11 10:48:07 +02:00
|
|
|
};
|
|
|
|
|
WalletStore.Save(doc, path, Opt(o, "--password"));
|
|
|
|
|
|
2026-07-17 19:41:31 +02:00
|
|
|
Console.WriteLine($"Balance: {CoinAmount.Format(result.SpendableSats, account.Profile.CoinUnit)} spendable"
|
2026-07-07 14:46:03 +02:00
|
|
|
+ (result.ImmatureSats != 0 ? $" + {CoinAmount.Format(result.ImmatureSats)} maturing (not spendable)" : "")
|
2026-07-17 19:41:31 +02:00
|
|
|
+ (result.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(result.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
|
2026-07-07 14:46:03 +02:00
|
|
|
+ (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} pending confirmation (not spendable)" : ""));
|
|
|
|
|
Console.WriteLine($"History ({result.History.Count}):");
|
2026-06-11 10:48:07 +02:00
|
|
|
foreach (var tx in result.History)
|
|
|
|
|
Console.WriteLine($" {(tx.Height > 0 ? tx.Height.ToString() : "mempool"),7} " +
|
|
|
|
|
$"{(tx.DeltaSats >= 0 ? "+" : "")}{CoinAmount.Format(tx.DeltaSats)} {tx.Txid}" +
|
2026-07-07 14:46:03 +02:00
|
|
|
(tx.Verified ? "" : " (unverified)"));
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<int> Send(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var (doc, account, path) = OpenWallet(o);
|
|
|
|
|
if (doc.Cache is null)
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.Error.WriteLine("Wallet never synchronized: run 'sync' first.");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 1;
|
|
|
|
|
}
|
2026-07-07 14:46:03 +02:00
|
|
|
var to = Opt(o, "--to") ?? throw new WalletSpendException("--to missing.");
|
2026-06-11 10:48:07 +02:00
|
|
|
var destination = BitcoinAddress.Create(to, PalladiumNetworks.For(account.Profile.Kind));
|
|
|
|
|
var sendAll = o.Contains("--all");
|
|
|
|
|
long amount = 0;
|
|
|
|
|
if (!sendAll && !CoinAmount.TryParseCoins(Opt(o, "--amount") ?? "", out amount))
|
2026-07-07 14:46:03 +02:00
|
|
|
throw new WalletSpendException("--amount missing or invalid (or use --all).");
|
2026-06-11 10:48:07 +02:00
|
|
|
var feeRate = decimal.TryParse(Opt(o, "--feerate"), out var fr) ? fr : 1m;
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
// Source transactions of the UTXOs: needed for amounts and signing.
|
2026-06-11 10:48:07 +02:00
|
|
|
await using var client = await Connect(o, account.Profile);
|
|
|
|
|
var network = PalladiumNetworks.For(account.Profile.Kind);
|
|
|
|
|
var transactions = new Dictionary<string, Transaction>();
|
|
|
|
|
foreach (var txid in doc.Cache.Utxos.Select(u => u.Txid).Distinct())
|
|
|
|
|
transactions[txid] = Transaction.Parse(await client.GetTransactionAsync(txid), network);
|
|
|
|
|
|
|
|
|
|
var built = new TransactionFactory(account).Build(
|
|
|
|
|
doc.Cache.Utxos, transactions, destination, amount, feeRate,
|
2026-07-02 16:25:38 +02:00
|
|
|
doc.Cache.NextChangeIndex, doc.Cache.TipHeight, sendAll);
|
2026-06-11 10:48:07 +02:00
|
|
|
|
|
|
|
|
Console.WriteLine($"txid: {built.Txid}");
|
|
|
|
|
Console.WriteLine($"fee: {CoinAmount.Format(built.Fee.Satoshi, account.Profile.CoinUnit)} " +
|
|
|
|
|
$"({feeRate} sat/vB, {built.Transaction.GetVirtualSize()} vB)");
|
|
|
|
|
|
|
|
|
|
if (!built.Signed)
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine("Watch-only wallet: PSBT to sign offline (§6.5):");
|
2026-06-11 10:48:07 +02:00
|
|
|
Console.WriteLine(built.Psbt.ToBase64());
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!o.Contains("--broadcast"))
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine("Signed transaction (NOT broadcast, add --broadcast):");
|
2026-06-11 10:48:07 +02:00
|
|
|
Console.WriteLine(built.ToHex());
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var txid2 = await client.BroadcastAsync(built.ToHex());
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Broadcast: {txid2}");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int ResetCerts(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var profile = Profile(o);
|
|
|
|
|
new CertificatePinStore(AppPaths.CertificatePinsPath(profile.Kind)).ResetAll();
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Saved SSL certificates for {profile.NetName} cleared.");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 11:27:52 +02:00
|
|
|
static async Task<int> Servers(string[] o)
|
|
|
|
|
{
|
|
|
|
|
var profile = Profile(o);
|
|
|
|
|
var registry = new ServerRegistry(profile, AppPaths.ServersPath(profile.Kind));
|
|
|
|
|
|
|
|
|
|
if (o.Contains("--discover"))
|
|
|
|
|
{
|
|
|
|
|
await using var client = await Connect(o, profile);
|
|
|
|
|
var added = await registry.DiscoverAsync(client);
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Discovered {added} new servers from peers.");
|
2026-06-11 11:27:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Known servers ({profile.NetName}):");
|
2026-06-11 11:27:52 +02:00
|
|
|
foreach (var server in registry.All)
|
|
|
|
|
Console.WriteLine($" {server}");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
// ----- shared helpers -----
|
2026-06-11 10:48:07 +02:00
|
|
|
|
|
|
|
|
static ChainProfile Profile(string[] o) => Opt(o, "--net") switch
|
|
|
|
|
{
|
|
|
|
|
"testnet" => ChainProfiles.Testnet,
|
|
|
|
|
"regtest" => ChainProfiles.Regtest,
|
|
|
|
|
_ => ChainProfiles.Mainnet,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static ScriptKind Kind(string[] o) => Opt(o, "--kind") switch
|
|
|
|
|
{
|
|
|
|
|
"legacy" => ScriptKind.Legacy,
|
|
|
|
|
"wrapped" => ScriptKind.WrappedSegwit,
|
|
|
|
|
_ => ScriptKind.NativeSegwit,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static string WalletPath(string[] o, ChainProfile profile) =>
|
|
|
|
|
Opt(o, "--file") ?? AppPaths.DefaultWalletPath(profile.Kind);
|
|
|
|
|
|
|
|
|
|
static HdAccount? AccountFromWords(string words, string[] o)
|
|
|
|
|
{
|
|
|
|
|
if (!Bip39.TryParse(words, out var mnemonic))
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.Error.WriteLine("Invalid mnemonic (wrong words or checksum).");
|
2026-06-11 10:48:07 +02:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
var profile = Profile(o);
|
|
|
|
|
var kind = Kind(o);
|
|
|
|
|
var passphrase = Opt(o, "--passphrase");
|
|
|
|
|
return Opt(o, "--path") is { } path
|
|
|
|
|
? HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, passphrase), kind, profile, KeyPath.Parse(path))
|
|
|
|
|
: HdAccount.FromMnemonic(mnemonic!, passphrase, kind, profile);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int SaveWallet(string words, string[] o)
|
|
|
|
|
{
|
|
|
|
|
var profile = Profile(o);
|
|
|
|
|
var (doc, account) = WalletLoader.NewFromMnemonic(
|
|
|
|
|
words, Opt(o, "--passphrase"), Kind(o), profile,
|
|
|
|
|
Opt(o, "--path") is { } p ? KeyPath.Parse(p) : null);
|
|
|
|
|
var password = Opt(o, "--password");
|
|
|
|
|
if (string.IsNullOrEmpty(password))
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine("WARNING: no --password, the seed will be stored in plaintext on disk (§17).");
|
2026-06-11 10:48:07 +02:00
|
|
|
var path = WalletPath(o, profile);
|
|
|
|
|
WalletStore.Save(doc, path, password);
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Wallet saved to {path}");
|
|
|
|
|
Console.WriteLine($"First address: {account.GetReceiveAddress(0)}");
|
2026-06-11 10:48:07 +02:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 14:20:10 +02:00
|
|
|
static (WalletDocument, IWalletAccount, string) OpenWallet(string[] o)
|
2026-06-11 10:48:07 +02:00
|
|
|
{
|
|
|
|
|
var path = WalletPath(o, Profile(o));
|
|
|
|
|
if (!WalletStore.Exists(path))
|
2026-07-07 14:46:03 +02:00
|
|
|
throw new WalletSpendException($"No wallet at {path}: use 'create' or 'restore'.");
|
2026-06-11 10:48:07 +02:00
|
|
|
var doc = WalletStore.Load(path, Opt(o, "--password"));
|
|
|
|
|
return (doc, WalletLoader.ToAccount(doc), path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<ElectrumClient> Connect(string[] o, ChainProfile profile)
|
|
|
|
|
{
|
|
|
|
|
var useSsl = o.Contains("--ssl");
|
2026-06-11 11:27:52 +02:00
|
|
|
string host;
|
|
|
|
|
int port;
|
|
|
|
|
if (Opt(o, "--server") is { } server)
|
|
|
|
|
{
|
|
|
|
|
var parts = server.Split(':');
|
|
|
|
|
host = parts[0];
|
|
|
|
|
port = parts.Length > 1 ? int.Parse(parts[1])
|
|
|
|
|
: useSsl ? profile.DefaultSslPort : profile.DefaultTcpPort;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-07-07 14:46:03 +02:00
|
|
|
// Without --server, use the first known server (bootstrap §3 or discovered §9).
|
2026-06-11 11:27:52 +02:00
|
|
|
var known = new ServerRegistry(profile, AppPaths.ServersPath(profile.Kind)).Default
|
2026-07-07 14:46:03 +02:00
|
|
|
?? throw new WalletSpendException("No known server for this network: use --server host:port.");
|
2026-06-11 11:27:52 +02:00
|
|
|
host = known.Host;
|
|
|
|
|
port = known.PortFor(useSsl);
|
|
|
|
|
}
|
2026-06-11 10:48:07 +02:00
|
|
|
|
|
|
|
|
var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(profile.Kind));
|
2026-07-07 14:46:03 +02:00
|
|
|
Console.WriteLine($"Connecting to {host}:{port}{(useSsl ? " (TLS)" : "")}…");
|
2026-06-11 10:48:07 +02:00
|
|
|
return await ElectrumClient.ConnectAsync(host, port, useSsl, pins);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static string? Opt(string[] options, string name)
|
|
|
|
|
{
|
|
|
|
|
var i = Array.IndexOf(options, name);
|
|
|
|
|
return i >= 0 && i + 1 < options.Length ? options[i + 1] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int Usage()
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("""
|
|
|
|
|
PalladiumWallet CLI
|
|
|
|
|
|
|
|
|
|
Wallet:
|
|
|
|
|
create [--words 12|24] [--kind segwit|wrapped|legacy] [--net mainnet|testnet|regtest]
|
|
|
|
|
[--passphrase W] [--password P] [--file PATH]
|
2026-07-07 14:46:03 +02:00
|
|
|
restore "<mnemonic>" [same options as create] [--path m/...]
|
|
|
|
|
restore-xpub <slip132 xpub> [--net ...] [--password P] [--file PATH] (watch-only)
|
2026-06-11 10:48:07 +02:00
|
|
|
info [--net ...] [--password P] [--file PATH]
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
Network (indexing server; without --server the first known server is used):
|
|
|
|
|
sync [--server host[:port]] [--ssl] [--net ...] [--password P] [--file PATH]
|
|
|
|
|
send --to ADDRESS (--amount X | --all) [--feerate sat/vB]
|
|
|
|
|
[--server host[:port]] [--ssl] [--broadcast] [...]
|
|
|
|
|
servers [--discover] [--server host[:port]] [--ssl] [--net ...]
|
2026-06-11 10:48:07 +02:00
|
|
|
reset-certs [--net ...]
|
|
|
|
|
|
2026-07-07 14:46:03 +02:00
|
|
|
Tools:
|
2026-06-11 10:48:07 +02:00
|
|
|
newseed [--words 12|24]
|
2026-07-07 14:46:03 +02:00
|
|
|
addresses "<mnemonic>" [--kind ...] [--net ...] [--count N] [--passphrase W] [--path m/...]
|
2026-06-11 10:48:07 +02:00
|
|
|
""");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|