Exemple #1
0
        private async Task FetchTransactionDetails(DerivationSchemeSettings derivationSchemeSettings, WalletPSBTReadyViewModel vm, BTCPayNetwork network)
        {
            var psbtObject = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);

            if (!psbtObject.IsAllFinalized())
            {
                psbtObject = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbtObject) ?? psbtObject;
            }
            IHDKey        signingKey     = null;
            RootedKeyPath signingKeyPath = null;

            try
            {
                signingKey = new BitcoinExtPubKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }
            try
            {
                signingKey = signingKey ?? new BitcoinExtKey(vm.SigningKey, network.NBitcoinNetwork);
            }
            catch { }

            try
            {
                signingKeyPath = RootedKeyPath.Parse(vm.SigningKeyPath);
            }
            catch { }

            if (signingKey == null || signingKeyPath == null)
            {
                var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings();
                if (signingKey == null)
                {
                    signingKey    = signingKeySettings.AccountKey;
                    vm.SigningKey = signingKey.ToString();
                }
                if (vm.SigningKeyPath == null)
                {
                    signingKeyPath    = signingKeySettings.GetRootedKeyPath();
                    vm.SigningKeyPath = signingKeyPath?.ToString();
                }
            }

            if (psbtObject.IsAllFinalized())
            {
                vm.CanCalculateBalance = false;
            }
            else
            {
                var balanceChange = psbtObject.GetBalance(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath);
                vm.BalanceChange       = ValueToString(balanceChange, network);
                vm.CanCalculateBalance = true;
                vm.Positive            = balanceChange >= Money.Zero;
            }
            vm.Inputs = new List <WalletPSBTReadyViewModel.InputViewModel>();
            foreach (var input in psbtObject.Inputs)
            {
                var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
                vm.Inputs.Add(inputVm);
                var mine           = input.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
                var balanceChange2 = input.GetTxOut()?.Value ?? Money.Zero;
                if (mine)
                {
                    balanceChange2 = -balanceChange2;
                }
                inputVm.BalanceChange = ValueToString(balanceChange2, network);
                inputVm.Positive      = balanceChange2 >= Money.Zero;
                inputVm.Index         = (int)input.Index;
            }
            vm.Destinations = new List <WalletPSBTReadyViewModel.DestinationViewModel>();
            foreach (var output in psbtObject.Outputs)
            {
                var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
                vm.Destinations.Add(dest);
                var mine           = output.HDKeysFor(derivationSchemeSettings.AccountDerivation, signingKey, signingKeyPath).Any();
                var balanceChange2 = output.Value;
                if (!mine)
                {
                    balanceChange2 = -balanceChange2;
                }
                dest.Balance     = ValueToString(balanceChange2, network);
                dest.Positive    = balanceChange2 >= Money.Zero;
                dest.Destination = output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork)?.ToString() ?? output.ScriptPubKey.ToString();
            }

            if (psbtObject.TryGetFee(out var fee))
            {
                vm.Destinations.Add(new WalletPSBTReadyViewModel.DestinationViewModel
                {
                    Positive    = false,
                    Balance     = ValueToString(-fee, network),
                    Destination = "Mining fees"
                });
            }
            if (psbtObject.TryGetEstimatedFeeRate(out var feeRate))
            {
                vm.FeeRate = feeRate.ToString();
            }

            var sanityErrors = psbtObject.CheckSanity();

            if (sanityErrors.Count != 0)
            {
                vm.SetErrors(sanityErrors);
            }
            else if (!psbtObject.IsAllFinalized() && !psbtObject.TryFinalize(out var errors))
            {
                vm.SetErrors(errors);
            }
        }
