public void Setup()
        {
            var seed = new ExtKey();

            accPath = new KeyPath("87'/0'/0'").ToRootedKeyPath(seed.GetPublicKey().GetHDFingerPrint());
            acc     = seed.Derive(accPath.KeyPath).AsHDKeyCache();
            var coins = Enumerable
                        .Range(0, 1300)
                        .Select(i => new Coin(RandomOutpoint(), new TxOut(Money.Coins(1.0m), acc.Derive(0).Derive((uint)i).GetPublicKey().GetScriptPubKey(ScriptPubKeyType.Segwit))))
                        .ToArray();
            var tx = Transaction.Create(Network.Main);

            foreach (var c in coins)
            {
                tx.Inputs.Add(c.Outpoint);
            }
            tx.Outputs.Add(Money.Coins(1299.0m), new Key());
            var psbt = PSBT.FromTransaction(tx, Network.Main);

            psbt.AddCoins(coins);
            for (int i = 0; i < coins.Length; i++)
            {
                psbt.Inputs[i].AddKeyPath(acc.Derive(0).Derive((uint)i).GetPublicKey(), accPath.Derive(0).Derive((uint)i));
            }
            psbtStr = psbt.ToBase64();
            psbt.SignAll(acc.AsHDScriptPubKey(ScriptPubKeyType.Segwit), acc, accPath);
            psbtSignedStr = psbt.ToBase64();
        }
Example #2
0
 public static void WriteTransaction(this InvocationContext ctx, Transaction tx, Coin?fundingCoin, Network network)
 {
     if (ctx.ParseResult.ValueForOption <bool>("psbt"))
     {
         var psbt = PSBT.FromTransaction(tx, network);
         for (int i = 0; i < tx.Inputs.Count; i++)
         {
             psbt.Inputs[i].FinalScriptSig     = tx.Inputs[i].ScriptSig;
             psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
         }
         if (fundingCoin is Coin)
         {
             psbt.AddCoins(fundingCoin);
         }
         ctx.WritePSBT(psbt);
     }
     else
     {
         if (ctx.ParseResult.ValueForOption <bool>("json"))
         {
             ctx.Console.Out.Write(tx.ToString());
         }
         else
         {
             ctx.Console.Out.Write(tx.ToHex());
         }
     }
 }
Example #3
0
        public BroadcastTransactionViewModel(
            BitcoinStore store,
            Network network,
            TransactionBroadcaster broadcaster,
            SmartTransaction transaction)
        {
            Title = "Broadcast Transaction";

            var nullMoney  = new Money(-1L);
            var nullOutput = new TxOut(nullMoney, Script.Empty);

            var psbt = PSBT.FromTransaction(transaction.Transaction, network);

            TxOut GetOutput(OutPoint outpoint) =>
            store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn)
                                        ? prevTxn.Transaction.Outputs[outpoint.N]
                                        : nullOutput;

            var inputAddressAmount = psbt.Inputs
                                     .Select(x => x.PrevOut)
                                     .Select(GetOutput)
                                     .ToArray();

            var outputAddressAmount = psbt.Outputs
                                      .Select(x => x.GetCoin().TxOut)
                                      .ToArray();

            var psbtTxn = psbt.GetOriginalTransaction();

            _transactionId     = psbtTxn.GetHash().ToString();
            _inputCount        = inputAddressAmount.Length;
            _inputCountString  = $" input{TextHelpers.AddSIfPlural(_inputCount)} and ";
            _outputCount       = outputAddressAmount.Length;
            _outputCountString = $" output{TextHelpers.AddSIfPlural(_outputCount)}.";
            _totalInputValue   = inputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : inputAddressAmount.Select(x => x.Value).Sum();
            _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : outputAddressAmount.Select(x => x.Value).Sum();
            _networkFee = TotalInputValue is null || TotalOutputValue is null
                                ? null
                                : TotalInputValue - TotalOutputValue;

            EnableCancel = true;

            EnableBack = false;

            this.WhenAnyValue(x => x.IsBusy)
            .Subscribe(x => EnableCancel = !x);

            var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy)
                                        .Select(x => !x);

            NextCommand = ReactiveCommand.CreateFromTask(
                async() => await OnNext(broadcaster, transaction),
                nextCommandCanExecute);

            EnableAutoBusyOn(NextCommand);
        }
Example #4
0
        public PSBT CreateSetupPSBT(Network network)
        {
            if (FundingInputs is null || PubKeys is null)
            {
                throw new InvalidOperationException("Funding inputs or pubkeys are null");
            }
            Transaction tx = network.CreateTransaction();

            foreach (var input in FundingInputs)
            {
                var c = input.AsCoin();
                tx.Inputs.Add(c.Outpoint);
            }
            var total = FundingInputs.Select(c => c.AsCoin().Amount).Sum();

            tx.Outputs.Add(TotalCollateral, PubKeys.PayoutAddress);
            tx.Outputs.Add(TotalCollateral - total, ChangeAddress);
            var psbt = PSBT.FromTransaction(tx, network);

            foreach (var input in FundingInputs)
            {
                var c         = input.AsCoin();
                var psbtInput = psbt.Inputs.FindIndexedInput(c.Outpoint);
                psbtInput.NonWitnessUtxo = input.InputTransaction;
                psbtInput.RedeemScript   = input.RedeemScript;
                if (input.MaxWitnessLength is int)
                {
                    psbtInput.Unknown.Add(MaxWitnessLengthKey, Utils.ToBytes((uint)input.MaxWitnessLength, true));
                }
            }
            return(psbt);
        }
Example #5
0
        public FundingPSBT Build(Network network)
        {
            var         fundingScript = GetFundingScript();
            var         p2wsh         = fundingScript.WitHash.ScriptPubKey;
            Transaction?tx            = null;

            if (transactionOverride is null)
            {
                tx          = network.CreateTransaction();
                tx.Version  = 2;
                tx.LockTime = 0;
                foreach (var input in Offerer.FundingCoins)
                {
                    tx.Inputs.Add(input.Outpoint, Script.Empty);
                }
                foreach (var input in Acceptor.FundingCoins)
                {
                    tx.Inputs.Add(input.Outpoint, Script.Empty);
                }
                foreach (var input in tx.Inputs)
                {
                    input.Sequence = 0xffffffff;
                }
                tx.Outputs.Add(Offerer.Collateral + Acceptor.Collateral, p2wsh);
                var totalInput = Offerer.FundingCoins.Select(s => s.Amount).Sum();
                if (Offerer.Change is Script change)
                {
                    tx.Outputs.Add(totalInput - Offerer.Collateral, change);
                }

                totalInput = Acceptor.FundingCoins.Select(s => s.Amount).Sum();

                if (Acceptor.Change is Script change2)
                {
                    tx.Outputs.Add(totalInput
                                   - Acceptor.Collateral, change2);
                }

                var expectedFee = FeeRate.GetFee(700);
                var parts       = expectedFee.Split(2).ToArray();
                tx.Outputs[1].Value -= parts[1];
                tx.Outputs[2].Value -= parts[1];

                var futureFee = FeeRate.GetFee(169);
                parts = futureFee.Split(2).ToArray();
                tx.Outputs[1].Value -= parts[1];
                tx.Outputs[2].Value -= parts[1];
                tx.Outputs[0].Value += futureFee;
            }
            else
            {
                tx = transactionOverride;
            }
            var psbt = PSBT.FromTransaction(tx, network);

            psbt.AddCoins(Offerer.FundingCoins);
            psbt.AddCoins(Acceptor.FundingCoins);
            return(new FundingPSBT(psbt, new ScriptCoin(tx, 0, fundingScript)));
        }
