mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-03 17:36:59 +01:00
Lightning Network support implementation
This commit is contained in:
parent
3d33ecf397
commit
c8923af573
40 changed files with 2580 additions and 408 deletions
|
@ -34,5 +34,6 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
return GetNodeInfoAsync().GetAwaiter().GetResult();
|
return GetNodeInfoAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,7 +65,7 @@ namespace BTCPayServer.Tests
|
||||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||||
PayTester.Start();
|
PayTester.Start();
|
||||||
|
|
||||||
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
var btc = NetworkProvider.GetNetwork("BTC").NBitcoinNetwork;
|
||||||
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
CustomerEclair = new EclairTester(this, "TEST_ECLAIR", "http://eclair-cli:gpwefwmmewci@127.0.0.1:30992/", "eclair", btc);
|
||||||
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
|
MerchantCharge = new ChargeTester(this, "TEST_CHARGE", "http://api-token:foiewnccewuify@127.0.0.1:54938/", "lightning-charged", btc);
|
||||||
}
|
}
|
||||||
|
@ -89,13 +89,35 @@ namespace BTCPayServer.Tests
|
||||||
var channels = CustomerEclair.RPC.ChannelsAsync();
|
var channels = CustomerEclair.RPC.ChannelsAsync();
|
||||||
|
|
||||||
var info = await merchantInfo;
|
var info = await merchantInfo;
|
||||||
var connect = CustomerEclair.RPC.ConnectAsync(new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port));
|
var clightning = new NodeInfo(info.Id, MerchantCharge.P2PHost, info.Port);
|
||||||
|
var connect = CustomerEclair.RPC.ConnectAsync(clightning);
|
||||||
await Task.WhenAll(blockCount, customer, channels, connect);
|
await Task.WhenAll(blockCount, customer, channels, connect);
|
||||||
// Mine until segwit is activated
|
// Mine until segwit is activated
|
||||||
if (blockCount.Result <= 432)
|
if (blockCount.Result <= 432)
|
||||||
{
|
{
|
||||||
ExplorerNode.Generate(433 - blockCount.Result);
|
ExplorerNode.Generate(433 - blockCount.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channels.Result.Length == 0)
|
||||||
|
{
|
||||||
|
await CustomerEclair.RPC.OpenAsync(clightning, Money.Satoshis(16777215));
|
||||||
|
while ((await CustomerEclair.RPC.ChannelsAsync())[0].State != "NORMAL")
|
||||||
|
{
|
||||||
|
ExplorerNode.Generate(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendLightningPayment(Invoice invoice)
|
||||||
|
{
|
||||||
|
SendLightningPaymentAsync(invoice).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendLightningPaymentAsync(Invoice invoice)
|
||||||
|
{
|
||||||
|
var bolt11 = invoice.CryptoInfo.Where(o => o.PaymentUrls.BOLT11 != null).First().PaymentUrls.BOLT11;
|
||||||
|
bolt11 = bolt11.Replace("lightning:", "", StringComparison.OrdinalIgnoreCase);
|
||||||
|
await CustomerEclair.RPC.SendAsync(bolt11);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EclairTester MerchantEclair { get; set; }
|
public EclairTester MerchantEclair { get; set; }
|
||||||
|
|
|
@ -81,7 +81,7 @@ namespace BTCPayServer.Tests
|
||||||
DerivationSchemeFormat = "BTCPay",
|
DerivationSchemeFormat = "BTCPay",
|
||||||
DerivationScheme = DerivationScheme.ToString(),
|
DerivationScheme = DerivationScheme.ToString(),
|
||||||
Confirmation = true
|
Confirmation = true
|
||||||
}, "Save");
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||||
|
@ -111,5 +111,20 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RegisterLightningNode(string cryptoCode)
|
||||||
|
{
|
||||||
|
RegisterLightningNodeAsync(cryptoCode).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RegisterLightningNodeAsync(string cryptoCode)
|
||||||
|
{
|
||||||
|
var storeController = parent.PayTester.GetController<StoresController>(UserId);
|
||||||
|
await storeController.AddLightningNode(StoreId, new LightningNodeViewModel()
|
||||||
|
{
|
||||||
|
CryptoCurrency = "BTC",
|
||||||
|
Url = parent.MerchantCharge.Client.Uri.AbsoluteUri
|
||||||
|
}, "save");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ using System.Globalization;
|
||||||
using BTCPayServer.Payments;
|
using BTCPayServer.Payments;
|
||||||
using BTCPayServer.Payments.Bitcoin;
|
using BTCPayServer.Payments.Bitcoin;
|
||||||
using BTCPayServer.HostedServices;
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
|
||||||
namespace BTCPayServer.Tests
|
namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
|
@ -181,7 +182,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxCount);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
|
@ -199,7 +200,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||||
Assert.Equal(2, accounting.TxCount);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
|
@ -207,7 +208,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.01m), accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxCount);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
|
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
|
||||||
|
@ -219,7 +220,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue);
|
||||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||||
Assert.Equal(2, accounting.TxCount);
|
Assert.Equal(2, accounting.TxRequired);
|
||||||
|
|
||||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||||
accounting = paymentMethod.Calculate();
|
accounting = paymentMethod.Calculate();
|
||||||
|
@ -228,7 +229,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||||
// Paying 2 BTC fee, LTC fee removed because fully paid
|
// Paying 2 BTC fee, LTC fee removed because fully paid
|
||||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
|
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 + 0.1m * 2 /* + 0.01m no need to pay this fee anymore */), accounting.TotalDue);
|
||||||
Assert.Equal(1, accounting.TxCount);
|
Assert.Equal(1, accounting.TxRequired);
|
||||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
}
|
}
|
||||||
|
@ -287,6 +288,41 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
var light = LightMoney.MilliSatoshis(1);
|
var light = LightMoney.MilliSatoshis(1);
|
||||||
Assert.Equal("0.00000000001", light.ToString());
|
Assert.Equal("0.00000000001", light.ToString());
|
||||||
|
|
||||||
|
light = LightMoney.MilliSatoshis(200000);
|
||||||
|
Assert.Equal(200m, light.ToDecimal(LightMoneyUnit.Satoshi));
|
||||||
|
Assert.Equal(0.00000001m * 200m, light.ToDecimal(LightMoneyUnit.BTC));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSetLightningServer()
|
||||||
|
{
|
||||||
|
using (var tester = ServerTester.Create())
|
||||||
|
{
|
||||||
|
tester.Start();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||||
|
Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult());
|
||||||
|
Assert.IsType<ViewResult>(storeController.AddLightningNode(user.StoreId).GetAwaiter().GetResult());
|
||||||
|
|
||||||
|
var testResult = storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||||
|
{
|
||||||
|
CryptoCurrency = "BTC",
|
||||||
|
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||||
|
}, "test").GetAwaiter().GetResult();
|
||||||
|
Assert.DoesNotContain("Error", ((LightningNodeViewModel)Assert.IsType<ViewResult>(testResult).Model).StatusMessage, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.True(storeController.ModelState.IsValid);
|
||||||
|
|
||||||
|
Assert.IsType<RedirectToActionResult>(storeController.AddLightningNode(user.StoreId, new LightningNodeViewModel()
|
||||||
|
{
|
||||||
|
CryptoCurrency = "BTC",
|
||||||
|
Url = tester.MerchantCharge.Client.Uri.AbsoluteUri
|
||||||
|
}, "save").GetAwaiter().GetResult());
|
||||||
|
|
||||||
|
var storeVm = Assert.IsType<Models.StoreViewModels.StoreViewModel>(Assert.IsType<ViewResult>(storeController.UpdateStore(user.StoreId).GetAwaiter().GetResult()).Model);
|
||||||
|
Assert.Single(storeVm.LightningNodes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -297,7 +333,29 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
tester.Start();
|
tester.Start();
|
||||||
tester.PrepareLightning();
|
tester.PrepareLightning();
|
||||||
|
var user = tester.NewAccount();
|
||||||
|
user.GrantAccess();
|
||||||
|
user.RegisterLightningNode("BTC");
|
||||||
|
user.RegisterDerivationScheme("BTC");
|
||||||
|
|
||||||
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
|
{
|
||||||
|
Price = 0.01,
|
||||||
|
Currency = "USD",
|
||||||
|
PosData = "posData",
|
||||||
|
OrderId = "orderId",
|
||||||
|
ItemDesc = "Some description"
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
tester.SendLightningPayment(invoice);
|
||||||
|
|
||||||
|
Eventually(() =>
|
||||||
|
{
|
||||||
|
var localInvoice = user.BitPay.GetInvoice(invoice.Id);
|
||||||
|
Assert.Equal("complete", localInvoice.Status);
|
||||||
|
Assert.Equal("False", localInvoice.ExceptionStatus.ToString());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -674,7 +732,7 @@ namespace BTCPayServer.Tests
|
||||||
}, Facade.Merchant);
|
}, Facade.Merchant);
|
||||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||||
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
|
||||||
|
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
|
||||||
Eventually(() =>
|
Eventually(() =>
|
||||||
{
|
{
|
||||||
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||||
|
@ -730,6 +788,7 @@ namespace BTCPayServer.Tests
|
||||||
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
Assert.Equal(firstPayment, localInvoice.BtcPaid);
|
||||||
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
txFee = localInvoice.BtcDue - invoice.BtcDue;
|
||||||
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
Assert.Equal("paidPartial", localInvoice.ExceptionStatus.ToString());
|
||||||
|
Assert.Equal(1, localInvoice.CryptoInfo[0].TxCount);
|
||||||
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
Assert.NotEqual(localInvoice.BitcoinAddress, invoice.BitcoinAddress); //New address
|
||||||
Assert.True(IsMapped(invoice, ctx));
|
Assert.True(IsMapped(invoice, ctx));
|
||||||
Assert.True(IsMapped(localInvoice, ctx));
|
Assert.True(IsMapped(localInvoice, ctx));
|
||||||
|
@ -749,6 +808,7 @@ namespace BTCPayServer.Tests
|
||||||
{
|
{
|
||||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||||
Assert.Equal("paid", localInvoice.Status);
|
Assert.Equal("paid", localInvoice.Status);
|
||||||
|
Assert.Equal(2, localInvoice.CryptoInfo[0].TxCount);
|
||||||
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
Assert.Equal(firstPayment + secondPayment, localInvoice.BtcPaid);
|
||||||
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
Assert.Equal(Money.Zero, localInvoice.BtcDue);
|
||||||
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
|
Assert.Equal(localInvoice.BitcoinAddress, invoiceAddress.ToString()); //no new address generated
|
||||||
|
@ -835,7 +895,7 @@ namespace BTCPayServer.Tests
|
||||||
|
|
||||||
private void Eventually(Action act)
|
private void Eventually(Action act)
|
||||||
{
|
{
|
||||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
CancellationTokenSource cts = new CancellationTokenSource(200000);
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -62,11 +62,13 @@ namespace BTCPayServer
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CryptoImagePath { get; set; }
|
public string CryptoImagePath { get; set; }
|
||||||
|
public string LightningImagePath { get; set; }
|
||||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||||
|
|
||||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||||
public KeyPath CoinType { get; internal set; }
|
public KeyPath CoinType { get; internal set; }
|
||||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||||
|
public string CLightningNetworkName { get; internal set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
|
@ -20,14 +20,18 @@ namespace BTCPayServer
|
||||||
Add(new BTCPayNetwork()
|
Add(new BTCPayNetwork()
|
||||||
{
|
{
|
||||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||||
NBXplorerNetwork = nbxplorerNetwork,
|
NBXplorerNetwork = nbxplorerNetwork,
|
||||||
UriScheme = "bitcoin",
|
UriScheme = "bitcoin",
|
||||||
DefaultRateProvider = btcRate,
|
DefaultRateProvider = btcRate,
|
||||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||||
|
LightningImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'"),
|
||||||
|
CLightningNetworkName = ChainType == ChainType.Main ? "bitcoin" :
|
||||||
|
ChainType == ChainType.Test ? "testnet" :
|
||||||
|
ChainType == ChainType.Regtest ? "regtest" : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,11 @@ namespace BTCPayServer
|
||||||
UriScheme = "litecoin",
|
UriScheme = "litecoin",
|
||||||
DefaultRateProvider = ltcRate,
|
DefaultRateProvider = ltcRate,
|
||||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||||
|
LightningImagePath = "imlegacy/litecoin-symbol.svg",
|
||||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
|
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'"),
|
||||||
|
CLightningNetworkName = ChainType == ChainType.Main ? "litecoin" :
|
||||||
|
ChainType == ChainType.Test ? "litecoin-testnet" : null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,13 +25,40 @@ namespace BTCPayServer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||||
|
{
|
||||||
|
ChainType = filtered.ChainType;
|
||||||
|
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
|
||||||
|
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||||
|
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||||
|
foreach (var network in filtered._Networks)
|
||||||
|
{
|
||||||
|
if(cryptoCodes.Contains(network.Key))
|
||||||
|
{
|
||||||
|
_Networks.Add(network.Key, network.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChainType ChainType { get; set; }
|
||||||
public BTCPayNetworkProvider(ChainType chainType)
|
public BTCPayNetworkProvider(ChainType chainType)
|
||||||
{
|
{
|
||||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||||
|
ChainType = chainType;
|
||||||
InitBitcoin();
|
InitBitcoin();
|
||||||
InitLitecoin();
|
InitLitecoin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep only the specified crypto
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cryptoCodes">Crypto to support</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public BTCPayNetworkProvider Filter(string[] cryptoCodes)
|
||||||
|
{
|
||||||
|
return new BTCPayNetworkProvider(this, cryptoCodes);
|
||||||
|
}
|
||||||
|
|
||||||
[Obsolete("To use only for legacy stuff")]
|
[Obsolete("To use only for legacy stuff")]
|
||||||
public BTCPayNetwork BTC
|
public BTCPayNetwork BTC
|
||||||
{
|
{
|
||||||
|
@ -43,7 +70,7 @@ namespace BTCPayServer
|
||||||
|
|
||||||
public void Add(BTCPayNetwork network)
|
public void Add(BTCPayNetwork network)
|
||||||
{
|
{
|
||||||
_Networks.Add(network.CryptoCode, network);
|
_Networks.Add(network.CryptoCode.ToUpperInvariant(), network);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<BTCPayNetwork> GetAll()
|
public IEnumerable<BTCPayNetwork> GetAll()
|
||||||
|
@ -51,6 +78,11 @@ namespace BTCPayServer
|
||||||
return _Networks.Values.ToArray();
|
return _Networks.Values.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Support(string cryptoCode)
|
||||||
|
{
|
||||||
|
return _Networks.ContainsKey(cryptoCode.ToUpperInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||||
{
|
{
|
||||||
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
_Networks.TryGetValue(cryptoCode.ToUpperInvariant(), out BTCPayNetwork network);
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||||
<PackageReference Include="NBitcoin" Version="4.0.0.56" />
|
<PackageReference Include="NBitcoin" Version="4.0.0.56" />
|
||||||
<PackageReference Include="NBitpayClient" Version="1.0.0.17" />
|
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
||||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||||
|
|
|
@ -58,28 +58,30 @@ namespace BTCPayServer.Configuration
|
||||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
.Select(t => t.ToUpperInvariant());
|
.Select(t => t.ToUpperInvariant());
|
||||||
var validChains = new List<string>();
|
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
|
||||||
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
|
foreach (var chain in supportedChains)
|
||||||
{
|
{
|
||||||
if (supportedChains.Contains(net.CryptoCode))
|
if (NetworkProvider.GetNetwork(chain) == null)
|
||||||
{
|
throw new ConfigException($"Invalid chains \"{chain}\"");
|
||||||
validChains.Add(net.CryptoCode);
|
}
|
||||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
|
||||||
setting.CryptoCode = net.CryptoCode;
|
var validChains = new List<string>();
|
||||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
foreach (var net in NetworkProvider.GetAll())
|
||||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
{
|
||||||
NBXplorerConnectionSettings.Add(setting);
|
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||||
}
|
setting.CryptoCode = net.CryptoCode;
|
||||||
|
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||||
|
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||||
|
NBXplorerConnectionSettings.Add(setting);
|
||||||
}
|
}
|
||||||
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
|
|
||||||
if(!string.IsNullOrEmpty(invalidChains))
|
|
||||||
throw new ConfigException($"Invalid chains {invalidChains}");
|
|
||||||
|
|
||||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||||
|
|
||||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||||
public string PostgresConnectionString
|
public string PostgresConnectionString
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
|
|
|
@ -198,24 +198,29 @@ namespace BTCPayServer.Controllers
|
||||||
Rate = FormatCurrency(paymentMethod),
|
Rate = FormatCurrency(paymentMethod),
|
||||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||||
StoreName = store.StoreName,
|
StoreName = store.StoreName,
|
||||||
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
|
InvoiceBitcoinUrl = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||||
TxCount = accounting.TxCount,
|
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11 :
|
||||||
|
throw new NotSupportedException(),
|
||||||
|
InvoiceBitcoinUrlQR = paymentMethodId.PaymentType == PaymentTypes.BTCLike ? cryptoInfo.PaymentUrls.BIP21 :
|
||||||
|
paymentMethodId.PaymentType == PaymentTypes.LightningLike ? cryptoInfo.PaymentUrls.BOLT11.ToUpperInvariant() :
|
||||||
|
throw new NotSupportedException(),
|
||||||
|
TxCount = accounting.TxRequired,
|
||||||
BtcPaid = accounting.Paid.ToString(),
|
BtcPaid = accounting.Paid.ToString(),
|
||||||
Status = invoice.Status,
|
Status = invoice.Status,
|
||||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
|
CryptoImage = "/" + GetImage(paymentMethodId, network),
|
||||||
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
||||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||||
.Where(i => i.Network != null)
|
.Where(i => i.Network != null)
|
||||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||||
{
|
{
|
||||||
PaymentMethodId = kv.GetId().ToString(),
|
PaymentMethodId = kv.GetId().ToString(),
|
||||||
CryptoImage = "/" + kv.Network.CryptoImagePath,
|
CryptoImage = "/" + GetImage(kv.GetId(), kv.Network),
|
||||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||||
}).Where(c => c.CryptoImage != "/")
|
}).Where(c => c.CryptoImage != "/")
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
|
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetpaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1;
|
||||||
if (isMultiCurrency)
|
if (isMultiCurrency)
|
||||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||||
|
|
||||||
|
@ -224,6 +229,11 @@ namespace BTCPayServer.Controllers
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
return (paymentMethodId.PaymentType == PaymentTypes.BTCLike ? Url.Content(network.CryptoImagePath) : Url.Content(network.LightningImagePath));
|
||||||
|
}
|
||||||
|
|
||||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||||
{
|
{
|
||||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||||
|
|
|
@ -138,6 +138,7 @@ namespace BTCPayServer.Controllers
|
||||||
{
|
{
|
||||||
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
|
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
|
||||||
PaymentMethod paymentMethod = new PaymentMethod();
|
PaymentMethod paymentMethod = new PaymentMethod();
|
||||||
|
paymentMethod.ParentEntity = entity;
|
||||||
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
|
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
|
||||||
paymentMethod.Rate = rate;
|
paymentMethod.Rate = rate;
|
||||||
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
|
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
|
||||||
|
@ -162,7 +163,7 @@ namespace BTCPayServer.Controllers
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||||
if (!legacyBTCisSet)
|
if (!legacyBTCisSet && _NetworkProvider.BTC != null)
|
||||||
{
|
{
|
||||||
var btc = _NetworkProvider.BTC;
|
var btc = _NetworkProvider.BTC;
|
||||||
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
|
var feeProvider = ((IFeeProviderFactory)_ServiceProvider.GetService(typeof(IFeeProviderFactory))).CreateFeeProvider(btc);
|
||||||
|
|
302
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
302
BTCPayServer/Controllers/StoresController.BTCLike.cs
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class StoresController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/derivations")]
|
||||||
|
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
|
||||||
|
{
|
||||||
|
selectedScheme = selectedScheme ?? "BTC";
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||||
|
vm.ServerUrl = GetStoreUrl(storeId);
|
||||||
|
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/derivations")]
|
||||||
|
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm)
|
||||||
|
{
|
||||||
|
vm.ServerUrl = GetStoreUrl(storeId);
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||||
|
vm.SetCryptoCurrencies(_ExplorerProvider, vm.CryptoCurrency);
|
||||||
|
if (network == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
var wallet = _WalletProvider.GetWallet(network);
|
||||||
|
if (wallet == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||||
|
DerivationStrategy strategy = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||||
|
{
|
||||||
|
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||||
|
vm.DerivationScheme = strategy.ToString();
|
||||||
|
}
|
||||||
|
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||||
|
vm.Confirmation = false;
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (vm.Confirmation)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (strategy != null)
|
||||||
|
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||||
|
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _Repo.UpdateStore(store);
|
||||||
|
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||||
|
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||||
|
{
|
||||||
|
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var address = line.Derive((uint)i);
|
||||||
|
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.Confirmation = true;
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class GetInfoResult
|
||||||
|
{
|
||||||
|
public int RecommendedSatoshiPerByte { get; set; }
|
||||||
|
public double Balance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SendToAddressResult
|
||||||
|
{
|
||||||
|
public string TransactionId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/ws/ledger")]
|
||||||
|
public async Task<IActionResult> LedgerConnection(
|
||||||
|
string storeId,
|
||||||
|
string command,
|
||||||
|
// getinfo
|
||||||
|
string cryptoCode = null,
|
||||||
|
// sendtoaddress
|
||||||
|
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
|
return NotFound();
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
|
var hw = new HardwareWalletService(webSocket);
|
||||||
|
object result = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BTCPayNetwork network = null;
|
||||||
|
if (cryptoCode != null)
|
||||||
|
{
|
||||||
|
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||||
|
if (network == null)
|
||||||
|
throw new FormatException("Invalid value for crypto code");
|
||||||
|
}
|
||||||
|
|
||||||
|
BitcoinAddress destinationAddress = null;
|
||||||
|
if (destination != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destinationAddress = BitcoinAddress.Create(destination);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (destinationAddress == null)
|
||||||
|
throw new FormatException("Invalid value for destination");
|
||||||
|
}
|
||||||
|
|
||||||
|
FeeRate feeRateValue = null;
|
||||||
|
if (feeRate != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||||
|
throw new FormatException("Invalid value for fee rate");
|
||||||
|
}
|
||||||
|
|
||||||
|
Money amountBTC = null;
|
||||||
|
if (amount != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
amountBTC = Money.Parse(amount);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||||
|
throw new FormatException("Invalid value for amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool subsctractFeesValue = false;
|
||||||
|
if (substractFees != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subsctractFeesValue = bool.Parse(substractFees);
|
||||||
|
}
|
||||||
|
catch { throw new FormatException("Invalid value for substract fees"); }
|
||||||
|
}
|
||||||
|
if (command == "test")
|
||||||
|
{
|
||||||
|
result = await hw.Test();
|
||||||
|
}
|
||||||
|
if (command == "getxpub")
|
||||||
|
{
|
||||||
|
result = await hw.GetExtPubKey(network);
|
||||||
|
}
|
||||||
|
if (command == "getinfo")
|
||||||
|
{
|
||||||
|
var strategy = GetDirectDerivationStrategy(store, network);
|
||||||
|
var strategyBase = GetDerivationStrategy(store, network);
|
||||||
|
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
||||||
|
{
|
||||||
|
throw new Exception($"This store is not configured to use this ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||||
|
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||||
|
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
||||||
|
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command == "sendtoaddress")
|
||||||
|
{
|
||||||
|
var strategy = GetDirectDerivationStrategy(store, network);
|
||||||
|
var strategyBase = GetDerivationStrategy(store, network);
|
||||||
|
var wallet = _WalletProvider.GetWallet(network);
|
||||||
|
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||||
|
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||||
|
var changeAddress = await change;
|
||||||
|
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
||||||
|
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
||||||
|
feeRateValue,
|
||||||
|
changeAddress.Item1,
|
||||||
|
changeAddress.Item2);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||||
|
if (!broadcastResult[0].Success)
|
||||||
|
{
|
||||||
|
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||||
|
}
|
||||||
|
wallet.InvalidateCache(strategyBase);
|
||||||
|
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||||
|
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||||
|
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await webSocket.CloseSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmptyResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
var strategy = GetDerivationStrategy(store, network);
|
||||||
|
var directStrategy = strategy as DirectDerivationStrategy;
|
||||||
|
if (directStrategy == null)
|
||||||
|
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||||
|
if (!directStrategy.Segwit)
|
||||||
|
return null;
|
||||||
|
return directStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
var strategy = store
|
||||||
|
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
|
.OfType<DerivationStrategy>()
|
||||||
|
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||||
|
if (strategy == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
||||||
|
}
|
||||||
|
|
||||||
|
return strategy.DerivationStrategyBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
103
BTCPayServer/Controllers/StoresController.LightningLike.cs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
|
using BTCPayServer.Payments;
|
||||||
|
using BTCPayServer.Payments.Lightning.CLightning;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class StoresController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
[Route("{storeId}/lightning")]
|
||||||
|
public async Task<IActionResult> AddLightningNode(string storeId, string selectedCrypto = null)
|
||||||
|
{
|
||||||
|
selectedCrypto = selectedCrypto ?? "BTC";
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
LightningNodeViewModel vm = new LightningNodeViewModel();
|
||||||
|
vm.SetCryptoCurrencies(_NetworkProvider, selectedCrypto);
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Route("{storeId}/lightning")]
|
||||||
|
public async Task<IActionResult> AddLightningNode(string storeId, LightningNodeViewModel vm, string command)
|
||||||
|
{
|
||||||
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
|
if (store == null)
|
||||||
|
return NotFound();
|
||||||
|
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
||||||
|
vm.SetCryptoCurrencies(_NetworkProvider, vm.CryptoCurrency);
|
||||||
|
if (network == null || network.CLightningNetworkName == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike);
|
||||||
|
Payments.Lightning.LightningSupportedPaymentMethod paymentMethod = null;
|
||||||
|
if (!string.IsNullOrEmpty(vm.Url))
|
||||||
|
{
|
||||||
|
Uri uri;
|
||||||
|
if (!Uri.TryCreate(vm.Url, UriKind.Absolute, out uri))
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Url), "Invalid URL");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
if (_NetworkProvider.ChainType == NBXplorer.ChainType.Main)
|
||||||
|
{
|
||||||
|
if (uri.Scheme != "https")
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Url), "The url must be HTTPS");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(uri.UserInfo) || uri.UserInfo.Split(':').Length != 2)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Url), "The url is missing user and password");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentMethod = new Payments.Lightning.LightningSupportedPaymentMethod()
|
||||||
|
{
|
||||||
|
CryptoCode = paymentMethodId.CryptoCode
|
||||||
|
};
|
||||||
|
paymentMethod.SetLightningChargeUrl(uri);
|
||||||
|
}
|
||||||
|
if (command == "save")
|
||||||
|
{
|
||||||
|
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
|
||||||
|
await _Repo.UpdateStore(store);
|
||||||
|
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
|
||||||
|
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||||
|
}
|
||||||
|
else // if(command == "test")
|
||||||
|
{
|
||||||
|
if (paymentMethod == null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(nameof(vm.Url), "Missing url parameter");
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await handler.Test(paymentMethod, network);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
vm.StatusMessage = $"Error: {ex.Message}";
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
vm.StatusMessage = "Connection to the lightning node succeed";
|
||||||
|
return View(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,13 +2,9 @@
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models;
|
using BTCPayServer.Models;
|
||||||
using BTCPayServer.Models.StoreViewModels;
|
using BTCPayServer.Models.StoreViewModels;
|
||||||
using BTCPayServer.Payments;
|
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Fees;
|
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using LedgerWallet;
|
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
@ -17,16 +13,11 @@ using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.DataEncoders;
|
using NBitcoin.DataEncoders;
|
||||||
using NBitpayClient;
|
|
||||||
using NBXplorer.DerivationStrategy;
|
using NBXplorer.DerivationStrategy;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.WebSockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
@ -36,9 +27,10 @@ namespace BTCPayServer.Controllers
|
||||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||||
[Authorize(Policy = "CanAccessStore")]
|
[Authorize(Policy = "CanAccessStore")]
|
||||||
[AutoValidateAntiforgeryToken]
|
[AutoValidateAntiforgeryToken]
|
||||||
public class StoresController : Controller
|
public partial class StoresController : Controller
|
||||||
{
|
{
|
||||||
public StoresController(
|
public StoresController(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||||
StoreRepository repo,
|
StoreRepository repo,
|
||||||
TokenRepository tokenRepo,
|
TokenRepository tokenRepo,
|
||||||
|
@ -60,7 +52,9 @@ namespace BTCPayServer.Controllers
|
||||||
_ExplorerProvider = explorerProvider;
|
_ExplorerProvider = explorerProvider;
|
||||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||||
_FeeRateProvider = feeRateProvider;
|
_FeeRateProvider = feeRateProvider;
|
||||||
|
_ServiceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
IServiceProvider _ServiceProvider;
|
||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
private ExplorerClientProvider _ExplorerProvider;
|
private ExplorerClientProvider _ExplorerProvider;
|
||||||
private MvcJsonOptions _MvcJsonOptions;
|
private MvcJsonOptions _MvcJsonOptions;
|
||||||
|
@ -122,193 +116,6 @@ namespace BTCPayServer.Controllers
|
||||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetInfoResult
|
|
||||||
{
|
|
||||||
public int RecommendedSatoshiPerByte { get; set; }
|
|
||||||
public double Balance { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SendToAddressResult
|
|
||||||
{
|
|
||||||
public string TransactionId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("{storeId}/ws/ledger")]
|
|
||||||
public async Task<IActionResult> LedgerConnection(
|
|
||||||
string storeId,
|
|
||||||
string command,
|
|
||||||
// getinfo
|
|
||||||
string cryptoCode = null,
|
|
||||||
// sendtoaddress
|
|
||||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
|
||||||
return NotFound();
|
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
|
||||||
|
|
||||||
var hw = new HardwareWalletService(webSocket);
|
|
||||||
object result = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
BTCPayNetwork network = null;
|
|
||||||
if (cryptoCode != null)
|
|
||||||
{
|
|
||||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
|
||||||
if (network == null)
|
|
||||||
throw new FormatException("Invalid value for crypto code");
|
|
||||||
}
|
|
||||||
|
|
||||||
BitcoinAddress destinationAddress = null;
|
|
||||||
if (destination != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
destinationAddress = BitcoinAddress.Create(destination);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (destinationAddress == null)
|
|
||||||
throw new FormatException("Invalid value for destination");
|
|
||||||
}
|
|
||||||
|
|
||||||
FeeRate feeRateValue = null;
|
|
||||||
if (feeRate != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
|
||||||
throw new FormatException("Invalid value for fee rate");
|
|
||||||
}
|
|
||||||
|
|
||||||
Money amountBTC = null;
|
|
||||||
if (amount != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
amountBTC = Money.Parse(amount);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
|
||||||
throw new FormatException("Invalid value for amount");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool subsctractFeesValue = false;
|
|
||||||
if (substractFees != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
subsctractFeesValue = bool.Parse(substractFees);
|
|
||||||
}
|
|
||||||
catch { throw new FormatException("Invalid value for substract fees"); }
|
|
||||||
}
|
|
||||||
if (command == "test")
|
|
||||||
{
|
|
||||||
result = await hw.Test();
|
|
||||||
}
|
|
||||||
if (command == "getxpub")
|
|
||||||
{
|
|
||||||
result = await hw.GetExtPubKey(network);
|
|
||||||
}
|
|
||||||
if (command == "getinfo")
|
|
||||||
{
|
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
|
||||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
|
||||||
{
|
|
||||||
throw new Exception($"This store is not configured to use this ledger");
|
|
||||||
}
|
|
||||||
|
|
||||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
|
||||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
|
||||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
|
||||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command == "sendtoaddress")
|
|
||||||
{
|
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
|
||||||
var wallet = _WalletProvider.GetWallet(network);
|
|
||||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
|
||||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
|
||||||
var changeAddress = await change;
|
|
||||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
|
||||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
|
||||||
feeRateValue,
|
|
||||||
changeAddress.Item1,
|
|
||||||
changeAddress.Item2);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
|
||||||
if (!broadcastResult[0].Success)
|
|
||||||
{
|
|
||||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
|
||||||
}
|
|
||||||
wallet.InvalidateCache(strategyBase);
|
|
||||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
|
||||||
catch (Exception ex)
|
|
||||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
|
||||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
|
||||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await webSocket.CloseSocket();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmptyResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var strategy = GetDerivationStrategy(store, network);
|
|
||||||
var directStrategy = strategy as DirectDerivationStrategy;
|
|
||||||
if (directStrategy == null)
|
|
||||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
|
||||||
if (!directStrategy.Segwit)
|
|
||||||
return null;
|
|
||||||
return directStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var strategy = store
|
|
||||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
|
||||||
.OfType<DerivationStrategy>()
|
|
||||||
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
|
||||||
if (strategy == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
return strategy.DerivationStrategyBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ListStores()
|
public async Task<IActionResult> ListStores()
|
||||||
{
|
{
|
||||||
|
@ -398,7 +205,7 @@ namespace BTCPayServer.Controllers
|
||||||
vm.StoreWebsite = store.StoreWebsite;
|
vm.StoreWebsite = store.StoreWebsite;
|
||||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||||
vm.SpeedPolicy = store.SpeedPolicy;
|
vm.SpeedPolicy = store.SpeedPolicy;
|
||||||
AddDerivationSchemes(store, vm);
|
AddPaymentMethods(store, vm);
|
||||||
vm.StatusMessage = StatusMessage;
|
vm.StatusMessage = StatusMessage;
|
||||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||||
|
@ -407,114 +214,28 @@ namespace BTCPayServer.Controllers
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDerivationSchemes(StoreData store, StoreViewModel vm)
|
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||||
{
|
{
|
||||||
var strategies = store
|
foreach(var strategy in store
|
||||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
.OfType<DerivationStrategy>()
|
.OfType<DerivationStrategy>())
|
||||||
.ToDictionary(s => s.Network.CryptoCode);
|
|
||||||
foreach (var explorerProvider in _ExplorerProvider.GetAll())
|
|
||||||
{
|
{
|
||||||
if (strategies.TryGetValue(explorerProvider.Item1.CryptoCode, out DerivationStrategy strat))
|
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||||
{
|
{
|
||||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
Crypto = strategy.PaymentId.CryptoCode,
|
||||||
{
|
Value = strategy.DerivationStrategyBase.ToString()
|
||||||
Crypto = explorerProvider.Item1.CryptoCode,
|
});
|
||||||
Value = strat.DerivationStrategyBase.ToString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("{storeId}/derivations")]
|
|
||||||
public async Task<IActionResult> AddDerivationScheme(string storeId, string selectedScheme = null)
|
|
||||||
{
|
|
||||||
selectedScheme = selectedScheme ?? "BTC";
|
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
|
||||||
vm.ServerUrl = GetStoreUrl(storeId);
|
|
||||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
[Route("{storeId}/derivations")]
|
|
||||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
|
|
||||||
{
|
|
||||||
selectedScheme = selectedScheme ?? "BTC";
|
|
||||||
vm.ServerUrl = GetStoreUrl(storeId);
|
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var network = vm.CryptoCurrency == null ? null : _ExplorerProvider.GetNetwork(vm.CryptoCurrency);
|
|
||||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
|
||||||
if (network == null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
var wallet = _WalletProvider.GetWallet(network);
|
|
||||||
if (wallet == null)
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
|
||||||
return View(vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
foreach(var lightning in store
|
||||||
DerivationStrategy strategy = null;
|
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
try
|
.OfType<Payments.Lightning.LightningSupportedPaymentMethod>())
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
|
||||||
{
|
{
|
||||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
CryptoCode = lightning.CryptoCode,
|
||||||
vm.DerivationScheme = strategy.ToString();
|
Address = lightning.GetLightningChargeUrl(false).AbsoluteUri
|
||||||
}
|
});
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
|
||||||
vm.Confirmation = false;
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (vm.Confirmation)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (strategy != null)
|
|
||||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
|
||||||
return View(vm);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _Repo.UpdateStore(store);
|
|
||||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
|
||||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
|
||||||
{
|
|
||||||
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
var address = line.Derive((uint)i);
|
|
||||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vm.Confirmation = true;
|
|
||||||
return View(vm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +252,7 @@ namespace BTCPayServer.Controllers
|
||||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||||
if (store == null)
|
if (store == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
AddDerivationSchemes(store, model);
|
AddPaymentMethods(store, model);
|
||||||
|
|
||||||
bool needUpdate = false;
|
bool needUpdate = false;
|
||||||
if (store.SpeedPolicy != model.SpeedPolicy)
|
if (store.SpeedPolicy != model.SpeedPolicy)
|
||||||
|
|
|
@ -129,7 +129,7 @@ namespace BTCPayServer.Hosting
|
||||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||||
{
|
{
|
||||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||||
return new BTCPayNetworkProvider(opts.ChainType);
|
return opts.NetworkProvider;
|
||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton<NBXplorerDashboard>();
|
services.TryAddSingleton<NBXplorerDashboard>();
|
||||||
|
@ -143,9 +143,12 @@ namespace BTCPayServer.Hosting
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||||
|
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||||
|
|
||||||
|
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||||
|
services.AddSingleton<IHostedService, Payments.Lightning.ChargeListener>();
|
||||||
|
|
||||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
|
||||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||||
|
|
||||||
|
|
40
BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs
Normal file
40
BTCPayServer/JsonConverters/LightMoneyJsonConverter.cs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Reflection;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using NBitcoin.JsonConverters;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace BTCPayServer.JsonConverters
|
||||||
|
{
|
||||||
|
public class LightMoneyJsonConverter : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
return typeof(LightMoneyJsonConverter).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return reader.TokenType == JsonToken.Null ? null :
|
||||||
|
reader.TokenType == JsonToken.Integer ? new LightMoney((long)reader.Value) :
|
||||||
|
reader.TokenType == JsonToken.String ? new LightMoney(long.Parse((string)reader.Value, CultureInfo.InvariantCulture))
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
catch (InvalidCastException)
|
||||||
|
{
|
||||||
|
throw new JsonObjectException("Money amount should be in millisatoshi", reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteValue(((LightMoney)value).MilliSatoshi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||||
public string Rate { get; set; }
|
public string Rate { get; set; }
|
||||||
public string OrderAmount { get; set; }
|
public string OrderAmount { get; set; }
|
||||||
public string InvoiceBitcoinUrl { get; set; }
|
public string InvoiceBitcoinUrl { get; set; }
|
||||||
|
public string InvoiceBitcoinUrlQR { get; set; }
|
||||||
public int TxCount { get; set; }
|
public int TxCount { get; set; }
|
||||||
public string BtcPaid { get; set; }
|
public string BtcPaid { get; set; }
|
||||||
public string StoreEmail { get; set; }
|
public string StoreEmail { get; set; }
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
|
{
|
||||||
|
public class LightningNodeViewModel
|
||||||
|
{
|
||||||
|
class Format
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
[Display(Name = "Lightning charge url")]
|
||||||
|
public string Url
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Display(Name = "Crypto currency")]
|
||||||
|
public string CryptoCurrency
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
public SelectList CryptoCurrencies { get; set; }
|
||||||
|
public string StatusMessage { get; set; }
|
||||||
|
|
||||||
|
public void SetCryptoCurrencies(BTCPayNetworkProvider networkProvider, string selectedScheme)
|
||||||
|
{
|
||||||
|
var choices = networkProvider.GetAll()
|
||||||
|
.Where(n => n.CLightningNetworkName != null)
|
||||||
|
.Select(o => new Format() { Name = o.CryptoCode, Value = o.CryptoCode }).ToArray();
|
||||||
|
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||||
|
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||||
|
CryptoCurrency = chosen.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,16 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||||
[Display(Name = "Default crypto currency on checkout")]
|
[Display(Name = "Default crypto currency on checkout")]
|
||||||
public string DefaultCryptoCurrency { get; set; }
|
public string DefaultCryptoCurrency { get; set; }
|
||||||
|
|
||||||
|
public class LightningNode
|
||||||
|
{
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
public string Address { get; set; }
|
||||||
|
}
|
||||||
|
public List<LightningNode> LightningNodes
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = new List<LightningNode>();
|
||||||
|
|
||||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
|
||||||
{
|
{
|
||||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||||
|
|
1114
BTCPayServer/MultiValueDictionary.cs
Normal file
1114
BTCPayServer/MultiValueDictionary.cs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,16 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.WebSockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
|
using NBXplorer;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||||
|
@ -40,29 +44,69 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||||
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
|
Credentials = new NetworkCredential(userInfo[0], userInfo[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CreateInvoiceResponse> CreateInvoiceAsync(CreateInvoiceRequest request, CancellationToken cancellation = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var message = CreateMessage(HttpMethod.Post, "invoice");
|
||||||
|
Dictionary<string, string> parameters = new Dictionary<string, string>();
|
||||||
|
parameters.Add("msatoshi", request.Amont.MilliSatoshi.ToString(CultureInfo.InvariantCulture));
|
||||||
|
parameters.Add("expiry", ((int)request.Expiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||||
|
message.Content = new FormUrlEncodedContent(parameters);
|
||||||
|
var result = await _Client.SendAsync(message, cancellation);
|
||||||
|
result.EnsureSuccessStatusCode();
|
||||||
|
var content = await result.Content.ReadAsStringAsync();
|
||||||
|
return JsonConvert.DeserializeObject<CreateInvoiceResponse>(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChargeSession> Listen(CancellationToken cancellation = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var socket = new ClientWebSocket();
|
||||||
|
socket.Options.SetRequestHeader("Authorization", $"Basic {GetBase64Creds()}");
|
||||||
|
var uri = new UriBuilder(Uri) { UserName = null, Password = null }.Uri.AbsoluteUri;
|
||||||
|
if (!uri.EndsWith('/'))
|
||||||
|
uri += "/";
|
||||||
|
uri += "ws";
|
||||||
|
uri = ToWebsocketUri(uri);
|
||||||
|
await socket.ConnectAsync(new Uri(uri), cancellation);
|
||||||
|
return new ChargeSession(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToWebsocketUri(string uri)
|
||||||
|
{
|
||||||
|
if (uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
uri = uri.Replace("https://", "wss://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
uri = uri.Replace("http://", "ws://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
public NetworkCredential Credentials { get; set; }
|
public NetworkCredential Credentials { get; set; }
|
||||||
|
|
||||||
public GetInfoResponse GetInfo()
|
public GetInfoResponse GetInfo()
|
||||||
{
|
{
|
||||||
return GetInfoAsync().GetAwaiter().GetResult();
|
return GetInfoAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
public async Task<GetInfoResponse> GetInfoAsync()
|
public async Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||||
{
|
{
|
||||||
var request = Get("info");
|
var request = CreateMessage(HttpMethod.Get, "info");
|
||||||
var message = await _Client.SendAsync(request);
|
var message = await _Client.SendAsync(request, cancellation);
|
||||||
message.EnsureSuccessStatusCode();
|
message.EnsureSuccessStatusCode();
|
||||||
var content = await message.Content.ReadAsStringAsync();
|
var content = await message.Content.ReadAsStringAsync();
|
||||||
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
|
return JsonConvert.DeserializeObject<GetInfoResponse>(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestMessage Get(string path)
|
private HttpRequestMessage CreateMessage(HttpMethod method, string path)
|
||||||
{
|
{
|
||||||
var uri = GetFullUri(path);
|
var uri = GetFullUri(path);
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, uri);
|
var request = new HttpRequestMessage(method, uri);
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}")));
|
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", GetBase64Creds());
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetBase64Creds()
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{Credentials.UserName}:{Credentials.Password}"));
|
||||||
|
}
|
||||||
|
|
||||||
private Uri GetFullUri(string partialUrl)
|
private Uri GetFullUri(string partialUrl)
|
||||||
{
|
{
|
||||||
var uri = _Uri.AbsoluteUri;
|
var uri = _Uri.AbsoluteUri;
|
||||||
|
|
124
BTCPayServer/Payments/Lightning/CLightning/ChargeSession.cs
Normal file
124
BTCPayServer/Payments/Lightning/CLightning/ChargeSession.cs
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NBXplorer;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||||
|
{
|
||||||
|
public class ChargeInvoiceNotification
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("msatoshi")]
|
||||||
|
[JsonConverter(typeof(JsonConverters.LightMoneyJsonConverter))]
|
||||||
|
public LightMoney MilliSatoshi { get; set; }
|
||||||
|
[JsonProperty("paid_at")]
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset? PaidAt { get; set; }
|
||||||
|
[JsonProperty("expires_at")]
|
||||||
|
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||||
|
public DateTimeOffset? ExpiresAt { get; set; }
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("payreq")]
|
||||||
|
public string PaymentRequest { get; set; }
|
||||||
|
}
|
||||||
|
public class ChargeSession : IDisposable
|
||||||
|
{
|
||||||
|
private ClientWebSocket socket;
|
||||||
|
|
||||||
|
const int ORIGINAL_BUFFER_SIZE = 1024 * 5;
|
||||||
|
const int MAX_BUFFER_SIZE = 1024 * 1024 * 5;
|
||||||
|
public ChargeSession(ClientWebSocket socket)
|
||||||
|
{
|
||||||
|
this.socket = socket;
|
||||||
|
var buffer = new byte[ORIGINAL_BUFFER_SIZE];
|
||||||
|
_Buffer = new ArraySegment<byte>(buffer, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArraySegment<byte> _Buffer;
|
||||||
|
public async Task<ChargeInvoiceNotification> NextEvent(CancellationToken cancellation = default(CancellationToken))
|
||||||
|
{
|
||||||
|
var buffer = _Buffer;
|
||||||
|
var array = _Buffer.Array;
|
||||||
|
var originalSize = _Buffer.Array.Length;
|
||||||
|
var newSize = _Buffer.Array.Length;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var message = await socket.ReceiveAsync(buffer, cancellation);
|
||||||
|
if (message.MessageType == WebSocketMessageType.Close)
|
||||||
|
{
|
||||||
|
await CloseSocketAndThrow(WebSocketCloseStatus.NormalClosure, "Close message received from the peer", cancellation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (message.MessageType != WebSocketMessageType.Text)
|
||||||
|
{
|
||||||
|
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidMessageType, "Only Text is supported", cancellation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (message.EndOfMessage)
|
||||||
|
{
|
||||||
|
buffer = new ArraySegment<byte>(array, 0, buffer.Offset + message.Count);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var o = ParseMessage(buffer);
|
||||||
|
if (newSize != originalSize)
|
||||||
|
{
|
||||||
|
Array.Resize(ref array, originalSize);
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await CloseSocketAndThrow(WebSocketCloseStatus.InvalidPayloadData, $"Invalid payload: {ex.Message}", cancellation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (buffer.Count - message.Count <= 0)
|
||||||
|
{
|
||||||
|
newSize *= 2;
|
||||||
|
if (newSize > MAX_BUFFER_SIZE)
|
||||||
|
await CloseSocketAndThrow(WebSocketCloseStatus.MessageTooBig, "Message is too big", cancellation);
|
||||||
|
Array.Resize(ref array, newSize);
|
||||||
|
buffer = new ArraySegment<byte>(array, buffer.Offset, newSize - buffer.Offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer = buffer.Slice(message.Count, buffer.Count - message.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Should never happen");
|
||||||
|
}
|
||||||
|
|
||||||
|
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
|
||||||
|
private ChargeInvoiceNotification ParseMessage(ArraySegment<byte> buffer)
|
||||||
|
{
|
||||||
|
var str = UTF8.GetString(buffer.Array, 0, buffer.Count);
|
||||||
|
return JsonConvert.DeserializeObject<ChargeInvoiceNotification>(str, new JsonSerializerSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseSocketAndThrow(WebSocketCloseStatus status, string description, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
var array = _Buffer.Array;
|
||||||
|
if (array.Length != ORIGINAL_BUFFER_SIZE)
|
||||||
|
Array.Resize(ref array, ORIGINAL_BUFFER_SIZE);
|
||||||
|
await socket.CloseSocket(status, description, cancellation);
|
||||||
|
throw new WebSocketException($"The socket has been closed ({status}: {description})");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Dispose()
|
||||||
|
{
|
||||||
|
await this.socket.CloseSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await this.socket.CloseSocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||||
|
{
|
||||||
|
public class CreateInvoiceRequest
|
||||||
|
{
|
||||||
|
public LightMoney Amont { get; set; }
|
||||||
|
public TimeSpan Expiry { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||||
|
{
|
||||||
|
public class CreateInvoiceResponse
|
||||||
|
{
|
||||||
|
public string PayReq { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
88
BTCPayServer/Payments/Lightning/ChargeListener.cs
Normal file
88
BTCPayServer/Payments/Lightning/ChargeListener.cs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.Payments.Lightning.CLightning;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using NBXplorer;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public class ChargeListener : IHostedService
|
||||||
|
{
|
||||||
|
EventAggregator _Aggregator;
|
||||||
|
InvoiceRepository _InvoiceRepository;
|
||||||
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
|
public ChargeListener(EventAggregator aggregator,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
|
BTCPayNetworkProvider networkProvider)
|
||||||
|
{
|
||||||
|
_Aggregator = aggregator;
|
||||||
|
_InvoiceRepository = invoiceRepository;
|
||||||
|
_NetworkProvider = networkProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositeDisposable leases = new CompositeDisposable();
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||||
|
{
|
||||||
|
if (inv.Name == "invoice_created")
|
||||||
|
{
|
||||||
|
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
|
||||||
|
await Task.WhenAll(invoice.GetPaymentMethods(_NetworkProvider)
|
||||||
|
.Where(c => c.GetId().PaymentType == PaymentTypes.LightningLike)
|
||||||
|
.Select(s => Listen(invoice, s, _NetworkProvider.GetNetwork(s.GetId().CryptoCode)))).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
//MultiValueDictionary<string,string>
|
||||||
|
private async Task Listen(InvoiceEntity invoice, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
var lightningMethod = paymentMethod.GetPaymentMethodDetails() as LightningLikePaymentMethodDetails;
|
||||||
|
if (lightningMethod == null)
|
||||||
|
return;
|
||||||
|
var lightningSupportedMethod = invoice.GetSupportedPaymentMethod<LightningSupportedPaymentMethod>(_NetworkProvider)
|
||||||
|
.FirstOrDefault(c => c.CryptoCode == network.CryptoCode);
|
||||||
|
if (lightningSupportedMethod == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var charge = new ChargeClient(lightningSupportedMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
|
||||||
|
var session = await charge.Listen();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var notification = await session.NextEvent();
|
||||||
|
if (notification.Id == lightningMethod.InvoiceId &&
|
||||||
|
notification.PaymentRequest == lightningMethod.BOLT11)
|
||||||
|
{
|
||||||
|
if (notification.Status == "paid" && notification.PaidAt.HasValue)
|
||||||
|
{
|
||||||
|
await _InvoiceRepository.AddPayment(invoice.Id, notification.PaidAt.Value, new LightningLikePaymentData()
|
||||||
|
{
|
||||||
|
BOLT11 = notification.PaymentRequest,
|
||||||
|
Amount = notification.MilliSatoshi
|
||||||
|
}, network.CryptoCode, accounted: true);
|
||||||
|
_Aggregator.Publish(new InvoiceEvent(invoice.Id, 1002, "invoice_receivedPayment"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(notification.Status == "expired")
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
leases.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,16 @@ using NBitcoin.RPC;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
{
|
{
|
||||||
|
public class SendResponse
|
||||||
|
{
|
||||||
|
public string PaymentHash { get; set; }
|
||||||
|
}
|
||||||
|
public class ChannelInfo
|
||||||
|
{
|
||||||
|
public string NodeId { get; set; }
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
public string State { get; set; }
|
||||||
|
}
|
||||||
public class EclairRPCClient
|
public class EclairRPCClient
|
||||||
{
|
{
|
||||||
public EclairRPCClient(Uri address, Network network)
|
public EclairRPCClient(Uri address, Network network)
|
||||||
|
@ -112,14 +122,14 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
|
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string[] Channels()
|
public ChannelInfo[] Channels()
|
||||||
{
|
{
|
||||||
return ChannelsAsync().GetAwaiter().GetResult();
|
return ChannelsAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string[]> ChannelsAsync()
|
public async Task<ChannelInfo[]> ChannelsAsync()
|
||||||
{
|
{
|
||||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
|
return await SendCommandAsync<ChannelInfo[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Close(string channelId)
|
public void Close(string channelId)
|
||||||
|
@ -127,6 +137,11 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
CloseAsync(channelId).GetAwaiter().GetResult();
|
CloseAsync(channelId).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendAsync(string paymentRequest)
|
||||||
|
{
|
||||||
|
await SendCommandAsync<SendResponse>(new RPCRequest("send", new[] { paymentRequest })).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task CloseAsync(string channelId)
|
public async Task CloseAsync(string channelId)
|
||||||
{
|
{
|
||||||
if (channelId == null)
|
if (channelId == null)
|
||||||
|
@ -227,7 +242,7 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
throw new ArgumentNullException(nameof(node));
|
throw new ArgumentNullException(nameof(node));
|
||||||
pushAmount = pushAmount ?? LightMoney.Zero;
|
pushAmount = pushAmount ?? LightMoney.Zero;
|
||||||
|
|
||||||
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi }));
|
||||||
|
|
||||||
return result.ResultString;
|
return result.ResultString;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using NBitcoin;
|
||||||
|
|
||||||
namespace BTCPayServer.Payments.Lightning.Eclair
|
namespace BTCPayServer.Payments.Lightning
|
||||||
{
|
{
|
||||||
public enum LightMoneyUnit : ulong
|
public enum LightMoneyUnit : ulong
|
||||||
{
|
{
|
||||||
|
@ -94,28 +95,31 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LightMoney(int satoshis)
|
public LightMoney(int msatoshis)
|
||||||
{
|
{
|
||||||
MilliSatoshi = satoshis;
|
MilliSatoshi = msatoshis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LightMoney(uint satoshis)
|
public LightMoney(uint msatoshis)
|
||||||
{
|
{
|
||||||
MilliSatoshi = satoshis;
|
MilliSatoshi = msatoshis;
|
||||||
|
}
|
||||||
|
public LightMoney(Money money)
|
||||||
|
{
|
||||||
|
MilliSatoshi = checked(money.Satoshi * 1000);
|
||||||
|
}
|
||||||
|
public LightMoney(long msatoshis)
|
||||||
|
{
|
||||||
|
MilliSatoshi = msatoshis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LightMoney(long satoshis)
|
public LightMoney(ulong msatoshis)
|
||||||
{
|
|
||||||
MilliSatoshi = satoshis;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LightMoney(ulong satoshis)
|
|
||||||
{
|
{
|
||||||
// overflow check.
|
// overflow check.
|
||||||
// ulong.MaxValue is greater than long.MaxValue
|
// ulong.MaxValue is greater than long.MaxValue
|
||||||
checked
|
checked
|
||||||
{
|
{
|
||||||
MilliSatoshi = (long)satoshis;
|
MilliSatoshi = (long)msatoshis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +175,7 @@ namespace BTCPayServer.Payments.Lightning.Eclair
|
||||||
CheckMoneyUnit(unit, "unit");
|
CheckMoneyUnit(unit, "unit");
|
||||||
// overflow safe because (long / int) always fit in decimal
|
// overflow safe because (long / int) always fit in decimal
|
||||||
// decimal operations are checked by default
|
// decimal operations are checked by default
|
||||||
return (decimal)MilliSatoshi / (int)unit;
|
return (decimal)MilliSatoshi / (ulong)unit;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert Money to decimal (same as ToUnit)
|
/// Convert Money to decimal (same as ToUnit)
|
47
BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs
Normal file
47
BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.JsonConverters;
|
||||||
|
using BTCPayServer.Payments.Lightning;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public class LightningLikePaymentData : CryptoPaymentData
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(LightMoneyJsonConverter))]
|
||||||
|
public LightMoney Amount { get; set; }
|
||||||
|
public string BOLT11 { get; set; }
|
||||||
|
public string GetPaymentId()
|
||||||
|
{
|
||||||
|
return BOLT11;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentTypes GetPaymentType()
|
||||||
|
{
|
||||||
|
return PaymentTypes.LightningLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetSearchTerms()
|
||||||
|
{
|
||||||
|
return new[] { BOLT11 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal GetValue()
|
||||||
|
{
|
||||||
|
return Amount.ToDecimal(LightMoneyUnit.BTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs
Normal file
136
BTCPayServer/Payments/Lightning/LightningLikePaymentHandler.cs
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Payments.Lightning.CLightning;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public class LightningLikePaymentHandler : PaymentMethodHandlerBase<LightningSupportedPaymentMethod>
|
||||||
|
{
|
||||||
|
ExplorerClientProvider _ExplorerClientProvider;
|
||||||
|
public LightningLikePaymentHandler(ExplorerClientProvider explorerClientProvider)
|
||||||
|
{
|
||||||
|
_ExplorerClientProvider = explorerClientProvider;
|
||||||
|
}
|
||||||
|
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
var invoice = paymentMethod.ParentEntity;
|
||||||
|
var due = invoice.ProductInformation.Price / paymentMethod.Rate;
|
||||||
|
var client = GetClient(supportedPaymentMethod, network);
|
||||||
|
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||||
|
var lightningInvoice = await client.CreateInvoiceAsync(new CreateInvoiceRequest()
|
||||||
|
{
|
||||||
|
Amont = new LightMoney(due, LightMoneyUnit.BTC),
|
||||||
|
Expiry = expiry < TimeSpan.Zero ? TimeSpan.FromSeconds(1) : expiry
|
||||||
|
});
|
||||||
|
return new LightningLikePaymentMethodDetails()
|
||||||
|
{
|
||||||
|
BOLT11 = lightningInvoice.PayReq,
|
||||||
|
InvoiceId = lightningInvoice.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Test(supportedPaymentMethod, network);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
if (!_ExplorerClientProvider.IsAvailable(network))
|
||||||
|
throw new Exception($"Full node not available");
|
||||||
|
|
||||||
|
var explorerClient = _ExplorerClientProvider.GetExplorerClient(network);
|
||||||
|
var cts = new CancellationTokenSource(5000);
|
||||||
|
var client = GetClient(supportedPaymentMethod, network);
|
||||||
|
var status = explorerClient.GetStatusAsync();
|
||||||
|
GetInfoResponse info = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
info = await client.GetInfoAsync(cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error while connecting to the lightning charge {client.Uri} ({ex.Message})");
|
||||||
|
}
|
||||||
|
var address = info.Address?.FirstOrDefault();
|
||||||
|
var port = info.Port;
|
||||||
|
address = address ?? client.Uri.DnsSafeHost;
|
||||||
|
|
||||||
|
if (info.Network != network.CLightningNetworkName)
|
||||||
|
{
|
||||||
|
throw new Exception($"Lightning node network {info.Network}, but expected is {network.CLightningNetworkName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Abs(info.BlockHeight - (await status).ChainHeight) > 10)
|
||||||
|
{
|
||||||
|
throw new Exception($"The lightning node is not synched");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await TestConnection(address, port, cts.Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Error while connecting to the lightning node via {address} ({ex.Message})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChargeClient GetClient(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||||
|
{
|
||||||
|
return new ChargeClient(supportedPaymentMethod.GetLightningChargeUrl(true), network.NBitcoinNetwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
IPAddress address = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
address = IPAddress.Parse(addressStr);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address == null)
|
||||||
|
throw new Exception($"DNS did not resolved {addressStr}");
|
||||||
|
|
||||||
|
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Task WithTimeout(Task task, CancellationToken token)
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||||
|
var registration = token.Register(() => { try { tcs.TrySetResult(true); } catch { } });
|
||||||
|
#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler
|
||||||
|
var timeoutTask = tcs.Task;
|
||||||
|
#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler
|
||||||
|
return Task.WhenAny(task, timeoutTask).Unwrap().ContinueWith(t => registration.Dispose(), TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public class LightningLikePaymentMethodDetails : IPaymentMethodDetails
|
||||||
|
{
|
||||||
|
public string BOLT11 { get; set; }
|
||||||
|
public string InvoiceId { get; set; }
|
||||||
|
|
||||||
|
public string GetPaymentDestination()
|
||||||
|
{
|
||||||
|
return BOLT11;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PaymentTypes GetPaymentType()
|
||||||
|
{
|
||||||
|
return PaymentTypes.LightningLike;
|
||||||
|
}
|
||||||
|
|
||||||
|
public decimal GetTxFee()
|
||||||
|
{
|
||||||
|
return 0.0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetNoTxFee()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPaymentDestination(string newPaymentDestination)
|
||||||
|
{
|
||||||
|
BOLT11 = newPaymentDestination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Payments.Lightning
|
||||||
|
{
|
||||||
|
public class LightningSupportedPaymentMethod : ISupportedPaymentMethod
|
||||||
|
{
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||||
|
public string LightningChargeUrl { get; set; }
|
||||||
|
|
||||||
|
public Uri GetLightningChargeUrl(bool withCredentials)
|
||||||
|
{
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
UriBuilder uri = new UriBuilder(LightningChargeUrl);
|
||||||
|
if (withCredentials)
|
||||||
|
{
|
||||||
|
uri.UserName = Username;
|
||||||
|
uri.Password = Password;
|
||||||
|
}
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
return uri.Uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLightningChargeUrl(Uri uri)
|
||||||
|
{
|
||||||
|
if (uri == null)
|
||||||
|
throw new ArgumentNullException(nameof(uri));
|
||||||
|
if (string.IsNullOrEmpty(uri.UserInfo))
|
||||||
|
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
|
||||||
|
var splitted = uri.UserInfo.Split(':');
|
||||||
|
if (splitted.Length != 2)
|
||||||
|
throw new ArgumentException(paramName: nameof(uri), message: "Uri should have credential information");
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
Username = splitted[0];
|
||||||
|
Password = splitted[1];
|
||||||
|
LightningChargeUrl = new UriBuilder(uri) { UserName = "", Password = "" }.Uri.AbsoluteUri;
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||||
|
public string Username { get; set; }
|
||||||
|
[Obsolete("Use Get/SetLightningChargeUrl")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
public PaymentMethodId PaymentId => new PaymentMethodId(CryptoCode, PaymentTypes.LightningLike);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,13 +17,27 @@ namespace BTCPayServer.Payments
|
||||||
return BTCPayServer.DerivationStrategy.Parse(((JValue)value).Value<string>(), network);
|
return BTCPayServer.DerivationStrategy.Parse(((JValue)value).Value<string>(), network);
|
||||||
}
|
}
|
||||||
//////////
|
//////////
|
||||||
else // if(paymentMethodId.PaymentType == PaymentTypes.Lightning)
|
else if (paymentMethodId.PaymentType == PaymentTypes.LightningLike)
|
||||||
{
|
{
|
||||||
// return JsonConvert.Deserialize<T>();
|
return JsonConvert.DeserializeObject<Payments.Lightning.LightningSupportedPaymentMethod>(value.ToString());
|
||||||
}
|
}
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IPaymentMethodDetails DeserializePaymentMethodDetails(PaymentMethodId paymentMethodId, JObject jobj)
|
||||||
|
{
|
||||||
|
if(paymentMethodId.PaymentType == PaymentTypes.BTCLike)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(jobj.ToString());
|
||||||
|
}
|
||||||
|
if (paymentMethodId.PaymentType == PaymentTypes.LightningLike)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentMethodDetails>(jobj.ToString());
|
||||||
|
}
|
||||||
|
throw new NotSupportedException(paymentMethodId.PaymentType.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static JToken Serialize(ISupportedPaymentMethod factory)
|
public static JToken Serialize(ISupportedPaymentMethod factory)
|
||||||
{
|
{
|
||||||
// Legacy
|
// Legacy
|
||||||
|
@ -39,5 +53,6 @@ namespace BTCPayServer.Payments
|
||||||
}
|
}
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,10 @@ namespace BTCPayServer.Payments
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// On-Chain UTXO based, bitcoin compatible
|
/// On-Chain UTXO based, bitcoin compatible
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BTCLike
|
BTCLike,
|
||||||
|
/// <summary>
|
||||||
|
/// Lightning payment
|
||||||
|
/// </summary>
|
||||||
|
LightningLike
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,14 @@ namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsDevelopping
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ChainType == ChainType.Regtest && Environment.IsDevelopment();
|
||||||
|
}
|
||||||
|
}
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
StringBuilder txt = new StringBuilder();
|
StringBuilder txt = new StringBuilder();
|
||||||
|
|
|
@ -178,7 +178,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
public IEnumerable<T> GetSupportedPaymentMethod<T>(BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod
|
public IEnumerable<T> GetSupportedPaymentMethod<T>(BTCPayNetworkProvider networks) where T : ISupportedPaymentMethod
|
||||||
{
|
{
|
||||||
return GetSupportedPaymentMethod<T>(null, networks);
|
return GetSupportedPaymentMethod<T>(null, networks);
|
||||||
}
|
}
|
||||||
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethod(BTCPayNetworkProvider networks)
|
public IEnumerable<ISupportedPaymentMethod> GetSupportedPaymentMethod(BTCPayNetworkProvider networks)
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
|
@ -365,13 +365,23 @@ namespace BTCPayServer.Services.Invoices
|
||||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
|
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
|
||||||
|
|
||||||
|
|
||||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
if (info.GetId().PaymentType == PaymentTypes.BTCLike)
|
||||||
{
|
{
|
||||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||||
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
{
|
||||||
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
|
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||||
};
|
BIP73 = ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}"),
|
||||||
|
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
|
||||||
|
{
|
||||||
|
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||||
|
{
|
||||||
|
BOLT11 = $"lightning:{cryptoInfo.Address}"
|
||||||
|
};
|
||||||
|
}
|
||||||
#pragma warning disable CS0618
|
#pragma warning disable CS0618
|
||||||
if (info.CryptoCode == "BTC")
|
if (info.CryptoCode == "BTC")
|
||||||
{
|
{
|
||||||
|
@ -479,7 +489,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
|
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
|
||||||
}
|
}
|
||||||
PaymentMethod = obj;
|
PaymentMethod = obj;
|
||||||
foreach(var cryptoData in paymentMethods)
|
foreach (var cryptoData in paymentMethods)
|
||||||
{
|
{
|
||||||
cryptoData.ParentEntity = this;
|
cryptoData.ParentEntity = this;
|
||||||
}
|
}
|
||||||
|
@ -516,6 +526,11 @@ namespace BTCPayServer.Services.Invoices
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of transactions required to pay
|
/// Number of transactions required to pay
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public int TxRequired { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of transactions using this payment method
|
||||||
|
/// </summary>
|
||||||
public int TxCount { get; set; }
|
public int TxCount { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total amount of network fee to pay to the invoice
|
/// Total amount of network fee to pay to the invoice
|
||||||
|
@ -573,25 +588,19 @@ namespace BTCPayServer.Services.Invoices
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails);
|
||||||
if (GetId().PaymentType == PaymentTypes.BTCLike)
|
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||||
{
|
{
|
||||||
var method = DeserializePaymentMethodDetails<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(PaymentMethodDetails);
|
btcLike.TxFee = TxFee;
|
||||||
method.TxFee = TxFee;
|
btcLike.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
|
||||||
method.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
|
btcLike.FeeRate = FeeRate;
|
||||||
method.FeeRate = FeeRate;
|
|
||||||
return method;
|
|
||||||
}
|
}
|
||||||
|
return details;
|
||||||
}
|
}
|
||||||
throw new NotSupportedException(PaymentType);
|
throw new NotSupportedException(PaymentType);
|
||||||
#pragma warning restore CS0618 // Type or member is obsolete
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
}
|
}
|
||||||
|
|
||||||
private T DeserializePaymentMethodDetails<T>(JObject jobj) where T : class, IPaymentMethodDetails
|
|
||||||
{
|
|
||||||
return JsonConvert.DeserializeObject<T>(jobj.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public PaymentMethod SetPaymentMethodDetails(IPaymentMethodDetails paymentMethod)
|
public PaymentMethod SetPaymentMethodDetails(IPaymentMethodDetails paymentMethod)
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618 // Type or member is obsolete
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
@ -639,7 +648,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
|
|
||||||
var paidTxFee = 0m;
|
var paidTxFee = 0m;
|
||||||
bool paidEnough = paid >= RoundUp(totalDue, 8);
|
bool paidEnough = paid >= RoundUp(totalDue, 8);
|
||||||
int txCount = 0;
|
int txRequired = 0;
|
||||||
var payments =
|
var payments =
|
||||||
ParentEntity.GetPayments()
|
ParentEntity.GetPayments()
|
||||||
.Where(p => p.Accounted && paymentPredicate(p))
|
.Where(p => p.Accounted && paymentPredicate(p))
|
||||||
|
@ -657,22 +666,24 @@ namespace BTCPayServer.Services.Invoices
|
||||||
if (GetId() == _.GetpaymentMethodId())
|
if (GetId() == _.GetpaymentMethodId())
|
||||||
{
|
{
|
||||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||||
txCount++;
|
txRequired++;
|
||||||
}
|
}
|
||||||
return _;
|
return _;
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
var accounting = new PaymentMethodAccounting();
|
||||||
|
accounting.TxCount = txRequired;
|
||||||
if (!paidEnough)
|
if (!paidEnough)
|
||||||
{
|
{
|
||||||
txCount++;
|
txRequired++;
|
||||||
totalDue += GetTxFee();
|
totalDue += GetTxFee();
|
||||||
paidTxFee += GetTxFee();
|
paidTxFee += GetTxFee();
|
||||||
}
|
}
|
||||||
var accounting = new PaymentMethodAccounting();
|
|
||||||
accounting.TotalDue = Money.Coins(RoundUp(totalDue, 8));
|
accounting.TotalDue = Money.Coins(RoundUp(totalDue, 8));
|
||||||
accounting.Paid = Money.Coins(paid);
|
accounting.Paid = Money.Coins(paid);
|
||||||
accounting.TxCount = txCount;
|
accounting.TxRequired = txRequired;
|
||||||
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
accounting.CryptoPaid = Money.Coins(cryptoPaid);
|
||||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||||
|
@ -763,6 +774,10 @@ namespace BTCPayServer.Services.Invoices
|
||||||
paymentData.Outpoint = Outpoint;
|
paymentData.Outpoint = Outpoint;
|
||||||
return paymentData;
|
return paymentData;
|
||||||
}
|
}
|
||||||
|
if(GetpaymentMethodId().PaymentType== PaymentTypes.LightningLike)
|
||||||
|
{
|
||||||
|
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentData>(CryptoPaymentData);
|
||||||
|
}
|
||||||
|
|
||||||
throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
|
throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
|
@ -778,18 +793,14 @@ namespace BTCPayServer.Services.Invoices
|
||||||
Output = paymentData.Output;
|
Output = paymentData.Output;
|
||||||
///
|
///
|
||||||
}
|
}
|
||||||
else
|
CryptoPaymentDataType = cryptoPaymentData.GetPaymentType().ToString();
|
||||||
throw new NotSupportedException(cryptoPaymentData.ToString());
|
|
||||||
CryptoPaymentDataType = paymentData.GetPaymentType().ToString();
|
|
||||||
CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData);
|
CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData);
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
|
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
|
||||||
{
|
{
|
||||||
#pragma warning disable CS0618
|
value = value ?? this.GetCryptoPaymentData().GetValue();
|
||||||
value = value ?? Output.Value.ToDecimal(MoneyUnit.BTC);
|
|
||||||
#pragma warning restore CS0618
|
|
||||||
var to = paymentMethodId;
|
var to = paymentMethodId;
|
||||||
var from = this.GetpaymentMethodId();
|
var from = this.GetpaymentMethodId();
|
||||||
if (to == from)
|
if (to == from)
|
||||||
|
@ -838,5 +849,6 @@ namespace BTCPayServer.Services.Invoices
|
||||||
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
|
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
|
||||||
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
|
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
|
||||||
|
|
||||||
|
PaymentTypes GetPaymentType();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -363,7 +363,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
{
|
{
|
||||||
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetpaymentMethodId().ToString()).ToHashSet();
|
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetpaymentMethodId().ToString()).ToHashSet();
|
||||||
}
|
}
|
||||||
if(invoice.Events != null)
|
if (invoice.Events != null)
|
||||||
{
|
{
|
||||||
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
|
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
|
||||||
}
|
}
|
||||||
|
@ -461,7 +461,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode)
|
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
|
||||||
{
|
{
|
||||||
using (var context = _ContextFactory.CreateContext())
|
using (var context = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
|
@ -471,7 +471,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
CryptoCode = cryptoCode,
|
CryptoCode = cryptoCode,
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
ReceivedTime = date.UtcDateTime,
|
ReceivedTime = date.UtcDateTime,
|
||||||
Accounted = false
|
Accounted = accounted
|
||||||
};
|
};
|
||||||
entity.SetCryptoPaymentData(paymentData);
|
entity.SetCryptoPaymentData(paymentData);
|
||||||
|
|
||||||
|
@ -481,7 +481,7 @@ namespace BTCPayServer.Services.Invoices
|
||||||
Id = paymentData.GetPaymentId(),
|
Id = paymentData.GetPaymentId(),
|
||||||
Blob = ToBytes(entity, null),
|
Blob = ToBytes(entity, null),
|
||||||
InvoiceDataId = invoiceId,
|
InvoiceDataId = invoiceId,
|
||||||
Accounted = false
|
Accounted = accounted
|
||||||
};
|
};
|
||||||
|
|
||||||
context.Payments.Add(data);
|
context.Payments.Add(data);
|
||||||
|
|
|
@ -159,7 +159,7 @@
|
||||||
<div class="bp-view payment scan" id="scan">
|
<div class="bp-view payment scan" id="scan">
|
||||||
<div class="payment__scan">
|
<div class="payment__scan">
|
||||||
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||||
<qrcode :val="srvModel.invoiceBitcoinUrl" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
<qrcode :val="srvModel.invoiceBitcoinUrlQR" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
||||||
</div>
|
</div>
|
||||||
<div class="payment__details__instruction__open-wallet">
|
<div class="payment__details__instruction__open-wallet">
|
||||||
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
|
||||||
|
@ -363,7 +363,7 @@
|
||||||
<div class="manual-box__address__wrapper__logo">
|
<div class="manual-box__address__wrapper__logo">
|
||||||
<img :src="srvModel.cryptoImage" />
|
<img :src="srvModel.cryptoImage" />
|
||||||
</div>
|
</div>
|
||||||
<div class="manual-box__address__wrapper__value">{{srvModel.btcAddress}}</div>
|
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copied-label" style="top: 5px;">
|
<div class="copied-label" style="top: 5px;">
|
||||||
<span i18n="">Copied</span>
|
<span i18n="">Copied</span>
|
||||||
|
|
47
BTCPayServer/Views/Stores/AddLightningNode.cshtml
Normal file
47
BTCPayServer/Views/Stores/AddLightningNode.cshtml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||||
|
@model LightningNodeViewModel
|
||||||
|
@{
|
||||||
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
|
ViewData["Title"] = "Add lightning node (Experimental)";
|
||||||
|
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
|
||||||
|
}
|
||||||
|
|
||||||
|
<h4>@ViewData["Title"]</h4>
|
||||||
|
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div asp-validation-summary="All" class="text-danger"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(env.IsDevelopping) {
|
||||||
|
<div class="alert alert-info alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||||
|
<span>To test during development, please use http://api-token:foiewnccewuify@127.0.0.1:54938/ </span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<form method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Lightning node url</h5>
|
||||||
|
<span>This URL should point to an installed lightning charge server</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="CryptoCurrency"></label>
|
||||||
|
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="Url"></label>
|
||||||
|
<input asp-for="Url" class="form-control" />
|
||||||
|
<span asp-validation-for="Url" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<button name="command" type="submit" value="save" class="btn btn-success">Submit</button>
|
||||||
|
<button name="command" type="submit" value="test" class="btn btn-info">Test connection</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||||
|
}
|
|
@ -72,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<h5>Derivation Scheme</h5>
|
<h5>Derivation Scheme</h5>
|
||||||
<span>The DerivationScheme represents the destination of the funds received by your invoice.</span>
|
<span>The DerivationScheme represents the destination of the funds received by your invoice on chain.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -86,15 +86,42 @@
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach(var scheme in Model.DerivationSchemes)
|
@foreach(var scheme in Model.DerivationSchemes)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@scheme.Crypto</td>
|
<td>@scheme.Crypto</td>
|
||||||
<td>@scheme.Value</td>
|
<td>@scheme.Value</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-group">
|
||||||
|
<h5>Lightning nodes (Experimental)</h5>
|
||||||
|
<span>Connection to lightning charge node used to generate lignting network payment</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<a asp-action="AddLightningNode" class="btn btn-success" role="button"><span class="glyphicon glyphicon-plus"></span>Add or modify a lightning node</a>
|
||||||
|
<table class="table">
|
||||||
|
<thead class="thead-inverse">
|
||||||
|
<tr>
|
||||||
|
<th>Crypto</th>
|
||||||
|
<th>Address</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach(var scheme in Model.LightningNodes)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@scheme.CryptoCode</td>
|
||||||
|
<td>@scheme.Address</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue