From 1a409a441d5543d13eba07ba02e814a1edddd1b8 Mon Sep 17 00:00:00 2001 From: Hans Looman Date: Wed, 12 Feb 2020 21:44:31 -0800 Subject: [PATCH] Update POLIS related entries (#1313) * Update Polis related info and services * Fix Polis Rate Fetcher * Fix Polis ratefetcher - Cryptopia is obsolete * POLIS rate provider changes to comply with internal testing * URL / pair alignment * Add small doc to re-trigger testing --- .../Altcoins/BTCPayNetworkProvider.Polis.cs | 4 +- .../Altcoins/BTCPayNetworkProvider.cs | 4 +- .../Providers/PolisRateProvider.cs | 26 +++++++ .../Services/RateProviderFactory.cs | 3 + BTCPayServer.Tests/UnitTest1.cs | 65 ++++++++++-------- BTCPayServer/wwwroot/imlegacy/polis.png | Bin 10819 -> 10320 bytes 6 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 BTCPayServer.Rating/Providers/PolisRateProvider.cs diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Polis.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Polis.cs index 48dfb5245..598063ce7 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Polis.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.Polis.cs @@ -16,13 +16,13 @@ namespace BTCPayServer { CryptoCode = nbxplorerNetwork.CryptoCode, DisplayName = "Polis", - BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}", + BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}", NBXplorerNetwork = nbxplorerNetwork, UriScheme = "polis", DefaultRateRules = new[] { "POLIS_X = POLIS_BTC * BTC_X", - "POLIS_BTC = cryptopia(POLIS_BTC)" + "POLIS_BTC = polispay(POLIS_BTC)" }, CryptoImagePath = "imlegacy/polis.png", DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType), diff --git a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.cs b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.cs index 721e3ddcd..db93a2a64 100644 --- a/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.cs +++ b/BTCPayServer.Common/Altcoins/BTCPayNetworkProvider.cs @@ -59,7 +59,8 @@ namespace BTCPayServer InitGroestlcoin(); InitViacoin(); InitMonero(); - + InitPolis(); + // Assume that electrum mappings are same as BTC if not specified foreach (var network in _Networks.Values.OfType()) { @@ -77,7 +78,6 @@ namespace BTCPayServer } // Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586 - //InitPolis(); //InitBitcoinplus(); //InitUfo(); } diff --git a/BTCPayServer.Rating/Providers/PolisRateProvider.cs b/BTCPayServer.Rating/Providers/PolisRateProvider.cs new file mode 100644 index 000000000..b4f55e69b --- /dev/null +++ b/BTCPayServer.Rating/Providers/PolisRateProvider.cs @@ -0,0 +1,26 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using BTCPayServer.Rating; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Services.Rates +{ + public class PolisRateProvider : IRateProvider + { + private readonly HttpClient _httpClient; + public PolisRateProvider(HttpClient httpClient) + { + _httpClient = httpClient ?? new HttpClient(); + } + + public async Task GetRatesAsync(CancellationToken cancellationToken) + { + var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS + var jobj = await response.Content.ReadAsAsync(cancellationToken); + var value = jobj["data"].Value(); + return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) }; + } + } +} diff --git a/BTCPayServer.Rating/Services/RateProviderFactory.cs b/BTCPayServer.Rating/Services/RateProviderFactory.cs index e9464d81a..c9b2e31ed 100644 --- a/BTCPayServer.Rating/Services/RateProviderFactory.cs +++ b/BTCPayServer.Rating/Services/RateProviderFactory.cs @@ -79,6 +79,8 @@ namespace BTCPayServer.Services.Rates yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices"); yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates"); + yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis"); + yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0"); yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker"); yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products"); @@ -98,6 +100,7 @@ namespace BTCPayServer.Services.Rates Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS"))); Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK"))); Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY"))); + Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS"))); // Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index ef2b8327c..d4b7840f4 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -173,7 +173,7 @@ namespace BTCPayServer.Tests invoiceEntity.Payments.Add( new PaymentEntity() { - + Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m, @@ -315,7 +315,7 @@ namespace BTCPayServer.Tests entity.Payments.Add( new PaymentEntity() { - Output = new TxOut(Money.Coins(0.2m), new Key()), + Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true }); @@ -330,15 +330,15 @@ namespace BTCPayServer.Tests paymentMethods.Add( new PaymentMethod() { - CryptoCode = "BTC", - Rate = 1000, + CryptoCode = "BTC", + Rate = 1000, NextNetworkFee = Money.Coins(0.1m) }); paymentMethods.Add( new PaymentMethod() { - CryptoCode = "LTC", - Rate = 500, + CryptoCode = "LTC", + Rate = 500, NextNetworkFee = Money.Coins(0.01m) }); entity.SetPaymentMethods(paymentMethods); @@ -852,7 +852,7 @@ namespace BTCPayServer.Tests var walletId = new WalletId(acc.StoreId, "BTC"); acc.IsAdmin = true; walletController = acc.GetController(); - + var rescan = Assert.IsType(Assert.IsType(walletController.WalletRescan(walletId).Result).Model); Assert.True(rescan.Ok); Assert.True(rescan.IsFullySync); @@ -1821,7 +1821,7 @@ namespace BTCPayServer.Tests string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content); derivationVM = (DerivationSchemeViewModel)Assert.IsType(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model; - Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network + Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network // And with a good file? (upub) content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}"; @@ -2003,15 +2003,15 @@ donation: Assert.Equal(10.00m, orangeInvoice.Price); Assert.Equal("CAD", orangeInvoice.Currency); Assert.Equal("orange", orangeInvoice.ItemDesc); - - + + Assert.IsType(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result); invoices = user.BitPay.GetInvoices(); var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple")); Assert.NotNull(appleInvoice); Assert.Equal("good apple", appleInvoice.ItemDesc); - + // testing custom amount var action = Assert.IsType(publicApps.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result); @@ -2059,8 +2059,8 @@ donation: Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility); Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace); } - - + + //test inventory related features vmpos = Assert.IsType(Assert.IsType(apps.UpdatePointOfSale(appId).Result).Model); vmpos.Title = "hello"; @@ -2073,13 +2073,13 @@ inventoryitem: noninventoryitem: price: 10.0"; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); - + //inventoryitem has 1 item available Assert.IsType(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result); //we already bought all available stock so this should fail await Task.Delay(100); Assert.IsType(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result); - + //inventoryitem has unlimited items available Assert.IsType(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result); Assert.IsType(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result); @@ -2089,7 +2089,7 @@ noninventoryitem: Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem"))); var inventoryItemInvoice = Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem"))); Assert.NotNull(inventoryItemInvoice); - + //let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock var controller = tester.PayTester.GetController(user.UserId, user.StoreId); var appService = tester.PayTester.GetService(); @@ -2101,7 +2101,7 @@ noninventoryitem: vmpos = Assert.IsType(Assert.IsType(apps.UpdatePointOfSale(appId).Result).Model); Assert.Equal(1, appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory); }, 10000); - + } } @@ -2284,7 +2284,7 @@ noninventoryitem: var cashCow = tester.ExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); - // + // var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee; cashCow.SendToAddress(invoiceAddress, firstPayment); Thread.Sleep(1000); // prevent race conditions, ordering payments @@ -2722,7 +2722,7 @@ noninventoryitem: .Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value)) .ToList()) { - + Logs.Tester.LogInformation($"Testing {result.ExpectedName}"); result.Fetcher.InvalidateCache(); var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result); @@ -2735,6 +2735,11 @@ noninventoryitem: Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY } + else if (result.ExpectedName == "polispay") + { + Assert.Contains(exchangeRates.ByExchange[result.ExpectedName], + e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS + } else { // This check if the currency pair is using right currency pair @@ -2992,7 +2997,7 @@ noninventoryitem: Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings)); Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit); } - + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] @@ -3013,8 +3018,8 @@ noninventoryitem: var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")); Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count); - - + + invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC") { SupportedTransactionCurrencies = new Dictionary() @@ -3025,13 +3030,13 @@ noninventoryitem: }} } }); - + Assert.Single(invoice.SupportedTransactionCurrencies); } } - - + + [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded() @@ -3052,7 +3057,7 @@ noninventoryitem: })).ActionName); var manageController = user.GetController(); - + //by default no u2f devices available Assert.Empty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); var addRequest = Assert.IsType(Assert.IsType(manageController.AddU2FDevice("label")).Model); @@ -3081,10 +3086,10 @@ noninventoryitem: }; await context.U2FDevices.AddAsync(newDevice); await context.SaveChangesAsync(); - + Assert.NotNull(newDevice.Id); Assert.NotEmpty(Assert.IsType(Assert.IsType(await manageController.U2FAuthentication()).Model).Devices); - + } //check if we are showing the u2f login screen now @@ -3098,10 +3103,10 @@ noninventoryitem: var vm = Assert.IsType(secondLoginResult.Model); //2fa was never enabled for user so this should be empty Assert.Null(vm.LoginWith2FaViewModel); - Assert.NotNull(vm.LoginWithU2FViewModel); + Assert.NotNull(vm.LoginWithU2FViewModel); } } - + private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx) { var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString(); diff --git a/BTCPayServer/wwwroot/imlegacy/polis.png b/BTCPayServer/wwwroot/imlegacy/polis.png index 0ae0ca7c13cad30b20e3a1674199709b378b1732..a67f0ac8285f9b17c9623e4cfb6678904edc90a3 100644 GIT binary patch literal 10320 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 PKY!Zlx7EsS+JyWs`s_+i literal 10819 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 vqhjv1xE2~1DToV4