Example #6
0
		public void CanRebaseKeypathInPSBT()
		{
			var masterExtkey = new BitcoinExtKey("tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF", Network.TestNet);
			var masterFP = masterExtkey.GetPublicKey().GetHDFingerPrint();
			var accountExtKey = masterExtkey.Derive(new KeyPath("0'/0'/0'"));
			var accountRootedKeyPath = new KeyPath("0'/0'/0'").ToRootedKeyPath(masterExtkey);
			uint hardenedFlag = 0x80000000U;
			retry:
			Transaction funding = masterExtkey.Network.CreateTransaction();
			funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(0 | hardenedFlag).ScriptPubKey);
			funding.Outputs.Add(Money.Coins(2.0m), accountExtKey.Derive(1 | hardenedFlag).ScriptPubKey);

			Transaction tx = masterExtkey.Network.CreateTransaction();
			tx.Version = 2;
			tx.Outputs.Add(Money.Coins(1.49990000m), new Script(Encoders.Hex.DecodeData("0014d85c2b71d0060b09c9886aeb815e50991dda124d")));
			tx.Outputs.Add(Money.Coins(1.00000000m), new Script(Encoders.Hex.DecodeData("001400aea9a2e5f0f876a588df5546e8742d1d87008f")));
			tx.Inputs.Add(funding, 0);
			tx.Inputs.Add(funding, 1);

			var psbt = PSBT.FromTransaction(tx, Network.TestNet);
			psbt.AddTransactions(funding);
			psbt.AddKeyPath(accountExtKey, Tuple.Create(new KeyPath(0 | hardenedFlag), funding.Outputs[0].ScriptPubKey), 
										   Tuple.Create(new KeyPath(1 | hardenedFlag), funding.Outputs[1].ScriptPubKey));
			Assert.Equal(new KeyPath(0 | hardenedFlag), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].KeyPath);
			Assert.Equal(new KeyPath(1 | hardenedFlag), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].KeyPath);
			Assert.Equal(accountExtKey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].MasterFingerprint);
			Assert.Equal(accountExtKey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].MasterFingerprint);

			var memento = psbt.Clone();
			psbt.GlobalXPubs.Add(accountExtKey.Neuter(), new RootedKeyPath(accountExtKey, new KeyPath()));
			var rebasingKeyPath = new RootedKeyPath(masterExtkey.GetPublicKey().GetHDFingerPrint(), new KeyPath("0'/0'/0'"));
			psbt.RebaseKeyPaths(accountExtKey, rebasingKeyPath);
			Assert.Equal(psbt.GlobalXPubs.Single().Value, rebasingKeyPath);
			psbt.ToString();
			psbt = PSBT.Parse(psbt.ToHex(), psbt.Network);
			Assert.Equal(psbt.GlobalXPubs.Single().Value, rebasingKeyPath);
			Assert.Equal(new KeyPath("0'/0'/0'").Derive(0 | hardenedFlag), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].KeyPath);
			Assert.Equal(new KeyPath("0'/0'/0'").Derive(1 | hardenedFlag), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].KeyPath);
			Assert.Equal(masterExtkey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[0].HDKeyPaths[accountExtKey.Derive(0 | hardenedFlag).GetPublicKey()].MasterFingerprint);
			Assert.Equal(masterExtkey.GetPublicKey().GetHDFingerPrint(), psbt.Inputs[1].HDKeyPaths[accountExtKey.Derive(1 | hardenedFlag).GetPublicKey()].MasterFingerprint);

			Assert.NotEqual(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey));
			Assert.Equal(psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey), psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey, accountRootedKeyPath));
			if (hardenedFlag != 0) // If hardened, we can't get the balance from the account pubkey
			{
				Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey.Neuter(), accountRootedKeyPath));
			}
			else
			{
				Assert.Equal(psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey), psbt.GetBalance(ScriptPubKeyType.Legacy, accountExtKey.Neuter(), accountRootedKeyPath));
			}
			Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey.Derive(new KeyPath("0'/0'/1'")), new KeyPath("0'/0'/1'").ToRootedKeyPath(masterFP)));
			Assert.Equal(Money.Zero, psbt.GetBalance(ScriptPubKeyType.Legacy, masterExtkey.Neuter())); // Can't derive!
			if (hardenedFlag != 0)
			{
				hardenedFlag = 0;
				goto retry;
			}
		}
Example #7
0
        public void ShouldCaptureExceptionInFinalization()
        {
            var keys    = new Key[] { new Key(), new Key(), new Key() };
            var redeem  = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
            var network = Network.Main;
            var funds   = CreateDummyFunds(network, keys, redeem);

            var tx   = CreateTxToSpendFunds(funds, keys, redeem, false, false);
            var psbt = PSBT.FromTransaction(tx);

            psbt.TryFinalize(out var errors);
            Assert.Equal(6, errors.Length);
        }
Example #8
0
		public void ShouldCaptureExceptionInFinalization()
		{
			var keys = new Key[] { new Key(), new Key(), new Key() };
			var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
			var network = Network.Main;
			var funds = CreateDummyFunds(network, keys, redeem);

			var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false);
			var psbt = PSBT.FromTransaction(tx, Network.Main);

			var ex = Assert.Throws<PSBTException>(() => psbt.Finalize());
			Assert.Equal(6, ex.Errors.GroupBy(e => e.InputIndex).Count());
		}
        public Task <string> HandleAsync(string body, CancellationToken cancellationToken)
        {
            if (!PSBT.TryParse(body, Network, out var psbt))
            {
                throw new Exception("What the heck are you trying to do?");
            }
            if (!psbt.IsAllFinalized())
            {
                throw new Exception("The PSBT should be finalized");
            }
            // Which coin to use
            var toUse = WalletManager.GetWallets()
                        .Where(x => x.State == WalletState.Started && !x.KeyManager.IsWatchOnly && !x.KeyManager.IsHardwareWallet)
                        .SelectMany(wallet => wallet.Coins.Select(coin => new { wallet.KeyManager, coin }))
                        .Where(x => x.coin.AnonymitySet >= PrivacyLevelThreshold && !x.coin.Unavailable)
                        .OrderBy(x => x.coin.IsBanned)
                        .ThenBy(x => x.coin.Confirmed)
                        .ThenBy(x => x.coin.Height)
                        .First();
            // Fees
            var originalFeeRate = psbt.GetEstimatedFeeRate();
            var paymentTx       = psbt.ExtractTransaction();

            foreach (var input in paymentTx.Inputs)
            {
                input.WitScript = WitScript.Empty;
            }
            // Get prv key for signature
            var serverCoinKey = toUse.KeyManager.GetSecrets("chaincase", toUse.coin.ScriptPubKey).First();
            var serverCoin    = toUse.coin.GetCoin();

            paymentTx.Inputs.Add(serverCoin.Outpoint);
            var paymentOutput     = paymentTx.Outputs.First();
            var inputSizeInVBytes = (int)Math.Ceiling(((3 * Constants.P2wpkhInputSizeInBytes) + Constants.P2pkhInputSizeInBytes) / 4m);

            // Get final value
            paymentOutput.Value += (Money)serverCoin.Amount - originalFeeRate.GetFee(inputSizeInVBytes);

            var newPsbt          = PSBT.FromTransaction(paymentTx, Network.Main);
            var serverCoinToSign = newPsbt.Inputs.FindIndexedInput(serverCoin.Outpoint);

            serverCoinToSign.UpdateFromCoin(serverCoin);
            serverCoinToSign.Sign(serverCoinKey.PrivateKey);
            serverCoinToSign.FinalizeInput();

            NotificationManager.ScheduleNotification("PayJoin Received", "The sender has paid you in a CoinJoin style transaction", 1);
            return(Task.FromResult(newPsbt.ToHex()));
        }
        public PSBT BuildFundingPSBT()
        {
            var psbt = PSBT.FromTransaction(BuildFunding(), this.network);

            foreach (var coin in Remote?.FundingCoins ?? Array.Empty <Coin>())
            {
                psbt.AddCoins(coin);
            }
            foreach (var coin in Us?.FundingCoins ?? Array.Empty <Coin>())
            {
                psbt.AddCoins(coin);
            }
            AddFundingSigs(Remote, psbt);
            AddFundingSigs(Us, psbt);
            return(psbt);
        }
Example #11
0
    private void ProcessTransaction(SmartTransaction transaction, Network network)
    {
        var nullMoney  = new Money(-1L);
        var nullOutput = new TxOut(nullMoney, Script.Empty);

        var psbt = PSBT.FromTransaction(transaction.Transaction, network);

        TxOut GetOutput(OutPoint outpoint) =>
        Services.BitcoinStore.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn)
                                ? prevTxn.Transaction.Outputs[outpoint.N]
                                : nullOutput;

        var inputAddressAmount = psbt.Inputs
                                 .Select(x => x.PrevOut)
                                 .Select(GetOutput)
                                 .ToArray();

        var outputAddressAmount = psbt.Outputs
                                  .Select(x => x.GetCoin().TxOut)
                                  .ToArray();

        var psbtTxn = psbt.GetOriginalTransaction();

        TransactionId = psbtTxn.GetHash().ToString();

        InputCount = inputAddressAmount.Length;
        var totalInputValue = inputAddressAmount.Any(x => x.Value == nullMoney)
                        ? null
                        : inputAddressAmount.Select(x => x.Value).Sum();

        InputAmountString = totalInputValue is null ? "Unknown" : $"{totalInputValue.ToFormattedString()} BTC";

        OutputCount = outputAddressAmount.Length;
        var totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney)
                        ? null
                        : outputAddressAmount.Select(x => x.Value).Sum();

        OutputAmountString = totalOutputValue is null ? "Unknown" : $"{totalOutputValue.ToFormattedString()} BTC";

        var networkFee = totalInputValue is null || totalOutputValue is null
                        ? null
                        : totalInputValue - totalOutputValue;

        FeeString = networkFee?.ToFeeDisplayUnitString() ?? "Unknown";
    }
Example #12
0
        public void ShouldPreserveOriginalTxPropertyAsPossible()
        {
            var keys    = new Key[] { new Key(), new Key(), new Key() };
            var redeem  = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
            var network = Network.Main;
            var funds   = CreateDummyFunds(network, keys, redeem);

            // 1. without signature nor scripts.
            var tx = CreateTxToSpendFunds(funds, keys, redeem, false, false);

            // 2. with (unsigned) scriptSig and witness.
            tx = CreateTxToSpendFunds(funds, keys, redeem, true, false);
            var psbt = PSBT.FromTransaction(tx).AddCoins(funds);

            Assert.Null(psbt.Inputs[0].FinalScriptSig);             // it is not finalized since it is not signed
            Assert.Null(psbt.Inputs[1].FinalScriptWitness);         // This too
            Assert.NotNull(psbt.Inputs[2].RedeemScript);            // But it holds redeem script.
            Assert.NotNull(psbt.Inputs[3].WitnessScript);           // And witness script.
            Assert.NotNull(psbt.Inputs[5].WitnessScript);           // even in p2sh-nested-p2wsh
            Assert.NotNull(psbt.Inputs[5].RedeemScript);

            // 3. with finalized scriptSig and witness
            tx   = CreateTxToSpendFunds(funds, keys, redeem, true, true);
            psbt = PSBT.FromTransaction(tx)
                   .AddTransactions(funds)
                   .Finalize();

            Assert.Equal(tx.ToHex(), psbt.GetOriginalTransaction().ToHex()); // Check that we can still get the original tx

            Assert.NotNull(psbt.Inputs[0].FinalScriptSig);                   // it should be finalized
            Assert.NotNull(psbt.Inputs[0].NonWitnessUtxo);
            Assert.NotNull(psbt.Inputs[1].FinalScriptWitness);               // p2wpkh too
            Assert.NotNull(psbt.Inputs[1].WitnessUtxo);
            Assert.Null(psbt.Inputs[2].RedeemScript);
            Assert.Null(psbt.Inputs[3].WitnessScript);

            Assert.NotNull(psbt.Inputs[4].FinalScriptSig);             // Same principle holds for p2sh-nested version.
            Assert.NotNull(psbt.Inputs[4].FinalScriptWitness);
            Assert.Null(psbt.Inputs[5].WitnessScript);
            Assert.Null(psbt.Inputs[5].RedeemScript);

            Assert.Empty(psbt.Inputs[2].PartialSigs);             // It can not hold partial_sigs
            Assert.Empty(psbt.Inputs[3].PartialSigs);             // Even in p2wsh
            Assert.Empty(psbt.Inputs[5].PartialSigs);             // And p2sh-p2wsh
        }