Exemple #2
0
        public async Task <IActionResult> WalletPSBT(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            WalletPSBTViewModel vm, string command)
        {
            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            vm.CryptoCode = network.CryptoCode;

            var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);

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

            vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;

            var psbt = await vm.GetPSBT(network.NBitcoinNetwork);

            if (vm.InvalidPSBT)
            {
                ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
                return(View(vm));
            }
            if (psbt is null)
            {
                return(View("WalletPSBT", vm));
            }
            switch (command)
            {
            case "sign":
                return(await WalletSign(walletId, vm));

            case "decode":
                ModelState.Remove(nameof(vm.PSBT));
                ModelState.Remove(nameof(vm.FileName));
                ModelState.Remove(nameof(vm.UploadedPSBTFile));
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);

                return(View("WalletPSBTDecoded", vm));

            case "save-psbt":
                return(FilePSBT(psbt, vm.FileName));

            case "update":
                psbt = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbt);

                if (psbt == null)
                {
                    TempData[WellKnownTempData.ErrorMessage] = "You need to update your version of NBXplorer";
                    return(View(vm));
                }
                TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
                return(RedirectToWalletPSBT(new WalletPSBTViewModel
                {
                    PSBT = psbt.ToBase64(),
                    FileName = vm.FileName
                }));

            case "combine":
                ModelState.Remove(nameof(vm.PSBT));
                return(View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel {
                    OtherPSBT = psbt.ToBase64()
                }));

            case "broadcast":
            {
                return(RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel
                    {
                        SigningContext = new SigningContextModel(psbt)
                    }));
            }

            default:
                return(View("WalletPSBTDecoded", vm));
            }
        }
Exemple #3
0
        public async Task <IActionResult> WalletPSBT(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            WalletPSBTViewModel vm, string command = null)
        {
            if (command == null)
            {
                return(await WalletPSBT(walletId, vm));
            }
            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            vm.CryptoCode = network.CryptoCode;
            var psbt = await vm.GetPSBT(network.NBitcoinNetwork);

            if (psbt == null)
            {
                ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
                return(View(vm));
            }
            switch (command)
            {
            case "decode":
                vm.Decoded = psbt.ToString();
                ModelState.Remove(nameof(vm.PSBT));
                ModelState.Remove(nameof(vm.FileName));
                ModelState.Remove(nameof(vm.UploadedPSBTFile));
                vm.PSBT     = psbt.ToBase64();
                vm.FileName = vm.UploadedPSBTFile?.FileName;
                return(View(vm));

            case "vault":
                return(ViewVault(walletId, psbt, vm.PayJoinEndpointUrl));

            case "ledger":
                return(ViewWalletSendLedger(walletId, psbt));

            case "update":
                var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                psbt = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbt);

                if (psbt == null)
                {
                    ModelState.AddModelError(nameof(vm.PSBT), "You need to update your version of NBXplorer");
                    return(View(vm));
                }
                TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
                return(RedirectToWalletPSBT(psbt, vm.FileName));

            case "seed":
                return(SignWithSeed(walletId, psbt.ToBase64(), vm.PayJoinEndpointUrl));

            case "nbx-seed":
                if (await CanUseHotWallet())
                {
                    var derivationScheme = GetDerivationSchemeSettings(walletId);
                    var extKey           = await ExplorerClientProvider.GetExplorerClient(network)
                                           .GetMetadataAsync <string>(derivationScheme.AccountDerivation,
                                                                      WellknownMetadataKeys.MasterHDKey);

                    return(await SignWithSeed(walletId,
                                              new SignWithSeedViewModel()
                    {
                        SeedOrKey = extKey, PSBT = psbt.ToBase64(), PayJoinEndpointUrl = vm.PayJoinEndpointUrl
                    }));
                }

                return(View(vm));

            case "broadcast":
            {
                return(RedirectToWalletPSBTReady(psbt.ToBase64()));
            }

            case "combine":
                ModelState.Remove(nameof(vm.PSBT));
                return(View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel()
                {
                    OtherPSBT = psbt.ToBase64()
                }));

            case "save-psbt":
                return(FilePSBT(psbt, vm.FileName));

            default:
                return(View(vm));
            }
        }
