feat(core): support pure watch-only address import

Adds a WalletDocument.WatchAddresses field and WalletLoader.NewFromAddresses
to build an ImportedKeyAccount with no private key at all (unlike xpub/WIF
imports, which can still derive/hold key material). ScriptKind is inferred
from the address itself via a new DerivationPaths.KindFor helper. Signing
already refuses to run for any IsWatchOnly account (TransactionFactory), so
this only had to wire up construction/reload of the new account shape.
This commit is contained in:
2026-07-19 12:37:18 +02:00
parent b6440484c1
commit 214abd2892
6 changed files with 192 additions and 1 deletions
@@ -250,6 +250,33 @@ public class TransactionFactoryTests
Assert.Contains(tx.Outputs, o => o.Value.Satoshi == 400_000);
}
[Fact]
public void Un_account_di_soli_indirizzi_watch_only_produce_una_psbt_non_firmata()
{
var full = Account();
var (utxos, txs) = Fund(full, 1_000_000);
// Pure address import: no private key at all (unlike xpub watch-only, which
// can still derive public keys/scriptPubKeys — this account can't upgrade
// to spendable without the user separately importing the matching key).
var watchOnly = new ImportedKeyAccount(
[(full.GetReceiveAddress(0), (Key?)null)], ScriptKind.NativeSegwit, Profile);
var built = new TransactionFactory(watchOnly).Build(
utxos, txs, full.GetReceiveAddress(5), amountSats: 400_000,
feeRateSatPerVByte: 2, changeIndex: 0, tipHeight: 100);
Assert.False(built.Signed);
Assert.True(watchOnly.IsWatchOnly);
Assert.Null(watchOnly.GetPrivateKey(false, 0));
var psbt = built.Psbt;
psbt.SignWithKeys(full.GetExtPrivateKey(false, 0));
psbt.Finalize();
var tx = psbt.ExtractTransaction();
Assert.Contains(tx.Outputs, o => o.Value.Satoshi == 400_000);
}
[Theory]
[InlineData("0.00000001", 1L)]
[InlineData("1", 100_000_000L)]