Example #13
0
 /// <summary>
 /// This is slow, provably because `Add*` methods will iterate over inputs.
 /// </summary>
 /// <param name="network"></param>
 /// <returns></returns>
 public static Gen <PSBT> SanePSBT(Network network) =>
 from inputN in Gen.Choose(0, 8)
 from scripts in Gen.ListOf(inputN, ScriptGenerator.RandomScriptSig())
 from txOuts in Gen.Sequence(scripts.Select(sc => OutputFromRedeem(sc)))
 from prevN in Gen.Choose(0, 5)
 from prevTxs in Gen.Sequence(txOuts.Select(o => TXFromOutput(o, network, prevN)))
 let txins = prevTxs.Select(tx => new TxIn(new OutPoint(tx.GetHash(), prevN)))
             from locktime in PrimitiveGenerator.UInt32()
             let tx = LegacyTransactionGenerators.ComposeTx(network.CreateTransaction(), txins.ToList(), txOuts.ToList(), locktime)
                      from TxsToAdd in Gen.SubListOf(prevTxs)
                      from CoinsToAdd in Gen.SubListOf(prevTxs.SelectMany(tx => tx.Outputs.AsCoins()))
                      from scriptsToAdd in Gen.SubListOf <Script>(scripts)
                      let psbt = PSBT
                                 .FromTransaction(tx)
                                 .AddTransactions(prevTxs.ToArray())
                                 .AddCoins(CoinsToAdd.ToArray())
                                 .TryAddScript(scriptsToAdd.ToArray())
                                 select psbt;
Example #14
0
        public TransactionBroadcasterViewModel() : base("Transaction Broadcaster")
        {
            Global = Locator.Current.GetService <Global>();

            ButtonText = "Broadcast Transaction";

            this.WhenAnyValue(x => x.FinalTransaction)
            .ObserveOn(RxApp.MainThreadScheduler)
            .Subscribe(x =>
            {
                try
                {
                    if (x is null)
                    {
                        TransactionDetails = null;
                    }
                    else
                    {
                        TransactionDetails = TransactionDetailsViewModel.FromBuildTxnResult(Global.BitcoinStore, PSBT.FromTransaction(x.Transaction, Global.Network));
                        NotificationHelpers.Information("Transaction imported successfully!");
                    }
                }
                catch (Exception ex)
                {
                    TransactionDetails = null;
                    NotificationHelpers.Error(ex.ToUserFriendlyString());
                    Logger.LogError(ex);
                }
            });

            PasteCommand = ReactiveCommand.CreateFromTask(async() =>
            {
                try
                {
                    var textToPaste = await Application.Current.Clipboard.GetTextAsync();

                    if (string.IsNullOrWhiteSpace(textToPaste))
                    {
                        FinalTransaction = null;
                        NotificationHelpers.Information("Clipboard is empty!");
                    }
                    else if (PSBT.TryParse(textToPaste, Global.Network ?? Network.Main, out var signedPsbt))
                    {
                        if (!signedPsbt.IsAllFinalized())
                        {
                            signedPsbt.Finalize();
                        }

                        FinalTransaction = signedPsbt.ExtractSmartTransaction();
                    }
                    else
                    {
                        FinalTransaction = new SmartTransaction(Transaction.Parse(textToPaste, Global.Network ?? Network.Main), WalletWasabi.Models.Height.Unknown);
                    }
                }
                catch (Exception ex)
                {
                    FinalTransaction = null;
                    NotificationHelpers.Error(ex.ToUserFriendlyString());
                    Logger.LogError(ex);
                }
            });

            IObservable <bool> broadcastTransactionCanExecute = this
                                                                .WhenAny(x => x.FinalTransaction, (tx) => tx.Value is { })
Example #15
0
        public void ShouldPassTheLongestTestInBIP174()
        {
            JObject testcase = (JObject)testdata["final"];
            var     network  = Network.TestNet;
            var     master   = ExtKey.Parse((string)testcase["master"], network);
            var     masterFP = BitConverter.ToUInt32(master.PrivateKey.PubKey.Hash.ToBytes().SafeSubarray(0, 4), 0);
            var     tx       = network.CreateTransaction();

            tx.Version = 2;

            var scriptPubKey1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out1"]["script"]));
            var money1        = Money.Coins((decimal)testcase["out1"]["value"]);
            var scriptPubKey2 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["out2"]["script"]));
            var money2        = Money.Coins((decimal)testcase["out2"]["value"]);

            tx.Outputs.Add(new TxOut(value: money1, scriptPubKey: scriptPubKey1));
            tx.Outputs.Add(new TxOut(value: money2, scriptPubKey: scriptPubKey2));
            tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in1"]["txid"]), (uint)testcase["in1"]["index"]));
            tx.Inputs.Add(new OutPoint(uint256.Parse((string)testcase["in2"]["txid"]), (uint)testcase["in2"]["index"]));

            var expected = PSBT.Parse((string)testcase["psbt1"], Network.Main);

            var psbt = PSBT.FromTransaction(tx);

            Assert.Equal(expected, psbt, ComparerInstance);

            var prevtx1 = Transaction.Parse((string)testcase["prevtx1"], network);
            var prevtx2 = Transaction.Parse((string)testcase["prevtx2"], network);

            psbt.AddTransactions(prevtx1, prevtx2);
            var redeem1         = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem1"]));
            var redeem2         = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["redeem2"]));
            var witness_script1 = Script.FromBytesUnsafe(Encoders.Hex.DecodeData((string)testcase["witness1"]));

            foreach (var sc in new Script[] { redeem1, redeem2, witness_script1 })
            {
                psbt.AddScript(sc);
            }

            for (int i = 0; i < 6; i++)
            {
                var pk     = testcase[$"pubkey{i}"];
                var pubkey = new PubKey((string)pk["hex"]);
                var path   = KeyPath.Parse((string)pk["path"]);
                psbt.AddKeyPath(pubkey, Tuple.Create(masterFP, path));
            }

            expected = PSBT.Parse((string)testcase["psbt2"], Network.Main);
            Assert.Equal(expected, psbt, ComparerInstance);

            foreach (var psbtin in psbt.Inputs)
            {
                psbtin.SighashType = SigHash.All;
            }
            expected = PSBT.Parse((string)testcase["psbt3"], Network.Main);
            Assert.Equal(expected, psbt, ComparerInstance);

            psbt.CheckSanity();
            var psbtForBob = psbt.Clone();

            // path 1 ... alice
            Assert.Equal(psbt, psbtForBob, ComparerInstance);
            var aliceKey1 = master.Derive(new KeyPath((string)testcase["key7"]["path"])).PrivateKey;
            var aliceKey2 = master.Derive(new KeyPath((string)testcase["key8"]["path"])).PrivateKey;

            psbt.SignAll(aliceKey1, aliceKey2);
            expected = PSBT.Parse((string)testcase["psbt4"], Network.Main);
            Assert.Equal(expected, psbt);

            // path 2 ... bob.
            var bobKey1    = master.Derive(new KeyPath((string)testcase["key9"]["path"])).PrivateKey;
            var bobKey2    = master.Derive(new KeyPath((string)testcase["key10"]["path"])).PrivateKey;
            var bobKeyhex1 = (string)testcase["key9"]["wif"];
            var bobKeyhex2 = (string)testcase["key10"]["wif"];

            Assert.Equal(bobKey1, new BitcoinSecret(bobKeyhex1, network).PrivateKey);
            Assert.Equal(bobKey2, new BitcoinSecret(bobKeyhex2, network).PrivateKey);
            psbtForBob.UseLowR = false;
            psbtForBob.SignAll(bobKey1, bobKey2);
            expected = PSBT.Parse((string)testcase["psbt5"], Network.Main);
            Assert.Equal(expected, psbtForBob);

            // merge above 2
            var combined = psbt.Combine(psbtForBob);

            expected = PSBT.Parse((string)testcase["psbtcombined"], Network.Main);
            Assert.Equal(expected, combined);

            var finalized = psbt.Finalize();

            expected = PSBT.Parse((string)testcase["psbtfinalized"], Network.Main);
            Assert.Equal(expected, finalized);

            var finalTX    = psbt.ExtractTX();
            var expectedTX = Transaction.Parse((string)testcase["txextracted"], network);

            AssertEx.CollectionEquals(expectedTX.ToBytes(), finalTX.ToBytes());
        }