Exemple #4
0
        public async Task <IActionResult> WalletPSBT(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            WalletPSBTViewModel vm, string command = null)
        {
            if (command == null)
            {
                return(await WalletPSBT(walletId, vm));
            }
            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            vm.CryptoCode       = network.CryptoCode;
            vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
                                                                                   .GetMetadataAsync <string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
                                                                                                              WellknownMetadataKeys.Mnemonic));

            var psbt = await vm.GetPSBT(network.NBitcoinNetwork);

            if (psbt == null)
            {
                ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
                return(View(vm));
            }
            var res = await TryHandleSigningCommands(walletId, psbt, command, new SigningContextModel(psbt));

            if (res != null)
            {
                return(res);
            }
            switch (command)
            {
            case "decode":
                vm.Decoded = psbt.ToString();
                ModelState.Remove(nameof(vm.PSBT));
                ModelState.Remove(nameof(vm.FileName));
                ModelState.Remove(nameof(vm.UploadedPSBTFile));
                vm.PSBT     = psbt.ToBase64();
                vm.FileName = vm.UploadedPSBTFile?.FileName;
                return(View(vm));

            case "update":
                var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                psbt = await ExplorerClientProvider.UpdatePSBT(derivationSchemeSettings, psbt);

                if (psbt == null)
                {
                    ModelState.AddModelError(nameof(vm.PSBT), "You need to update your version of NBXplorer");
                    return(View(vm));
                }
                TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
                return(RedirectToWalletPSBT(new WalletPSBTViewModel()
                {
                    PSBT = psbt.ToBase64(),
                    FileName = vm.FileName
                }));

            case "broadcast":
            {
                return(RedirectToWalletPSBTReady(new WalletPSBTReadyViewModel()
                    {
                        SigningContext = new SigningContextModel(psbt)
                    }));
            }

            case "combine":
                ModelState.Remove(nameof(vm.PSBT));
                return(View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel()
                {
                    OtherPSBT = psbt.ToBase64()
                }));

            case "save-psbt":
                return(FilePSBT(psbt, vm.FileName));

            default:
                return(View(vm));
            }
        }
