using NBitcoin; using PalladiumWallet.Core.Chain; using PalladiumWallet.Core.Crypto; using PalladiumWallet.Core.Storage; using PalladiumWallet.Core.Wallet; namespace PalladiumWallet.Tests.Wallet; /// Spend-policy limits: send-all affordability and the standard-size cap. public class TransactionFactoryLimitsTests { private static readonly Network Net = PalladiumNetworks.Regtest; private static HdAccount Account() { Assert.True(Bip39.TryParse( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", out var mnemonic)); return HdAccount.FromMnemonic(mnemonic!, null, ScriptKind.NativeSegwit, ChainProfiles.Regtest); } /// One funding tx with outputs to receive/0, each a verified UTXO. private static (List, Dictionary) Fund(HdAccount account, int count, long satsEach) { var funding = Net.CreateTransaction(); funding.Inputs.Add(new TxIn(new OutPoint(uint256.One, 0))); for (var i = 0; i < count; i++) funding.Outputs.Add(Money.Satoshis(satsEach), account.GetReceiveAddress(0)); var txid = funding.GetHash().ToString(); var utxos = Enumerable.Range(0, count).Select(i => new CachedUtxo { Txid = txid, Vout = i, ValueSats = satsEach, Address = account.GetReceiveAddress(0).ToString(), AddressIndex = 0, Height = 100, Verified = true, }).ToList(); return (utxos, new Dictionary { [txid] = funding }); } [Fact] public void SendAll_WithBalanceBelowTheFee_IsRejectedAsInsufficientFunds() { var account = Account(); var (utxos, txs) = Fund(account, 1, 50); // 50 sat cannot pay for a ~110 vB transaction var ex = Assert.Throws(() => new TransactionFactory(account).Build( utxos, txs, account.GetReceiveAddress(5), amountSats: 0, feeRateSatPerVByte: 2, changeIndex: 0, tipHeight: 1_000, sendAll: true)); Assert.Contains("Insufficient funds", ex.Message); } [Fact] public void SendAll_OverTheStandardSizeCap_IsRejectedWithGuidance() { var account = Account(); // ~68 vB per P2WPKH input: 1,700 inputs exceed the 100,000 vB standard limit. var (utxos, txs) = Fund(account, 1_700, 10_000); var ex = Assert.Throws(() => new TransactionFactory(account).Build( utxos, txs, account.GetReceiveAddress(5), amountSats: 0, feeRateSatPerVByte: 1, changeIndex: 0, tipHeight: 1_000, sendAll: true)); Assert.Contains("Send-all cannot fit in one standard transaction", ex.Message); } [Fact] public void AmountAboveTheBalance_IsRejectedAsInsufficientFunds() { var account = Account(); var (utxos, txs) = Fund(account, 1, 100_000); Assert.Throws(() => new TransactionFactory(account).Build( utxos, txs, account.GetReceiveAddress(5), amountSats: 500_000, feeRateSatPerVByte: 2, changeIndex: 0, tipHeight: 1_000)); } }