public async Task TrackClaim(PaymentMethodId paymentMethodId, IClaimDestination claimDestination)
    {
        var network        = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(paymentMethodId.CryptoCode);
        var explorerClient = _explorerClientProvider.GetExplorerClient(network);

        if (claimDestination is IBitcoinLikeClaimDestination bitcoinLikeClaimDestination)
        {
            await explorerClient.TrackAsync(TrackedSource.Create(bitcoinLikeClaimDestination.Address));
        }
    }
Пример #2
0
        private async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendLedgerModel sendModel, CancellationToken cancellationToken)
        {
            var nbx = ExplorerClientProvider.GetExplorerClient(network);
            CreatePSBTRequest     psbtRequest     = new CreatePSBTRequest();
            CreatePSBTDestination psbtDestination = new CreatePSBTDestination();

            psbtRequest.Destinations.Add(psbtDestination);
            if (network.SupportRBF)
            {
                psbtRequest.RBF = !sendModel.DisableRBF;
            }
            psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
            psbtDestination.Amount      = Money.Coins(sendModel.Amount);
            psbtRequest.FeePreference   = new FeePreference();
            psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
            if (sendModel.NoChange)
            {
                psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
            }
            psbtDestination.SubstractFees = sendModel.SubstractFees;

            var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));

            if (psbt == null)
            {
                throw new NotSupportedException("You need to update your version of NBXplorer");
            }

            if (network.MinFee != null)
            {
                psbt.PSBT.TryGetFee(out var fee);
                if (fee < network.MinFee)
                {
                    psbtRequest.FeePreference = new FeePreference()
                    {
                        ExplicitFee = network.MinFee
                    };
                    psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));
                }
            }

            if (derivationSettings.AccountKeyPath != null && derivationSettings.AccountKeyPath.Indexes.Length != 0)
            {
                // NBX only know the path relative to the account xpub.
                // Here we rebase the hd_keys in the PSBT to have a keypath relative to the root HD so the wallet can sign
                // Note that the fingerprint of the hd keys are now 0, which is wrong
                // However, hardware wallets does not give a damn, and sometimes does not even allow us to get this fingerprint anyway.
                foreach (var o in psbt.PSBT.Inputs.OfType <PSBTCoin>().Concat(psbt.PSBT.Outputs))
                {
                    var rootFP = derivationSettings.RootFingerprint is HDFingerprint fp ? fp : default;
                    foreach (var keypath in o.HDKeyPaths.ToList())
                    {
                        var newKeyPath = derivationSettings.AccountKeyPath.Derive(keypath.Value.Item2);
                        o.HDKeyPaths.Remove(keypath.Key);
                        o.HDKeyPaths.Add(keypath.Key, Tuple.Create(rootFP, newKeyPath));
                    }
                }
            }
            return(psbt);
        }
Пример #3
0
        public async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
        {
            var nbx = ExplorerClientProvider.GetExplorerClient(network);
            CreatePSBTRequest     psbtRequest     = new CreatePSBTRequest();
            CreatePSBTDestination psbtDestination = new CreatePSBTDestination();

            psbtRequest.Destinations.Add(psbtDestination);
            if (network.SupportRBF)
            {
                psbtRequest.RBF = !sendModel.DisableRBF;
            }
            psbtDestination.Destination = BitcoinAddress.Create(sendModel.Destination, network.NBitcoinNetwork);
            psbtDestination.Amount      = Money.Coins(sendModel.Amount.Value);
            psbtRequest.FeePreference   = new FeePreference();
            psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
            if (sendModel.NoChange)
            {
                psbtRequest.ExplicitChangeAddress = psbtDestination.Destination;
            }
            psbtDestination.SubstractFees = sendModel.SubstractFees;
            var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));

            if (psbt == null)
            {
                throw new NotSupportedException("You need to update your version of NBXplorer");
            }
            return(psbt);
        }