Example #16
0
        public void AddingScriptCoinShouldResultMoreInfoThanAddingSeparatelyInCaseOfP2SH()
        {
            var keys    = new Key[] { new Key(), new Key(), new Key() };
            var redeem  = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
            var network = Network.Main;
            var funds   = CreateDummyFunds(network, keys, redeem);

            var tx   = CreateTxToSpendFunds(funds, keys, redeem, false, false);
            var psbt = PSBT.FromTransaction(tx);

            // case 1: Check that it will result to more info by adding ScriptCoin in case of p2sh-p2wpkh
            var coins1       = DummyFundsToCoins(funds, null, null);       // without script
            var scriptCoins2 = DummyFundsToCoins(funds, null, keys[0]);    // only with p2sh-p2wpkh redeem.
            var psbt1        = psbt.Clone().AddCoins(coins1).AddScript(redeem);
            var psbt2        = psbt.Clone().AddCoins(scriptCoins2).AddScript(redeem);

            for (int i = 0; i < 6; i++)
            {
                Output.WriteLine($"Testing {i}");
                var a = psbt1.Inputs[i];
                var e = psbt2.Inputs[i];
                // Since there are no way psbt can know p2sh-p2wpkh is actually a witness input in case we add coins and scripts separately,
                // coin will not be added to the inputs[4].
                if (i == 4)                 // p2sh-p2wpkh
                {
                    Assert.NotEqual(a.ToBytes(), e.ToBytes());
                    Assert.Null(a.RedeemScript);
                    Assert.Null(a.WitnessUtxo);
                    Assert.NotNull(e.RedeemScript);
                    Assert.NotNull(e.WitnessUtxo);
                }
                // but otherwise, it will be the same.
                else
                {
                    AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes());
                }
            }

            // case 2: bare p2sh and p2sh-pw2sh
            var scriptCoins3 = DummyFundsToCoins(funds, redeem, keys[0]);             // with full scripts.
            var psbt3        = psbt.Clone().AddCoins(scriptCoins3);

            for (int i = 0; i < 6; i++)
            {
                Output.WriteLine($"Testing {i}");
                var a = psbt2.Inputs[i];
                var e = psbt3.Inputs[i];
                if (i == 2 || i == 5)                 // p2sh or p2sh-p2wsh
                {
                    Assert.NotEqual <byte[]>(a.ToBytes(), e.ToBytes());
                    Assert.Null(a.WitnessUtxo);
                    Assert.Null(a.RedeemScript);
                    Assert.NotNull(e.RedeemScript);
                    if (i == 5)                     // p2sh-p2wsh
                    {
                        Assert.NotNull(e.WitnessUtxo);
                        Assert.NotNull(e.WitnessScript);
                    }
                }
                else
                {
                    AssertEx.CollectionEquals(a.ToBytes(), e.ToBytes());
                }
            }
        }
Example #17
0
        public void CanUpdate()
        {
            var network = Network.Main;
            var alice   = Key.Parse("L23Ng7B8iXTcQ9emwDYpabUJVsxDQDxKwePrTwAZo1VT9xcDPfBF", network);
            var bob     = Key.Parse("L2nRrbzZytXSTjn95a4droGrAj5uwSEeG3JUHeNwdB9pUHu8Znjo", network);
            var carol   = Key.Parse("KzHNhJn4P3FML22cQ9yr6rc35hmTPwVmaCnXkc114fQ8ZKRR9hoK", network);
            var keys    = new Key[] { alice, bob, carol };
            var redeem  = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(3, keys.Select(k => k.PubKey).ToArray());
            var funds   = CreateDummyFunds(network, keys, redeem);

            var tx            = CreateTxToSpendFunds(funds, keys, redeem, true, false);
            var coins         = DummyFundsToCoins(funds, redeem, alice);
            var psbtWithCoins = PSBT.FromTransaction(tx, true)
                                .AddCoins(coins);

            Assert.Null(psbtWithCoins.Inputs[0].WitnessUtxo);
            Assert.NotNull(psbtWithCoins.Inputs[1].WitnessUtxo);
            Assert.Null(psbtWithCoins.Inputs[2].WitnessUtxo);
            Assert.NotNull(psbtWithCoins.Inputs[3].WitnessUtxo);
            Assert.NotNull(psbtWithCoins.Inputs[4].WitnessUtxo);
            Assert.NotNull(psbtWithCoins.Inputs[5].WitnessUtxo);

            // Check if it holds scripts as expected.
            Assert.Null(psbtWithCoins.Inputs[0].RedeemScript);             // p2pkh
            Assert.Null(psbtWithCoins.Inputs[0].WitnessScript);            // p2pkh
            Assert.Null(psbtWithCoins.Inputs[1].WitnessScript);            // p2wpkh
            Assert.NotNull(psbtWithCoins.Inputs[2].RedeemScript);          // p2sh
            Assert.NotNull(psbtWithCoins.Inputs[4].RedeemScript);          // p2sh-p2wpkh
            Assert.NotNull(psbtWithCoins.Inputs[5].RedeemScript);          // p2sh-p2wsh
            Assert.NotNull(psbtWithCoins.Inputs[3].WitnessScript);         // p2wsh
            Assert.NotNull(psbtWithCoins.Inputs[5].WitnessScript);         // p2sh-p2wsh

            // Operation must be idempotent.
            var tmp = psbtWithCoins.Clone().AddCoins(coins);

            Assert.Equal(tmp, psbtWithCoins, ComparerInstance);

            var signedPSBTWithCoins = psbtWithCoins
                                      .SignAll(alice);

            Assert.Empty(signedPSBTWithCoins.Inputs[0].PartialSigs);             // can not sign for non segwit input without non-witness UTXO
            Assert.Empty(signedPSBTWithCoins.Inputs[2].PartialSigs);             // This too.
            // otherwise, It will increase Partial sigs count.
            Assert.Single(signedPSBTWithCoins.Inputs[1].PartialSigs);
            Assert.Single(signedPSBTWithCoins.Inputs[3].PartialSigs);
            Assert.Single(signedPSBTWithCoins.Inputs[4].PartialSigs);
            Assert.Single(signedPSBTWithCoins.Inputs[5].PartialSigs);
            var ex = Assert.Throws <AggregateException>(() =>
                                                        signedPSBTWithCoins.Finalize()
                                                        );
            var finalizationErrors = ex.InnerExceptions;

            // Only p2wpkh and p2sh-p2wpkh will succeed.
            Assert.Equal(4, finalizationErrors.Count);

            var psbtWithTXs = PSBT.FromTransaction(tx, true)
                              .AddTransactions(funds);

            Assert.Null(psbtWithTXs.Inputs[0].WitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[0].NonWitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[1].WitnessUtxo);
            Assert.Null(psbtWithTXs.Inputs[2].WitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[2].NonWitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[3].WitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[4].WitnessUtxo);
            Assert.NotNull(psbtWithTXs.Inputs[5].WitnessUtxo);

            // Operation must be idempotent.
            tmp = psbtWithTXs.Clone()
                  .AddCoins(coins)
                  .AddTransactions(funds);
            Assert.Equal(psbtWithTXs, tmp, ComparerInstance);

            var clonedPSBT = psbtWithTXs.Clone();

            clonedPSBT.SignAll(keys[0]);
            psbtWithTXs.SignAll(keys[1], keys[2]);

            var whollySignedPSBT = clonedPSBT.Combine(psbtWithTXs);

            // must sign only once for whole kinds of non-multisig tx.
            Assert.Single(whollySignedPSBT.Inputs[0].PartialSigs);
            Assert.Single(whollySignedPSBT.Inputs[1].PartialSigs);
            Assert.Single(whollySignedPSBT.Inputs[4].PartialSigs);

            // for multisig
            Assert.Equal(3, whollySignedPSBT.Inputs[2].PartialSigs.Count);
            Assert.Equal(3, whollySignedPSBT.Inputs[2].PartialSigs.Values.Select(v => v.Item2).Distinct().Count());
            Assert.Equal(3, whollySignedPSBT.Inputs[3].PartialSigs.Count);
            Assert.Equal(3, whollySignedPSBT.Inputs[3].PartialSigs.Values.Select(v => v.Item2).Distinct().Count());
            Assert.Equal(3, whollySignedPSBT.Inputs[5].PartialSigs.Count);
            Assert.Equal(3, whollySignedPSBT.Inputs[5].PartialSigs.Values.Select(v => v.Item2).Distinct().Count());

            Assert.False(whollySignedPSBT.CanExtractTX());

            var finalizedPSBT = whollySignedPSBT.Finalize();

            Assert.True(finalizedPSBT.CanExtractTX());

            var finalTX = finalizedPSBT.ExtractTX();
            var result  = finalTX.Check();

            Assert.Equal(TransactionCheckResult.Success, result);

            var builder = network.CreateTransactionBuilder();

            builder.AddCoins(coins).AddKeys(keys);
            if (!builder.Verify(finalTX, (Money)null, out var errors))
            {
                throw new InvalidOperationException(errors.Aggregate(string.Empty, (a, b) => a + ";\n" + b));
            }
        }
