Ejemplo n.º 1
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = null)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

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

            if (command == "noob" || command == "expert")
            {
                ModelState.Clear();
                vm.AdvancedMode = command == "expert";
                return(View(vm));
            }

            var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);

            if (destination == null)
            {
                ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
            }

            if (vm.Amount.HasValue)
            {
                if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
                }
                if (vm.CurrentBalance < vm.Amount.Value)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
                }
            }
            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            return(RedirectToAction(nameof(WalletSendLedger), new WalletSendLedgerModel()
            {
                Destination = vm.Destination,
                Amount = vm.Amount.Value,
                SubstractFees = vm.SubstractFees,
                FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
                NoChange = vm.NoChange
            }));
        }
Ejemplo n.º 2
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork <BTCPayNetwork>(walletId?.CryptoCode);

            if (network == null || network.ReadonlyWallet)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            decimal transactionAmountSum = 0;

            if (command == "add-output")
            {
                ModelState.Clear();
                vm.Outputs.Add(new WalletSendModel.TransactionOutput());
                return(View(vm));
            }
            if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
            {
                ModelState.Clear();
                var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
                vm.Outputs.RemoveAt(index);
                return(View(vm));
            }


            if (!vm.Outputs.Any())
            {
                ModelState.AddModelError(string.Empty,
                                         "Please add at least one transaction output");
                return(View(vm));
            }

            var subtractFeesOutputsCount = new List <int>();
            var substractFees            = vm.Outputs.Any(o => o.SubtractFeesFromOutput);

            for (var i = 0; i < vm.Outputs.Count; i++)
            {
                var transactionOutput = vm.Outputs[i];
                if (transactionOutput.SubtractFeesFromOutput)
                {
                    subtractFeesOutputsCount.Add(i);
                }
                transactionOutput.DestinationAddress = transactionOutput.DestinationAddress?.Trim() ?? string.Empty;

                try
                {
                    BitcoinAddress.Create(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                }
                catch
                {
                    var inputName =
                        string.Format(CultureInfo.InvariantCulture, "Outputs[{0}].", i.ToString(CultureInfo.InvariantCulture)) +
                        nameof(transactionOutput.DestinationAddress);

                    ModelState.AddModelError(inputName, "Invalid address");
                }

                if (transactionOutput.Amount.HasValue)
                {
                    transactionAmountSum += transactionOutput.Amount.Value;

                    if (vm.CurrentBalance == transactionOutput.Amount.Value &&
                        !transactionOutput.SubtractFeesFromOutput)
                    {
                        vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput,
                                         "You are sending your entire balance to the same destination, you should subtract the fees",
                                         this);
                    }
                }
            }

            if (subtractFeesOutputsCount.Count > 1)
            {
                foreach (var subtractFeesOutput in subtractFeesOutputsCount)
                {
                    vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
                                     "You can only subtract fees from one output", this);
                }
            }
            else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
            {
                ModelState.AddModelError(string.Empty,
                                         "You are sending your entire balance, you should subtract the fees from an output");
            }

            if (vm.CurrentBalance < transactionAmountSum)
            {
                for (var i = 0; i < vm.Outputs.Count; i++)
                {
                    vm.AddModelError(model => model.Outputs[i].Amount,
                                     "You are sending more than what you own", this);
                }
            }

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

            DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(string.Empty, ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(string.Empty, "You need to update your version of NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);

            switch (command)
            {
            case "vault":
                return(ViewVault(walletId, psbt.PSBT));

            case "ledger":
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));

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

            case "analyze-psbt":
                var name =
                    $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
                return(RedirectToWalletPSBT(walletId, psbt.PSBT, name));

            default:
                return(View(vm));
            }
        }