Пример #4
0
        public async Task <IActionResult> WalletPSBT(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId,
            WalletPSBTViewModel vm, string command = null)
        {
            var network = NetworkProvider.GetNetwork(walletId.CryptoCode);
            var psbt    = vm.GetPSBT(network.NBitcoinNetwork);

            if (psbt == null)
            {
                ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
                return(View(vm));
            }

            if (command == null)
            {
                vm.Decoded  = psbt.ToString();
                vm.FileName = string.Empty;
                return(View(vm));
            }
            else if (command == "ledger")
            {
                return(ViewWalletSendLedger(psbt));
            }
            else if (command == "broadcast")
            {
                if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
                {
                    return(ViewPSBT(psbt, errors));
                }
                var transaction = psbt.ExtractTransaction();
                try
                {
                    var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                    if (!broadcastResult.Success)
                    {
                        return(ViewPSBT(psbt, new[] { $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" }));
                    }
                }
                catch (Exception ex)
                {
                    return(ViewPSBT(psbt, "Error while broadcasting: " + ex.Message));
                }
                return(await RedirectToWalletTransaction(walletId, transaction));
            }
            else if (command == "combine")
            {
                ModelState.Remove(nameof(vm.PSBT));
                return(View(nameof(WalletPSBTCombine), new WalletPSBTCombineViewModel()
                {
                    OtherPSBT = psbt.ToBase64()
                }));
            }
            else if (command == "save-psbt")
            {
                return(FilePSBT(psbt, vm.FileName));
            }
            return(View(vm));
        }
Пример #5
0
        public async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
        {
            var nbx = ExplorerClientProvider.GetExplorerClient(network);
            CreatePSBTRequest psbtRequest = new CreatePSBTRequest();

            foreach (var transactionOutput in sendModel.Outputs)
            {
                var psbtDestination = new CreatePSBTDestination();
                psbtRequest.Destinations.Add(psbtDestination);
                psbtDestination.Destination   = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                psbtDestination.Amount        = Money.Coins(transactionOutput.Amount.Value);
                psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
            }

            if (network.SupportRBF)
            {
                psbtRequest.RBF = !sendModel.DisableRBF;
            }

            psbtRequest.FeePreference = new FeePreference();
            psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(Money.Satoshis(sendModel.FeeSatoshiPerByte), 1);
            if (sendModel.NoChange)
            {
                psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().Destination;
            }

            var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));

            if (psbt == null)
            {
                throw new NotSupportedException("Necesitas actualizar tu versión de NBXplorer");
            }
            return(psbt);
        }
Пример #6
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, RescanWalletModel vm)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);

            if (paymentMethod == null)
            {
                return(NotFound());
            }
            var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);

            try
            {
                await explorer.ScanUTXOSetAsync(paymentMethod.AccountDerivation, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
            }
            catch (NBXplorerException ex) when(ex.Error.Code == "scanutxoset-in-progress")
            {
            }
            return(RedirectToAction());
        }
Пример #7
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, RescanWalletModel vm)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);

            if (paymentMethod == null)
            {
                return(NotFound());
            }
            var explorer = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);

            try
            {
                await explorer.ScanUTXOSetAsync(paymentMethod.DerivationStrategyBase, vm.BatchSize, vm.GapLimit, vm.StartingIndex);
            }
            catch (NBXplorerException ex) when(ex.Error.Code == "scanutxoset-in-progress")
            {
            }
            return(RedirectToAction());
        }
Пример #8
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
        {
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            try
            {
                psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
                var derivationSchemeSettings = await 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(await RedirectToWalletTransaction(walletId, transaction));
            }
            else if (command == "analyze-psbt")
            {
                return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() }));
            }
            else
            {
                vm.GlobalError = "Unknown command";
                return(View(vm));
            }
        }
Пример #9
0
        public async Task <IActionResult> WalletPSBTReady(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletPSBTReadyViewModel vm, string command = null)
        {
            PSBT psbt    = null;
            var  network = NetworkProvider.GetNetwork(walletId.CryptoCode);

            try
            {
                psbt = PSBT.Parse(vm.PSBT, network.NBitcoinNetwork);
                await FetchTransactionDetails(walletId, vm, network);
            }
            catch
            {
                vm.Errors = new List <string>();
                vm.Errors.Add("Invalid PSBT");
                return(View(vm));
            }
            if (command == "broadcast")
            {
                if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
                {
                    vm.Errors = new List <string>();
                    vm.Errors.AddRange(errors.Select(e => e.ToString()));
                    return(View(vm));
                }
                var transaction = psbt.ExtractTransaction();
                try
                {
                    var broadcastResult = await ExplorerClientProvider.GetExplorerClient(network).BroadcastAsync(transaction);

                    if (!broadcastResult.Success)
                    {
                        vm.Errors = new List <string>();
                        vm.Errors.Add($"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}");
                        return(View(vm));
                    }
                }
                catch (Exception ex)
                {
                    vm.Errors = new List <string>();
                    vm.Errors.Add("Error while broadcasting: " + ex.Message);
                    return(View(vm));
                }
                return(await RedirectToWalletTransaction(walletId, transaction));
            }
            else if (command == "analyze-psbt")
            {
                return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.ToBase64() }));
            }
            else
            {
                vm.Errors = new List <string>();
                vm.Errors.Add("Unknown command");
                return(View(vm));
            }
        }
