Merge branch 'watch-only'
Adds a pure address-only watch-only wallet mode (Core + CLI + App wizard + Send UI), on top of the existing xpub/xprv/WIF import flows which are unchanged. TransactionFactory already refuses to sign for any IsWatchOnly account, so the new address-only accounts inherit that guarantee for free.
This commit is contained in:
@@ -52,6 +52,19 @@ public static class DerivationPaths
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Best-effort reverse mapping from an already-known address to a ScriptKind, used to
|
||||
/// label pure address imports (no derivation involved, so this is informational only).
|
||||
/// </summary>
|
||||
public static ScriptKind KindFor(BitcoinAddress address) => address switch
|
||||
{
|
||||
BitcoinWitPubKeyAddress => ScriptKind.NativeSegwit,
|
||||
TaprootAddress => ScriptKind.Taproot,
|
||||
BitcoinWitScriptAddress => ScriptKind.NativeSegwitMultisig,
|
||||
BitcoinScriptAddress => ScriptKind.WrappedSegwit,
|
||||
_ => ScriptKind.Legacy,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Account path relative to the root: purpose'/coin'/account' (§4.2).
|
||||
/// coin_type is taken from the profile (746 mainnet, 1 testnet).
|
||||
|
||||
@@ -40,6 +40,9 @@ public sealed class WalletDocument
|
||||
/// <summary>Imported WIF keys (in plaintext in the document — must be encrypted!).</summary>
|
||||
public List<string>? WifKeys { get; set; }
|
||||
|
||||
/// <summary>Watch-only addresses with no associated private key (pure address import).</summary>
|
||||
public List<string>? WatchAddresses { get; set; }
|
||||
|
||||
/// <summary>Gap limit for address scanning (§5), configurable.</summary>
|
||||
public int GapLimit { get; set; } = 20;
|
||||
|
||||
|
||||
@@ -51,7 +51,15 @@ public static class WalletLoader
|
||||
return new ImportedKeyAccount(entries, kind, profile);
|
||||
}
|
||||
|
||||
// 4. Watch-only from xpub
|
||||
// 4. Watch-only imported addresses (no keys, no HD derivation)
|
||||
if (doc.WatchAddresses is { Count: > 0 } watchAddresses)
|
||||
{
|
||||
var entries = watchAddresses.Select(a =>
|
||||
(BitcoinAddress.Create(a.Trim(), network), (Key?)null)).ToList();
|
||||
return new ImportedKeyAccount(entries, kind, profile);
|
||||
}
|
||||
|
||||
// 5. Watch-only from xpub
|
||||
if (doc.AccountXpub is null)
|
||||
throw new InvalidDataException("Wallet file has no xpub and no seed.");
|
||||
if (!Slip132.TryDecodePublic(doc.AccountXpub, profile, out var xpub, out _))
|
||||
@@ -170,4 +178,47 @@ public static class WalletLoader
|
||||
};
|
||||
return (doc, account);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the document from one or more plain addresses, with no private key at all
|
||||
/// (pure watch-only import — the account can never sign, unlike xpub- or WIF-based accounts
|
||||
/// which can be upgraded later by supplying the matching key). ScriptKind is informational
|
||||
/// only here (no derivation happens from a fixed address list) and is auto-detected from the
|
||||
/// first address unless <paramref name="kindOverride"/> is given.
|
||||
/// </summary>
|
||||
public static (WalletDocument Doc, ImportedKeyAccount Account) NewFromAddresses(
|
||||
IReadOnlyList<string> addresses, ChainProfile profile, ScriptKind? kindOverride = null)
|
||||
{
|
||||
if (addresses.Count == 0)
|
||||
throw new InvalidDataException("At least one address is required.");
|
||||
|
||||
var network = PalladiumNetworks.For(profile.Kind);
|
||||
var entries = new List<(BitcoinAddress, Key?)>();
|
||||
var addressStrings = new List<string>();
|
||||
|
||||
foreach (var raw in addresses)
|
||||
{
|
||||
BitcoinAddress addr;
|
||||
try
|
||||
{
|
||||
addr = BitcoinAddress.Create(raw.Trim(), network);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidDataException($"Invalid address: {ex.Message}");
|
||||
}
|
||||
entries.Add((addr, null));
|
||||
addressStrings.Add(raw.Trim());
|
||||
}
|
||||
|
||||
var kind = kindOverride ?? DerivationPaths.KindFor(entries[0].Item1);
|
||||
var account = new ImportedKeyAccount(entries, kind, profile);
|
||||
var doc = new WalletDocument
|
||||
{
|
||||
Network = profile.NetName,
|
||||
ScriptKind = kind.ToString(),
|
||||
WatchAddresses = addressStrings,
|
||||
};
|
||||
return (doc, account);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user