Exemple #5
0
        public async Task <PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
                                                PSBT originalTx, CancellationToken cancellationToken)
        {
            if (endpoint == null)
            {
                throw new ArgumentNullException(nameof(endpoint));
            }
            if (derivationSchemeSettings == null)
            {
                throw new ArgumentNullException(nameof(derivationSchemeSettings));
            }
            if (originalTx == null)
            {
                throw new ArgumentNullException(nameof(originalTx));
            }

            var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
            var sentBefore     = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
                                                        signingAccount.AccountKey,
                                                        signingAccount.GetRootedKeyPath());
            var oldGlobalTx = originalTx.GetGlobalTransaction();

            if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
            {
                throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
            }
            var originalFee = originalTx.GetFee();
            var cloned      = originalTx.Clone();

            if (!cloned.IsAllFinalized() && !cloned.TryFinalize(out var errors))
            {
                return(null);
            }

            // We make sure we don't send unnecessary information to the receiver
            foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }

            foreach (var output in cloned.Outputs)
            {
                output.HDKeyPaths.Clear();
            }

            cloned.GlobalXPubs.Clear();
            var bpuresponse = await _httpClient.PostAsync(endpoint,
                                                          new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);

            if (!bpuresponse.IsSuccessStatusCode)
            {
                var errorStr = await bpuresponse.Content.ReadAsStringAsync();

                try
                {
                    var error = JObject.Parse(errorStr);
                    throw new PayjoinReceiverException((int)bpuresponse.StatusCode, error["errorCode"].Value <string>(),
                                                       error["message"].Value <string>());
                }
                catch (JsonReaderException)
                {
                    // will throw
                    bpuresponse.EnsureSuccessStatusCode();
                    throw;
                }
            }

            var hex = await bpuresponse.Content.ReadAsStringAsync();

            var newPSBT = PSBT.Parse(hex, originalTx.Network);

            // Checking that the PSBT of the receiver is clean
            if (newPSBT.GlobalXPubs.Any())
            {
                throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
            }

            if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
            {
                throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
            }
            ////////////

            newPSBT = await _explorerClientProvider.UpdatePSBT(derivationSchemeSettings, newPSBT);

            if (newPSBT.CheckSanity() is IList <PSBTError> errors2 && errors2.Count != 0)
            {
                throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
            }
            // We make sure we don't sign things what should not be signed
            foreach (var finalized in newPSBT.Inputs.Where(i => i.IsFinalized()))
            {
                finalized.ClearForFinalize();
            }
            // Make sure only the only our output have any information
            foreach (var output in newPSBT.Outputs)
            {
                output.HDKeyPaths.Clear();
                foreach (var originalOutput in  originalTx.Outputs)
                {
                    if (output.ScriptPubKey == originalOutput.ScriptPubKey)
                    {
                        output.UpdateFrom(originalOutput);
                    }
                }
            }

            // Making sure that our inputs are finalized, and that some of our inputs have not been added
            var newGlobalTx   = newPSBT.GetGlobalTransaction();
            int ourInputCount = 0;

            if (newGlobalTx.Version != oldGlobalTx.Version)
            {
                throw new PayjoinSenderException("The version field of the transaction has been modified");
            }
            if (newGlobalTx.LockTime != oldGlobalTx.LockTime)
            {
                throw new PayjoinSenderException("The LockTime field of the transaction has been modified");
            }
            foreach (var input in newPSBT.Inputs.CoinsFor(derivationSchemeSettings.AccountDerivation,
                                                          signingAccount.AccountKey, signingAccount.GetRootedKeyPath()))
            {
                if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is PSBTInput ourInput)
                {
                    ourInputCount++;
                    if (input.IsFinalized())
                    {
                        throw new PayjoinSenderException("A PSBT input from us should not be finalized");
                    }
                    if (newGlobalTx.Inputs[input.Index].Sequence != newGlobalTx.Inputs[ourInput.Index].Sequence)
                    {
                        throw new PayjoinSenderException("The sequence of one of our input has been modified");
                    }
                }
                else
                {
                    throw new PayjoinSenderException(
                              "The payjoin receiver added some of our own inputs in the proposal");
                }
            }

            // Making sure that the receiver's inputs are finalized and P2PWKH
            foreach (var input in newPSBT.Inputs)
            {
                if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is null)
                {
                    if (!input.IsFinalized())
                    {
                        throw new PayjoinSenderException("The payjoin receiver included a non finalized input");
                    }
                    if (!(input.FinalScriptWitness.GetSigner() is WitKeyId))
                    {
                        throw new PayjoinSenderException("The payjoin receiver included an input that is not P2PWKH");
                    }
                }
            }

            if (ourInputCount < originalTx.Inputs.Count)
            {
                throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
            }

            // We limit the number of inputs the receiver can add
            var addedInputs = newPSBT.Inputs.Count - originalTx.Inputs.Count;

            if (addedInputs == 0)
            {
                throw new PayjoinSenderException("The payjoin receiver did not added any input");
            }

            var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
                                                signingAccount.AccountKey,
                                                signingAccount.GetRootedKeyPath());

            if (sentAfter > sentBefore)
            {
                var overPaying = sentAfter - sentBefore;
                if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize))
                {
                    throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
                }
                var additionalFee = newPSBT.GetFee() - originalFee;
                if (overPaying > additionalFee)
                {
                    throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
                }
                if (overPaying > originalFee)
                {
                    throw new PayjoinSenderException("The payjoin receiver is making us pay more than twice the original fee");
                }

                // Let's check the difference is only for the fee and that feerate
                // did not changed that much
                var expectedFee = originalFeeRate.GetFee(newVirtualSize);
                // Signing precisely is hard science, give some breathing room for error.
                expectedFee += originalFeeRate.GetFee(newPSBT.Inputs.Count * 2);
                if (overPaying > (expectedFee - originalFee))
                {
                    throw new PayjoinSenderException("The payjoin receiver increased the fee rate we are paying too much");
                }
            }

            return(newPSBT);
        }