Пример #10
0
        public async Task <CreatePSBTResponse> CreatePSBT(BTCPayNetwork network, DerivationSchemeSettings derivationSettings, WalletSendModel sendModel, CancellationToken cancellationToken)
        {
            var nbx = ExplorerClientProvider.GetExplorerClient(network);
            CreatePSBTRequest psbtRequest = new CreatePSBTRequest();

            if (sendModel.InputSelection)
            {
                psbtRequest.IncludeOnlyOutpoints = sendModel.SelectedInputs?.Select(OutPoint.Parse)?.ToList() ?? new List <OutPoint>();
            }
            foreach (var transactionOutput in sendModel.Outputs)
            {
                var psbtDestination = new CreatePSBTDestination();
                psbtRequest.Destinations.Add(psbtDestination);
                psbtDestination.Destination   = BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                psbtDestination.Amount        = Money.Coins(transactionOutput.Amount.Value);
                psbtDestination.SubstractFees = transactionOutput.SubtractFeesFromOutput;
            }

            if (network.SupportRBF)
            {
                if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.Yes)
                {
                    psbtRequest.RBF = true;
                }
                if (sendModel.AllowFeeBump is WalletSendModel.ThreeStateBool.No)
                {
                    psbtRequest.RBF = false;
                }
            }

            psbtRequest.AlwaysIncludeNonWitnessUTXO = sendModel.AlwaysIncludeNonWitnessUTXO;

            psbtRequest.FeePreference = new FeePreference();
            if (sendModel.FeeSatoshiPerByte is decimal v &&
                v > decimal.Zero)
            {
                psbtRequest.FeePreference.ExplicitFeeRate = new FeeRate(v);
            }
            if (sendModel.NoChange)
            {
                psbtRequest.ExplicitChangeAddress = psbtRequest.Destinations.First().Destination;
            }

            var psbt = (await nbx.CreatePSBTAsync(derivationSettings.AccountDerivation, psbtRequest, cancellationToken));

            if (psbt == null)
            {
                throw new NotSupportedException("You need to update your version of NBXplorer");
            }
            // Not supported by coldcard, remove when they do support it
            psbt.PSBT.GlobalXPubs.Clear();
            return(psbt);
        }
Пример #11
0
        public async Task <BitcoinAddress> ReserveAddressAsync(DerivationStrategy derivationStrategy)
        {
            var client   = _Client.GetExplorerClient(derivationStrategy.Network);
            var pathInfo = await client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);

            return(pathInfo.ScriptPubKey.GetDestinationAddress(client.Network));
        }
        public async Task ProcessAll(CancellationToken cancellationToken = default)
        {
            if (disabled)
            {
                return;
            }
            var           now         = DateTimeOffset.UtcNow;
            List <Record> rescheduled = new List <Record>();
            List <Record> scheduled   = new List <Record>();
            List <Record> broadcasted = new List <Record>();

            while (_Records.Reader.TryRead(out var r))
            {
                (r.BroadcastTime > now ? rescheduled : scheduled).Add(r);
            }

            var broadcasts = scheduled.Select(async(record) =>
            {
                var explorer = _explorerClientProvider.GetExplorerClient(record.Network);
                if (explorer is null)
                {
                    return(false);
                }
                try
                {
                    // We don't look the result, this is a best effort basis.
                    var result = await explorer.BroadcastAsync(record.Transaction, cancellationToken);
                    if (result.Success)
                    {
                        Logs.PayServer.LogInformation($"{record.Network.CryptoCode}: {record.Transaction.GetHash()} has been successfully broadcasted");
                    }
                    return(false);
                }
                catch
                {
                    // If this goes here, maybe RPC is down or NBX is down, we should reschedule
                    return(true);
                }
            }).ToArray();

            for (int i = 0; i < scheduled.Count; i++)
            {
                var needReschedule = await broadcasts[i];
                (needReschedule ? rescheduled : broadcasted).Add(scheduled[i]);
            }
            foreach (var record in rescheduled)
            {
                _Records.Writer.TryWrite(record);
            }
            // TODO: Remove everything in broadcasted from DB
        }
Пример #13
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);

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

            var vm = new RescanWalletModel();

            vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
            // We need to ensure it is segwit,
            // because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
            vm.IsSegwit              = paymentMethod.DerivationStrategyBase.IsSegwit();
            vm.IsServerAdmin         = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
            vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
            var explorer     = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
            var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);

            if (scanProgress != null)
            {
                vm.PreviousError = scanProgress.Error;
                if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
                {
                    if (scanProgress.Progress == null)
                    {
                        vm.Progress = 0;
                    }
                    else
                    {
                        vm.Progress      = scanProgress.Progress.OverallProgress;
                        vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
                    }
                }
                if (scanProgress.Status == ScanUTXOStatus.Complete)
                {
                    vm.LastSuccess = scanProgress.Progress;
                    vm.TimeOfScan  = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
                }
            }
            return(View(vm));
        }