Ejemplo n.º 3
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, string defaultDestination = null, string defaultAmount = null)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);

            if (paymentMethod == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork <BTCPayNetwork>(walletId?.CryptoCode);

            if (network == null || network.ReadonlyWallet)
            {
                return(NotFound());
            }
            var storeData = store.GetStoreBlob();
            var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);

            rateRules.Spread = 0.0m;
            var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");

            double.TryParse(defaultAmount, out var amount);
            var model = new WalletSendModel()
            {
                Outputs = new List <WalletSendModel.TransactionOutput>()
                {
                    new WalletSendModel.TransactionOutput()
                    {
                        Amount             = Convert.ToDecimal(amount),
                        DestinationAddress = defaultDestination
                    }
                },
                CryptoCode = walletId.CryptoCode
            };


            var feeProvider     = _feeRateProvider.CreateFeeProvider(network);
            var recommendedFees = feeProvider.GetFeeRateAsync();
            var balance         = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);

            model.CurrentBalance            = await balance;
            model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
            model.FeeSatoshiPerByte         = model.RecommendedSatoshiPerByte;
            model.SupportRBF = network.SupportRBF;
            using (CancellationTokenSource cts = new CancellationTokenSource())
            {
                try
                {
                    cts.CancelAfter(TimeSpan.FromSeconds(5));
                    var result = await RateFetcher.FetchRate(currencyPair, rateRules, cts.Token).WithCancellation(cts.Token);

                    if (result.BidAsk != null)
                    {
                        model.Rate         = result.BidAsk.Center;
                        model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
                        model.Fiat         = currencyPair.Right;
                    }
                    else
                    {
                        model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
                    }
                }
                catch (Exception ex) { model.RateError = ex.Message; }
            }
            return(View(model));
        }
Ejemplo n.º 4
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);

            if (destination == null)
            {
                ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
            }

            if (vm.Amount.HasValue)
            {
                if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
                }
                if (vm.CurrentBalance < vm.Amount.Value)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
                }
            }
            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            var sendModel = new WalletSendLedgerModel()
            {
                Destination       = vm.Destination,
                Amount            = vm.Amount.Value,
                SubstractFees     = vm.SubstractFees,
                FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
                NoChange          = vm.NoChange,
                DisableRBF        = vm.DisableRBF
            };

            if (command == "ledger")
            {
                return(RedirectToAction(nameof(WalletSendLedger), sendModel));
            }
            else
            {
                var storeData        = (await Repository.FindStore(walletId.StoreId, GetUserId()));
                var derivationScheme = GetPaymentMethod(walletId, storeData);
                try
                {
                    var psbt = await CreatePSBT(network, derivationScheme, sendModel, cancellation);

                    return(File(psbt.PSBT.ToBytes(), "application/octet-stream", $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt"));
                }
                catch (NBXplorerException ex)
                {
                    ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
                    return(View(vm));
                }
                catch (NotSupportedException)
                {
                    ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
                    return(View(vm));
                }
            }
        }
Ejemplo n.º 5
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);
        }
Ejemplo n.º 6
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("You need to update your version of NBXplorer");
            }
            // Not supported by coldcard, remove when they do support it
            psbt.PSBT.GlobalXPubs.Clear();
            return(psbt);
        }
