Exemple #1
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            DerivationSchemeSettings derivationSchemeSettings = null;

            try
            {
                psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
                derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(nameof(WalletPSBTReady), vm));
            }

            switch (command)
            {
            case "payjoin":
                string error = null;
                try
                {
                    var proposedPayjoin = await GetPayjoinProposedTX(vm.SigningContext.PayJoinEndpointUrl, psbt,
                                                                     derivationSchemeSettings, network, cancellationToken);

                    try
                    {
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath), new SigningOptions()
                        {
                            EnforceLowR = !(vm.SigningContext?.EnforceLowR is false)
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        proposedPayjoin.Finalize();
                        var hash = proposedPayjoin.ExtractTransaction().GetHash();
                        _EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Success,
                            AllowDismiss = false,
                            Html         = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
                        });
                        return(await WalletPSBTReady(walletId, vm, "broadcast"));
                    }
                    catch (Exception)
                    {
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Warning,
                            AllowDismiss = false,
                            Html         =
                                $"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
                                $"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
                                $"If you cancel or refuse to sign this transaction, the payment will proceed without payjoin"
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        return(ViewVault(walletId, vm.SigningContext));
                    }
                }
Exemple #2
0
 public void ActivateLTC()
 {
     LTCExplorerNode   = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork <BTCPayNetwork>("LTC").NBitcoinNetwork);
     LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork <BTCPayNetwork>("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
     PayTester.Chains.Add("LTC");
     PayTester.LTCNBXplorerUri = LTCExplorerClient.Address;
 }
        public async Task <IActionResult> LedgerConnection(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            string command,
            // getinfo
            // getxpub
            int account = 0,
            // sendtoaddress
            string psbt       = null,
            string hintChange = null
            )
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }
            var storeData = CurrentStore;
            var network   = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            if (network == null)
            {
                throw new FormatException("Invalid value for crypto code");
            }
            var derivationSettings = GetDerivationSchemeSettings(walletId);

            var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();

            using (var normalOperationTimeout = new CancellationTokenSource())
                using (var signTimeout = new CancellationTokenSource())
                {
                    normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
                    var    hw     = new LedgerHardwareWalletService(webSocket);
                    var    model  = new WalletSendLedgerModel();
                    object result = null;
                    try
                    {
                        if (command == "test")
                        {
                            result = await hw.Test(normalOperationTimeout.Token);
                        }
                        if (command == "sendtoaddress")
                        {
                            if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
                            {
                                throw new Exception($"{network.CryptoCode}: not started or fully synched");
                            }

                            var accountKey = derivationSettings.GetSigningAccountKeySettings();
                            // Some deployment does not have the AccountKeyPath set, let's fix this...
                            if (accountKey.AccountKeyPath == null)
                            {
                                // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
                                var foundKeyPath = await hw.FindKeyPathFromDerivation(network,
                                                                                      derivationSettings.AccountDerivation,
                                                                                      normalOperationTimeout.Token);

                                accountKey.AccountKeyPath = foundKeyPath ?? throw new HardwareWalletException($"This store is not configured to use this ledger");
                                storeData.SetSupportedPaymentMethod(derivationSettings);
                                await Repository.UpdateStore(storeData);
                            }
                            // If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
                            else
                            {
                                // Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
                                // but some deployment does not have it, so let's use AccountKeyPath instead
                                if (accountKey.RootFingerprint == null)
                                {
                                    var actualPubKey = await hw.GetExtPubKey(network, accountKey.AccountKeyPath, normalOperationTimeout.Token);

                                    if (!derivationSettings.AccountDerivation.GetExtPubKeys().Any(p => p.GetPublicKey() == actualPubKey.GetPublicKey()))
                                    {
                                        throw new HardwareWalletException($"This store is not configured to use this ledger");
                                    }
                                }
                                // We have the root fingerprint, we can check the root from it
                                else
                                {
                                    var actualPubKey = await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token);

                                    if (actualPubKey.GetHDFingerPrint() != accountKey.RootFingerprint.Value)
                                    {
                                        throw new HardwareWalletException($"This store is not configured to use this ledger");
                                    }
                                }
                            }

                            // Some deployment does not have the RootFingerprint set, let's fix this...
                            if (accountKey.RootFingerprint == null)
                            {
                                accountKey.RootFingerprint = (await hw.GetPubKey(network, new KeyPath(), normalOperationTimeout.Token)).GetHDFingerPrint();
                                storeData.SetSupportedPaymentMethod(derivationSettings);
                                await Repository.UpdateStore(storeData);
                            }

                            var psbtResponse = new CreatePSBTResponse()
                            {
                                PSBT          = PSBT.Parse(psbt, network.NBitcoinNetwork),
                                ChangeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork)
                            };


                            derivationSettings.RebaseKeyPaths(psbtResponse.PSBT);

                            signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
                            psbtResponse.PSBT = await hw.SignTransactionAsync(psbtResponse.PSBT, accountKey.GetRootedKeyPath(), accountKey.AccountKey, psbtResponse.ChangeAddress?.ScriptPubKey, signTimeout.Token);

                            result = new SendToAddressResult()
                            {
                                PSBT = psbtResponse.PSBT.ToBase64()
                            };
                        }
                    }
                    catch (OperationCanceledException)
                    { result = new LedgerTestResult()
                      {
                          Success = false, Error = "Timeout"
                      }; }
                    catch (Exception ex)
                    { result = new LedgerTestResult()
                      {
                          Success = false, Error = ex.Message
                      }; }
                    finally { hw.Dispose(); }
                    try
                    {
                        if (result != null)
                        {
                            UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
                            var          bytes     = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _serializerSettings));
                            await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
                        }
                    }
                    catch { }
                    finally
                    {
                        await webSocket.CloseSocket();
                    }
                }
            return(new EmptyResult());
        }
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));
            }
        }
        public void LoadArgs(IConfiguration conf)
        {
            NetworkType = DefaultConfiguration.GetNetworkType(conf);
            DataDir     = conf.GetDataDir(NetworkType);
            Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());

            if (conf.GetOrDefault <bool>("launchsettings", false) && NetworkType != NetworkType.Regtest)
            {
                throw new ConfigException($"You need to run BTCPayServer with the run.sh or run.ps1 script");
            }

            var supportedChains = conf.GetOrDefault <string>("chains", "btc")
                                  .Split(',', StringSplitOptions.RemoveEmptyEntries)
                                  .Select(t => t.ToUpperInvariant()).ToHashSet();

            var networkProvider = new BTCPayNetworkProvider(NetworkType);
            var filtered        = networkProvider.Filter(supportedChains.ToArray());
            var elementsBased   = filtered.GetAll().OfType <ElementsBTCPayNetwork>();
            var parentChains    = elementsBased.Select(network => network.NetworkbitcoinCode.ToUpperInvariant()).Distinct();
            var allSubChains    = networkProvider.GetAll().OfType <ElementsBTCPayNetwork>()
                                  .Where(network => parentChains.Contains(network.NetworkbitcoinCode)).Select(network => network.bitcoinCode.ToUpperInvariant());

            supportedChains.AddRange(allSubChains);
            NetworkProvider = networkProvider.Filter(supportedChains.ToArray());
            foreach (var chain in supportedChains)
            {
                if (NetworkProvider.GetNetwork <BTCPayNetworkBase>(chain) == null)
                {
                    throw new ConfigException($"Invalid chains \"{chain}\"");
                }
            }

            var validChains = new List <string>();

            foreach (var net in NetworkProvider.GetAll().OfType <BTCPayNetwork>())
            {
                NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
                setting.bitcoinCode = net.bitcoinCode;
                setting.ExplorerUri = conf.GetOrDefault <Uri>($"{net.bitcoinCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
                setting.CookieFile  = conf.GetOrDefault <string>($"{net.bitcoinCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
                NBXplorerConnectionSettings.Add(setting);

                {
                    var lightning = conf.GetOrDefault <string>($"{net.bitcoinCode}.lightning", string.Empty);
                    if (lightning.Length != 0)
                    {
                        if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
                        {
                            Logs.Configuration.LogWarning($"Invalid setting {net.bitcoinCode}.lightning, " + Environment.NewLine +
                                                          $"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
                                                          $"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
                                                          $"If you have a lnd server: 'type=lnd-rest;server=https://lnd:[email protected];macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
                                                          $"              lnd server: 'type=lnd-rest;server=https://lnd:[email protected];macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
                                                          $"If you have an eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393;bitcoin-auth=bitcoinrpcuser:bitcoinrpcpassword" + Environment.NewLine +
                                                          $"               eclair server: 'type=eclair;server=http://eclair.com:4570;password=eclairpassword;bitcoin-host=bitcoind:37393" + Environment.NewLine +
                                                          $"Error: {error}" + Environment.NewLine +
                                                          "This service will not be exposed through BTCPay Server");
                        }
                        else
                        {
                            if (connectionString.IsLegacy)
                            {
                                Logs.Configuration.LogWarning($"Setting {net.bitcoinCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
                            }
                            InternalLightningBybitcoinCode.Add(net.bitcoinCode, connectionString);
                        }
                    }
                }

                ExternalServices.Load(net.bitcoinCode, conf);
            }
        public async Task <IActionResult> SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
                                                       WalletId walletId, SignWithSeedViewModel viewModel)
        {
            if (!ModelState.IsValid)
            {
                return(View(viewModel));
            }
            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            if (network == null)
            {
                throw new FormatException("Invalid value for crypto code");
            }

            ExtKey extKey = viewModel.GetExtKey(network.NBitcoinNetwork);

            if (extKey == null)
            {
                ModelState.AddModelError(nameof(viewModel.SeedOrKey),
                                         "Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv");
            }

            var psbt = PSBT.Parse(viewModel.PSBT, network.NBitcoinNetwork);

            if (!psbt.IsReadyToSign())
            {
                ModelState.AddModelError(nameof(viewModel.PSBT), "PSBT is not ready to be signed");
            }

            if (!ModelState.IsValid)
            {
                return(View(viewModel));
            }

            ExtKey signingKey         = null;
            var    settings           = GetDerivationSchemeSettings(walletId);
            var    signingKeySettings = settings.GetSigningAccountKeySettings();

            if (signingKeySettings.RootFingerprint is null)
            {
                signingKeySettings.RootFingerprint = extKey.GetPublicKey().GetHDFingerPrint();
            }

            RootedKeyPath rootedKeyPath = signingKeySettings.GetRootedKeyPath();

            // The user gave the root key, let's try to rebase the PSBT, and derive the account private key
            if (rootedKeyPath?.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
            {
                psbt.RebaseKeyPaths(signingKeySettings.AccountKey, rootedKeyPath);
                signingKey = extKey.Derive(rootedKeyPath.KeyPath);
            }
            // The user maybe gave the account key, let's try to sign with it
            else
            {
                signingKey = extKey;
            }
            var balanceChange = psbt.GetBalance(settings.AccountDerivation, signingKey, rootedKeyPath);

            if (balanceChange == Money.Zero)
            {
                ModelState.AddModelError(nameof(viewModel.SeedOrKey), "This seed is unable to sign this transaction. Either the seed is incorrect, or the account path has not been properly configured in the Wallet Settings.");
                return(View(viewModel));
            }
            psbt.SignAll(settings.AccountDerivation, signingKey, rootedKeyPath);
            ModelState.Remove(nameof(viewModel.PSBT));
            return(await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString()));
        }
        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));

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

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

                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(walletId, psbt, vm.FileName));

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

            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()
                    }));
                }

                return(View(vm));

            case "broadcast":
            {
                return(await WalletPSBTReady(walletId, 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));
            }
        }
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm.PSBT, vm.SigningKey, vm.SigningKeyPath));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            try
            {
                psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
                var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(vm));
            }
            if (command == "broadcast")
            {
                if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
                {
                    vm.SetErrors(errors);
                    return(View(vm));
                }
                var transaction = psbt.ExtractTransaction();
                try
                {
                    var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                    if (!broadcastResult.Success)
                    {
                        vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
                        return(View(vm));
                    }
                }
                catch (Exception ex)
                {
                    vm.GlobalError = "Error while broadcasting: " + ex.Message;
                    return(View(vm));
                }
                return(RedirectToWalletTransaction(walletId, transaction));
            }
            else if (command == "analyze-psbt")
            {
                return(RedirectToWalletPSBT(walletId, psbt));
            }
            else
            {
                vm.GlobalError = "Unknown command";
                return(View(vm));
            }
        }
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null, CancellationToken cancellationToken = default)
        {
            if (command == null)
            {
                return(await WalletPSBTReady(walletId, vm));
            }
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);
            DerivationSchemeSettings derivationSchemeSettings = null;

            try
            {
                psbt = PSBT.Parse(vm.SigningContext.PSBT, network.NBitcoinNetwork);
                derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
                if (derivationSchemeSettings == null)
                {
                    return(NotFound());
                }
                await FetchTransactionDetails(derivationSchemeSettings, vm, network);
            }
            catch
            {
                vm.GlobalError = "Invalid PSBT";
                return(View(nameof(WalletPSBTReady), vm));
            }

            switch (command)
            {
            case "payjoin":
                string error = null;
                try
                {
                    var proposedPayjoin = await GetPayjoinProposedTX(new BitcoinUrlBuilder(vm.SigningContext.PayJoinBIP21, network.NBitcoinNetwork), psbt,
                                                                     derivationSchemeSettings, network, cancellationToken);

                    try
                    {
                        proposedPayjoin.Settings.SigningOptions = new SigningOptions()
                        {
                            EnforceLowR = !(vm.SigningContext?.EnforceLowR is false)
                        };
                        var extKey = ExtKey.Parse(vm.SigningKey, network.NBitcoinNetwork);
                        proposedPayjoin = proposedPayjoin.SignAll(derivationSchemeSettings.AccountDerivation,
                                                                  extKey,
                                                                  RootedKeyPath.Parse(vm.SigningKeyPath));
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        proposedPayjoin.Finalize();
                        var hash = proposedPayjoin.ExtractTransaction().GetHash();
                        _EventAggregator.Publish(new UpdateTransactionLabel(walletId, hash, UpdateTransactionLabel.PayjoinLabelTemplate()));
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Success,
                            AllowDismiss = false,
                            Html         = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
                        });
                        return(await WalletPSBTReady(walletId, vm, "broadcast"));
                    }
                    catch (Exception)
                    {
                        TempData.SetStatusMessageModel(new StatusMessageModel()
                        {
                            Severity     = StatusMessageModel.StatusSeverity.Warning,
                            AllowDismiss = false,
                            Html         =
                                $"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
                                $"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
                                $"If you cancel or refuse to sign this transaction, the payment will proceed without payjoin"
                        });
                        vm.SigningContext.PSBT         = proposedPayjoin.ToBase64();
                        vm.SigningContext.OriginalPSBT = psbt.ToBase64();
                        return(ViewVault(walletId, vm.SigningContext));
                    }
                }
                catch (PayjoinReceiverException ex)
                {
                    error = $"The payjoin receiver could not complete the payjoin: {ex.Message}";
                }
                catch (PayjoinSenderException ex)
                {
                    error = $"We rejected the receiver's payjoin proposal: {ex.Message}";
                }
                catch (Exception ex)
                {
                    error = $"Unexpected payjoin error: {ex.Message}";
                }

                //we possibly exposed the tx to the receiver, so we need to broadcast straight away
                psbt.Finalize();
                TempData.SetStatusMessageModel(new StatusMessageModel()
                {
                    Severity     = StatusMessageModel.StatusSeverity.Warning,
                    AllowDismiss = false,
                    Html         = $"The payjoin transaction could not be created.<br/>" +
                                   $"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" +
                                   $"{error}"
                });
                return(await WalletPSBTReady(walletId, vm, "broadcast"));

            case "broadcast" when !psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors):
                vm.SetErrors(errors);
                return(View(nameof(WalletPSBTReady), vm));

            case "broadcast":
            {
                var transaction = psbt.ExtractTransaction();
                try
                {
                    var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                    if (!broadcastResult.Success)
                    {
                        if (!string.IsNullOrEmpty(vm.SigningContext.OriginalPSBT))
                        {
                            TempData.SetStatusMessageModel(new StatusMessageModel()
                                {
                                    Severity     = StatusMessageModel.StatusSeverity.Warning,
                                    AllowDismiss = false,
                                    Html         = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast."
                                });
                            vm.SigningContext.PSBT         = vm.SigningContext.OriginalPSBT;
                            vm.SigningContext.OriginalPSBT = null;
                            return(await WalletPSBTReady(walletId, vm, "broadcast"));
                        }

                        vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
                        return(View(nameof(WalletPSBTReady), vm));
                    }
                }
                catch (Exception ex)
                {
                    vm.GlobalError = "Error while broadcasting: " + ex.Message;
                    return(View(nameof(WalletPSBTReady), vm));
                }

                if (!TempData.HasStatusMessage())
                {
                    TempData[WellKnownTempData.SuccessMessage] = $"Transaction broadcasted successfully ({transaction.GetHash()})";
                }
                return(RedirectToWalletTransaction(walletId, transaction));
            }

            case "analyze-psbt":
                return(RedirectToWalletPSBT(new WalletPSBTViewModel()
                {
                    PSBT = psbt.ToBase64()
                }));

            default:
                vm.GlobalError = "Unknown command";
                return(View(nameof(WalletPSBTReady), vm));
            }
        }