Пример #14
0
        public async Task <IActionResult> WalletSign([ModelBinder(typeof(WalletIdModelBinder))]
                                                     WalletId walletId, WalletPSBTViewModel vm, string returnUrl = null, string command = null)
        {
            var network = NetworkProvider.GetNetwork <BTCPayNetwork>(walletId.CryptoCode);

            if (returnUrl is null)
            {
                returnUrl = Url.Action(nameof(WalletTransactions), new { walletId });
            }
            var psbt = await vm.GetPSBT(network.NBitcoinNetwork);

            if (psbt is null || vm.InvalidPSBT)
            {
                ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
                return(View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl)));
            }
            switch (command)
            {
            case "vault":
                return(ViewVault(walletId, vm.SigningContext));

            case "seed":
                return(SignWithSeed(walletId, vm.SigningContext));

            case "decode":
                return(await WalletPSBT(walletId, vm, "decode"));

            default:
                break;
            }

            if (await CanUseHotWallet())
            {
                var derivationScheme = GetDerivationSchemeSettings(walletId);
                if (derivationScheme.IsHotWallet)
                {
                    var extKey = await ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode)
                                 .GetMetadataAsync <string>(derivationScheme.AccountDerivation,
                                                            WellknownMetadataKeys.MasterHDKey);

                    if (extKey != null)
                    {
                        return(await SignWithSeed(walletId,
                                                  new SignWithSeedViewModel { SeedOrKey = extKey, SigningContext = vm.SigningContext }));
                    }
                }
            }
            return(View("WalletSigningOptions", new WalletSigningOptionsModel(vm.SigningContext, returnUrl)));
        }
Пример #15
0
        public BTCPayWallet GetWallet(string cryptoCode)
        {
            if (cryptoCode == null)
            {
                throw new ArgumentNullException(nameof(cryptoCode));
            }
            var network = _NetworkProvider.GetNetwork(cryptoCode);
            var client  = _Client.GetExplorerClient(cryptoCode);

            if (network == null || client == null)
            {
                return(null);
            }
            return(new BTCPayWallet(client, _TransactionCacheProvider.GetTransactionCache(network), network));
        }
Пример #16
0
        private async Task <PSBT> UpdatePSBT(DerivationSchemeSettings derivationSchemeSettings, PSBT psbt, BTCPayNetwork network)
        {
            var result = await ExplorerClientProvider.GetExplorerClient(network).UpdatePSBTAsync(new UpdatePSBTRequest()
            {
                PSBT             = psbt,
                DerivationScheme = derivationSchemeSettings.AccountDerivation,
            });

            if (result == null)
            {
                return(null);
            }
            derivationSchemeSettings.RebaseKeyPaths(result.PSBT);
            return(result.PSBT);
        }
Пример #17
0
        public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
        {
            if (!_ExplorerClientProvider.IsAvailable(network))
            {
                throw new Exception($"Full node not available");
            }

            var             explorerClient = _ExplorerClientProvider.GetExplorerClient(network);
            var             cts            = new CancellationTokenSource(5000);
            var             client         = GetClient(supportedPaymentMethod, network);
            var             status         = explorerClient.GetStatusAsync();
            GetInfoResponse info           = null;

            try
            {
                info = await client.GetInfoAsync(cts.Token);
            }
            catch (Exception ex)
            {
                throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
            }
            var address = info.Address.Select(a => a.Address).FirstOrDefault();
            var port    = info.Port;

            address = address ?? client.Uri.DnsSafeHost;

            if (info.Network != network.CLightningNetworkName)
            {
                throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
            }

            var blocksGap = Math.Abs(info.BlockHeight - (await status).ChainHeight);

            if (blocksGap > 10)
            {
                throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
            }

            try
            {
                await TestConnection(address, port, cts.Token);
            }
            catch (Exception ex)
            {
                throw new Exception($"Error while connecting to the lightning node via {address}:{port} ({ex.Message})");
            }
        }
Пример #18
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);

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

            var vm = new RescanWalletModel();

            vm.IsFullySync           = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
            vm.IsServerAdmin         = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
            vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
            var explorer     = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
            var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);

            if (scanProgress != null)
            {
                vm.PreviousError = scanProgress.Error;
                if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
                {
                    if (scanProgress.Progress == null)
                    {
                        vm.Progress = 0;
                    }
                    else
                    {
                        vm.Progress      = scanProgress.Progress.OverallProgress;
                        vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
                    }
                }
                if (scanProgress.Status == ScanUTXOStatus.Complete)
                {
                    vm.LastSuccess = scanProgress.Progress;
                    vm.TimeOfScan  = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
                }
            }
            return(View(vm));
        }
