private BitcoinUrlBuilder CreateBuilder(string uri) { var builder = new BitcoinUrlBuilder(uri); Assert.Equal(builder.Uri.ToString(), uri); builder = new BitcoinUrlBuilder(new Uri(uri, UriKind.Absolute)); Assert.Equal(builder.ToString(), uri); return(builder); }
private async Task <PSBT> GetPayjoinProposedTX(BitcoinUrlBuilder bip21, PSBT psbt, DerivationSchemeSettings derivationSchemeSettings, BTCPayNetwork btcPayNetwork, CancellationToken cancellationToken) { var cloned = psbt.Clone(); cloned = cloned.Finalize(); await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), cloned.ExtractTransaction(), btcPayNetwork); return(await _payjoinClient.RequestPayjoin(bip21, new PayjoinWallet(derivationSchemeSettings), psbt, cancellationToken)); }
public UriClaimDestination(BitcoinUrlBuilder bitcoinUrl) { ArgumentNullException.ThrowIfNull(bitcoinUrl); if (bitcoinUrl.Address is null) { throw new ArgumentException(nameof(bitcoinUrl)); } _bitcoinUrl = bitcoinUrl; Address = bitcoinUrl.Address ?? throw new FormatException("the bip21 doesn't contain an address"); }
public async Task <IActionResult> BitpayTranslator(BitpayTranslatorViewModel vm) { if (!ModelState.IsValid) { return(View(vm)); } vm.BitpayLink = vm.BitpayLink ?? string.Empty; vm.BitpayLink = vm.BitpayLink.Trim(); if (!vm.BitpayLink.StartsWith("bitcoin:", StringComparison.OrdinalIgnoreCase)) { var invoiceId = vm.BitpayLink.Substring(vm.BitpayLink.LastIndexOf("=", StringComparison.OrdinalIgnoreCase) + 1); vm.BitpayLink = $"bitcoin:?r=https://bitpay.com/i/{invoiceId}"; } try { BitcoinUrlBuilder urlBuilder = new BitcoinUrlBuilder(vm.BitpayLink, Network.Main); #pragma warning disable CS0618 // Type or member is obsolete if (!urlBuilder.PaymentRequestUrl.DnsSafeHost.EndsWith("bitpay.com", StringComparison.OrdinalIgnoreCase)) { throw new Exception("This tool only work with bitpay"); } var client = HttpClientFactory.CreateClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, urlBuilder.PaymentRequestUrl); #pragma warning restore CS0618 // Type or member is obsolete request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/payment-request")); var result = await client.SendAsync(request); // {"network":"main","currency":"BTC","requiredFeeRate":29.834,"outputs":[{"amount":255900,"address":"1PgPo5d4swD6pKfCgoXtoW61zqTfX9H7tj"}],"time":"2018-12-03T14:39:47.162Z","expires":"2018-12-03T14:54:47.162Z","memo":"Payment request for BitPay invoice HHfG8cprRMzZG6MErCqbjv for merchant VULTR Holdings LLC","paymentUrl":"https://bitpay.com/i/HHfG8cprRMzZG6MErCqbjv","paymentId":"HHfG8cprRMzZG6MErCqbjv"} var str = await result.Content.ReadAsStringAsync(); try { var jobj = JObject.Parse(str); vm.Address = ((JArray)jobj["outputs"])[0]["address"].Value <string>(); var amount = Money.Satoshis(((JArray)jobj["outputs"])[0]["amount"].Value <long>()); vm.Amount = amount.ToString(); vm.BitcoinUri = $"bitcoin:{vm.Address}?amount={amount.ToString()}"; } catch (JsonReaderException) { ModelState.AddModelError(nameof(vm.BitpayLink), $"Invalid or expired bitpay invoice"); return(View(vm)); } } catch (Exception ex) { ModelState.AddModelError(nameof(vm.BitpayLink), $"Error while requesting {ex.Message}"); return(View(vm)); } return(View(vm)); }
public UriClaimDestination(BitcoinUrlBuilder bitcoinUrl) { if (bitcoinUrl == null) { throw new ArgumentNullException(nameof(bitcoinUrl)); } if (bitcoinUrl.Address is null) { throw new ArgumentException(nameof(bitcoinUrl)); } _bitcoinUrl = bitcoinUrl; }
public void CanTalkToPaymentServer() { using (var server = new PaymentServerTester()) { var uri = server.GetPaymentRequestUri(2); BitcoinUrlBuilder btcUri = new BitcoinUrlBuilder(uri); var request = btcUri.GetPaymentRequest(); Assert.True(request.VerifySignature()); Assert.Equal(2, BitConverter.ToInt32(request.Details.MerchantData, 0)); var ack = request.CreatePayment().SubmitPayment(); Assert.NotNull(ack); } }
public void CanPayUsingBIP70() { using (var tester = ServerTester.Create()) { tester.Start(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); var invoice = user.BitPay.CreateInvoice(new Invoice() { Buyer = new Buyer() { email = "*****@*****.**" }, Price = 5000.0, Currency = "USD", PosData = "posData", OrderId = "orderId", //RedirectURL = redirect + "redirect", //NotificationURL = CallbackUri + "/notification", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); Assert.False(invoice.Refundable); var url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP72); var request = url.GetPaymentRequest(); var payment = request.CreatePayment(); Transaction tx = new Transaction(); tx.Outputs.AddRange(request.Details.Outputs.Select(o => new TxOut(o.Amount, o.Script))); var cashCow = tester.ExplorerNode; tx = cashCow.FundRawTransaction(tx).Transaction; tx = cashCow.SignRawTransaction(tx); payment.Transactions.Add(tx); payment.RefundTo.Add(new PaymentOutput(Money.Coins(1.0m), new Key().ScriptPubKey)); var ack = payment.SubmitPayment(); Assert.NotNull(ack); Eventually(() => { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.True(localInvoice.Refundable); }); } }
public void CanParsePaymentUrl() { Assert.Equal("bitcoin:", new BitcoinUrlBuilder().Uri.ToString()); var url = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha"); Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); url = CreateBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06"); Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); Assert.Equal(Money.Parse("0.06"), url.Amount); url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=0.06&label=Tom%20%26%20Jerry"); Assert.Equal("129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha", url.Address.ToString()); Assert.Equal(Money.Parse("0.06"), url.Amount); Assert.Equal("Tom & Jerry", url.Label); Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString()).ToString()); //Request 50 BTC with message: url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz"); Assert.Equal(Money.Parse("50"), url.Amount); Assert.Equal("Luke-Jr", url.Label); Assert.Equal("Donation for project xyz", url.Message); Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString()).ToString()); //Some future version that has variables which are (currently) not understood and required and thus invalid: url = new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&unknownparam=lol"); //Some future version that has variables which are (currently) not understood but not required and thus valid: Assert.Throws <FormatException>(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz&req-unknownparam=lol")); Assert.Throws <FormatException>(() => new BitcoinUrlBuilder("bitcoin:129mVqKUmJ9uwPxKJBnNdABbuaaNfho4Ha?amount=50&amount=50")); url = new BitcoinUrlBuilder("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3D2a8628fc2fbe"); Assert.Equal("bitcoin:mq7se9wy2egettFxPbmn99cK8v5AFq55Lx?amount=0.11&r=https://merchant.com/pay.php?h%3d2a8628fc2fbe", url.ToString()); #pragma warning disable CS0618 // Type or member is obsolete Assert.Equal("https://merchant.com/pay.php?h=2a8628fc2fbe", url.PaymentRequestUrl.ToString()); #pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString()).ToString()); //Support no address url = new BitcoinUrlBuilder("bitcoin:?r=https://merchant.com/pay.php?h%3D2a8628fc2fbe"); #pragma warning disable CS0618 // Type or member is obsolete Assert.Equal("https://merchant.com/pay.php?h=2a8628fc2fbe", url.PaymentRequestUrl.ToString()); #pragma warning restore CS0618 // Type or member is obsolete Assert.Equal(url.ToString(), new BitcoinUrlBuilder(url.ToString()).ToString()); }
public static bool TryParse(string text, Network expectedNetwork, out BitcoinUrlBuilder result) { result = null; if (string.IsNullOrWhiteSpace(text) || text.Length > 1000) { return(false); } if (TryParseBitcoinAddress(text, expectedNetwork, out BitcoinUrlBuilder addressResult)) { result = addressResult; return(true); } else { if (TryParseBitcoinUrl(text, expectedNetwork, out BitcoinUrlBuilder urlResult)) { result = urlResult; return(true); } } return(false); }
public static bool TryParseBitcoinUrl(string text, Network expectedNetwork, out BitcoinUrlBuilder url) { url = null; if (text is null || expectedNetwork is null) { return(false); } text = text.Trim(); if (text.Length > 1000 || text.Length < 20) { return(false); } try { if (!text.StartsWith("bitcoin:", true, CultureInfo.InvariantCulture)) { return(false); } var bitcoinUrl = new BitcoinUrlBuilder(text, expectedNetwork); if (bitcoinUrl?.Address.Network == expectedNetwork) { url = bitcoinUrl; return(true); } return(false); } catch (FormatException) { return(false); } }
internal BitcoinUrlBuilder ParseDestinationString(string destinationString) { if (destinationString == null) { return(null); } destinationString = destinationString.Trim(); BitcoinUrlBuilder url = null; try { url = new BitcoinUrlBuilder(destinationString, Global.Network); if (url.Amount != null) { // since AmountText can be altered by hand, we set it instead // of binding to a calculated ObservableAsPropertyHelper AmountText = url.Amount.ToString(); } // we could check url.Label or url.Message for contact, but there is // no convention on their use yet so it's hard to say whether they // identify the sender or receiver. We care about the recipient only here. return(url); } catch (Exception) { /* invalid bitcoin uri */ } try { BitcoinAddress address = BitcoinAddress.Create(destinationString.Trim(), Global.Network); url = new BitcoinUrlBuilder(); url.Address = address; } catch (Exception) { /* invalid bitcoin address */ } return(url); }
internal bool HandleScan(string scannedIn) { BitcoinAddress address = null; try { address = BitcoinAddress.Create(scannedIn, Global.Network); } catch (Exception) { try { address = new BitcoinUrlBuilder(scannedIn, Global.Network).Address; } catch (Exception) { } } if (address != null) { Address = address.ToString(); return(true); } return(false); }
public async Task ElementsAssetsAreHandledCorrectly() { using (var tester = CreateServerTester()) { tester.ActivateLBTC(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("LBTC"); user.RegisterDerivationScheme("USDT"); user.RegisterDerivationScheme("ETB"); await tester.LBTCExplorerNode.GenerateAsync(4); //no tether on our regtest, lets create it and set it var tether = tester.NetworkProvider.GetNetwork <ElementsBTCPayNetwork>("USDT"); var lbtc = tester.NetworkProvider.GetNetwork <ElementsBTCPayNetwork>("LBTC"); var etb = tester.NetworkProvider.GetNetwork <ElementsBTCPayNetwork>("ETB"); var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0); tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString()); ((ElementsBTCPayNetwork)tester.PayTester.GetService <BTCPayWalletProvider>().GetWallet("USDT").Network) .AssetId = tether.AssetId; Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork <ElementsBTCPayNetwork>("USDT").AssetId); Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService <BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId); var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0); etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString()); ((ElementsBTCPayNetwork)tester.PayTester.GetService <BTCPayWalletProvider>().GetWallet("ETB").Network) .AssetId = etb.AssetId; //test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC")); Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); var ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC")); //1 lbtc = 1 btc Assert.Equal(1, ci.Rate); var star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true, 1, "UNSET", lbtc.AssetId); TestUtils.Eventually(() => { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("LBTC")).Payments); }); invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC")); ci = invoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT")); Assert.Equal(3, invoice.SupportedTransactionCurrencies.Count); star = await tester.LBTCExplorerNode.SendCommandAsync("sendtoaddress", ci.Address, ci.Due, "", "", false, true, 1, "UNSET", tether.AssetId); TestUtils.Eventually(() => { var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant); Assert.Equal("paid", localInvoice.Status); Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments); }); //test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606 var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork); //precision = 2, 1ETB = 0.00000100 Assert.Equal(100, etbBip21.Amount.Satoshi); var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork); //precision = 8, 0.1 = 0.1 Assert.Equal(0.1m, lbtcBip21.Amount.ToDecimal(MoneyUnit.BTC)); } }
public async Task <IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode, [FromBody] CreateOnChainTransactionRequest request) { if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network, out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) { return(actionResult); } if (network.ReadonlyWallet) { return(this.CreateAPIError("not-available", $"{cryptoCode} sending services are not currently available")); } //This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation. if (!(await CanUseHotWallet()).HotWallet) { return(Unauthorized()); } var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode); var wallet = _btcPayWalletProvider.GetWallet(network); var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation); if (request.SelectedInputs != null || !utxos.Any()) { utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true) .ToArray(); if (utxos.Any() is false) { //no valid utxos selected request.AddModelError(transactionRequest => transactionRequest.SelectedInputs, "There are no available utxos based on your request", this); } } var balanceAvailable = utxos.Sum(coin => coin.Value.GetValue(network)); var subtractFeesOutputsCount = new List <int>(); var subtractFees = request.Destinations.Any(o => o.SubtractFromAmount); int?payjoinOutputIndex = null; var sum = 0m; var outputs = new List <WalletSendModel.TransactionOutput>(); for (var index = 0; index < request.Destinations.Count; index++) { var destination = request.Destinations[index]; if (destination.SubtractFromAmount) { subtractFeesOutputsCount.Add(index); } BitcoinUrlBuilder bip21 = null; var amount = destination.Amount; if (amount.GetValueOrDefault(0) <= 0) { amount = null; } var address = string.Empty; try { destination.Destination = destination.Destination.Replace(network.UriScheme + ":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase); bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork); amount ??= bip21.Amount.GetValue(network); address = bip21.Address.ToString(); if (destination.SubtractFromAmount) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "You cannot use a BIP21 destination along with SubtractFromAmount", this); } } catch (FormatException) { try { address = BitcoinAddress.Create(destination.Destination, network.NBitcoinNetwork).ToString(); } catch (Exception e) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "Destination must be a BIP21 payment link or an address", this); } } if (amount is null || amount <= 0) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "Amount must be specified or destination must be a BIP21 payment link, and greater than 0", this); } if (request.ProceedWithPayjoin && bip21?.UnknowParameters?.ContainsKey("pj") is true) { payjoinOutputIndex = index; } outputs.Add(new WalletSendModel.TransactionOutput() { DestinationAddress = address, Amount = amount, SubtractFeesFromOutput = destination.SubtractFromAmount }); sum += destination.Amount ?? 0; } if (subtractFeesOutputsCount.Count > 1) { foreach (var subtractFeesOutput in subtractFeesOutputsCount) { request.AddModelError(model => model.Destinations[subtractFeesOutput].SubtractFromAmount, "You can only subtract fees from one destination", this); } } if (balanceAvailable < sum) { request.AddModelError(transactionRequest => transactionRequest.Destinations, "You are attempting to send more than is available", this); } else if (balanceAvailable == sum && !subtractFees) { request.AddModelError(transactionRequest => transactionRequest.Destinations, "You are sending your entire balance, you should subtract the fees from a destination", this); } var minRelayFee = this._nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); if (request.FeeRate != null && request.FeeRate < minRelayFee) { ModelState.AddModelError(nameof(request.FeeRate), "The fee rate specified is lower than the current minimum relay fee"); } if (!ModelState.IsValid) { return(this.CreateValidationError(ModelState)); } CreatePSBTResponse psbt; try { psbt = await _walletsController.CreatePSBT(network, derivationScheme, new WalletSendModel() { SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()), Outputs = outputs, AlwaysIncludeNonWitnessUTXO = true, InputSelection = request.SelectedInputs?.Any() is true, AllowFeeBump = !request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe : request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes : WalletSendModel.ThreeStateBool.No, FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte, NoChange = request.NoChange }, CancellationToken.None); }
public async Task CanUseBIP79() { using (var tester = ServerTester.Create()) { await tester.StartAsync(); ////var payJoinStateProvider = tester.PayTester.GetService<PayJoinStateProvider>(); var btcPayNetwork = tester.NetworkProvider.GetNetwork <BTCPayNetwork>("BTC"); var btcPayWallet = tester.PayTester.GetService <BTCPayWalletProvider>().GetWallet(btcPayNetwork); var cashCow = tester.ExplorerNode; cashCow.Generate(2); // get some money in case var senderUser = tester.NewAccount(); senderUser.GrantAccess(true); senderUser.RegisterDerivationScheme("BTC", true, true); var invoice = senderUser.BitPay.CreateInvoice( new Invoice() { Price = 100, Currency = "USD", FullNotifications = true }); //payjoin is not enabled by default. Assert.DoesNotContain("bpu", invoice.CryptoInfo.First().PaymentUrls.BIP21); cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network), Money.Coins(0.06m)); var receiverUser = tester.NewAccount(); receiverUser.GrantAccess(true); receiverUser.RegisterDerivationScheme("BTC", true, true); await receiverUser.EnablePayJoin(); // payjoin is enabled, with a segwit wallet, and the keys are available in nbxplorer invoice = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true }); cashCow.SendToAddress(BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network), Money.Coins(0.06m)); var receiverWalletId = new WalletId(receiverUser.StoreId, "BTC"); //give the cow some cash await cashCow.GenerateAsync(1); //let's get some more utxos first await receiverUser.ReceiveUTXO(Money.Coins(0.011m), btcPayNetwork); await receiverUser.ReceiveUTXO(Money.Coins(0.012m), btcPayNetwork); await receiverUser.ReceiveUTXO(Money.Coins(0.013m), btcPayNetwork); await receiverUser.ReceiveUTXO(Money.Coins(0.014m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.021m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.022m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.023m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.024m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.025m), btcPayNetwork); await senderUser.ReceiveUTXO(Money.Coins(0.026m), btcPayNetwork); var senderChange = await senderUser.GetNewAddress(btcPayNetwork); //Let's start the harassment invoice = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true }); var parsedBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var invoice2 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.02m, Currency = "BTC", FullNotifications = true }); var secondInvoiceParsedBip21 = new BitcoinUrlBuilder(invoice2.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var senderStore = await tester.PayTester.StoreRepository.FindStore(senderUser.StoreId); var paymentMethodId = new PaymentMethodId("BTC", PaymentTypes.BTCLike); var derivationSchemeSettings = senderStore.GetSupportedPaymentMethods(tester.NetworkProvider) .OfType <DerivationSchemeSettings>().SingleOrDefault(settings => settings.PaymentId == paymentMethodId); ReceivedCoin[] senderCoins = null; await TestUtils.EventuallyAsync(async() => { senderCoins = await btcPayWallet.GetUnspentCoins(senderUser.DerivationScheme); Assert.Contains(senderCoins, coin => coin.Value.GetValue(btcPayNetwork) == 0.026m); }); var coin = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.021m); var coin2 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.022m); var coin3 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.023m); var coin4 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.024m); var coin5 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.025m); var coin6 = senderCoins.Single(coin => coin.Value.GetValue(btcPayNetwork) == 0.026m); var signingKeySettings = derivationSchemeSettings.GetSigningAccountKeySettings(); signingKeySettings.RootFingerprint = senderUser.GenerateWalletResponseV.MasterHDKey.GetPublicKey().GetHDFingerPrint(); var extKey = senderUser.GenerateWalletResponseV.MasterHDKey.Derive(signingKeySettings.GetRootedKeyPath() .KeyPath); var n = tester.ExplorerClient.Network.NBitcoinNetwork; var Invoice1Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(parsedBip21.Address, parsedBip21.Amount) .AddCoins(coin.Coin) .AddKeys(extKey.Derive(coin.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .BuildTransaction(true); var Invoice1Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(parsedBip21.Address, parsedBip21.Amount) .AddCoins(coin2.Coin) .AddKeys(extKey.Derive(coin2.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .BuildTransaction(true); var Invoice2Coin1 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount) .AddCoins(coin.Coin) .AddKeys(extKey.Derive(coin.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .BuildTransaction(true); var Invoice2Coin2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(secondInvoiceParsedBip21.Address, secondInvoiceParsedBip21.Amount) .AddCoins(coin2.Coin) .AddKeys(extKey.Derive(coin2.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .BuildTransaction(true); //Attempt 1: Send a signed tx to invoice 1 that does not pay the invoice at all //Result: reject // Assert.False((await tester.PayTester.HttpClient.PostAsync(endpoint, // new StringContent(Invoice2Coin1.ToHex(), Encoding.UTF8, "text/plain"))).IsSuccessStatusCode); //Attempt 2: Create two transactions using different inputs and send them to the same invoice. //Result: Second Tx should be rejected. var Invoice1Coin1ResponseTx = await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork); await senderUser.SubmitPayjoin(invoice, Invoice1Coin1, btcPayNetwork, "already-paid"); var contributedInputsInvoice1Coin1ResponseTx = Invoice1Coin1ResponseTx.Inputs.Where(txin => coin.OutPoint != txin.PrevOut); Assert.Single(contributedInputsInvoice1Coin1ResponseTx); //Attempt 3: Send the same inputs from invoice 1 to invoice 2 while invoice 1 tx has not been broadcasted //Result: Reject Tx1 but accept tx 2 as its inputs were never accepted by invoice 1 await senderUser.SubmitPayjoin(invoice2, Invoice2Coin1, btcPayNetwork, "inputs-already-used"); var Invoice2Coin2ResponseTx = await senderUser.SubmitPayjoin(invoice2, Invoice2Coin2, btcPayNetwork); var contributedInputsInvoice2Coin2ResponseTx = Invoice2Coin2ResponseTx.Inputs.Where(txin => coin2.OutPoint != txin.PrevOut); Assert.Single(contributedInputsInvoice2Coin2ResponseTx); //Attempt 4: Make tx that pays invoice 3 and 4 and submit to both //Result: reject on 4: the protocol should not worry about this complexity var invoice3 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true }); var invoice3ParsedBip21 = new BitcoinUrlBuilder(invoice3.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var invoice4 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true }); var invoice4ParsedBip21 = new BitcoinUrlBuilder(invoice4.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var Invoice3AndInvoice4Coin3 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(invoice3ParsedBip21.Address, invoice3ParsedBip21.Amount) .Send(invoice4ParsedBip21.Address, invoice4ParsedBip21.Amount) .AddCoins(coin3.Coin) .AddKeys(extKey.Derive(coin3.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .BuildTransaction(true); await senderUser.SubmitPayjoin(invoice3, Invoice3AndInvoice4Coin3, btcPayNetwork); await senderUser.SubmitPayjoin(invoice4, Invoice3AndInvoice4Coin3, btcPayNetwork, "already-paid"); //Attempt 5: Make tx that pays invoice 5 with 2 outputs //Result: proposed tx consolidates the outputs var invoice5 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true }); var invoice5ParsedBip21 = new BitcoinUrlBuilder(invoice5.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var Invoice5Coin4TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2) .Send(invoice5ParsedBip21.Address, invoice5ParsedBip21.Amount / 2) .AddCoins(coin4.Coin) .AddKeys(extKey.Derive(coin4.KeyPath)) .SendEstimatedFees(new FeeRate(100m)); var Invoice5Coin4 = Invoice5Coin4TxBuilder.BuildTransaction(true); var Invoice5Coin4ResponseTx = await senderUser.SubmitPayjoin(invoice5, Invoice5Coin4, btcPayNetwork); Assert.Single(Invoice5Coin4ResponseTx.Outputs.To(invoice5ParsedBip21.Address)); //Attempt 10: send tx with rbf, broadcast payjoin tx, bump the rbf payjoin , attempt to submit tx again //Result: same tx gets sent back //give the receiver some more utxos Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync( (await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address, new Money(0.1m, MoneyUnit.BTC))); var invoice6 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true }); var invoice6ParsedBip21 = new BitcoinUrlBuilder(invoice6.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var invoice6Coin5TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(invoice6ParsedBip21.Address, invoice6ParsedBip21.Amount) .AddCoins(coin5.Coin) .AddKeys(extKey.Derive(coin5.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .SetLockTime(0); var invoice6Coin5 = invoice6Coin5TxBuilder .BuildTransaction(true); var Invoice6Coin5Response1Tx = await senderUser.SubmitPayjoin(invoice6, invoice6Coin5, btcPayNetwork); var Invoice6Coin5Response1TxSigned = invoice6Coin5TxBuilder.SignTransaction(Invoice6Coin5Response1Tx); //broadcast the first payjoin await tester.ExplorerClient.BroadcastAsync(Invoice6Coin5Response1TxSigned); // invoice6Coin5TxBuilder = invoice6Coin5TxBuilder.SendEstimatedFees(new FeeRate(100m)); // var invoice6Coin5Bumpedfee = invoice6Coin5TxBuilder // .BuildTransaction(true); // // var Invoice6Coin5Response3 = await tester.PayTester.HttpClient.PostAsync(invoice6Endpoint, // new StringContent(invoice6Coin5Bumpedfee.ToHex(), Encoding.UTF8, "text/plain")); // Assert.True(Invoice6Coin5Response3.IsSuccessStatusCode); // var Invoice6Coin5Response3Tx = // Transaction.Parse(await Invoice6Coin5Response3.Content.ReadAsStringAsync(), n); // Assert.True(invoice6Coin5Bumpedfee.Inputs.All(txin => // Invoice6Coin5Response3Tx.Inputs.Any(txin2 => txin2.PrevOut == txin.PrevOut))); //Attempt 11: //send tx with rbt, broadcast payjoin, //create tx spending the original tx inputs with rbf to self, //Result: the exposed utxos are priorized in the next p2ep //give the receiver some more utxos Assert.NotNull(await tester.ExplorerNode.SendToAddressAsync( (await btcPayWallet.ReserveAddressAsync(receiverUser.DerivationScheme)).Address, new Money(0.1m, MoneyUnit.BTC))); var invoice7 = receiverUser.BitPay.CreateInvoice( new Invoice() { Price = 0.01m, Currency = "BTC", FullNotifications = true }); var invoice7ParsedBip21 = new BitcoinUrlBuilder(invoice7.CryptoInfo.First().PaymentUrls.BIP21, tester.ExplorerClient.Network.NBitcoinNetwork); var invoice7Coin6TxBuilder = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .Send(invoice7ParsedBip21.Address, invoice7ParsedBip21.Amount) .AddCoins(coin6.Coin) .AddKeys(extKey.Derive(coin6.KeyPath)) .SendEstimatedFees(new FeeRate(100m)) .SetLockTime(0); var invoice7Coin6Tx = invoice7Coin6TxBuilder .BuildTransaction(true); var invoice7Coin6Response1Tx = await senderUser.SubmitPayjoin(invoice7, invoice7Coin6Tx, btcPayNetwork); var Invoice7Coin6Response1TxSigned = invoice7Coin6TxBuilder.SignTransaction(invoice7Coin6Response1Tx); var contributedInputsInvoice7Coin6Response1TxSigned = Invoice7Coin6Response1TxSigned.Inputs.Single(txin => coin6.OutPoint != txin.PrevOut); ////var receiverWalletPayJoinState = payJoinStateProvider.Get(receiverWalletId); ////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id); //broadcast the payjoin var res = (await tester.ExplorerClient.BroadcastAsync(Invoice7Coin6Response1TxSigned)); Assert.True(res.Success); // Paid with coinjoin await TestUtils.EventuallyAsync(async() => { var invoiceEntity = await tester.PayTester.GetService <InvoiceRepository>().GetInvoice(invoice7.Id); Assert.Equal(InvoiceStatus.Paid, invoiceEntity.Status); Assert.Contains(invoiceEntity.GetPayments(), p => p.Accounted && ((BitcoinLikePaymentData)p.GetCryptoPaymentData()).PayjoinInformation is null); }); ////Assert.Contains(receiverWalletPayJoinState.GetRecords(), item => item.InvoiceId == invoice7.Id && item.TxSeen); var invoice7Coin6Tx2 = tester.ExplorerClient.Network.NBitcoinNetwork.CreateTransactionBuilder() .SetChange(senderChange) .AddCoins(coin6.Coin) .SendAll(senderChange) .SubtractFees() .AddKeys(extKey.Derive(coin6.KeyPath)) .SendEstimatedFees(new FeeRate(200m)) .SetLockTime(0) .BuildTransaction(true); //broadcast the "rbf cancel" tx res = (await tester.ExplorerClient.BroadcastAsync(invoice7Coin6Tx2)); Assert.True(res.Success); // Make a block, this should put back the invoice to new var blockhash = tester.ExplorerNode.Generate(1)[0]; Assert.NotNull(await tester.ExplorerNode.GetRawTransactionAsync(invoice7Coin6Tx2.GetHash(), blockhash)); Assert.Null(await tester.ExplorerNode.GetRawTransactionAsync(Invoice7Coin6Response1TxSigned.GetHash(), blockhash, false)); // Now we should return to New OutPoint ourOutpoint = null; await TestUtils.EventuallyAsync(async() => { var invoiceEntity = await tester.PayTester.GetService <InvoiceRepository>().GetInvoice(invoice7.Id); Assert.Equal(InvoiceStatus.New, invoiceEntity.Status); Assert.True(invoiceEntity.GetPayments().All(p => !p.Accounted)); ourOutpoint = invoiceEntity.GetAllBitcoinPaymentData().First().PayjoinInformation.ContributedOutPoints[0]; }); var payjoinRepository = tester.PayTester.GetService <PayJoinRepository>(); // The outpoint should now be available for next pj selection Assert.False(await payjoinRepository.TryUnlock(ourOutpoint)); } }
public async Task CanManageWallet() { using (var s = SeleniumTester.Create()) { await s.StartAsync(); s.RegisterNewUser(true); var storeId = s.CreateNewStore(); // In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed // to sign the transaction s.GenerateWallet("BTC", "", true, false); //let's test quickly the receive wallet page s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.ScrollTo(By.Id("SendMenu")); s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); //you cant use the Sign with NBX option without saving private keys when generating the wallet. Assert.DoesNotContain("nbx-seed", s.Driver.PageSource); s.Driver.FindElement(By.Id("WalletReceive")).Click(); //generate a receiving address s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); var receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"); //unreserve s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click(); //generate it again, should be the same one as before as nothign got used in the meantime s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed); Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value")); //send money to addr and ensure it changed var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync(); sess.ListenAllTrackedSource(); var nextEvent = sess.NextEventAsync(); s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest), Money.Parse("0.1")); await nextEvent; await Task.Delay(200); s.Driver.Navigate().Refresh(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value")); receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"); //change the wallet and ensure old address is not there and generating a new one does not result in the prev one s.GoToStore(storeId.storeId); s.GenerateWallet("BTC", "", true, false); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("WalletReceive")).Click(); s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click(); Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value")); var invoiceId = s.CreateInvoice(storeId.storeName); var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); var address = invoice.EntityToDTO().Addresses["BTC"]; //wallet should have been imported to bitcoin core wallet in watch only mode. var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); Assert.True(result.IsWatchOnly); s.GoToStore(storeId.storeId); var mnemonic = s.GenerateWallet("BTC", "", true, true); //lets import and save private keys var root = mnemonic.DeriveExtKey(); invoiceId = s.CreateInvoice(storeId.storeName); invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId); address = invoice.EntityToDTO().Addresses["BTC"]; result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest)); //spendable from bitcoin core wallet! Assert.False(result.IsWatchOnly); var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m)); s.Server.ExplorerNode.Generate(1); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.ClickOnAllSideMenus(); // Make sure we can rescan, because we are admin! s.Driver.FindElement(By.Id("WalletRescan")).ForceClick(); Assert.Contains("The batch size make sure", s.Driver.PageSource); // We setup the fingerprint and the account key path s.Driver.FindElement(By.Id("WalletSettings")).ForceClick(); // s.Driver.FindElement(By.Id("AccountKeys_0__MasterFingerprint")).SendKeys("8bafd160"); // s.Driver.FindElement(By.Id("AccountKeys_0__AccountKeyPath")).SendKeys("m/49'/0'/0'" + Keys.Enter); // Check the tx sent earlier arrived s.Driver.FindElement(By.Id("WalletTransactions")).ForceClick(); var walletTransactionLink = s.Driver.Url; Assert.Contains(tx.ToString(), s.Driver.PageSource); void SignWith(Mnemonic signingSource) { // Send to bob s.Driver.FindElement(By.Id("WalletSend")).Click(); var bob = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, bob, 1); s.Driver.ScrollTo(By.Id("SendMenu")); s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=seed]")).Click(); // Input the seed s.Driver.FindElement(By.Id("SeedOrKey")).SendKeys(signingSource.ToString() + Keys.Enter); // Broadcast Assert.Contains(bob.ToString(), s.Driver.PageSource); Assert.Contains("1.00000000", s.Driver.PageSource); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); } SignWith(mnemonic); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("WalletSend")).Click(); var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest); SetTransactionOutput(s, 0, jack, 0.01m); s.Driver.ScrollTo(By.Id("SendMenu")); s.Driver.FindElement(By.Id("SendMenu")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click(); Assert.Contains(jack.ToString(), s.Driver.PageSource); Assert.Contains("0.01000000", s.Driver.PageSource); s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick(); Assert.EndsWith("psbt", s.Driver.Url); s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.EndsWith("psbt/ready", s.Driver.Url); s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick(); Assert.Equal(walletTransactionLink, s.Driver.Url); var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21; //let's make bip21 more interesting bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!"; var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest); s.Driver.FindElement(By.Id("Wallets")).Click(); s.Driver.FindElement(By.LinkText("Manage")).Click(); s.Driver.FindElement(By.Id("WalletSend")).Click(); s.Driver.FindElement(By.Id("bip21parse")).Click(); s.Driver.SwitchTo().Alert().SendKeys(bip21); s.Driver.SwitchTo().Alert().Accept(); s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info); Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value")); Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value")); s.GoToWallet(new WalletId(storeId.storeId, "BTC"), WalletsNavPages.Settings); var walletUrl = s.Driver.Url; s.Driver.FindElement(By.Id("SettingsMenu")).ForceClick(); s.Driver.FindElement(By.CssSelector("button[value=view-seed]")).Click(); // Seed backup page var recoveryPhrase = s.Driver.FindElements(By.Id("recovery-phrase")).First().GetAttribute("data-mnemonic"); Assert.Equal(mnemonic.ToString(), recoveryPhrase); Assert.Contains("The recovery phrase will also be stored on the server as a hot wallet.", s.Driver.PageSource); // No confirmation, just a link to return to the wallet Assert.Empty(s.Driver.FindElements(By.Id("confirm"))); s.Driver.FindElement(By.Id("proceed")).Click(); Assert.Equal(walletUrl, s.Driver.Url); } }
public static bool TryGetPayjoinEndpoint(this BitcoinUrlBuilder bip21, out Uri endpoint) { endpoint = bip21.UnknownParameters.TryGetValue($"{PayjoinClient.BIP21EndpointKey}", out var uri) ? new Uri(uri, UriKind.Absolute) : null; return(endpoint != null); }
public static bool TryParseBitcoinAddress(string text, Network expectedNetwork, out BitcoinUrlBuilder url) { url = null; if (text is null || expectedNetwork is null) { return(false); } text = text.Trim(); if (text.Length > 100 || text.Length < 20) { return(false); } try { var bitcoinAddress = BitcoinAddress.Create(text, expectedNetwork); url = new BitcoinUrlBuilder($"bitcoin:{bitcoinAddress}", expectedNetwork); return(true); } catch (FormatException) { return(false); } }