Example #18
0
        public BroadcastTransactionViewModel(
            BitcoinStore store,
            Network network,
            TransactionBroadcaster broadcaster,
            SmartTransaction transaction)
        {
            Title = "Broadcast Transaction";

            var nullMoney  = new Money(-1L);
            var nullOutput = new TxOut(nullMoney, Script.Empty);

            var psbt = PSBT.FromTransaction(transaction.Transaction, network);

            TxOut GetOutput(OutPoint outpoint) =>
            store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn)
                                        ? prevTxn.Transaction.Outputs[outpoint.N]
                                        : nullOutput;

            var inputAddressAmount = psbt.Inputs
                                     .Select(x => x.PrevOut)
                                     .Select(GetOutput)
                                     .ToArray();

            var outputAddressAmount = psbt.Outputs
                                      .Select(x => x.GetCoin().TxOut)
                                      .ToArray();

            var psbtTxn = psbt.GetOriginalTransaction();

            _transactionId     = psbtTxn.GetHash().ToString();
            _inputCount        = inputAddressAmount.Length;
            _inputCountString  = $" input{(_inputCount > 1 ? 's' : "")} and ";
            _outputCount       = outputAddressAmount.Length;
            _outputCountString = $" output{(_outputCount > 1 ? 's' : "")}.";
            _totalInputValue   = inputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : inputAddressAmount.Select(x => x.Value).Sum();
            _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : outputAddressAmount.Select(x => x.Value).Sum();
            _networkFee = TotalInputValue is null || TotalOutputValue is null
                                ? null
                                : TotalInputValue - TotalOutputValue;

            var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy)
                                        .Select(x => !x);

            NextCommand = ReactiveCommand.CreateFromTask(
                async() =>
            {
                try
                {
                    await broadcaster.SendTransactionAsync(transaction);
                    Navigate().To(new SuccessBroadcastTransactionViewModel());
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex);
                    await ShowErrorAsync(ex.ToUserFriendlyString(), "It was not possible to broadcast the transaction.");
                }
            },
                nextCommandCanExecute);

            EnableAutoBusyOn(NextCommand);
        }
Example #19
0
		public void CanFollowBIPExample()
		{
			var extkey = new BitcoinExtKey("tprv8ZgxMBicQKsPd9TeAdPADNnSyH9SSUUbTVeFszDE23Ki6TBB5nCefAdHkK8Fm3qMQR6sHwA56zqRmKmxnHk37JkiFzvncDqoKmPWubu7hDF", Network.TestNet);
			// A creator creating a PSBT for a transaction which creates the following outputs:
			Transaction tx = extkey.Network.CreateTransaction();
			tx.Version = 2;
			tx.Outputs.Add(Money.Coins(1.49990000m), new Script(Encoders.Hex.DecodeData("0014d85c2b71d0060b09c9886aeb815e50991dda124d")));
			tx.Outputs.Add(Money.Coins(1.00000000m), new Script(Encoders.Hex.DecodeData("001400aea9a2e5f0f876a588df5546e8742d1d87008f")));
			// and spends the following inputs:
			tx.Inputs.Add(OutPoint.Parse("75ddabb27b8845f5247975c8a5ba7c6f336c4570708ebe230caf6db5217ae858-0"));
			tx.Inputs.Add(OutPoint.Parse("1dea7cd05979072a3578cab271c02244ea8a090bbb46aa680a65ecd027048d83-1"));
			var actualPsbt = PSBT.FromTransaction(tx, Network.Main);
			// must create this PSBT:
			var expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000", extkey.Network);
			Assert.Equal(expectedPsbt, actualPsbt);

			// Given the above PSBT, an updater with only the following:
			// Previous Transactions:
			actualPsbt.AddTransactions(
				Transaction.Parse("0200000000010158e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7501000000171600145f275f436b09a8cc9a2eb2a2f528485c68a56323feffffff02d8231f1b0100000017a914aed962d6654f9a2b36608eb9d64d2b260db4f1118700c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88702483045022100a22edcc6e5bc511af4cc4ae0de0fcd75c7e04d8c1c3a8aa9d820ed4b967384ec02200642963597b9b1bc22c75e9f3e117284a962188bf5e8a74c895089046a20ad770121035509a48eb623e10aace8bfd0212fdb8a8e5af3c94b0b133b95e114cab89e4f7965000000", Network.Main),
				Transaction.Parse("0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000", Network.Main)
				);
			// Scripts
			actualPsbt.AddScripts(
				new Script(Encoders.Hex.DecodeData("5221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae")),
				new Script(Encoders.Hex.DecodeData("00208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903")),
				new Script(Encoders.Hex.DecodeData("522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae")));
			// Public Keys
			actualPsbt.AddKeyPath(extkey, new KeyPath("m/0'/0'/0'"),
										  new KeyPath("m/0'/0'/1'"),
										  new KeyPath("m/0'/0'/2'"),
										  new KeyPath("m/0'/0'/3'"),
										  new KeyPath("m/0'/0'/4'"),
										  new KeyPath("m/0'/0'/5'"));

			// Must create this PSBT:
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e88701042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			Assert.Equal(expectedPsbt, actualPsbt);

			// An updater which adds SIGHASH_ALL to the above PSBT must create this PSBT:
			foreach (var input in actualPsbt.Inputs)
				input.SighashType = SigHash.All;
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			Assert.Equal(expectedPsbt, actualPsbt);

			actualPsbt.Settings.UseLowR = false;
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			var tmp = actualPsbt.Clone();
			// Given the above updated PSBT, a signer that supports SIGHASH_ALL for P2PKH and P2WPKH spends and uses RFC6979 for nonce generation and has the following keys:
			actualPsbt.SignWithKeys(extkey.Derive(new KeyPath("m/0'/0'/0'")), extkey.Derive(new KeyPath("m/0'/0'/2'")));
			Assert.Equal(expectedPsbt, actualPsbt);

			actualPsbt = tmp.Clone();
			actualPsbt.SignWithKeys(new BitcoinSecret("cP53pDbR5WtAD8dYAW9hhTjuvvTVaEiQBdrz9XPrgLBeRFiyCbQr", Network.TestNet));
			actualPsbt.SignWithKeys(new BitcoinSecret("cR6SXDoyfQrcp4piaiHE97Rsgta9mNhGTen9XeonVgwsh4iSgw6d", Network.TestNet));
			var part1 = actualPsbt;
			Assert.Equal(expectedPsbt, actualPsbt);

			actualPsbt = tmp.Clone();
			// Given the above updated PSBT, a signer with the following keys:
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			actualPsbt.SignWithKeys(new BitcoinSecret("cT7J9YpCwY3AVRFSjN6ukeEeWY6mhpbJPxRaDaP5QTdygQRxP9Au", Network.TestNet));
			actualPsbt.SignWithKeys(new BitcoinSecret("cNBc3SWUip9PPm1GjRoLEJT6T41iNzCYtD7qro84FMnM5zEqeJsE", Network.TestNet));
			var part2 = actualPsbt;
			Assert.Equal(expectedPsbt, actualPsbt);

			// Given both of the above PSBTs, a combiner must create this PSBT:
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			actualPsbt = part1.Combine(part2);
			Assert.Equal(expectedPsbt, actualPsbt);

			// Given the above PSBT, an input finalizer must create this PSBT:
			expectedPsbt = PSBT.Parse("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", extkey.Network);
			actualPsbt.Finalize();
			Assert.Equal(expectedPsbt, actualPsbt);

			// Given the above PSBT, a transaction extractor must create this Bitcoin transaction:
			var expectedTx = Transaction.Parse("0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000", extkey.Network);
			Assert.True(actualPsbt.CanExtractTransaction());
			var actualTx = actualPsbt.ExtractTransaction().ToHex();
			Assert.Equal(expectedTx.ToHex(), actualTx);

			// Given these two PSBTs with unknown key-value pairs:
			var psbt1 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00", extkey.Network);
			var psbt2 = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network);
			// A combiner which orders keys lexicographically must produce the following PSBT:
			expectedPsbt = PSBT.Parse("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", extkey.Network);
			actualPsbt = psbt1.Combine(psbt2);
			Assert.Equal(expectedPsbt, actualPsbt);
		}