Пример #19
0
        private async Task <bool> UpdateConfirmationCount(InvoiceEntity invoice)
        {
            bool extendInvoiceMonitoring         = false;
            var  updateConfirmationCountIfNeeded = invoice
                                                   .GetPayments()
                                                   .Select <PaymentEntity, Task <PaymentEntity> >(async payment =>
            {
                var paymentData = payment.GetCryptoPaymentData();
                if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
                {
                    // Do update if confirmation count in the paymentData is not up to date
                    if ((onChainPaymentData.ConfirmationCount < payment.Network.MaxTrackedConfirmation && payment.Accounted) &&
                        (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
                    {
                        var transactionResult = await _ExplorerClientProvider.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash);
                        var confirmationCount = transactionResult?.Confirmations ?? 0;
                        if (onChainPaymentData.Network.CryptoCode == "DASH" && confirmationCount == 0 && transactionResult != null)
                        {
                            confirmationCount = await GetDashInsightExplorerTxLock(onChainPaymentData.Outpoint.Hash) ? 6 : 0;
                        }
                        onChainPaymentData.ConfirmationCount = confirmationCount;
                        payment.SetCryptoPaymentData(onChainPaymentData);

                        // we want to extend invoice monitoring until we reach max confirmations on all onchain payment methods
                        if (confirmationCount < payment.Network.MaxTrackedConfirmation)
                        {
                            extendInvoiceMonitoring = true;
                        }

                        return(payment);
                    }
                }
                return(null);
            })
                                                   .ToArray();
            await Task.WhenAll(updateConfirmationCountIfNeeded);

            var updatedPaymentData = updateConfirmationCountIfNeeded.Where(a => a.Result != null).Select(a => a.Result).ToList();

            if (updatedPaymentData.Count > 0)
            {
                await _InvoiceRepository.UpdatePayments(updatedPaymentData);
            }

            return(extendInvoiceMonitoring);
        }
Пример #20
0
        private async Task <IActionResult> TryHandleSigningCommands(WalletId walletId, PSBT psbt, string command,
                                                                    SigningContextModel signingContext, string actionBack)
        {
            signingContext.PSBT = psbt.ToBase64();
            switch (command)
            {
            case "sign":
                var routeBack = new Dictionary <string, string>
                {
                    { "action", actionBack }, { "walletId", walletId.ToString() }
                };
                return(View("WalletSigningOptions", new WalletSigningOptionsModel(signingContext, routeBack)));

            case "vault":
                return(ViewVault(walletId, signingContext));

            case "seed":
                return(SignWithSeed(walletId, signingContext));

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

                        return(SignWithSeed(walletId,
                                            new SignWithSeedViewModel {
                            SeedOrKey = extKey, SigningContext = signingContext
                        }));
                    }
                }
                TempData.SetStatusMessageModel(new StatusMessageModel
                {
                    Severity = StatusMessageModel.StatusSeverity.Error,
                    Message  = "NBX seed functionality is not available"
                });
                break;
            }

            return(null);
        }
Пример #21
0
        public async Task <string> UnReserveAddress(WalletId walletId)
        {
            var kpi = Get(walletId);

            if (kpi is null)
            {
                return(null);
            }

            var explorerClient = _explorerClientProvider.GetExplorerClient(walletId.CryptoCode);

            if (explorerClient is null)
            {
                return(null);
            }

            await explorerClient.CancelReservationAsync(kpi.DerivationStrategy, new[] { kpi.KeyPath });

            return(kpi.Address.ToString());
        }
Пример #22
0
        public BTCPayWalletProvider(ExplorerClientProvider client,
                                    IOptions <MemoryCacheOptions> memoryCacheOption,
                                    BTCPayNetworkProvider networkProvider)
        {
            if (client == null)
            {
                throw new ArgumentNullException(nameof(client));
            }
            _Client          = client;
            _NetworkProvider = networkProvider;
            _Options         = memoryCacheOption;

            foreach (var network in networkProvider.GetAll())
            {
                var explorerClient = _Client.GetExplorerClient(network.CryptoCode);
                if (explorerClient == null)
                {
                    continue;
                }
                _Wallets.Add(network.CryptoCode, new BTCPayWallet(explorerClient, new MemoryCache(_Options), network));
            }
        }
        public BTCPayWalletProvider(ExplorerClientProvider client,
                                    IOptions <MemoryCacheOptions> memoryCacheOption,
                                    Data.ApplicationDbContextFactory dbContextFactory,
                                    BTCPayNetworkProvider networkProvider,
                                    Logs logs)
        {
            ArgumentNullException.ThrowIfNull(client);
            this.Logs        = logs;
            _Client          = client;
            _NetworkProvider = networkProvider;
            _Options         = memoryCacheOption;

            foreach (var network in networkProvider.GetAll().OfType <BTCPayNetwork>())
            {
                var explorerClient = _Client.GetExplorerClient(network.CryptoCode);
                if (explorerClient == null)
                {
                    continue;
                }
                _Wallets.Add(network.CryptoCode.ToUpperInvariant(), new BTCPayWallet(explorerClient, new MemoryCache(_Options), network, dbContextFactory, Logs));
            }
        }
