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