Ejemplo n.º 7
0
        public async Task CanPlayWithPSBT()
        {
            using (var tester = ServerTester.Create())
            {
                tester.Start();
                var user = tester.NewAccount();
                user.GrantAccess();
                user.RegisterDerivationScheme("BTC");
                var invoice = user.BitPay.CreateInvoice(new Invoice()
                {
                    Price             = 10,
                    Currency          = "USD",
                    PosData           = "posData",
                    OrderId           = "orderId",
                    ItemDesc          = "Some \", description",
                    FullNotifications = true
                }, Facade.Merchant);
                var cashCow        = tester.ExplorerNode;
                var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
                cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
                TestUtils.Eventually(() =>
                {
                    invoice = user.BitPay.GetInvoice(invoice.Id);
                    Assert.Equal("paid", invoice.Status);
                });

                var walletController = tester.PayTester.GetController <WalletsController>(user.UserId);
                var walletId         = new WalletId(user.StoreId, "BTC");
                var sendDestination  = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
                var sendModel        = new WalletSendModel()
                {
                    Destination       = sendDestination,
                    Amount            = 0.1m,
                    FeeSatoshiPerByte = 1,
                    CurrentBalance    = 1.5m
                };
                var vmLedger = await walletController.WalletSend(walletId, sendModel, command : "ledger").AssertViewModelAsync <WalletSendLedgerModel>();

                PSBT.Parse(vmLedger.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmLedger.SuccessPath);
                Assert.NotNull(vmLedger.WebsocketPath);

                var redirectedPSBT = (string)Assert.IsType <RedirectToActionResult>(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt")).RouteValues["psbt"];
                var vmPSBT         = walletController.WalletPSBT(walletId, new WalletPSBTViewModel()
                {
                    PSBT = redirectedPSBT
                }).AssertViewModel <WalletPSBTViewModel>();
                var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmPSBT.Decoded);

                var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
                PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);

                await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync <WalletSendLedgerModel>();

                var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync <WalletPSBTViewModel>();

                Assert.NotEmpty(vmPSBT2.Errors);
                Assert.Equal(vmPSBT.Decoded, vmPSBT2.Decoded);
                Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);

                var signedPSBT = unsignedPSBT.Clone();
                signedPSBT.SignAll(user.ExtKey);
                vmPSBT.PSBT = signedPSBT.ToBase64();
                var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.Equal(2, psbtReady.Destinations.Count);
                Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
                Assert.Contains(psbtReady.Destinations, d => d.Positive);
                var redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, psbtReady, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);

                vmPSBT.PSBT = unsignedPSBT.ToBase64();
                var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync <WalletPSBTCombineViewModel>();

                Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
                combineVM.PSBT = signedPSBT.ToBase64();
                vmPSBT         = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync <WalletPSBTViewModel>();

                var signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                // Can use uploaded file?
                combineVM.PSBT             = null;
                combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
                vmPSBT = await walletController.WalletPSBTCombine(walletId, combineVM).AssertViewModelAsync <WalletPSBTViewModel>();

                signedPSBT2 = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel <WalletPSBTReadyViewModel>();
                Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
                redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
                Assert.Equal(signedPSBT.ToBase64(), (string)redirect.RouteValues["psbt"]);
                redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
            }
        }
Ejemplo n.º 8
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);
        }
Ejemplo n.º 9
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = null, CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);

            if (destination == null)
            {
                ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
            }

            if (vm.Amount.HasValue)
            {
                if (vm.CurrentBalance == vm.Amount.Value && !vm.SubstractFees)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending all your balance to the same destination, you should substract the fees");
                }
                if (vm.CurrentBalance < vm.Amount.Value)
                {
                    ModelState.AddModelError(nameof(vm.Amount), "You are sending more than what you own");
                }
            }
            if (!ModelState.IsValid)
            {
                return(View(vm));
            }

            DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(nameof(vm.Amount), ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(nameof(vm.Destination), "You need to update your version of NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);
            if (command == "ledger")
            {
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));
            }
            else if (command == "analyze-psbt")
            {
                return(ViewPSBT(psbt.PSBT, $"Send-{vm.Amount.Value}-{network.CryptoCode}-to-{destination[0].ToString()}.psbt"));
            }
            return(View(vm));
        }