Пример #24
0
        public BTCPayWalletProvider(ExplorerClientProvider client,
                                    IOptions <MemoryCacheOptions> memoryCacheOption,
                                    Data.ApplicationDbContextFactory dbContextFactory,
                                    BTCPayNetworkProvider networkProvider)
        {
            if (client == null)
            {
                throw new ArgumentNullException(nameof(client));
            }
            _Client          = client;
            _NetworkProvider = networkProvider;
            _Options         = memoryCacheOption;

            foreach (var network in networkProvider.GetAll().OfType <BTCPayNetwork>())
            {
                var explorerClient = _Client.GetExplorerClient(network.bitcoinCode);
                if (explorerClient == null)
                {
                    continue;
                }
                _Wallets.Add(network.bitcoinCode.ToUpperInvariant(), new BTCPayWallet(explorerClient, new MemoryCache(_Options), network, dbContextFactory));
            }
        }
Пример #25
0
        public async Task <IActionResult> Submit(string cryptoCode)
        {
            var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode);

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

            var explorer = _explorerClientProvider.GetExplorerClient(network);

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

            string rawBody;

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

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

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

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

            var sendersInputType = psbt.GetInputsScriptPubKeyType();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            var rand = new Random();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            if (psbtFormat && HexEncoder.IsWellFormed(rawBody))
            {
                return(Ok(newPsbt.ToHex()));
            }
            else if (psbtFormat)
            {
                return(Ok(newPsbt.ToBase64()));
            }
            else
            {
                return(Ok(newTx.ToHex()));
            }
        }
Пример #26
0
        public async Task <IActionResult> Submit(string cryptoCode,
                                                 long?maxadditionalfeecontribution,
                                                 int?additionalfeeoutputindex,
                                                 decimal minfeerate             = -1.0m,
                                                 bool disableoutputsubstitution = false,
                                                 int v = 1)
        {
            var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode);

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

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

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

            var explorer = _explorerClientProvider.GetExplorerClient(network);

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

            string rawBody;

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

            FeeRate originalFeeRate = null;
            bool    psbtFormat      = true;

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

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

            var sendersInputType = psbt.GetInputsScriptPubKeyType();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            _eventAggregator.Publish(new InvoiceEvent(invoice, 1002, InvoiceEvent.ReceivedPayment)
            {
                Payment = payment
            });
            _eventAggregator.Publish(new UpdateTransactionLabel()
            {
                WalletId          = new WalletId(invoice.StoreId, network.CryptoCode),
                TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo =>
                                                                                        new KeyValuePair <uint256, List <(string color, string label)> >(utxo.Key,
                                                                                                                                                         new List <(string color, string label)>()
                {
                    UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice.Id)
                }))
                                    .ToDictionary(pair => pair.Key, pair => pair.Value)
            });