Example #20
0
        public async Task <IActionResult> Submit(string cryptoCode,
                                                 long?maxadditionalfeecontribution,
                                                 int?additionalfeeoutputindex,
                                                 decimal minfeerate             = -1.0m,
                                                 bool disableoutputsubstitution = false,
                                                 int v = 1)
        {
            var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode);

            if (network == null)
            {
                return(NotFound());
            }

            if (v != 1)
            {
                return(BadRequest(new JObject
                {
                    new JProperty("errorCode", "version-unsupported"),
                    new JProperty("supported", new JArray(1)),
                    new JProperty("message", "This version of payjoin is not supported.")
                }));
            }

            await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository);
            ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug)
            {
                ctx.Logs.Write($"Payjoin error: {debug}");
                return(StatusCode(httpCode, CreatePayjoinError(err, debug)));
            }

            var explorer = _explorerClientProvider.GetExplorerClient(network);

            if (Request.ContentLength is long length)
            {
                if (length > 1_000_000)
                {
                    return(this.StatusCode(413,
                                           CreatePayjoinError("payload-too-large", "The transaction is too big to be processed")));
                }
            }
            else
            {
                return(StatusCode(411,
                                  CreatePayjoinError("missing-content-length",
                                                     "The http header Content-Length should be filled")));
            }

            string rawBody;

            using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
            {
                rawBody = (await reader.ReadToEndAsync()) ?? string.Empty;
            }

            FeeRate originalFeeRate = null;
            bool    psbtFormat      = true;

            if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt))
            {
                if (!psbt.IsAllFinalized())
                {
                    return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT should be finalized")));
                }
                ctx.OriginalTransaction = psbt.ExtractTransaction();
            }
            // BTCPay Server implementation support a transaction instead of PSBT
            else
            {
                psbtFormat = false;
                if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
                {
                    return(BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt")));
                }
                ctx.OriginalTransaction = tx;
                psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
                psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest()
                {
                    PSBT = psbt
                })).PSBT;
                for (int i = 0; i < tx.Inputs.Count; i++)
                {
                    psbt.Inputs[i].FinalScriptSig     = tx.Inputs[i].ScriptSig;
                    psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
                }
            }

            FeeRate senderMinFeeRate             = minfeerate >= 0.0m ? new FeeRate(minfeerate) : null;
            Money   allowedSenderFeeContribution = Money.Satoshis(maxadditionalfeecontribution is long t && t >= 0 ? t : 0);

            var sendersInputType = psbt.GetInputsScriptPubKeyType();

            if (psbt.CheckSanity() is var errors && errors.Count != 0)
            {
                return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"This PSBT is insane ({errors[0]})")));
            }
            if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate))
            {
                return(BadRequest(CreatePayjoinError("original-psbt-rejected",
                                                     "You need to provide Witness UTXO information to the PSBT.")));
            }

            // This is actually not a mandatory check, but we don't want implementers
            // to leak global xpubs
            if (psbt.GlobalXPubs.Any())
            {
                return(BadRequest(CreatePayjoinError("original-psbt-rejected",
                                                     "GlobalXPubs should not be included in the PSBT")));
            }

            if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                return(BadRequest(CreatePayjoinError("original-psbt-rejected",
                                                     "Keypath information should not be included in the PSBT")));
            }

            if (psbt.Inputs.Any(o => !o.IsFinalized()))
            {
                return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT Should be finalized")));
            }
            ////////////

            var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true);

            if (!mempool.Success)
            {
                ctx.DoNotBroadcast();
                return(BadRequest(CreatePayjoinError("original-psbt-rejected",
                                                     $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}")));
            }
            var   enforcedLowR    = ctx.OriginalTransaction.Inputs.All(IsLowR);
            var   paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
            bool  paidSomething   = false;
            Money due             = null;
            Dictionary <OutPoint, UTXO> selectedUTXOs      = new Dictionary <OutPoint, UTXO>();
            PSBTOutput               originalPaymentOutput = null;
            BitcoinAddress           paymentAddress        = null;
            InvoiceEntity            invoice = null;
            DerivationSchemeSettings derivationSchemeSettings = null;

            foreach (var output in psbt.Outputs)
            {
                var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
                invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault();
                if (invoice is null)
                {
                    continue;
                }
                derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId)
                                           .SingleOrDefault();
                if (derivationSchemeSettings is null)
                {
                    continue;
                }

                var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
                if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType))
                {
                    //this should never happen, unless the store owner changed the wallet mid way through an invoice
                    return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin"));
                }
                if (sendersInputType is ScriptPubKeyType t1 && t1 != receiverInputsType)
                {
                    return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type"));
                }
                var paymentMethod  = invoice.GetPaymentMethod(paymentMethodId);
                var paymentDetails =
                    paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
                if (paymentDetails is null || !paymentDetails.PayjoinEnabled)
                {
                    continue;
                }
                if (invoice.GetAllBitcoinPaymentData().Any())
                {
                    ctx.DoNotBroadcast();
                    return(UnprocessableEntity(CreatePayjoinError("already-paid",
                                                                  $"The invoice this PSBT is paying has already been partially or completely paid")));
                }

                paidSomething = true;
                due           = paymentMethod.Calculate().TotalDue - output.Value;
                if (due > Money.Zero)
                {
                    break;
                }

                if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray()))
                {
                    return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction"));
                }

                var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation))
                            .GetUnspentUTXOs(false);
                // In case we are paying ourselves, be need to make sure
                // we can't take spent outpoints.
                var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet();
                utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray();
                Array.Sort(utxos, UTXODeterministicComparer.Instance);
                foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC),
                                                       psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO)
                {
                    selectedUTXOs.Add(utxo.Outpoint, utxo);
                }
                ctx.LockedUTXOs       = selectedUTXOs.Select(u => u.Key).ToArray();
                originalPaymentOutput = output;
                paymentAddress        = paymentDetails.GetDepositAddress(network.NBitcoinNetwork);
                break;
            }

            if (!paidSomething)
            {
                return(BadRequest(CreatePayjoinError("invoice-not-found",
                                                     "This transaction does not pay any invoice with payjoin")));
            }

            if (due is null || due > Money.Zero)
            {
                return(BadRequest(CreatePayjoinError("invoice-not-fully-paid",
                                                     "The transaction must pay the whole invoice")));
            }

            if (selectedUTXOs.Count == 0)
            {
                return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin"));
            }

            var originalPaymentValue = originalPaymentOutput.Value;
            await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network);

            //check if wallet of store is configured to be hot wallet
            var extKeyStr = await explorer.GetMetadataAsync <string>(
                derivationSchemeSettings.AccountDerivation,
                WellknownMetadataKeys.AccountHDKey);

            if (extKeyStr == null)
            {
                // This should not happen, as we check the existance of private key before creating invoice with payjoin
                return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed"));
            }

            Money           contributedAmount = Money.Zero;
            var             newTx             = ctx.OriginalTransaction.Clone();
            var             ourNewOutput      = newTx.Outputs[originalPaymentOutput.Index];
            HashSet <TxOut> isOurOutput       = new HashSet <TxOut>();

            isOurOutput.Add(ourNewOutput);
            TxOut feeOutput =
                additionalfeeoutputindex is int feeOutputIndex &&
                maxadditionalfeecontribution is long v3 &&
                v3 >= 0 &&
                feeOutputIndex >= 0 &&
                feeOutputIndex < newTx.Outputs.Count &&
                !isOurOutput.Contains(newTx.Outputs[feeOutputIndex])
                ? newTx.Outputs[feeOutputIndex] : null;
            var rand             = new Random();
            int senderInputCount = newTx.Inputs.Count;

            foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
            {
                contributedAmount += (Money)selectedUTXO.Value;
                var newInput = newTx.Inputs.Add(selectedUTXO.Outpoint);
                newInput.Sequence = newTx.Inputs[rand.Next(0, senderInputCount)].Sequence;
            }
            ourNewOutput.Value += contributedAmount;
            var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
                                new FeeRate(1.0m);

            // Remove old signatures as they are not valid anymore
            foreach (var input in newTx.Inputs)
            {
                input.WitScript = WitScript.Empty;
            }

            Money ourFeeContribution = Money.Zero;
            // We need to adjust the fee to keep a constant fee rate
            var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();
            var coins     = psbt.Inputs.Select(i => i.GetSignableCoin())
                            .Concat(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))).ToArray();

            txBuilder.AddCoins(coins);
            Money expectedFee   = txBuilder.EstimateFees(newTx, originalFeeRate);
            Money actualFee     = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
            Money additionalFee = expectedFee - actualFee;

            if (additionalFee > Money.Zero)
            {
                // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
                for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
                {
                    if (disableoutputsubstitution)
                    {
                        break;
                    }
                    if (isOurOutput.Contains(newTx.Outputs[i]))
                    {
                        var outputContribution = Money.Min(additionalFee, -due);
                        outputContribution = Money.Min(outputContribution,
                                                       newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
                        newTx.Outputs[i].Value -= outputContribution;
                        additionalFee          -= outputContribution;
                        due += outputContribution;
                        ourFeeContribution += outputContribution;
                    }
                }

                // The rest, we take from user's change
                if (feeOutput != null)
                {
                    var outputContribution = Money.Min(additionalFee, feeOutput.Value);
                    outputContribution = Money.Min(outputContribution,
                                                   feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee));
                    outputContribution            = Money.Min(outputContribution, allowedSenderFeeContribution);
                    feeOutput.Value              -= outputContribution;
                    additionalFee                -= outputContribution;
                    allowedSenderFeeContribution -= outputContribution;
                }

                if (additionalFee > Money.Zero)
                {
                    // We could not pay fully the additional fee, however, as long as
                    // we are not under the relay fee, it should be OK.
                    var newVSize   = txBuilder.EstimateSize(newTx, true);
                    var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
                    if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee))
                    {
                        return(CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs"));
                    }
                }
            }

            var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
            var newPsbt    = PSBT.FromTransaction(newTx, network.NBitcoinNetwork);

            foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value))
            {
                var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint);
                var coin        = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation);
                signedInput.UpdateFromCoin(coin);
                var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey;
                signedInput.Sign(privateKey, new SigningOptions()
                {
                    EnforceLowR = enforcedLowR
                });
                signedInput.FinalizeInput();
                newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness;
            }

            // Add the transaction to the payments with a confirmation of -1.
            // This will make the invoice paid even if the user do not
            // broadcast the payjoin.
            var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
                                                                 originalPaymentOutput.Value,
                                                                 new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index),
                                                                 ctx.OriginalTransaction.RBF);

            originalPaymentData.ConfirmationCount  = -1;
            originalPaymentData.PayjoinInformation = new PayjoinInformation()
            {
                CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins),
                CoinjoinValue           = originalPaymentValue - ourFeeContribution,
                ContributedOutPoints    = selectedUTXOs.Select(o => o.Key).ToArray()
            };
            var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true);

            if (payment is null)
            {
                return(UnprocessableEntity(CreatePayjoinError("already-paid",
                                                              $"The original transaction has already been accounted")));
            }
            await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction);

            _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment)
            {
                Payment = payment
            });
            _eventAggregator.Publish(new UpdateTransactionLabel()
            {
                WalletId          = new WalletId(invoice.StoreId, network.CryptoCode),
                TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
                                                                                        new KeyValuePair <uint256, List <(string color, string label)> >(utxo.Key,
                                                                                                                                                         new List <(string color, string label)>()
                {
                    UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id)
                }))
                                    .ToDictionary(pair => pair.Key, pair => pair.Value)
            });
