Example #1
0
        public async Task <IActionResult> LedgerConnection(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            string command,
            // getinfo
            // getxpub
            int account = 0,
            // sendtoaddress
            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");
            }
            PSBT psbt = GetAmbientPSBT(network.NBitcoinNetwork, true);
            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);
                    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);
                            }

                            derivationSettings.RebaseKeyPaths(psbt);
                            var changeAddress = string.IsNullOrEmpty(hintChange) ? null : BitcoinAddress.Create(hintChange, network.NBitcoinNetwork);
                            signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
                            psbt = await hw.SignTransactionAsync(psbt, accountKey.GetRootedKeyPath(), accountKey.AccountKey, changeAddress?.ScriptPubKey, signTimeout.Token);

                            SetAmbientPSBT(null);
                            result = new SendToAddressResult()
                            {
                                PSBT = 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());
        }
Example #2
0
        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, bool substractFees = false, bool disableRBF = false
            )
        {
            if (!HttpContext.WebSockets.IsWebSocketRequest)
            {
                return(NotFound());
            }

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

            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);
                    var    model  = new WalletSendLedgerModel();
                    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");
                            }
                        }

                        if (destination != null)
                        {
                            try
                            {
                                BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
                                model.Destination = destination.Trim();
                            }
                            catch { }
                        }


                        if (feeRate != null)
                        {
                            try
                            {
                                model.FeeSatoshiPerByte = int.Parse(feeRate, CultureInfo.InvariantCulture);
                            }
                            catch { }
                            if (model.FeeSatoshiPerByte <= 0)
                            {
                                throw new FormatException("Invalid value for fee rate");
                            }
                        }

                        if (amount != null)
                        {
                            try
                            {
                                model.Amount = Money.Parse(amount).ToDecimal(MoneyUnit.BTC);
                            }
                            catch { }
                            if (model.Amount <= 0m)
                            {
                                throw new FormatException("Invalid value for amount");
                            }
                        }

                        model.SubstractFees = substractFees;
                        model.NoChange      = noChange;
                        model.DisableRBF    = disableRBF;
                        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(derivationSettings.AccountDerivation);
                            // 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 (derivationSettings.AccountKeyPath == null || !await hw.CanSign(network, strategy, derivationSettings.AccountKeyPath, 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
                                var foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);

                                if (foundKeyPath == null)
                                {
                                    throw new HardwareWalletException($"This store is not configured to use this ledger");
                                }
                                derivationSettings.AccountKeyPath = foundKeyPath;
                                storeData.SetSupportedPaymentMethod(derivationSettings);
                                await Repository.UpdateStore(storeData);
                            }


                            var psbt = await CreatePSBT(network, derivationSettings, model, normalOperationTimeout.Token);

                            signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
                            psbt.PSBT = await hw.SignTransactionAsync(psbt.PSBT, psbt.ChangeAddress?.ScriptPubKey, signTimeout.Token);

                            if (!psbt.PSBT.TryFinalize(out var errors))
                            {
                                throw new Exception($"Error while finalizing the transaction ({new PSBTException(errors).ToString()})");
                            }
                            var transaction = psbt.PSBT.ExtractTransaction();
                            try
                            {
                                var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                                if (!broadcastResult.Success)
                                {
                                    throw new Exception($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
                                }
                            }
                            catch (Exception ex)
                            {
                                throw new Exception("Error while broadcasting: " + ex.Message);
                            }
                            var wallet = _walletProvider.GetWallet(network);
                            wallet.InvalidateCache(derivationSettings.AccountDerivation);
                            result = new SendToAddressResult()
                            {
                                TransactionId = transaction.GetHash().ToString()
                            };
                        }
                    }
                    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, _mvcJsonOptions.Value.SerializerSettings));
                            await webSocket.SendAsync(new ArraySegment <byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
                        }
                    }
                    catch { }
                    finally
                    {
                        await webSocket.CloseSocket();
                    }
                }
            return(new EmptyResult());
        }