Пример #27
0
        protected override async Task Process(ISupportedPaymentMethod paymentMethod, PayoutData[] payouts)
        {
            var storePaymentMethod = paymentMethod as DerivationSchemeSettings;

            if (storePaymentMethod?.IsHotWallet is not true)
            {
                Logs.PayServer.LogInformation($"Wallet is not a hot wallet.");
                return;
            }

            if (!_explorerClientProvider.IsAvailable(PaymentMethodId.CryptoCode))
            {
                Logs.PayServer.LogInformation($"{paymentMethod.PaymentId.CryptoCode} node is not available");
                return;
            }
            var explorerClient  = _explorerClientProvider.GetExplorerClient(PaymentMethodId.CryptoCode);
            var paymentMethodId = PaymentMethodId.Parse(PaymentMethodId.CryptoCode);
            var network         = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(paymentMethodId.CryptoCode);

            var extKeyStr = await explorerClient.GetMetadataAsync <string>(
                storePaymentMethod.AccountDerivation,
                WellknownMetadataKeys.AccountHDKey);

            if (extKeyStr == null)
            {
                Logs.PayServer.LogInformation($"Wallet keys not found.");
                return;
            }

            var wallet = _btcPayWalletProvider.GetWallet(PaymentMethodId.CryptoCode);

            var reccoins = (await wallet.GetUnspentCoins(storePaymentMethod.AccountDerivation)).ToArray();
            var coins    = reccoins.Select(coin => coin.Coin).ToArray();

            var         accountKey    = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork);
            var         keys          = reccoins.Select(coin => accountKey.Derive(coin.KeyPath).PrivateKey).ToArray();
            Transaction workingTx     = null;
            decimal?    failedAmount  = null;
            var         changeAddress = await explorerClient.GetUnusedAsync(
                storePaymentMethod.AccountDerivation, DerivationFeature.Change, 0, true);

            var feeRate = await explorerClient.GetFeeRateAsync(1, new FeeRate(1m));

            var transfersProcessing = new List <PayoutData>();

            foreach (var transferRequest in payouts)
            {
                var blob = transferRequest.GetBlob(_btcPayNetworkJsonSerializerSettings);
                if (failedAmount.HasValue && blob.CryptoAmount >= failedAmount)
                {
                    continue;
                }

                var claimDestination =
                    await _bitcoinLikePayoutHandler.ParseClaimDestination(paymentMethodId, blob.Destination);

                if (!string.IsNullOrEmpty(claimDestination.error))
                {
                    Logs.PayServer.LogInformation($"Could not process payout {transferRequest.Id} because {claimDestination.error}.");
                    continue;
                }

                var bitcoinClaimDestination = (IBitcoinLikeClaimDestination)claimDestination.destination;
                var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder()
                                .AddCoins(coins)
                                .AddKeys(keys);

                if (workingTx is not null)
                {
                    foreach (var txout in workingTx.Outputs.Where(txout =>
                                                                  !txout.IsTo(changeAddress.Address)))
                    {
                        txBuilder.Send(txout.ScriptPubKey, txout.Value);
                    }
                }

                txBuilder.Send(bitcoinClaimDestination.Address,
                               new Money(blob.CryptoAmount.Value, MoneyUnit.BTC));

                try
                {
                    txBuilder.SetChange(changeAddress.Address);
                    txBuilder.SendEstimatedFees(feeRate.FeeRate);
                    workingTx = txBuilder.BuildTransaction(true);
                    transfersProcessing.Add(transferRequest);
                }
                catch (NotEnoughFundsException e)
                {
                    Logs.PayServer.LogInformation($"Could not process payout {transferRequest.Id} because of not enough funds. ({e.Missing.GetValue(network)})");
                    failedAmount = blob.CryptoAmount;
                    //keep going, we prioritize withdraws by time but if there is some other we can fit, we should
                }
            }

            if (workingTx is not null)
            {
                try
                {
                    await using var context = _applicationDbContextFactory.CreateContext();
                    var txHash = workingTx.GetHash();
                    Logs.PayServer.LogInformation($"Processing {transfersProcessing.Count} payouts in tx {txHash}");
                    foreach (PayoutData payoutData in transfersProcessing)
                    {
                        context.Attach(payoutData);
                        payoutData.State = PayoutState.InProgress;
                        _bitcoinLikePayoutHandler.SetProofBlob(payoutData,
                                                               new PayoutTransactionOnChainBlob()
                        {
                            Accounted     = true,
                            TransactionId = txHash,
                            Candidates    = new HashSet <uint256>()
                            {
                                txHash
                            }
                        });
                        await context.SaveChangesAsync();
                    }
                    TaskCompletionSource <bool> tcs = new();
                    var cts = new CancellationTokenSource();
                    cts.CancelAfter(TimeSpan.FromSeconds(20));
                    var task = _eventAggregator.WaitNext <NewOnChainTransactionEvent>(
                        e => e.NewTransactionEvent.TransactionData.TransactionHash == txHash,
                        cts.Token);
                    var broadcastResult = await explorerClient.BroadcastAsync(workingTx, cts.Token);

                    if (!broadcastResult.Success)
                    {
                        tcs.SetResult(false);
                    }
                    var walletId = new WalletId(_PayoutProcesserSettings.StoreId, PaymentMethodId.CryptoCode);
                    foreach (PayoutData payoutData in transfersProcessing)
                    {
                        _eventAggregator.Publish(new UpdateTransactionLabel(walletId,
                                                                            txHash,
                                                                            UpdateTransactionLabel.PayoutTemplate(payoutData.Id, payoutData.PullPaymentDataId,
                                                                                                                  walletId.ToString())));
                    }
                    await Task.WhenAny(tcs.Task, task);
                }
                catch (OperationCanceledException)
                {
                }
                catch (Exception e)
                {
                    Logs.PayServer.LogError(e, "Could not finalize and broadcast");
                }
            }
        }
