Beispiel #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);
            }
        }
Beispiel #2
0
        public async Task <IActionResult> GenerateWallet(string storeId, string cryptoCode, WalletSetupMethod method, WalletSetupRequest request)
        {
            var checkResult = IsAvailable(cryptoCode, out var store, out var network);

            if (checkResult != null)
            {
                return(checkResult);
            }

            var(hotWallet, rpcImport) = await CanUseHotWallet();

            if (!hotWallet && request.SavePrivateKeys || !rpcImport && request.ImportKeysToRPC)
            {
                return(NotFound());
            }

            var client   = _ExplorerProvider.GetExplorerClient(cryptoCode);
            var isImport = method == WalletSetupMethod.Seed;
            var vm       = new WalletSetupViewModel
            {
                StoreId                = storeId,
                CryptoCode             = cryptoCode,
                Method                 = method,
                SetupRequest           = request,
                Confirmation           = string.IsNullOrEmpty(request.ExistingMnemonic),
                Network                = network,
                Source                 = isImport ? "SeedImported" : "NBXplorerGenerated",
                IsHotWallet            = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
                DerivationSchemeFormat = "BTCPay",
                CanUseHotWallet        = hotWallet,
                CanUseRPCImport        = rpcImport,
                IsTaprootActivated     = TaprootActivated(cryptoCode),
                SupportTaproot         = network.NBitcoinNetwork.Consensus.SupportTaproot,
                SupportSegwit          = network.NBitcoinNetwork.Consensus.SupportSegwit
            };

            if (isImport && string.IsNullOrEmpty(request.ExistingMnemonic))
            {
                ModelState.AddModelError(nameof(request.ExistingMnemonic), "Please provide your existing seed");
                return(View(vm.ViewName, vm));
            }

            GenerateWalletResponse response;

            try
            {
                response = await client.GenerateWalletAsync(request);

                if (response == null)
                {
                    throw new Exception("Node unavailable");
                }
            }
            catch (Exception e)
            {
                TempData.SetStatusMessageModel(new StatusMessageModel
                {
                    Severity = StatusMessageModel.StatusSeverity.Error,
                    Html     = $"There was an error generating your wallet: {e.Message}"
                });
                return(View(vm.ViewName, vm));
            }

            var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network);

            if (method == WalletSetupMethod.Seed)
            {
                derivationSchemeSettings.Source      = "ImportedSeed";
                derivationSchemeSettings.IsHotWallet = request.SavePrivateKeys;
            }
            else
            {
                derivationSchemeSettings.Source      = "NBXplorerGenerated";
                derivationSchemeSettings.IsHotWallet = method == WalletSetupMethod.HotWallet;
            }

            var accountSettings = derivationSchemeSettings.GetSigningAccountKeySettings();

            accountSettings.AccountKeyPath           = response.AccountKeyPath.KeyPath;
            accountSettings.RootFingerprint          = response.AccountKeyPath.MasterFingerprint;
            derivationSchemeSettings.AccountOriginal = response.DerivationScheme.ToString();

            // Set wallet properties from generate response
            vm.RootFingerprint = response.AccountKeyPath.MasterFingerprint.ToString();
            vm.AccountKey      = response.AccountHDKey.Neuter().ToWif();
            vm.KeyPath         = response.AccountKeyPath.KeyPath.ToString();
            vm.Config          = ProtectString(derivationSchemeSettings.ToJson());

            var result = await UpdateWallet(vm);

            if (!ModelState.IsValid || !(result is RedirectToActionResult))
            {
                return(result);
            }

            if (!isImport)
            {
                TempData.SetStatusMessageModel(new StatusMessageModel
                {
                    Severity = StatusMessageModel.StatusSeverity.Success,
                    Html     = "<span class='text-centered'>Your wallet has been generated.</span>"
                });
                var seedVm = new RecoverySeedBackupViewModel
                {
                    CryptoCode = cryptoCode,
                    Mnemonic   = response.Mnemonic,
                    Passphrase = response.Passphrase,
                    IsStored   = request.SavePrivateKeys,
                    ReturnUrl  = Url.Action(nameof(GenerateWalletConfirm), new { storeId, cryptoCode })
                };
                if (this._BTCPayEnv.IsDeveloping)
                {
                    GenerateWalletResponse = response;
                }
                return(this.RedirectToRecoverySeedBackup(seedVm));
            }

            TempData.SetStatusMessageModel(new StatusMessageModel
            {
                Severity = StatusMessageModel.StatusSeverity.Warning,
                Html     = "Please check your addresses and confirm."
            });
            return(result);
        }
        public async Task <IActionResult> GenerateOnChainWallet(string storeId, string cryptoCode,
                                                                GenerateWalletRequest request)
        {
            var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode);

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

            if (!_walletProvider.IsAvailable(network))
            {
                return(this.CreateAPIError("not-available",
                                           $"{cryptoCode} services are not currently available"));
            }

            var method = GetExistingBtcLikePaymentMethod(cryptoCode);

            if (method != null)
            {
                return(this.CreateAPIError("already-configured",
                                           $"{cryptoCode} wallet is already configured for this store"));
            }

            var canUseHotWallet = await CanUseHotWallet();

            if (request.SavePrivateKeys && !canUseHotWallet.HotWallet)
            {
                ModelState.AddModelError(nameof(request.SavePrivateKeys),
                                         "This instance forbids non-admins from having a hot wallet for your store.");
            }

            if (request.ImportKeysToRPC && !canUseHotWallet.RPCImport)
            {
                ModelState.AddModelError(nameof(request.ImportKeysToRPC),
                                         "This instance forbids non-admins from having importing the wallet addresses/keys to the underlying node.");
            }

            if (!ModelState.IsValid)
            {
                return(this.CreateValidationError(ModelState));
            }

            var client = _explorerClientProvider.GetExplorerClient(network);
            GenerateWalletResponse response;

            try
            {
                response = await client.GenerateWalletAsync(request);

                if (response == null)
                {
                    return(this.CreateAPIError("not-available",
                                               $"{cryptoCode} services are not currently available"));
                }
            }
            catch (Exception e)
            {
                return(this.CreateAPIError("not-available",
                                           $"{cryptoCode} error: {e.Message}"));
            }

            var derivationSchemeSettings = new DerivationSchemeSettings(response.DerivationScheme, network);

            derivationSchemeSettings.Source =
                string.IsNullOrEmpty(request.ExistingMnemonic) ? "NBXplorerGenerated" : "ImportedSeed";
            derivationSchemeSettings.IsHotWallet = request.SavePrivateKeys;

            var accountSettings = derivationSchemeSettings.GetSigningAccountKeySettings();

            accountSettings.AccountKeyPath           = response.AccountKeyPath.KeyPath;
            accountSettings.RootFingerprint          = response.AccountKeyPath.MasterFingerprint;
            derivationSchemeSettings.AccountOriginal = response.DerivationScheme.ToString();

            var store     = Store;
            var storeBlob = store.GetStoreBlob();

            store.SetSupportedPaymentMethod(new PaymentMethodId(cryptoCode, PaymentTypes.BTCLike),
                                            derivationSchemeSettings);
            store.SetStoreBlob(storeBlob);
            await _storeRepository.UpdateStore(store);

            var rawResult = GetExistingBtcLikePaymentMethod(cryptoCode, store);
            var result    = new OnChainPaymentMethodDataWithSensitiveData(rawResult.CryptoCode, rawResult.DerivationScheme,
                                                                          rawResult.Enabled, rawResult.Label, rawResult.AccountKeyPath, response.GetMnemonic());

            return(Ok(result));
        }
Beispiel #4
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);
        }