Example #21
0
        public BroadcastTransactionViewModel(
            BitcoinStore store,
            Network network,
            TransactionBroadcaster broadcaster,
            SmartTransaction transaction)
        {
            var nullMoney  = new Money(-1L);
            var nullOutput = new TxOut(nullMoney, Script.Empty);

            var psbt = PSBT.FromTransaction(transaction.Transaction, network);

            TxOut GetOutput(OutPoint outpoint) =>
            store.TransactionStore.TryGetTransaction(outpoint.Hash, out var prevTxn)
                                        ? prevTxn.Transaction.Outputs[outpoint.N]
                                        : nullOutput;

            var inputAddressAmount = psbt.Inputs
                                     .Select(x => x.PrevOut)
                                     .Select(GetOutput)
                                     .ToArray();

            var outputAddressAmount = psbt.Outputs
                                      .Select(x => x.GetCoin().TxOut)
                                      .ToArray();

            var psbtTxn = psbt.GetOriginalTransaction();

            _transactionId     = psbtTxn.GetHash().ToString();
            _inputCount        = inputAddressAmount.Length;
            _inputCountString  = $" input{(_inputCount > 1 ? 's' : "")} and ";
            _outputCount       = outputAddressAmount.Length;
            _outputCountString = $" output{(_outputCount > 1 ? 's' : "")}.";
            _totalInputValue   = inputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : inputAddressAmount.Select(x => x.Value).Sum();
            _totalOutputValue = outputAddressAmount.Any(x => x.Value == nullMoney)
                                ? null
                                : outputAddressAmount.Select(x => x.Value).Sum();
            _networkFee = TotalInputValue is null || TotalOutputValue is null
                                ? null
                                : TotalInputValue - TotalOutputValue;

            var nextCommandCanExecute = this.WhenAnyValue(x => x.IsBusy)
                                        .Select(x => !x);

            NextCommand = ReactiveCommand.CreateFromTask(
                async() =>
            {
                IsBusy = true;

                try
                {
                    await broadcaster.SendTransactionAsync(transaction);
                }
                catch (Exception ex)
                {
                    // TODO: Notify the user
                    Logger.LogError(ex);
                }

                Navigate().Back();

                IsBusy = false;
            },
                nextCommandCanExecute);
        }
Example #22
0
        public async Task <IActionResult> Submit(string cryptoCode)
        {
            var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode);

            if (network == null)
            {
                return(BadRequest(CreatePayjoinError(400, "invalid-network", "Incorrect network")));
            }

            var explorer = _explorerClientProvider.GetExplorerClient(network);

            if (Request.ContentLength is long length)
            {
                if (length > 1_000_000)
                {
                    return(this.StatusCode(413,
                                           CreatePayjoinError(413, "payload-too-large", "The transaction is too big to be processed")));
                }
            }
            else
            {
                return(StatusCode(411,
                                  CreatePayjoinError(411, "missing-content-length",
                                                     "The http header Content-Length should be filled")));
            }

            string rawBody;

            using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
            {
                rawBody = (await reader.ReadToEndAsync()) ?? string.Empty;
            }

            Transaction originalTx      = null;
            FeeRate     originalFeeRate = null;
            bool        psbtFormat      = true;

            if (!PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt))
            {
                psbtFormat = false;
                if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx))
                {
                    return(BadRequest(CreatePayjoinError(400, "invalid-format", "invalid transaction or psbt")));
                }
                originalTx = tx;
                psbt       = PSBT.FromTransaction(tx, network.NBitcoinNetwork);
                psbt       = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest()
                {
                    PSBT = psbt
                })).PSBT;
                for (int i = 0; i < tx.Inputs.Count; i++)
                {
                    psbt.Inputs[i].FinalScriptSig     = tx.Inputs[i].ScriptSig;
                    psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript;
                }
            }
            else
            {
                if (!psbt.IsAllFinalized())
                {
                    return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT should be finalized")));
                }
                originalTx = psbt.ExtractTransaction();
            }

            async Task BroadcastNow()
            {
                await _explorerClientProvider.GetExplorerClient(network).BroadcastAsync(originalTx);
            }

            var sendersInputType = psbt.GetInputsScriptPubKeyType();

            if (sendersInputType is null)
            {
                return(BadRequest(CreatePayjoinError(400, "unsupported-inputs", "Payjoin only support segwit inputs (of the same type)")));
            }
            if (psbt.CheckSanity() is var errors && errors.Count != 0)
            {
                return(BadRequest(CreatePayjoinError(400, "insane-psbt", $"This PSBT is insane ({errors[0]})")));
            }
            if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate))
            {
                return(BadRequest(CreatePayjoinError(400, "need-utxo-information",
                                                     "You need to provide Witness UTXO information to the PSBT.")));
            }

            // This is actually not a mandatory check, but we don't want implementers
            // to leak global xpubs
            if (psbt.GlobalXPubs.Any())
            {
                return(BadRequest(CreatePayjoinError(400, "leaking-data",
                                                     "GlobalXPubs should not be included in the PSBT")));
            }

            if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                return(BadRequest(CreatePayjoinError(400, "leaking-data",
                                                     "Keypath information should not be included in the PSBT")));
            }

            if (psbt.Inputs.Any(o => !o.IsFinalized()))
            {
                return(BadRequest(CreatePayjoinError(400, "psbt-not-finalized", "The PSBT Should be finalized")));
            }
            ////////////

            var mempool = await explorer.BroadcastAsync(originalTx, true);

            if (!mempool.Success)
            {
                return(BadRequest(CreatePayjoinError(400, "invalid-transaction",
                                                     $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}")));
            }

            var   paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
            bool  paidSomething   = false;
            Money due             = null;
            Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>();

            async Task UnlockUTXOs()
            {
                await _payJoinRepository.TryUnlock(selectedUTXOs.Select(o => o.Key).ToArray());
            }

            PSBTOutput               originalPaymentOutput = null;
            BitcoinAddress           paymentAddress        = null;
            InvoiceEntity            invoice = null;
            DerivationSchemeSettings derivationSchemeSettings = null;

            foreach (var output in psbt.Outputs)
            {
                var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
                invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault();
                if (invoice is null)
                {
                    continue;
                }
                derivationSchemeSettings = invoice.GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId)
                                           .SingleOrDefault();
                if (derivationSchemeSettings is null)
                {
                    continue;
                }

                var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
                if (!PayjoinClient.SupportedFormats.Contains(receiverInputsType))
                {
                    //this should never happen, unless the store owner changed the wallet mid way through an invoice
                    return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now")));
                }
                if (sendersInputType != receiverInputsType)
                {
                    return(StatusCode(503,
                                      CreatePayjoinError(503, "out-of-utxos",
                                                         "We do not have any UTXO available for making a payjoin with the sender's inputs type")));
                }
                var paymentMethod  = invoice.GetPaymentMethod(paymentMethodId);
                var paymentDetails =
                    paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
                if (paymentDetails is null || !paymentDetails.PayjoinEnabled)
                {
                    continue;
                }
                if (invoice.GetAllBitcoinPaymentData().Any())
                {
                    return(UnprocessableEntity(CreatePayjoinError(422, "already-paid",
                                                                  $"The invoice this PSBT is paying has already been partially or completely paid")));
                }

                paidSomething = true;
                due           = paymentMethod.Calculate().TotalDue - output.Value;
                if (due > Money.Zero)
                {
                    break;
                }

                if (!await _payJoinRepository.TryLockInputs(originalTx.Inputs.Select(i => i.PrevOut).ToArray()))
                {
                    return(BadRequest(CreatePayjoinError(400, "inputs-already-used",
                                                         "Some of those inputs have already been used to make payjoin transaction")));
                }

                var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation))
                            .GetUnspentUTXOs(false);
                // In case we are paying ourselves, be need to make sure
                // we can't take spent outpoints.
                var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet();
                utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray();
                Array.Sort(utxos, UTXODeterministicComparer.Instance);
                foreach (var utxo in await SelectUTXO(network, utxos, output.Value,
                                                      psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray()))
                {
                    selectedUTXOs.Add(utxo.Outpoint, utxo);
                }

                originalPaymentOutput = output;
                paymentAddress        = paymentDetails.GetDepositAddress(network.NBitcoinNetwork);
                break;
            }

            if (!paidSomething)
            {
                return(BadRequest(CreatePayjoinError(400, "invoice-not-found",
                                                     "This transaction does not pay any invoice with payjoin")));
            }

            if (due is null || due > Money.Zero)
            {
                return(BadRequest(CreatePayjoinError(400, "invoice-not-fully-paid",
                                                     "The transaction must pay the whole invoice")));
            }

            if (selectedUTXOs.Count == 0)
            {
                await BroadcastNow();

                return(StatusCode(503,
                                  CreatePayjoinError(503, "out-of-utxos",
                                                     "We do not have any UTXO available for making a payjoin for now")));
            }

            var originalPaymentValue = originalPaymentOutput.Value;
            await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(1.0), originalTx, network);

            //check if wallet of store is configured to be hot wallet
            var extKeyStr = await explorer.GetMetadataAsync <string>(
                derivationSchemeSettings.AccountDerivation,
                WellknownMetadataKeys.AccountHDKey);

            if (extKeyStr == null)
            {
                // This should not happen, as we check the existance of private key before creating invoice with payjoin
                await UnlockUTXOs();
                await BroadcastNow();

                return(StatusCode(500, CreatePayjoinError(500, "unavailable", $"This service is unavailable for now")));
            }

            Money           contributedAmount = Money.Zero;
            var             newTx             = originalTx.Clone();
            var             ourNewOutput      = newTx.Outputs[originalPaymentOutput.Index];
            HashSet <TxOut> isOurOutput       = new HashSet <TxOut>();

            isOurOutput.Add(ourNewOutput);
            foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value))
            {
                contributedAmount += (Money)selectedUTXO.Value;
                newTx.Inputs.Add(selectedUTXO.Outpoint);
            }
            ourNewOutput.Value += contributedAmount;
            var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ??
                                new FeeRate(1.0m);

            // Probably receiving some spare change, let's add an output to make
            // it looks more like a normal transaction
            if (newTx.Outputs.Count == 1)
            {
                var change = await explorer.GetUnusedAsync(derivationSchemeSettings.AccountDerivation, DerivationFeature.Change);

                var randomChangeAmount = RandomUtils.GetUInt64() % (ulong)contributedAmount.Satoshi;

                // Randomly round the amount to make the payment output look like a change output
                var roundMultiple = (ulong)Math.Pow(10, (ulong)Math.Log10(randomChangeAmount));
                while (roundMultiple > 1_000UL)
                {
                    if (RandomUtils.GetUInt32() % 2 == 0)
                    {
                        roundMultiple = roundMultiple / 10;
                    }
                    else
                    {
                        randomChangeAmount = (randomChangeAmount / roundMultiple) * roundMultiple;
                        break;
                    }
                }

                var fakeChange = newTx.Outputs.CreateNewTxOut(randomChangeAmount, change.ScriptPubKey);
                if (fakeChange.IsDust(minRelayTxFee))
                {
                    randomChangeAmount = fakeChange.GetDustThreshold(minRelayTxFee);
                    fakeChange.Value   = randomChangeAmount;
                }
                if (randomChangeAmount < contributedAmount)
                {
                    ourNewOutput.Value -= fakeChange.Value;
                    newTx.Outputs.Add(fakeChange);
                    isOurOutput.Add(fakeChange);
                }
            }

            var rand = new Random();

            Utils.Shuffle(newTx.Inputs, rand);
            Utils.Shuffle(newTx.Outputs, rand);

            // Remove old signatures as they are not valid anymore
            foreach (var input in newTx.Inputs)
            {
                input.WitScript = WitScript.Empty;
            }

            Money ourFeeContribution = Money.Zero;
            // We need to adjust the fee to keep a constant fee rate
            var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder();

            txBuilder.AddCoins(psbt.Inputs.Select(i => i.GetSignableCoin()));
            txBuilder.AddCoins(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation)));
            Money expectedFee   = txBuilder.EstimateFees(newTx, originalFeeRate);
            Money actualFee     = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
            Money additionalFee = expectedFee - actualFee;

            if (additionalFee > Money.Zero)
            {
                // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy)
                for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero; i++)
                {
                    if (isOurOutput.Contains(newTx.Outputs[i]))
                    {
                        var outputContribution = Money.Min(additionalFee, -due);
                        outputContribution = Money.Min(outputContribution,
                                                       newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
                        newTx.Outputs[i].Value -= outputContribution;
                        additionalFee          -= outputContribution;
                        due += outputContribution;
                        ourFeeContribution += outputContribution;
                    }
                }

                // The rest, we take from user's change
                for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero; i++)
                {
                    if (!isOurOutput.Contains(newTx.Outputs[i]))
                    {
                        var outputContribution = Money.Min(additionalFee, newTx.Outputs[i].Value);
                        outputContribution = Money.Min(outputContribution,
                                                       newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee));
                        newTx.Outputs[i].Value -= outputContribution;
                        additionalFee          -= outputContribution;
                    }
                }

                if (additionalFee > Money.Zero)
                {
                    // We could not pay fully the additional fee, however, as long as
                    // we are not under the relay fee, it should be OK.
                    var newVSize   = txBuilder.EstimateSize(newTx, true);
                    var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx));
                    if (new FeeRate(newFeePaid, newVSize) < minRelayTxFee)
                    {
                        await UnlockUTXOs();
                        await BroadcastNow();

                        return(UnprocessableEntity(CreatePayjoinError(422, "not-enough-money",
                                                                      "Not enough money is sent to pay for the additional payjoin inputs")));
                    }
                }
            }

            var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
            var newPsbt    = PSBT.FromTransaction(newTx, network.NBitcoinNetwork);

            foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value))
            {
                var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint);
                var coin        = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation);
                signedInput.UpdateFromCoin(coin);
                var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey;
                signedInput.Sign(privateKey);
                signedInput.FinalizeInput();
                newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness;
            }

            // Add the transaction to the payments with a confirmation of -1.
            // This will make the invoice paid even if the user do not
            // broadcast the payjoin.
            var originalPaymentData = new BitcoinLikePaymentData(paymentAddress,
                                                                 originalPaymentOutput.Value,
                                                                 new OutPoint(originalTx.GetHash(), originalPaymentOutput.Index),
                                                                 originalTx.RBF);

            originalPaymentData.ConfirmationCount  = -1;
            originalPaymentData.PayjoinInformation = new PayjoinInformation()
            {
                CoinjoinTransactionHash = newPsbt.GetGlobalTransaction().GetHash(),
                CoinjoinValue           = originalPaymentValue - ourFeeContribution,
                ContributedOutPoints    = selectedUTXOs.Select(o => o.Key).ToArray()
            };
            var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true);

            if (payment is null)
            {
                await UnlockUTXOs();
                await BroadcastNow();

                return(UnprocessableEntity(CreatePayjoinError(422, "already-paid",
                                                              $"The original transaction has already been accounted")));
            }
            await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(originalTx);

            _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment)
            {
                Payment = payment
            });

            if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
            {
                return(Ok(newPsbt.ToHex()));
            }
            else if (psbtFormat)
            {
                return(Ok(newPsbt.ToBase64()));
            }
            else
            {
                return(Ok(newTx.ToHex()));
            }
        }