Ejemplo n.º 10
0
        public async Task CanPlayWithPSBT()
        {
            using (var tester = ServerTester.Create())
            {
                await tester.StartAsync();

                var user = tester.NewAccount();
                user.GrantAccess();
                user.RegisterDerivationScheme("BTC");
                var invoice = user.BitPay.CreateInvoice(new Invoice()
                {
                    Price             = 10,
                    Currency          = "USD",
                    PosData           = "posData",
                    OrderId           = "orderId",
                    ItemDesc          = "Some \", description",
                    FullNotifications = true
                }, Facade.Merchant);
                var cashCow        = tester.ExplorerNode;
                var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
                cashCow.SendToAddress(invoiceAddress, Money.Coins(1.5m));
                TestUtils.Eventually(() =>
                {
                    invoice = user.BitPay.GetInvoice(invoice.Id);
                    Assert.Equal("paid", invoice.Status);
                });

                var walletController = user.GetController <WalletsController>();
                var walletId         = new WalletId(user.StoreId, "BTC");
                var sendDestination  = new Key().PubKey.Hash.GetAddress(user.SupportedNetwork.NBitcoinNetwork).ToString();
                var sendModel        = new WalletSendModel()
                {
                    Outputs = new List <WalletSendModel.TransactionOutput>()
                    {
                        new WalletSendModel.TransactionOutput()
                        {
                            DestinationAddress = sendDestination,
                            Amount             = 0.1m,
                        }
                    },
                    FeeSatoshiPerByte = 1,
                    CurrentBalance    = 1.5m
                };

                string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
                var    vmPSBT         = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel()
                {
                    PSBT = redirectedPSBT
                }).AssertViewModelAsync <WalletPSBTViewModel>();

                var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
                Assert.NotNull(vmPSBT.Decoded);

                var filePSBT = (FileContentResult)(await walletController.WalletPSBT(walletId, vmPSBT, "save-psbt"));
                PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);

                var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
                {
                    SigningContext = new SigningContextModel()
                    {
                        PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
                    }
                }).AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
                Assert.Equal(vmPSBT.PSBT, vmPSBT2.SigningContext.PSBT);

                var signedPSBT = unsignedPSBT.Clone();
                signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
                vmPSBT.PSBT = signedPSBT.ToBase64();
                var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
                {
                    SigningContext = new SigningContextModel
                    {
                        PSBT = AssertRedirectedPSBT(await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
                    }
                }).AssertViewModelAsync <WalletPSBTReadyViewModel>();

                Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
                Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
                Assert.Contains(psbtReady.Destinations, d => d.Positive);

                vmPSBT.PSBT = unsignedPSBT.ToBase64();
                var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync <WalletPSBTCombineViewModel>();

                Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
                combineVM.PSBT = signedPSBT.ToBase64();
                var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));

                var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                // Can use uploaded file?
                combineVM.PSBT             = null;
                combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
                psbt        = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
                signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
                Assert.True(signedPSBT.TryFinalize(out _));
                Assert.True(signedPSBT2.TryFinalize(out _));
                Assert.Equal(signedPSBT, signedPSBT2);

                var ready = (await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel
                {
                    SigningContext = new SigningContextModel(signedPSBT)
                })).AssertViewModel <WalletPSBTReadyViewModel>();
                Assert.Equal(signedPSBT.ToBase64(), ready.SigningContext.PSBT);
                psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
                Assert.Equal(signedPSBT.ToBase64(), psbt);
                var redirect = Assert.IsType <RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
                Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);

                //test base64 psbt file
                Assert.False(string.IsNullOrEmpty(Assert.IsType <WalletPSBTViewModel>(
                                                      Assert.IsType <ViewResult>(
                                                          await walletController.WalletPSBT(walletId,
                                                                                            new WalletPSBTViewModel()
                {
                    UploadedPSBTFile = TestUtils.GetFormFile("base64", signedPSBT.ToBase64())
                })).Model).PSBT));
            }
        }
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            if (store == null)
            {
                return(NotFound());
            }
            var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            vm.SupportRBF = network.SupportRBF;
            decimal transactionAmountSum = 0;

            if (command == "add-output")
            {
                ModelState.Clear();
                vm.Outputs.Add(new WalletSendModel.TransactionOutput());
                return(View(vm));
            }
            if (command.StartsWith("remove-output", StringComparison.InvariantCultureIgnoreCase))
            {
                ModelState.Clear();
                var index = int.Parse(command.Substring(command.IndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1), CultureInfo.InvariantCulture);
                vm.Outputs.RemoveAt(index);
                return(View(vm));
            }


            if (!vm.Outputs.Any())
            {
                ModelState.AddModelError(string.Empty,
                                         "Por favor agregue al menos una salida de transacción");
                return(View(vm));
            }

            var subtractFeesOutputsCount = new List <int>();
            var substractFees            = vm.Outputs.Any(o => o.SubtractFeesFromOutput);

            for (var i = 0; i < vm.Outputs.Count; i++)
            {
                var transactionOutput = vm.Outputs[i];
                if (transactionOutput.SubtractFeesFromOutput)
                {
                    subtractFeesOutputsCount.Add(i);
                }
                var destination = ParseDestination(transactionOutput.DestinationAddress, network.NBitcoinNetwork);
                if (destination == null)
                {
                    ModelState.AddModelError(nameof(transactionOutput.DestinationAddress), "Dirección inválida");
                }

                if (transactionOutput.Amount.HasValue)
                {
                    transactionAmountSum += transactionOutput.Amount.Value;

                    if (vm.CurrentBalance == transactionOutput.Amount.Value &&
                        !transactionOutput.SubtractFeesFromOutput)
                    {
                        vm.AddModelError(model => model.Outputs[i].SubtractFeesFromOutput,
                                         "Está enviando todo su saldo al mismo destino, debe restar las tarifas",
                                         ModelState);
                    }
                }
            }

            if (subtractFeesOutputsCount.Count > 1)
            {
                foreach (var subtractFeesOutput in subtractFeesOutputsCount)
                {
                    vm.AddModelError(model => model.Outputs[subtractFeesOutput].SubtractFeesFromOutput,
                                     "Solo puedes restar tarifas de una salida", ModelState);
                }
            }
            else if (vm.CurrentBalance == transactionAmountSum && !substractFees)
            {
                ModelState.AddModelError(string.Empty,
                                         "Está enviando todo su saldo, debe restar las tarifas de una salida");
            }

            if (vm.CurrentBalance < transactionAmountSum)
            {
                for (var i = 0; i < vm.Outputs.Count; i++)
                {
                    vm.AddModelError(model => model.Outputs[i].Amount,
                                     "Estás enviando más de lo que tienes", ModelState);
                }
            }

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

            DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings(walletId);

            CreatePSBTResponse psbt = null;

            try
            {
                psbt = await CreatePSBT(network, derivationScheme, vm, cancellation);
            }
            catch (NBXplorerException ex)
            {
                ModelState.AddModelError(string.Empty, ex.Error.Message);
                return(View(vm));
            }
            catch (NotSupportedException)
            {
                ModelState.AddModelError(string.Empty, "Necesitas actualizar tu versión de NBXplorer");
                return(View(vm));
            }
            derivationScheme.RebaseKeyPaths(psbt.PSBT);

            switch (command)
            {
            case "ledger":
                return(ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress));

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

            case "analyze-psbt":
                var name =
                    $"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
                return(RedirectToAction(nameof(WalletPSBT), new { walletId = walletId, psbt = psbt.PSBT.ToBase64(), FileName = name }));

            default:
                return(View(vm));
            }
        }