Exemple #10
0
        public async Task <IActionResult> LedgerConnection(
            string command,
            // getinfo
            string cryptoCode = null,
            // getxpub
            [ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
            DerivationStrategyBase derivationScheme = null,
            int account = 0,
            // sendtoaddress
            string destination = null, string amount = null, string feeRate = null, string substractFees = null
            )
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }
            var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();

            using (var normalOperationTimeout = new CancellationTokenSource())
                using (var signTimeout = new CancellationTokenSource())
                {
                    normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
                    var    hw     = new HardwareWalletService(webSocket);
                    object result = null;
                    try
                    {
                        BTCPayNetwork network = null;
                        if (cryptoCode != null)
                        {
                            network = NetworkProvider.GetNetwork(cryptoCode);
                            if (network == null)
                            {
                                throw new FormatException("Invalid value for crypto code");
                            }
                        }

                        BitcoinAddress destinationAddress = null;
                        if (destination != null)
                        {
                            try
                            {
                                destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
                            }
                            catch { }
                            if (destinationAddress == null)
                            {
                                throw new FormatException("Invalid value for destination");
                            }
                        }

                        FeeRate feeRateValue = null;
                        if (feeRate != null)
                        {
                            try
                            {
                                feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
                            }
                            catch { }
                            if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
                            {
                                throw new FormatException("Invalid value for fee rate");
                            }
                        }

                        Money amountBTC = null;
                        if (amount != null)
                        {
                            try
                            {
                                amountBTC = Money.Parse(amount);
                            }
                            catch { }
                            if (amountBTC == null || amountBTC <= Money.Zero)
                            {
                                throw new FormatException("Invalid value for amount");
                            }
                        }

                        bool subsctractFeesValue = false;
                        if (substractFees != null)
                        {
                            try
                            {
                                subsctractFeesValue = bool.Parse(substractFees);
                            }
                            catch { throw new FormatException("Invalid value for subtract fees"); }
                        }
                        if (command == "test")
                        {
                            result = await hw.Test(normalOperationTimeout.Token);
                        }
                        if (command == "getxpub")
                        {
                            var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);

                            result = getxpubResult;
                        }
                        if (command == "getinfo")
                        {
                            var strategy = GetDirectDerivationStrategy(derivationScheme);
                            if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
                            {
                                throw new Exception($"This store is not configured to use this ledger");
                            }

                            var feeProvider     = _feeRateProvider.CreateFeeProvider(network);
                            var recommendedFees = feeProvider.GetFeeRateAsync();
                            var balance         = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
                            result = new GetInfoResult()
                            {
                                Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi
                            };
                        }

                        if (command == "sendtoaddress")
                        {
                            if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
                            {
                                throw new Exception($"{network.CryptoCode}: not started or fully synched");
                            }
                            var strategy = GetDirectDerivationStrategy(derivationScheme);
                            var wallet   = _walletProvider.GetWallet(network);
                            var change   = wallet.GetChangeAddressAsync(derivationScheme);

                            var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);

                            var changeAddress = await change;
                            var send          = new[] { (
        public async Task <IActionResult> LedgerConnection(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            string command,
            // getinfo
            // getxpub
            int account = 0,
            // sendtoaddress
            bool noChange      = false,
            string destination = null, string amount = null, string feeRate = null, string substractFees = null
            )
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }

            var cryptoCode       = walletId.CryptoCode;
            var storeData        = (await Repository.FindStore(walletId.StoreId, GetUserId()));
            var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;

            var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();

            using (var normalOperationTimeout = new CancellationTokenSource())
                using (var signTimeout = new CancellationTokenSource())
                {
                    normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
                    var    hw     = new HardwareWalletService(webSocket);
                    object result = null;
                    try
                    {
                        BTCPayNetwork network = null;
                        if (cryptoCode != null)
                        {
                            network = NetworkProvider.GetNetwork(cryptoCode);
                            if (network == null)
                            {
                                throw new FormatException("Invalid value for crypto code");
                            }
                        }

                        BitcoinAddress destinationAddress = null;
                        if (destination != null)
                        {
                            try
                            {
                                destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
                            }
                            catch { }
                            if (destinationAddress == null)
                            {
                                throw new FormatException("Invalid value for destination");
                            }
                        }

                        FeeRate feeRateValue = null;
                        if (feeRate != null)
                        {
                            try
                            {
                                feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
                            }
                            catch { }
                            if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
                            {
                                throw new FormatException("Invalid value for fee rate");
                            }
                        }

                        Money amountBTC = null;
                        if (amount != null)
                        {
                            try
                            {
                                amountBTC = Money.Parse(amount);
                            }
                            catch { }
                            if (amountBTC == null || amountBTC <= Money.Zero)
                            {
                                throw new FormatException("Invalid value for amount");
                            }
                        }

                        bool subsctractFeesValue = false;
                        if (substractFees != null)
                        {
                            try
                            {
                                subsctractFeesValue = bool.Parse(substractFees);
                            }
                            catch { throw new FormatException("Invalid value for subtract fees"); }
                        }
                        if (command == "test")
                        {
                            result = await hw.Test(normalOperationTimeout.Token);
                        }
                        if (command == "sendtoaddress")
                        {
                            if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
                            {
                                throw new Exception($"{network.CryptoCode}: not started or fully synched");
                            }
                            var         strategy       = GetDirectDerivationStrategy(derivationScheme);
                            var         wallet         = _walletProvider.GetWallet(network);
                            var         change         = wallet.GetChangeAddressAsync(derivationScheme);
                            var         keypaths       = new Dictionary <Script, KeyPath>();
                            List <Coin> availableCoins = new List <Coin>();
                            foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
                            {
                                keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
                                availableCoins.Add(c.Coin);
                            }

                            var changeAddress = await change;

                            var storeBlob    = storeData.GetStoreBlob();
                            var paymentId    = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
                            var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
                            // Some deployment have the wallet root key path saved in the store blob
                            // If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
                            if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
                            {
                                // If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
                                foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);

                                if (foundKeyPath == null)
                                {
                                    throw new HardwareWalletException($"This store is not configured to use this ledger");
                                }
                                storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
                                storeData.SetStoreBlob(storeBlob);
                                await Repository.UpdateStore(storeData);
                            }
retry:
                            var send = new[] { (