Example #23
0
        public FundingPSBT Build(Network network)
        {
            var         offererCoins  = Offerer.FundingInputs.Select(f => f.AsCoin()).ToArray();
            var         acceptorCoins = Acceptor.FundingInputs.Select(f => f.AsCoin()).ToArray();
            var         fundingScript = GetFundingScript();
            var         p2wsh         = fundingScript.WitHash.ScriptPubKey;
            Transaction?tx            = null;

            if (transactionOverride is null)
            {
                tx          = network.CreateTransaction();
                tx.Version  = 2;
                tx.LockTime = 0;
                foreach (var input in Offerer.FundingInputs.Concat(Acceptor.FundingInputs))
                {
                    var txin = tx.Inputs.Add(input.AsCoin().Outpoint, Script.Empty);
                    if (input.RedeemScript is Script)
                    {
                        txin.ScriptSig = new Script(Op.GetPushOp(input.RedeemScript.ToBytes()));
                    }
                }
                foreach (var input in tx.Inputs)
                {
                    input.Sequence = 0xffffffff;
                }
                tx.Outputs.Add(Offerer.Collateral + Acceptor.Collateral, p2wsh);
                var totalInput = offererCoins.Select(s => s.Amount).Sum();
                if (Offerer.Change is Script change)
                {
                    tx.Outputs.Add(totalInput - Offerer.Collateral, change);
                }

                totalInput = acceptorCoins.Select(s => s.Amount).Sum();

                if (Acceptor.Change is Script change2)
                {
                    tx.Outputs.Add(totalInput
                                   - Acceptor.Collateral, change2);
                }

                tx.Outputs[1].Value -= FeeRate.GetFee(Offerer.VSizes.Funding);
                tx.Outputs[2].Value -= FeeRate.GetFee(Acceptor.VSizes.Funding);

                var offererFee  = FeeRate.GetFee(Offerer.VSizes.CET);
                var acceptorFee = FeeRate.GetFee(Acceptor.VSizes.CET);
                tx.Outputs[1].Value -= offererFee;
                tx.Outputs[2].Value -= acceptorFee;
                tx.Outputs[0].Value += offererFee + acceptorFee;
            }
            else
            {
                tx = transactionOverride;
            }
            var psbt = PSBT.FromTransaction(tx, network);

            foreach (var input in Offerer.FundingInputs.Concat(Acceptor.FundingInputs))
            {
                var txin = psbt.Inputs.FindIndexedInput(input.AsCoin().Outpoint);
                txin.RedeemScript   = input.RedeemScript;
                txin.NonWitnessUtxo = input.InputTransaction;
                if (txin.NonWitnessUtxo is null)
                {
                    txin.WitnessUtxo = input.Output;
                    if (txin.WitnessUtxo is null)
                    {
                        throw new InvalidOperationException("Input funding not having witness Utxo nor NonWitnessUtxo");
                    }
                }
            }
            return(new FundingPSBT(psbt, new ScriptCoin(tx, 0, fundingScript)));
        }