using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Data; using BTCPayServer.HostedServices; using BTCPayServer.Lightning; using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.WalletViewModels; using BTCPayServer.Payments; using BTCPayServer.Payments.Bitcoin; using BTCPayServer.Payments.Lightning; using BTCPayServer.Services.Apps; using BTCPayServer.Services.Invoices; using BTCPayServer.Tests.Logging; using Microsoft.AspNetCore.Mvc; using NBitcoin; using NBitcoin.Scripting.Parser; using NBitpayClient; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json.Linq; using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; namespace BTCPayServer.Tests { public class AltcoinTests { public const int TestTimeout = 60_000; public AltcoinTests(ITestOutputHelper helper) { Logs.Tester = new XUnitLog(helper) { Name = "Tests" }; Logs.LogProvider = new XUnitLogProvider(helper); } [Fact] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] [Trait("Lightning", "Lightning")] public async Task CanSetupWallet() { using (var tester = ServerTester.Create()) { tester.ActivateLTC(); tester.ActivateLightning(); await tester.StartAsync(); var user = tester.NewAccount(); var cryptoCode = "BTC"; await user.GrantAccessAsync(true); user.RegisterDerivationScheme(cryptoCode); user.RegisterDerivationScheme("LTC"); user.RegisterLightningNode(cryptoCode, LightningConnectionType.CLightning); var btcNetwork = tester.PayTester.Networks.GetNetwork(cryptoCode); var invoice = await user.BitPay.CreateInvoiceAsync( new Invoice { Price = 1.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); Assert.Equal(3, invoice.CryptoInfo.Length); var controller = user.GetController(); var lightningVm = (LightningNodeViewModel)Assert.IsType(await controller.SetupLightningNode(user.StoreId, cryptoCode)).Model; Assert.True(lightningVm.Enabled); var response = await controller.SetLightningNodeEnabled(user.StoreId, cryptoCode, false); Assert.IsType(response); // Get enabled state from overview action StoreViewModel storeModel; response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var lnNode = storeModel.LightningNodes.Find(node => node.CryptoCode == cryptoCode); Assert.NotNull(lnNode); Assert.False(lnNode.Enabled); WalletSetupViewModel setupVm; var storeId = user.StoreId; response = await controller.GenerateWallet(storeId, cryptoCode, WalletSetupMethod.GenerateOptions, new WalletSetupRequest()); Assert.IsType(response); // Get enabled state from overview action response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; var derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); Assert.True(derivationScheme.Enabled); // Disable wallet response = controller.SetWalletEnabled(storeId, cryptoCode, false).GetAwaiter().GetResult(); Assert.IsType(response); response = await controller.UpdateStore(); storeModel = (StoreViewModel)Assert.IsType(response).Model; derivationScheme = storeModel.DerivationSchemes.Find(scheme => scheme.Crypto == cryptoCode); Assert.NotNull(derivationScheme); Assert.False(derivationScheme.Enabled); var oldScheme = derivationScheme.Value; invoice = await user.BitPay.CreateInvoiceAsync( new Invoice { Price = 1.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); Assert.Single(invoice.CryptoInfo); Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); // Removing the derivation scheme, should redirect to store page response = controller.ConfirmDeleteWallet(user.StoreId, cryptoCode).GetAwaiter().GetResult(); Assert.IsType(response); // Setting it again should show the confirmation page response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, DerivationScheme = oldScheme }); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.True(setupVm.Confirmation); // The following part posts a wallet update, confirms it and checks the result // cobo vault file var content = "{\"ExtPubKey\":\"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\"MasterFingerprint\":\"7a7563b5\",\"DerivationPath\":\"M\\/84'\\/0'\\/0'\",\"CoboVaultFirmwareVersion\":\"1.2.0(BTC-Only)\"}"; response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("cobovault.json", content)}); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.True(setupVm.Confirmation); response = await controller.UpdateWallet(setupVm); Assert.IsType(response); response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode }); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.Equal("CoboVault", setupVm.Source); // wasabi wallet file content = "{\r\n \"EncryptedSecret\": \"6PYWBQ1zsukowsnTNA57UUx791aBuJusm7E4egXUmF5WGw3tcdG3cmTL57\",\r\n \"ChainCode\": \"waSIVbn8HaoovoQg/0t8IS1+ZCxGsJRGFT21i06nWnc=\",\r\n \"MasterFingerprint\": \"7a7563b5\",\r\n \"ExtPubKey\": \"xpub6CEqRFZ7yZxCFXuEWZBAdnC8bdvu9SRHevaoU2SsW9ZmKhrCShmbpGZWwaR15hdLURf8hg47g4TpPGaqEU8hw5LEJCE35AUhne67XNyFGBk\",\r\n \"PasswordVerified\": false,\r\n \"MinGapLimit\": 21,\r\n \"AccountKeyPath\": \"84'/0'/0'\",\r\n \"BlockchainState\": {\r\n \"Network\": \"RegTest\",\r\n \"Height\": \"0\"\r\n },\r\n \"HdPubKeys\": []\r\n}"; response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("wasabi.json", content)}); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.True(setupVm.Confirmation); response = await controller.UpdateWallet(setupVm); Assert.IsType(response); response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode }); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.Equal("WasabiFile", setupVm.Source); // Can we upload coldcard settings? (Should fail, we are giving a mainnet file to a testnet network) 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}"; response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-ypub.json", content)}); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.False(setupVm.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}"; response = await controller.UpdateWallet(new WalletSetupViewModel {StoreId = storeId, CryptoCode = cryptoCode, WalletFile = TestUtils.GetFormFile("coldcard-upub.json", content)}); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.True(setupVm.Confirmation); response = await controller.UpdateWallet(setupVm); Assert.IsType(response); response = await controller.ModifyWallet(new WalletSetupViewModel { StoreId = storeId, CryptoCode = cryptoCode }); setupVm = (WalletSetupViewModel)Assert.IsType(response).Model; Assert.Equal("ElectrumFile", setupVm.Source); // Now let's check that no data has been lost in the process var store = tester.PayTester.StoreRepository.FindStore(storeId).GetAwaiter().GetResult(); var onchainBTC = store.GetSupportedPaymentMethods(tester.PayTester.Networks) #pragma warning disable CS0618 // Type or member is obsolete .OfType().First(o => o.PaymentId.IsBTCOnChain); #pragma warning restore CS0618 // Type or member is obsolete DerivationSchemeSettings.TryParseFromWalletFile(content, onchainBTC.Network, out var expected); Assert.Equal(expected.ToJson(), onchainBTC.ToJson()); // Let's check that the root hdkey and account key path are taken into account when making a PSBT invoice = await user.BitPay.CreateInvoiceAsync( new Invoice { Price = 1.5m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); tester.ExplorerNode.Generate(1); var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo.First(c => c.CryptoCode == cryptoCode).Address, tester.ExplorerNode.Network); tester.ExplorerNode.SendToAddress(invoiceAddress, Money.Coins(1m)); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.Equal("paid", invoice.Status); }); var wallet = tester.PayTester.GetController(); var psbt = wallet.CreatePSBT(btcNetwork, onchainBTC, new WalletSendModel() { Outputs = new List { new WalletSendModel.TransactionOutput { Amount = 0.5m, DestinationAddress = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, btcNetwork.NBitcoinNetwork) .ToString(), } }, FeeSatoshiPerByte = 1 }, default).GetAwaiter().GetResult(); Assert.NotNull(psbt); var root = new Mnemonic( "usage fever hen zero slide mammal silent heavy donate budget pulse say brain thank sausage brand craft about save attract muffin advance illegal cabbage") .DeriveExtKey().AsHDKeyCache(); var account = root.Derive(new KeyPath("m/49'/0'/0'")); Assert.All(psbt.PSBT.Inputs, input => { var keyPath = input.HDKeyPaths.Single(); Assert.False(keyPath.Value.KeyPath.IsHardened); Assert.Equal(account.Derive(keyPath.Value.KeyPath).GetPublicKey(), keyPath.Key); Assert.Equal(keyPath.Value.MasterFingerprint, onchainBTC.AccountKeySettings[0].AccountKey.GetPublicKey().GetHDFingerPrint()); }); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] [Trait("Lightning", "Lightning")] public async Task CanCreateInvoiceWithSpecificPaymentMethods() { using (var tester = ServerTester.Create()) { tester.ActivateLightning(); tester.ActivateLTC(); await tester.StartAsync(); await tester.EnsureChannelsSetup(); var user = tester.NewAccount(); user.GrantAccess(true); user.RegisterLightningNode("BTC", LightningConnectionType.Charge); user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("LTC"); 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() { {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} } }); Assert.Single(invoice.SupportedTransactionCurrencies); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] public async Task CanHaveLTCOnlyStore() { using (var tester = ServerTester.Create()) { tester.ActivateLTC(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("LTC"); // First we try payment with a merchant having only BTC var invoice = user.BitPay.CreateInvoice( new Invoice() { Price = 500, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); Assert.Single(invoice.CryptoInfo); Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode); Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); var cashCow = tester.LTCExplorerNode; var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network); var firstPayment = Money.Coins(0.1m); cashCow.SendToAddress(invoiceAddress, firstPayment); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid); }); Assert.Single(invoice.CryptoInfo); // Only BTC should be presented var controller = tester.PayTester.GetController(null); var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id) .GetAwaiter().GetResult()).Value; Assert.Single(checkout.AvailableCryptos); Assert.Equal("LTC", checkout.CryptoCode); ////////////////////// // Despite it is called BitcoinAddress it should be LTC because BTC is not available Assert.Null(invoice.BitcoinAddress); Assert.NotEqual(1.0m, invoice.Rate); Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.Equal("paid", invoice.Status); checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id) .GetAwaiter().GetResult()).Value; Assert.Equal("paid", checkout.Status); }); } } [Fact] [Trait("Selenium", "Selenium")] [Trait("Altcoins", "Altcoins")] public async Task CanCreateRefunds() { using (var s = SeleniumTester.Create()) { s.Server.ActivateLTC(); await s.StartAsync(); var user = s.Server.NewAccount(); await user.GrantAccessAsync(); s.GoToLogin(); s.Login(user.RegisterDetails.Email, user.RegisterDetails.Password); user.RegisterDerivationScheme("BTC"); await s.Server.ExplorerNode.GenerateAsync(1); foreach (var multiCurrency in new[] { false, true }) { if (multiCurrency) user.RegisterDerivationScheme("LTC"); foreach (var rateSelection in new[] { "FiatText", "CurrentRateText", "RateThenText" }) await CanCreateRefundsCore(s, user, multiCurrency, rateSelection); } } } private static async Task CanCreateRefundsCore(SeleniumTester s, TestAccount user, bool multiCurrency, string rateSelection) { s.GoToHome(); s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m, 5100.0m)); var invoice = await user.BitPay.CreateInvoiceAsync(new NBitpayClient.Invoice() { Currency = "USD", Price = 5000.0m }); var info = invoice.CryptoInfo.First(o => o.CryptoCode == "BTC"); var totalDue = decimal.Parse(info.TotalDue, CultureInfo.InvariantCulture); var paid = totalDue + 0.1m; await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(info.Address, Network.RegTest), Money.Coins(paid)); await s.Server.ExplorerNode.GenerateAsync(1); await TestUtils.EventuallyAsync(async () => { invoice = await user.BitPay.GetInvoiceAsync(invoice.Id); Assert.Equal("confirmed", invoice.Status); }); // BTC crash by 50% s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m / 2.0m, 5100.0m / 2.0m)); s.GoToInvoice(invoice.Id); s.Driver.FindElement(By.Id("refundlink")).Click(); if (multiCurrency) { s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter); s.Driver.FindElement(By.Id("ok")).Click(); } Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate s.Driver.FindElement(By.Id(rateSelection)).Click(); s.Driver.FindElement(By.Id("ok")).Click(); Assert.Contains("pull-payments", s.Driver.Url); if (rateSelection == "FiatText") Assert.Contains("$5,500.00", s.Driver.PageSource); if (rateSelection == "CurrentRateText") Assert.Contains("2.20000000 ₿", s.Driver.PageSource); if (rateSelection == "RateThenText") Assert.Contains("1.10000000 ₿", s.Driver.PageSource); s.GoToHome(); s.GoToInvoices(); s.GoToInvoice(invoice.Id); s.Driver.FindElement(By.Id("refundlink")).Click(); Assert.Contains("pull-payments", s.Driver.Url); } [Fact(Timeout = TestTimeout)] [Trait("Altcoins", "Altcoins")] [Trait("Lightning", "Lightning")] public async Task CanUsePaymentMethodDropdown() { using (var s = SeleniumTester.Create()) { s.Server.ActivateLTC(); s.Server.ActivateLightning(); await s.StartAsync(); s.GoToRegister(); s.RegisterNewUser(); var store = s.CreateNewStore(); s.AddDerivationScheme("BTC"); //check that there is no dropdown since only one payment method is set var invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); s.Driver.FindElement(By.ClassName("payment__currencies_noborder")); s.GoToHome(); s.GoToStore(store.storeId); s.AddDerivationScheme("LTC"); s.AddLightningNode("BTC", LightningConnectionType.CLightning); //there should be three now invoiceId = s.CreateInvoice(store.storeName, 10, "USD", "a@g.com"); s.GoToInvoiceCheckout(invoiceId); var currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("BTC", currencyDropdownButton.Text); currencyDropdownButton.Click(); var elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); Assert.Equal(3, elements.Count); elements.Single(element => element.Text.Contains("LTC")).Click(); currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("LTC", currencyDropdownButton.Text); currencyDropdownButton.Click(); elements = s.Driver.FindElement(By.ClassName("vex-content")).FindElements(By.ClassName("vexmenuitem")); elements.Single(element => element.Text.Contains("Lightning")).Click(); currencyDropdownButton = s.Driver.FindElement(By.ClassName("payment__currencies")); Assert.Contains("Lightning", currencyDropdownButton.Text); s.Driver.Quit(); } } [Fact(Timeout = TestTimeout)] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] public async Task CanPayWithTwoCurrencies() { using (var tester = ServerTester.Create()) { tester.ActivateLTC(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); // First we try payment with a merchant having only BTC var invoice = user.BitPay.CreateInvoice( new Invoice() { Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); var cashCow = tester.ExplorerNode; cashCow.Generate(2); // get some money in case var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); var firstPayment = Money.Coins(0.04m); cashCow.SendToAddress(invoiceAddress, firstPayment); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.True(invoice.BtcPaid == firstPayment); }); Assert.Single(invoice.CryptoInfo); // Only BTC should be presented var controller = tester.PayTester.GetController(null); var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null) .GetAwaiter().GetResult()).Value; Assert.Single(checkout.AvailableCryptos); Assert.Equal("BTC", checkout.CryptoCode); Assert.Single(invoice.PaymentCodes); Assert.Single(invoice.SupportedTransactionCurrencies); Assert.Single(invoice.SupportedTransactionCurrencies); Assert.Single(invoice.PaymentSubtotals); Assert.Single(invoice.PaymentTotals); Assert.True(invoice.PaymentCodes.ContainsKey("BTC")); Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("BTC")); Assert.True(invoice.SupportedTransactionCurrencies["BTC"].Enabled); Assert.True(invoice.PaymentSubtotals.ContainsKey("BTC")); Assert.True(invoice.PaymentTotals.ContainsKey("BTC")); ////////////////////// // Retry now with LTC enabled user.RegisterDerivationScheme("LTC"); invoice = user.BitPay.CreateInvoice( new Invoice() { Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true }, Facade.Merchant); cashCow = tester.ExplorerNode; invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network); firstPayment = Money.Coins(0.04m); cashCow.SendToAddress(invoiceAddress, firstPayment); Logs.Tester.LogInformation("First payment sent to " + invoiceAddress); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.True(invoice.BtcPaid == firstPayment); }); cashCow = tester.LTCExplorerNode; var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC"); Assert.NotNull(ltcCryptoInfo); invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network); var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture)); cashCow.Generate(4); // LTC is not worth a lot, so just to make sure we have money... cashCow.SendToAddress(invoiceAddress, secondPayment); Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress); TestUtils.Eventually(() => { invoice = user.BitPay.GetInvoice(invoice.Id); Assert.Equal(Money.Zero, invoice.BtcDue); var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC"); Assert.Equal(Money.Zero, ltcPaid.Due); Assert.Equal(secondPayment, ltcPaid.CryptoPaid); Assert.Equal("paid", invoice.Status); Assert.False((bool)((JValue)invoice.ExceptionStatus).Value); }); controller = tester.PayTester.GetController(null); checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC") .GetAwaiter().GetResult()).Value; Assert.Equal(2, checkout.AvailableCryptos.Count); Assert.Equal("LTC", checkout.CryptoCode); Assert.Equal(2, invoice.PaymentCodes.Count()); Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count()); Assert.Equal(2, invoice.PaymentSubtotals.Count()); Assert.Equal(2, invoice.PaymentTotals.Count()); Assert.True(invoice.PaymentCodes.ContainsKey("LTC")); Assert.True(invoice.SupportedTransactionCurrencies.ContainsKey("LTC")); Assert.True(invoice.SupportedTransactionCurrencies["LTC"].Enabled); Assert.True(invoice.PaymentSubtotals.ContainsKey("LTC")); Assert.True(invoice.PaymentTotals.ContainsKey("LTC")); // Check if we can disable LTC invoice = user.BitPay.CreateInvoice( new Invoice() { Price = 5000.0m, Currency = "USD", PosData = "posData", OrderId = "orderId", ItemDesc = "Some description", FullNotifications = true, SupportedTransactionCurrencies = new Dictionary() { {"BTC", new InvoiceSupportedTransactionCurrency() {Enabled = true}} } }, Facade.Merchant); Assert.Single(invoice.CryptoInfo.Where(c => c.CryptoCode == "BTC")); Assert.Empty(invoice.CryptoInfo.Where(c => c.CryptoCode == "LTC")); } } [Fact] [Trait("Integration", "Integration")] [Trait("Altcoins", "Altcoins")] public async Task CanUsePoSApp() { using (var tester = ServerTester.Create()) { tester.ActivateLTC(); await tester.StartAsync(); var user = tester.NewAccount(); user.GrantAccess(); user.RegisterDerivationScheme("BTC"); user.RegisterDerivationScheme("LTC"); var apps = user.GetController(); var vm = Assert.IsType(Assert.IsType(apps.CreateApp().Result).Model); vm.Name = "test"; vm.SelectedAppType = AppType.PointOfSale.ToString(); Assert.IsType(apps.CreateApp(vm).Result); var appId = Assert.IsType(Assert.IsType(apps.ListApps().Result).Model) .Apps[0].Id; var vmpos = Assert.IsType(Assert .IsType(apps.UpdatePointOfSale(appId).Result).Model); vmpos.Title = "hello"; vmpos.Currency = "CAD"; vmpos.ButtonText = "{0} Purchase"; vmpos.CustomButtonText = "Nicolas Sexy Hair"; vmpos.CustomTipText = "Wanna tip?"; vmpos.CustomTipPercentages = "15,18,20"; vmpos.Template = @" apple: price: 5.0 title: good apple orange: price: 10.0 donation: price: 1.02 custom: true "; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); vmpos = Assert.IsType(Assert .IsType(apps.UpdatePointOfSale(appId).Result).Model); Assert.Equal("hello", vmpos.Title); var publicApps = user.GetController(); var vmview = Assert.IsType(Assert .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); Assert.Equal("hello", vmview.Title); Assert.Equal(3, vmview.Items.Length); Assert.Equal("good apple", vmview.Items[0].Title); Assert.Equal("orange", vmview.Items[1].Title); Assert.Equal(10.0m, vmview.Items[1].Price.Value); Assert.Equal("$5.00", vmview.Items[0].Price.Formatted); Assert.Equal("{0} Purchase", vmview.ButtonText); Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText); Assert.Equal("Wanna tip?", vmview.CustomTipText); Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages)); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 0, null, null, null, null, "orange").Result); // var invoices = user.BitPay.GetInvoices(); var orangeInvoice = invoices.First(); Assert.Equal(10.00m, orangeInvoice.Price); Assert.Equal("CAD", orangeInvoice.Currency); Assert.Equal("orange", orangeInvoice.ItemDesc); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 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, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result); Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName); invoices = user.BitPay.GetInvoices(); var donationInvoice = invoices.Single(i => i.Price == 6.6m); Assert.NotNull(donationInvoice); Assert.Equal("CAD", donationInvoice.Currency); Assert.Equal("donation", donationInvoice.ItemDesc); foreach (var test in new[] { (Code: "EUR", ExpectedSymbol: "€", ExpectedDecimalSeparator: ",", ExpectedDivisibility: 2, ExpectedThousandSeparator: "\xa0", ExpectedPrefixed: false, ExpectedSymbolSpace: true), (Code: "INR", ExpectedSymbol: "₹", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 2, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true), (Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false), (Code: "BTC", ExpectedSymbol: "₿", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8, ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true), }) { Logs.Tester.LogInformation($"Testing for {test.Code}"); vmpos = Assert.IsType(Assert .IsType(apps.UpdatePointOfSale(appId).Result).Model); vmpos.Title = "hello"; vmpos.Currency = test.Item1; vmpos.ButtonText = "{0} Purchase"; vmpos.CustomButtonText = "Nicolas Sexy Hair"; vmpos.CustomTipText = "Wanna tip?"; vmpos.Template = @" apple: price: 1000.0 title: good apple orange: price: 10.0 donation: price: 1.02 custom: true "; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); publicApps = user.GetController(); vmview = Assert.IsType(Assert .IsType(publicApps.ViewPointOfSale(appId, PosViewType.Cart).Result).Model); Assert.Equal(test.Code, vmview.CurrencyCode); Assert.Equal(test.ExpectedSymbol, vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); Assert.Equal(test.ExpectedSymbol, vmview.CurrencyInfo.CurrencySymbol .Replace("¥", "¥")); // Hack so JPY test pass on linux as well); Assert.Equal(test.ExpectedDecimalSeparator, vmview.CurrencyInfo.DecimalSeparator); Assert.Equal(test.ExpectedThousandSeparator, vmview.CurrencyInfo.ThousandSeparator); Assert.Equal(test.ExpectedPrefixed, vmview.CurrencyInfo.Prefixed); 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"; vmpos.Currency = "BTC"; vmpos.Template = @" inventoryitem: price: 1.0 title: good apple inventory: 1 noninventoryitem: price: 10.0"; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); //inventoryitem has 1 item available await tester.WaitForEvent(() => { Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); return Task.CompletedTask; }); //we already bought all available stock so this should fail await Task.Delay(100); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result); //inventoryitem has unlimited items available Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result); //verify invoices where created invoices = user.BitPay.GetInvoices(); 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(); var eventAggregator = tester.PayTester.GetService(); Assert.IsType(await controller.ChangeInvoiceState(inventoryItemInvoice.Id, "invalid")); //check that item is back in stock TestUtils.Eventually(() => { 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); //test payment methods option vmpos = Assert.IsType(Assert .IsType(apps.UpdatePointOfSale(appId).Result).Model); vmpos.Title = "hello"; vmpos.Currency = "BTC"; vmpos.Template = @" btconly: price: 1.0 title: good apple payment_methods: - BTC normal: price: 1.0"; Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "btconly").Result); Assert.IsType(publicApps .ViewPointOfSale(appId, PosViewType.Cart, 1, null, null, null, null, "normal").Result); invoices = user.BitPay.GetInvoices(); var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal"); var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly"); Assert.Single(btcOnlyInvoice.CryptoInfo); Assert.Equal("BTC", btcOnlyInvoice.CryptoInfo.First().CryptoCode); Assert.Equal(PaymentTypes.BTCLike.ToString(), btcOnlyInvoice.CryptoInfo.First().PaymentType); Assert.Equal(2, normalInvoice.CryptoInfo.Length); Assert.Contains( normalInvoice.CryptoInfo, s => PaymentTypes.BTCLike.ToString() == s.PaymentType && new[] { "BTC", "LTC" }.Contains( s.CryptoCode)); } } [Fact] [Trait("Fast", "Fast")] [Trait("Altcoins", "Altcoins")] public void CanCalculateCryptoDue2() { #pragma warning disable CS0618 var dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.RegTest).ToString(); var networkProvider = new BTCPayNetworkProvider(ChainName.Regtest); var paymentMethodHandlerDictionary = new PaymentMethodHandlerDictionary(new IPaymentMethodHandler[] { new BitcoinLikePaymentHandler(null, networkProvider, null, null, null), new LightningLikePaymentHandler(null, null, networkProvider, null, null, null), }); var networkBTC = networkProvider.GetNetwork("BTC"); var networkLTC = networkProvider.GetNetwork("LTC"); InvoiceEntity invoiceEntity = new InvoiceEntity(); invoiceEntity.Networks = networkProvider; invoiceEntity.Payments = new System.Collections.Generic.List(); invoiceEntity.Price = 100; PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary(); paymentMethods.Add(new PaymentMethod() { Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m, } .SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy })); paymentMethods.Add(new PaymentMethod() { Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m } .SetPaymentMethodDetails( new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod() { NextNetworkFee = Money.Coins(0.00010000m), DepositAddress = dummy })); invoiceEntity.SetPaymentMethods(paymentMethods); var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike)); var accounting = btc.Calculate(); invoiceEntity.Payments.Add( new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m, Network = networkProvider.GetNetwork("BTC"), } .SetCryptoPaymentData(new BitcoinLikePaymentData() { Network = networkProvider.GetNetwork("BTC"), Output = new TxOut() { Value = Money.Coins(0.00151263m) } })); accounting = btc.Calculate(); invoiceEntity.Payments.Add( new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m, Network = networkProvider.GetNetwork("BTC") } .SetCryptoPaymentData(new BitcoinLikePaymentData() { Network = networkProvider.GetNetwork("BTC"), Output = new TxOut() { Value = accounting.Due } })); accounting = btc.Calculate(); Assert.Equal(Money.Zero, accounting.Due); Assert.Equal(Money.Zero, accounting.DueUncapped); var ltc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike)); accounting = ltc.Calculate(); Assert.Equal(Money.Zero, accounting.Due); // LTC might have over paid due to BTC paying above what it should (round 1 satoshi up) Assert.True(accounting.DueUncapped < Money.Zero); var paymentMethod = InvoiceWatcher.GetNearestClearedPayment(paymentMethods, out var accounting2); Assert.Equal(btc.CryptoCode, paymentMethod.CryptoCode); #pragma warning restore CS0618 } [Fact] [Trait("Fast", "Fast")] [Trait("Altcoins", "Altcoins")] public void CanParseDerivationScheme() { var testnetNetworkProvider = new BTCPayNetworkProvider(ChainName.Testnet); var regtestNetworkProvider = new BTCPayNetworkProvider(ChainName.Regtest); var mainnetNetworkProvider = new BTCPayNetworkProvider(ChainName.Mainnet); var testnetParser = new DerivationSchemeParser(testnetNetworkProvider.GetNetwork("BTC")); var mainnetParser = new DerivationSchemeParser(mainnetNetworkProvider.GetNetwork("BTC")); NBXplorer.DerivationStrategy.DerivationStrategyBase result; // Passing electrum stuff // Passing a native segwit from mainnet to a testnet parser, means the testnet parser will try to convert it into segwit result = testnetParser.Parse( "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); Assert.Equal( "tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString()); result = mainnetParser.Parse( "zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t"); Assert.Equal( "xpub68fZn8w5ZTP5X4zymr1B1vKsMtJUiudtN2DZHQzJJc87gW1tXh7S4SALCsQijUzXstg2reVyuZYFuPnTDKXNiNgDZNpNiC4BrVzaaGEaRHj", result.ToString()); // P2SH result = testnetParser.Parse( "upub57Wa4MvRPNyAipy1MCpERxcFpHR2ZatyikppkyeWkoRL6QJvLVMo39jYdcaJVxyvBURyRVmErBEA5oGicKBgk1j72GAXSPFH5tUDoGZ8nEu"); Assert.Equal( "tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString()); result = mainnetParser.Parse( "ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5"); Assert.Equal( "xpub661MyMwAqRbcGiYMrHB74DtmLBrNPSsUU6PV7ALAtpYyFhkc6TrUuLhxhET4VgwgQPnPfvYvEAHojf7QmQRj8imudHFoC7hju4f9xxri8wR-[p2sh]", result.ToString()); // if prefix not recognize, assume it is segwit result = testnetParser.Parse( "xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X"); Assert.Equal( "tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu", result.ToString()); //////////////// var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o"; result = testnetParser.Parse(tpub); Assert.Equal(tpub, result.ToString()); var regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("BTC")); var parsed = regtestParser.Parse( "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]", parsed.ToString()); // Let's make sure we can't generate segwit with dogecoin regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); parsed = regtestParser.Parse( "xpub6DG1rMYXiQtCc6CfdLFD9CtxqhzzRh7j6Sq6EdE9abgYy3cfDRrniLLv2AdwqHL1exiLnnKR5XXcaoiiexf3Y9R6J6rxkJtqJHzNzMW9QMZ-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); regtestParser = new DerivationSchemeParser(regtestNetworkProvider.GetNetwork("DOGE")); parsed = regtestParser.Parse( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[p2sh]"); Assert.Equal( "tpubDDdeNbNDRgqestPX5XEJM8ELAq6eR5cne5RPbBHHvWSSiLHNHehsrn1kGCijMnHFSsFFQMqHcdMfGzDL3pWHRasPMhcGRqZ4tFankQ3i4ok-[legacy]", parsed.ToString()); //let's test output descriptor parsing support //we don't support every descriptor, only the ones which represent an HD wallet with stndard derivation paths Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")); Assert.Throws(() => mainnetParser.ParseOutputDescriptor("sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")); //let's see what we actually support now //standard legacy hd wallet var parsedDescriptor = mainnetParser.ParseOutputDescriptor( "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); Assert.Equal(KeyPath.Parse("44'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath); Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() ); //masterfingerprint and key path are optional parsedDescriptor = mainnetParser.ParseOutputDescriptor( "pkh([d34db33f]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() ); //a master fingerprint must always be present if youre providing rooted path Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh([44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); parsedDescriptor = mainnetParser.ParseOutputDescriptor( "pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() ); //but a different deriv path from standard (0/*) is not supported Assert.Throws(() => mainnetParser.ParseOutputDescriptor("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")); //p2sh-segwit hd wallet parsedDescriptor = mainnetParser.ParseOutputDescriptor( "sh(wpkh([d34db33f/49'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))"); Assert.Equal(KeyPath.Parse("49'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath); Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[p2sh]",parsedDescriptor.Item1.ToString() ); //segwit hd wallet parsedDescriptor = mainnetParser.ParseOutputDescriptor( "wpkh([d34db33f/84'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)"); Assert.Equal(KeyPath.Parse("84'/0'/0'"),Assert.Single(parsedDescriptor.Item2).KeyPath); Assert.Equal( HDFingerprint.Parse("d34db33f"),Assert.Single(parsedDescriptor.Item2).MasterFingerprint); Assert.Equal("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL",parsedDescriptor.Item1.ToString() ); //multisig tests //legacy parsedDescriptor = mainnetParser.ParseOutputDescriptor( "sh(multi(1,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/45'/0]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))"); Assert.Equal(2, parsedDescriptor.Item2.Length); var strat = Assert.IsType(Assert.IsType(parsedDescriptor.Item1).Inner); Assert.True(strat.IsLegacy); Assert.Equal(1,strat.RequiredSignatures); Assert.Equal(2,strat.Keys.Count()); Assert.False(strat.LexicographicOrder); Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]-[keeporder]",parsedDescriptor.Item1.ToString() ); //segwit parsedDescriptor = mainnetParser.ParseOutputDescriptor( "wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))"); Assert.Equal(2, parsedDescriptor.Item2.Length); strat = Assert.IsType(Assert.IsType(parsedDescriptor.Item1).Inner); Assert.False(strat.IsLegacy); Assert.Equal(1,strat.RequiredSignatures); Assert.Equal(2,strat.Keys.Count()); Assert.False(strat.LexicographicOrder); Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]",parsedDescriptor.Item1.ToString() ); //segwit-p2sh parsedDescriptor = mainnetParser.ParseOutputDescriptor( "sh(wsh(multi(1,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/2']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*)))"); Assert.Equal(2, parsedDescriptor.Item2.Length); strat = Assert.IsType(Assert.IsType(Assert.IsType(parsedDescriptor.Item1).Inner).Inner); Assert.False(strat.IsLegacy); Assert.Equal(1,strat.RequiredSignatures); Assert.Equal(2,strat.Keys.Count()); Assert.False(strat.LexicographicOrder); Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[keeporder]-[p2sh]",parsedDescriptor.Item1.ToString() ); //sorted parsedDescriptor = mainnetParser.ParseOutputDescriptor( "sh(sortedmulti(1,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*,[d34db33f/48'/0'/0'/1']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/0/*))"); Assert.Equal("1-of-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL-[legacy]",parsedDescriptor.Item1.ToString() ); } } }