Ejemplo n.º 12
0
        public async Task <IActionResult> WalletSend(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
        {
            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 network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);

            if (network == null)
            {
                return(NotFound());
            }
            var storeData = store.GetStoreBlob();
            var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);

            rateRules.Spread = 0.0m;
            var             currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
            WalletSendModel model        = new WalletSendModel()
            {
                Destination = defaultDestination,
                CryptoCode  = walletId.CryptoCode
            };

            if (double.TryParse(defaultAmount, out var amount))
            {
                model.Amount = (decimal)amount;
            }

            var feeProvider     = _feeRateProvider.CreateFeeProvider(network);
            var recommendedFees = feeProvider.GetFeeRateAsync();
            var balance         = _walletProvider.GetWallet(network).GetBalance(paymentMethod.DerivationStrategyBase);

            model.CurrentBalance            = (await balance).ToDecimal(MoneyUnit.BTC);
            model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
            model.FeeSatoshiPerByte         = model.RecommendedSatoshiPerByte;

            using (CancellationTokenSource cts = new CancellationTokenSource())
            {
                try
                {
                    cts.CancelAfter(TimeSpan.FromSeconds(5));
                    var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);

                    if (result.BidAsk != null)
                    {
                        model.Rate         = result.BidAsk.Center;
                        model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
                        model.Fiat         = currencyPair.Right;
                    }
                    else
                    {
                        model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
                    }
                }
                catch (Exception ex) { model.RateError = ex.Message; }
            }
            model.AdvancedMode = advancedMode;
            return(View(model));
        }