Пример #28
0
        private async Task Listen(BTCPayWallet wallet)
        {
            var  network = wallet.Network;
            bool cleanup = false;

            try
            {
                if (_SessionsByCryptoCode.ContainsKey(network.CryptoCode))
                {
                    return;
                }
                var client = _ExplorerClients.GetExplorerClient(network);
                if (client == null)
                {
                    return;
                }
                if (_Cts.IsCancellationRequested)
                {
                    return;
                }
                var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);

                if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
                {
                    await session.DisposeAsync();

                    return;
                }
                cleanup = true;

                using (session)
                {
                    await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);

                    await session.ListenAllTrackedSourceAsync(cancellation : _Cts.Token).ConfigureAwait(false);

                    Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
                    int paymentCount = await FindPaymentViaPolling(wallet, network);

                    Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");

                    Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
                    while (!_Cts.IsCancellationRequested)
                    {
                        var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);

                        switch (newEvent)
                        {
                        case NBXplorer.Models.NewBlockEvent evt:
                            await UpdatePaymentStates(wallet, await _InvoiceRepository.GetPendingInvoices());

                            _Aggregator.Publish(new Events.NewBlockEvent()
                            {
                                CryptoCode = evt.CryptoCode
                            });
                            break;

                        case NBXplorer.Models.NewTransactionEvent evt:
                            if (evt.DerivationStrategy != null)
                            {
                                wallet.InvalidateCache(evt.DerivationStrategy);
                                var validOutputs = network.GetValidOutputs(evt).ToList();
                                if (!validOutputs.Any())
                                {
                                    break;
                                }
                                foreach (var output in validOutputs)
                                {
                                    var key = output.Item1.ScriptPubKey.Hash + "#" +
                                              network.CryptoCode.ToUpperInvariant();
                                    var invoice = (await _InvoiceRepository.GetInvoicesFromAddresses(new[] { key }))
                                                  .FirstOrDefault();
                                    if (invoice != null)
                                    {
                                        var address = network.NBXplorerNetwork.CreateAddress(evt.DerivationStrategy,
                                                                                             output.Item1.KeyPath, output.Item1.ScriptPubKey);

                                        var paymentData = new BitcoinLikePaymentData(address,
                                                                                     output.matchedOutput.Value, output.outPoint,
                                                                                     evt.TransactionData.Transaction.RBF, output.matchedOutput.KeyPath);

                                        var alreadyExist = invoice
                                                           .GetAllBitcoinPaymentData(false).Any(c => c.GetPaymentId() == paymentData.GetPaymentId());
                                        if (!alreadyExist)
                                        {
                                            var payment = await _paymentService.AddPayment(invoice.Id,
                                                                                           DateTimeOffset.UtcNow, paymentData, network);

                                            if (payment != null)
                                            {
                                                await ReceivedPayment(wallet, invoice, payment,
                                                                      evt.DerivationStrategy);
                                            }
                                        }
                                        else
                                        {
                                            await UpdatePaymentStates(wallet, invoice.Id);
                                        }
                                    }
                                }
                            }

                            _Aggregator.Publish(new NewOnChainTransactionEvent()
                            {
                                CryptoCode          = wallet.Network.CryptoCode,
                                NewTransactionEvent = evt
                            });

                            break;

                        default:
                            Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
                            break;
                        }
                    }
                }
            }
            catch when(_Cts.IsCancellationRequested)
            {
            }
Пример #29
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());
        }
Пример #30
0
        public async Task <int> ProcessAll(CancellationToken cancellationToken = default)
        {
            if (disabled)
            {
                return(0);
            }
            List <Record> scheduled = new List <Record>();

            using (var db = _dbContextFactory.CreateContext())
            {
                scheduled = (await db.PlannedTransactions
                             .ToListAsync()).Select(ToRecord)
                            .Where(r => r != null)
                            // Client side filtering because entity framework is retarded.
                            .Where(r => r.BroadcastTime < DateTimeOffset.UtcNow).ToList();
            }

            List <Record> rescheduled = new List <Record>();
            List <Record> broadcasted = new List <Record>();

            var broadcasts = scheduled.Select(async(record) =>
            {
                var explorer = _explorerClientProvider.GetExplorerClient(record.Network);
                if (explorer is null)
                {
                    return(false);
                }
                try
                {
                    // We don't look the result, this is a best effort basis.
                    var result = await explorer.BroadcastAsync(record.Transaction, cancellationToken);
                    if (result.Success)
                    {
                        Logs.PayServer.LogInformation($"{record.Network.bitcoinCode}: {record.Transaction.GetHash()} has been successfully broadcasted");
                    }
                    return(false);
                }
                catch
                {
                    // If this goes here, maybe RPC is down or NBX is down, we should reschedule
                    return(true);
                }
            }).ToArray();

            for (int i = 0; i < scheduled.Count; i++)
            {
                var needReschedule = await broadcasts[i];
                (needReschedule ? rescheduled : broadcasted).Add(scheduled[i]);
            }

            using (var db = _dbContextFactory.CreateContext())
            {
                foreach (Record record in broadcasted)
                {
                    db.PlannedTransactions.Remove(new PlannedTransaction()
                    {
                        Id = record.Id
                    });
                }
                return(await db.SaveChangesAsync());
            }
        }