Compare commits
53 Commits
Author | SHA1 | Date | |
---|---|---|---|
0d91b3286a | |||
396432b873 | |||
15c58434e8 | |||
daad1bdd25 | |||
c60966c725 | |||
fb57d8c3ce | |||
799ce74f65 | |||
8e38d7ceb4 | |||
a1c22e8071 | |||
6d8acf54d6 | |||
a500a89138 | |||
c6d44e7a89 | |||
9eb3aad072 | |||
9355454953 | |||
6467f06c54 | |||
b9b4b5ea39 | |||
e23243565f | |||
d3420532ae | |||
ade1b9d4eb | |||
fc278d12fc | |||
8e5ec822dc | |||
26aac66a76 | |||
a562e90bdb | |||
a0f3698701 | |||
02163f9482 | |||
b74fe171e2 | |||
2785bb4d9b | |||
5eac84d3a3 | |||
a0a2ab6fcd | |||
7730ead8e4 | |||
8eee0dd14c | |||
7dd88d8d8f | |||
56d1d3e645 | |||
c2308675b2 | |||
cb866a1c05 | |||
95290e8331 | |||
f5e62c775b | |||
f533309b49 | |||
d1c70a7cb3 | |||
2f8590ca7a | |||
08badbde56 | |||
8e38da80e0 | |||
cd2e3350b0 | |||
a0d2790491 | |||
8ca99e5635 | |||
5a2563ca7f | |||
a23cd28531 | |||
58a967b59e | |||
9bf0c20198 | |||
6b7ac0e000 | |||
188c0a9a86 | |||
c49479c8ad | |||
2072b6e136 |
@ -82,7 +82,6 @@ namespace BTCPayServer.Tests
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, cryptoCode);
|
||||
|
@ -806,6 +806,66 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCurrencyValue()
|
||||
{
|
||||
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.50 USD", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.50 usd", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1 usd", out result));
|
||||
Assert.Equal("1 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1usd", out result));
|
||||
Assert.Equal("1 USD", result.ToString());
|
||||
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
|
||||
Assert.Equal("1.50 USD", result.ToString());
|
||||
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
|
||||
Assert.False(CurrencyValue.TryParse("1.501", out result));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
|
||||
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
|
||||
// Passing electrum stuff
|
||||
// Native
|
||||
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
|
||||
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
|
||||
// P2SH
|
||||
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
|
||||
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
|
||||
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
|
||||
////////////////
|
||||
|
||||
var tpub = "tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o";
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal(tpub, result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[legacy]", result.ToString());
|
||||
|
||||
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
|
||||
result = parser.Parse($"{tpub}-[legacy]");
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
|
||||
result = parser.Parse(tpub);
|
||||
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.59</Version>
|
||||
<Version>1.0.1.70</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@ -208,10 +208,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en",
|
||||
DefaultLang = storeBlob.DefaultLang ?? "en-US",
|
||||
CustomCSSLink = storeBlob.CustomCSS?.AbsoluteUri,
|
||||
CustomLogoLink = storeBlob.CustomLogo?.AbsoluteUri,
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
|
@ -79,33 +79,10 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
|
||||
IsAvailable: Task.FromResult(false)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(c =>
|
||||
{
|
||||
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
|
||||
return c;
|
||||
})
|
||||
.ToList();
|
||||
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
|
||||
{
|
||||
if(!await supportedPaymentMethod.IsAvailable)
|
||||
{
|
||||
supportedPaymentMethods.Remove(supportedPaymentMethod);
|
||||
}
|
||||
}
|
||||
if (supportedPaymentMethods.Count == 0)
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
};
|
||||
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
|
||||
@ -133,33 +110,54 @@ namespace BTCPayServer.Controllers
|
||||
entity.Status = "new";
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
var methods = supportedPaymentMethods
|
||||
.Select(async o =>
|
||||
{
|
||||
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
});
|
||||
|
||||
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.Select(c =>
|
||||
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
|
||||
SupportedPaymentMethod: c,
|
||||
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode)))
|
||||
.Where(c => c.Network != null)
|
||||
.Select(o =>
|
||||
(SupportedPaymentMethod: o.SupportedPaymentMethod,
|
||||
PaymentMethod: CreatePaymentMethodAsync(o.Handler, o.SupportedPaymentMethod, o.Network, entity, storeBlob)))
|
||||
.ToList();
|
||||
|
||||
List<string> paymentMethodErrors = new List<string>();
|
||||
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var method in methods)
|
||||
foreach (var o in supportedPaymentMethods)
|
||||
{
|
||||
paymentMethods.Add(await method);
|
||||
try
|
||||
{
|
||||
var paymentMethod = await o.PaymentMethod;
|
||||
if (paymentMethod == null)
|
||||
throw new PaymentMethodUnavailableException("Payment method unavailable (The handler returned null)");
|
||||
supported.Add(o.SupportedPaymentMethod);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
catch (PaymentMethodUnavailableException ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Payment method unavailable ({ex.Message})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
paymentMethodErrors.Add($"{o.SupportedPaymentMethod.PaymentId.CryptoCode} ({o.SupportedPaymentMethod.PaymentId.PaymentType}): Unexpected exception ({ex.ToString()})");
|
||||
}
|
||||
}
|
||||
|
||||
if (supported.Count == 0)
|
||||
{
|
||||
StringBuilder errors = new StringBuilder();
|
||||
errors.AppendLine("No payment method available for this store");
|
||||
foreach(var error in paymentMethodErrors)
|
||||
{
|
||||
errors.AppendLine(error);
|
||||
}
|
||||
throw new BitpayHttpException(400, errors.ToString());
|
||||
}
|
||||
|
||||
entity.SetSupportedPaymentMethods(supported);
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
#pragma warning disable CS0618
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
|
||||
@ -177,15 +175,58 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, paymentMethodErrors, _NetworkProvider);
|
||||
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private async Task<PaymentMethod> CreatePaymentMethodAsync(IPaymentMethodHandler handler, ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceEntity entity, StoreBlob storeBlob)
|
||||
{
|
||||
var rate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(entity.ProductInformation.Currency);
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.ParentEntity = entity;
|
||||
paymentMethod.Network = network;
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, network);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
// Check if Lightning Max value is exceeded
|
||||
if (supportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
|
||||
storeBlob.LightningMaxValue != null)
|
||||
{
|
||||
var lightningMaxValue = storeBlob.LightningMaxValue;
|
||||
var lightningMaxValueRate = 0.0m;
|
||||
if (lightningMaxValue.Currency == entity.ProductInformation.Currency)
|
||||
lightningMaxValueRate = paymentMethod.Rate;
|
||||
else
|
||||
lightningMaxValueRate = await storeBlob.ApplyRateRules(network, _RateProviders.GetRateProvider(network, false)).GetRateAsync(lightningMaxValue.Currency);
|
||||
|
||||
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / lightningMaxValueRate);
|
||||
if (paymentMethod.Calculate().Due > lightningMaxValueCrypto)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("Lightning max value exceeded");
|
||||
}
|
||||
}
|
||||
///////////////
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return paymentMethod;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
|
@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
}
|
||||
@ -86,8 +86,38 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (!vm.Confirmation && strategy != null)
|
||||
return ShowAddresses(vm, strategy);
|
||||
|
||||
if (vm.Confirmation || strategy == null)
|
||||
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
|
||||
{
|
||||
BitcoinAddress address = null;
|
||||
try
|
||||
{
|
||||
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
vm.HintAddress = "";
|
||||
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
|
||||
ModelState.Remove(nameof(vm.HintAddress));
|
||||
ModelState.Remove(nameof(vm.DerivationScheme));
|
||||
return ShowAddresses(vm, strategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -105,23 +135,24 @@ namespace BTCPayServer.Controllers
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
|
||||
{
|
||||
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
|
||||
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(strategy.Network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
public class GetInfoResult
|
||||
@ -219,7 +250,8 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account); ;
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account);
|
||||
;
|
||||
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
|
||||
result = getxpubResult;
|
||||
}
|
||||
@ -240,13 +272,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||
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,
|
||||
|
@ -103,7 +103,7 @@ namespace BTCPayServer.Controllers
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
@ -131,22 +131,22 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
await FillUsers(storeId, vm);
|
||||
if(!ModelState.IsValid)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
var user = await _UserManager.FindByEmailAsync(vm.Email);
|
||||
if(user == null)
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "User not found");
|
||||
return View(vm);
|
||||
}
|
||||
if(!StoreRoles.AllRoles.Contains(vm.Role))
|
||||
if (!StoreRoles.AllRoles.Contains(vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
|
||||
return View(vm);
|
||||
}
|
||||
if(!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
|
||||
return View(vm);
|
||||
@ -183,6 +183,69 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new CheckoutExperienceViewModel();
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.CustomCSS = storeBlob.CustomCSS;
|
||||
vm.CustomLogo = storeBlob.CustomLogo;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/checkout")]
|
||||
public async Task<IActionResult> CheckoutExperience(string storeId, CheckoutExperienceViewModel model)
|
||||
{
|
||||
CurrencyValue currencyValue = null;
|
||||
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
|
||||
{
|
||||
if (!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value");
|
||||
}
|
||||
}
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
bool needUpdate = false;
|
||||
var blob = store.GetStoreBlob();
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.LightningMaxValue = currencyValue;
|
||||
blob.CustomLogo = model.CustomLogo;
|
||||
blob.CustomCSS = model.CustomCSS;
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "Store successfully updated";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(CheckoutExperience), new
|
||||
{
|
||||
storeId = storeId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId)
|
||||
@ -195,25 +258,21 @@ namespace BTCPayServer.Controllers
|
||||
var vm = new StoreViewModel();
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
AddPaymentMethods(store, vm);
|
||||
vm.StatusMessage = StatusMessage;
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
{
|
||||
var derivationByCryptoCode =
|
||||
var derivationByCryptoCode =
|
||||
store
|
||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||
.OfType<DerivationStrategy>()
|
||||
@ -248,7 +307,6 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
{
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -277,25 +335,15 @@ namespace BTCPayServer.Controllers
|
||||
store.StoreWebsite = model.StoreWebsite;
|
||||
}
|
||||
|
||||
if (store.GetDefaultCrypto() != model.DefaultCryptoCurrency)
|
||||
{
|
||||
needUpdate = true;
|
||||
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
|
||||
}
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
@ -327,41 +375,11 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
if (format == "Electrum")
|
||||
{
|
||||
//Unsupported Electrum
|
||||
//var p2wsh_p2sh = 0x295b43fU;
|
||||
//var p2wsh = 0x2aa7ed3U;
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var data = Encoders.Base58Check.DecodeData(derivationScheme);
|
||||
if (data.Length < 4)
|
||||
throw new FormatException("data.Length < 4");
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
|
||||
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
|
||||
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
data[i] = standardPrefix[i];
|
||||
|
||||
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
|
||||
foreach (var label in labels)
|
||||
{
|
||||
derivationScheme = derivationScheme + $"-[{label}]";
|
||||
}
|
||||
}
|
||||
|
||||
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -519,7 +537,7 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null || pairing == null)
|
||||
return NotFound();
|
||||
|
||||
if(store.Role != StoreRoles.Owner)
|
||||
if (store.Role != StoreRoles.Owner)
|
||||
{
|
||||
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
|
||||
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
|
||||
|
44
BTCPayServer/CurrencyValue.cs
Normal file
44
BTCPayServer/CurrencyValue.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class CurrencyValue
|
||||
{
|
||||
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
|
||||
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
|
||||
public static bool TryParse(string str, out CurrencyValue value)
|
||||
{
|
||||
value = null;
|
||||
var match = _Regex.Match(str);
|
||||
if (!match.Success ||
|
||||
!decimal.TryParse(match.Groups[1].Value, out var v))
|
||||
return false;
|
||||
|
||||
var currency = match.Groups.Last().Value.ToUpperInvariant();
|
||||
var currencyData = _CurrencyTable.GetCurrencyData(currency);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
v = Math.Round(v, currencyData.Divisibility);
|
||||
value = new CurrencyValue()
|
||||
{
|
||||
Value = v,
|
||||
Currency = currency
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
public decimal Value { get; set; }
|
||||
public string Currency { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -254,6 +255,14 @@ namespace BTCPayServer.Data
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
[JsonConverter(typeof(CurrencyValueJsonConverter))]
|
||||
public CurrencyValue LightningMaxValue { get; set; }
|
||||
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomLogo { get; set; }
|
||||
[JsonConverter(typeof(UriJsonConverter))]
|
||||
public Uri CustomCSS { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
|
180
BTCPayServer/DerivationSchemeParser.cs
Normal file
180
BTCPayServer/DerivationSchemeParser.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public ChainType ChainType { get; set; }
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
ChainType = chainType;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
str = str.Trim();
|
||||
|
||||
HashSet<string> hintedLabels = new HashSet<string>();
|
||||
|
||||
var hintDestination = HintScriptPubKey?.GetDestination();
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is KeyId)
|
||||
{
|
||||
hintedLabels.Add("legacy");
|
||||
}
|
||||
if (hintDestination is ScriptId)
|
||||
{
|
||||
hintedLabels.Add("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = new DerivationStrategyFactory(Network).Parse(str);
|
||||
return FindMatch(hintedLabels, result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
|
||||
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
|
||||
var standard = 0x0488b21eU;
|
||||
electrumMapping.Add(standard, new[] { "legacy" });
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var parts = str.Split('-');
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (IsLabel(parts[i]))
|
||||
{
|
||||
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = Encoders.Base58Check.DecodeData(parts[i]);
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
|
||||
electrumMapping.TryGetValue(prefix, out string[] labels);
|
||||
if (labels != null)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
{
|
||||
hintedLabels.Add(label.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
parts[i] = derivationScheme;
|
||||
}
|
||||
catch { continue; }
|
||||
}
|
||||
|
||||
if (hintDestination != null)
|
||||
{
|
||||
if (hintDestination is WitKeyId)
|
||||
{
|
||||
hintedLabels.Remove("legacy");
|
||||
hintedLabels.Remove("p2sh");
|
||||
}
|
||||
}
|
||||
|
||||
str = string.Join('-', parts.Where(p => !IsLabel(p)));
|
||||
foreach (var label in hintedLabels)
|
||||
{
|
||||
str = $"{str}-[{label}]";
|
||||
}
|
||||
|
||||
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
|
||||
}
|
||||
|
||||
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
|
||||
{
|
||||
var facto = new DerivationStrategyFactory(Network);
|
||||
var firstKeyPath = new KeyPath("0/0");
|
||||
if (HintScriptPubKey == null)
|
||||
return result;
|
||||
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
|
||||
return result;
|
||||
|
||||
if (result is MultisigDerivationStrategy)
|
||||
hintLabels.Add("keeporder");
|
||||
|
||||
var resultNoLabels = result.ToString();
|
||||
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
|
||||
foreach (var labels in ItemCombinations(hintLabels.ToList()))
|
||||
{
|
||||
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
|
||||
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
|
||||
return hinted;
|
||||
}
|
||||
throw new FormatException("Could not find any match");
|
||||
}
|
||||
|
||||
private static bool IsLabel(string v)
|
||||
{
|
||||
return v.StartsWith('[') && v.EndsWith(']');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method to create lists containing possible combinations of an input list of items. This is
|
||||
/// basically copied from code by user "jaolho" on this thread:
|
||||
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
|
||||
/// </summary>
|
||||
/// <typeparam name="T">type of the items on the input list</typeparam>
|
||||
/// <param name="inputList">list of items</param>
|
||||
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
|
||||
/// if zero the empty combination is included,
|
||||
/// default is one</param>
|
||||
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
|
||||
/// default is no maximum limit</param>
|
||||
/// <returns>list of lists for possible combinations of the input items</returns>
|
||||
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
|
||||
int maximumItems = int.MaxValue)
|
||||
{
|
||||
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
|
||||
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
|
||||
|
||||
if (minimumItems == 0) // Optimize default case
|
||||
listOfLists.Add(new List<T>());
|
||||
|
||||
for (int i = 1; i <= nonEmptyCombinations; i++)
|
||||
{
|
||||
List<T> thisCombination = new List<T>(inputList.Count);
|
||||
for (int j = 0; j < inputList.Count; j++)
|
||||
{
|
||||
if ((i >> j & 1) == 1)
|
||||
thisCombination.Add(inputList[j]);
|
||||
}
|
||||
|
||||
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
|
||||
listOfLists.Add(thisCombination);
|
||||
}
|
||||
|
||||
return listOfLists;
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/JsonConverters/CurrencyValueJsonConverter.cs
Normal file
38
BTCPayServer/JsonConverters/CurrencyValueJsonConverter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class CurrencyValueJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
CurrencyValue.TryParse((string)reader.Value, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if(value != null)
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/JsonConverters/UriJsonConverter.cs
Normal file
38
BTCPayServer/JsonConverters/UriJsonConverter.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace BTCPayServer.JsonConverters
|
||||
{
|
||||
public class UriJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(Uri).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TokenType == JsonToken.Null ? null :
|
||||
Uri.TryCreate((string)reader.Value, UriKind.Absolute, out var result) ? result :
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Invalid Currency value", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value != null)
|
||||
writer.WriteValue(((Uri)value).AbsoluteUri);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,9 +13,11 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string DefaultLang { get; set; }
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public bool IsLightning { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
|
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class CheckoutExperienceViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList Languages { get; set; }
|
||||
|
||||
[Display(Name = "Default crypto currency on checkout")]
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Link to a custom CSS stylesheet")]
|
||||
[Url]
|
||||
public Uri CustomCSS { get; set; }
|
||||
[Display(Name = "Link to a custom logo")]
|
||||
[Url]
|
||||
public Uri CustomLogo { get; set; }
|
||||
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 chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultCryptoCurrency = chosen.Name;
|
||||
}
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
defaultLang = defaultLang ?? "en-US";
|
||||
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
|
||||
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultLang = chosen.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,20 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class DerivationSchemeViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public DerivationSchemeViewModel()
|
||||
{
|
||||
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
|
||||
DerivationSchemeFormat = btcPay.Value;
|
||||
DerivationSchemeFormats = new SelectList(new Format[]
|
||||
{
|
||||
btcPay,
|
||||
new Format { Name = "Electrum", Value = "Electrum" },
|
||||
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
|
||||
}
|
||||
public string DerivationScheme
|
||||
{
|
||||
@ -34,18 +22,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
[Display(Name = "Derivation Scheme format")]
|
||||
public string DerivationSchemeFormat
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
[Display(Name = "Hint address")]
|
||||
public string HintAddress { get; set; }
|
||||
public bool Confirmation { get; set; }
|
||||
|
||||
public SelectList DerivationSchemeFormats { get; set; }
|
||||
|
||||
public string ServerUrl { get; set; }
|
||||
public string StatusMessage { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string Crypto { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public StoreViewModel()
|
||||
{
|
||||
|
||||
@ -95,24 +91,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Allow conversion through third party (Shapeshift, Changelly...)")]
|
||||
public bool AllowCoinConversion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList Languages { get; set; }
|
||||
|
||||
[Display(Name = "Default crypto currency on checkout")]
|
||||
public string DefaultCryptoCurrency { get; set; }
|
||||
[Display(Name = "Default language on checkout")]
|
||||
public string DefaultLang { get; set; }
|
||||
|
||||
public class LightningNode
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
@ -123,21 +101,5 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
} = new List<LightningNode>();
|
||||
|
||||
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 chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultCryptoCurrency = chosen.Name;
|
||||
}
|
||||
|
||||
public void SetLanguages(LanguageService langService, string defaultLang)
|
||||
{
|
||||
defaultLang = defaultLang ?? "en-US";
|
||||
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
|
||||
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
DefaultLang = chosen.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_ExplorerProvider.IsAvailable(network))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
|
||||
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
@ -37,10 +39,5 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
onchainMethod.DepositAddress = (await getAddress).ToString();
|
||||
return onchainMethod;
|
||||
}
|
||||
|
||||
public override Task<bool> IsAvailable(DerivationStrategy supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
return Task.FromResult(_ExplorerProvider.IsAvailable(network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,6 @@ namespace BTCPayServer.Payments
|
||||
/// </summary>
|
||||
public interface IPaymentMethodHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the dependencies for a specific payment method are satisfied.
|
||||
/// </summary>
|
||||
/// <param name="supportedPaymentMethod"></param>
|
||||
/// <param name="network"></param>
|
||||
/// <returns>true if this payment method is available</returns>
|
||||
Task<bool> IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
/// <summary>
|
||||
/// Create needed to track payments of this invoice
|
||||
/// </summary>
|
||||
@ -31,7 +23,6 @@ namespace BTCPayServer.Payments
|
||||
|
||||
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
|
||||
{
|
||||
Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network);
|
||||
}
|
||||
|
||||
@ -47,16 +38,5 @@ namespace BTCPayServer.Payments
|
||||
}
|
||||
throw new NotSupportedException("Invalid supportedPaymentMethod");
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsAvailable(T supportedPaymentMethod, BTCPayNetwork network);
|
||||
|
||||
Task<bool> IPaymentMethodHandler.IsAvailable(ISupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if(supportedPaymentMethod is T method)
|
||||
{
|
||||
return IsAvailable(method, network);
|
||||
}
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
{
|
||||
public class CreateInvoiceResponse
|
||||
public class CLightningInvoice
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
[JsonProperty("payment_hash")]
|
@ -7,7 +7,6 @@ using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments.Lightning.Charge;
|
||||
using Mono.Unix;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
@ -42,9 +41,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Task<GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
public Task<Charge.GetInfoResponse> GetInfoAsync(CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
return SendCommandAsync<GetInfoResponse>("getinfo", cancellation: cancellation);
|
||||
return SendCommandAsync<Charge.GetInfoResponse>("getinfo", cancellation: cancellation);
|
||||
}
|
||||
|
||||
public Task SendAsync(string bolt11)
|
||||
@ -168,24 +167,24 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.GetInvoice(string invoiceId, CancellationToken cancellation)
|
||||
{
|
||||
var invoices = await SendCommandAsync<ChargeInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
|
||||
var invoices = await SendCommandAsync<CLightningInvoice[]>("listinvoices", new[] { invoiceId }, false, true, cancellation);
|
||||
if (invoices.Length == 0)
|
||||
return null;
|
||||
return ChargeClient.ToLightningInvoice(invoices[0]);
|
||||
return ToLightningInvoice(invoices[0]);
|
||||
}
|
||||
|
||||
static NBitcoin.DataEncoders.DataEncoder InvoiceIdEncoder = NBitcoin.DataEncoders.Encoders.Base58;
|
||||
async Task<LightningInvoice> ILightningInvoiceClient.CreateInvoice(LightMoney amount, TimeSpan expiry, CancellationToken cancellation)
|
||||
{
|
||||
var id = InvoiceIdEncoder.EncodeData(RandomUtils.GetBytes(20));
|
||||
var invoice = await SendCommandAsync<CreateInvoiceResponse>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("invoice", new object[] { amount.MilliSatoshi, id, "" }, cancellation: cancellation);
|
||||
invoice.Label = id;
|
||||
invoice.MilliSatoshi = amount;
|
||||
invoice.Status = "unpaid";
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
private static LightningInvoice ToLightningInvoice(CreateInvoiceResponse invoice)
|
||||
private static LightningInvoice ToLightningInvoice(CLightningInvoice invoice)
|
||||
{
|
||||
return new LightningInvoice()
|
||||
{
|
||||
@ -204,9 +203,9 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
long lastInvoiceIndex = 99999999999;
|
||||
async Task<LightningInvoice> ILightningListenInvoiceSession.WaitInvoice(CancellationToken cancellation)
|
||||
{
|
||||
var chargeInvoice = await SendCommandAsync<CreateInvoiceResponse>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
|
||||
lastInvoiceIndex = chargeInvoice.PayIndex.Value;
|
||||
return ToLightningInvoice(chargeInvoice);
|
||||
var invoice = await SendCommandAsync<CLightningInvoice>("waitanyinvoice", new object[] { lastInvoiceIndex }, cancellation: cancellation);
|
||||
lastInvoiceIndex = invoice.PayIndex.Value;
|
||||
return ToLightningInvoice(invoice);
|
||||
}
|
||||
|
||||
async Task<LightningNodeInformation> ILightningInvoiceClient.GetInfo(CancellationToken cancellation)
|
||||
|
@ -25,30 +25,32 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
var test = Test(supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
|
||||
if (expiry < TimeSpan.Zero)
|
||||
expiry = TimeSpan.FromSeconds(1);
|
||||
var lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
|
||||
|
||||
LightningInvoice lightningInvoice = null;
|
||||
try
|
||||
{
|
||||
lightningInvoice = await client.CreateInvoice(new LightMoney(due, LightMoneyUnit.BTC), expiry);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Impossible to create lightning invoice ({ex.Message})", ex);
|
||||
}
|
||||
var nodeInfo = await test;
|
||||
return new LightningLikePaymentMethodDetails()
|
||||
{
|
||||
BOLT11 = lightningInvoice.BOLT11,
|
||||
InvoiceId = lightningInvoice.Id
|
||||
InvoiceId = lightningInvoice.Id,
|
||||
NodeInfo = nodeInfo.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
public async override Task<bool> IsAvailable(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Test(supportedPaymentMethod, network);
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used for testing
|
||||
/// </summary>
|
||||
@ -57,8 +59,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"Full node not available");
|
||||
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
|
||||
var cts = new CancellationTokenSource(5000);
|
||||
var client = _LightningClientFactory.CreateClient(supportedPaymentMethod, network);
|
||||
LightningNodeInformation info = null;
|
||||
@ -68,37 +70,39 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new Exception($"The lightning node did not replied in a timely maner");
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while connecting to the API ({ex.Message})");
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API ({ex.Message})");
|
||||
}
|
||||
|
||||
if(info.Address == null)
|
||||
if (info.Address == null)
|
||||
{
|
||||
throw new Exception($"No lightning node public address has been configured");
|
||||
throw new PaymentMethodUnavailableException($"No lightning node public address has been configured");
|
||||
}
|
||||
|
||||
var blocksGap = Math.Abs(info.BlockHeight - summary.Status.ChainHeight);
|
||||
if (blocksGap > 10)
|
||||
{
|
||||
throw new Exception($"The lightning is not synched ({blocksGap} blocks)");
|
||||
throw new PaymentMethodUnavailableException($"The lightning is not synched ({blocksGap} blocks)");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if(!SkipP2PTest)
|
||||
if (!SkipP2PTest)
|
||||
{
|
||||
await TestConnection(info.Address, info.P2PPort, cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
|
||||
}
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
}
|
||||
|
||||
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||
private async Task TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||
{
|
||||
IPAddress address = null;
|
||||
try
|
||||
@ -107,25 +111,16 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
|
||||
}
|
||||
catch { }
|
||||
address = (await Dns.GetHostAddressesAsync(addressStr)).FirstOrDefault();
|
||||
}
|
||||
|
||||
if (address == null)
|
||||
throw new Exception($"DNS did not resolved {addressStr}");
|
||||
throw new PaymentMethodUnavailableException($"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; }
|
||||
await WithTimeout(tcp.ConnectAsync(new IPEndPoint(address, port)), cancellation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Task WithTimeout(Task task, CancellationToken token)
|
||||
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
public string BOLT11 { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string NodeInfo { get; set; }
|
||||
|
||||
public string GetPaymentDestination()
|
||||
{
|
||||
|
19
BTCPayServer/Payments/PaymentMethodUnavailableException.cs
Normal file
19
BTCPayServer/Payments/PaymentMethodUnavailableException.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
public class PaymentMethodUnavailableException : Exception
|
||||
{
|
||||
public PaymentMethodUnavailableException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
public PaymentMethodUnavailableException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning disable CS0618
|
||||
public List<PaymentEntity> GetPayments()
|
||||
{
|
||||
return Payments.ToList();
|
||||
return Payments?.ToList() ?? new List<PaymentEntity>();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode)
|
||||
{
|
||||
@ -375,7 +375,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
|
||||
};
|
||||
}
|
||||
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
|
||||
var paymentId = info.GetId();
|
||||
if (paymentId.PaymentType == PaymentTypes.LightningLike)
|
||||
{
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
@ -383,7 +384,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
};
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
if (info.CryptoCode == "BTC")
|
||||
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
dto.Url = cryptoInfo.Url;
|
||||
dto.BTCPrice = cryptoInfo.Price;
|
||||
|
@ -101,7 +101,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, IEnumerable<string> creationLogs, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
List<string> textSearch = new List<string>();
|
||||
invoice = Clone(invoice, null);
|
||||
@ -146,6 +146,17 @@ namespace BTCPayServer.Services.Invoices
|
||||
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
|
||||
}
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
|
||||
foreach(var log in creationLogs)
|
||||
{
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
Message = log,
|
||||
Timestamp = invoice.InvoiceTime,
|
||||
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
|
||||
});
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
@ -23,11 +23,12 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
new Language("en-US", "English"),
|
||||
new Language("de-DE", "Deutsch"),
|
||||
//new Language("ja-JP", "日本語"),
|
||||
new Language("ja-JP", "日本語"),
|
||||
new Language("fr-FR", "Français"),
|
||||
//new Language("es-ES", "Spanish"),
|
||||
new Language("es-ES", "Spanish"),
|
||||
new Language("pt-BR", "Portuguese (Brazil)"),
|
||||
new Language("nl-NL", "Dutch"),
|
||||
new Language("cs-CZ", "Česky"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
|
||||
{
|
||||
if (currency == "BTC")
|
||||
return 1.0m;
|
||||
if (rates.TryGetValue(currency, out decimal result))
|
||||
return result;
|
||||
throw new RateUnavailableException(currency);
|
||||
|
@ -3,7 +3,14 @@
|
||||
<div class="top-header">
|
||||
<div class="header">
|
||||
<div class="header__icon">
|
||||
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
|
||||
@if(Model.CustomLogoLink != null)
|
||||
{
|
||||
<img class="header__icon__img" src="@Model.CustomLogoLink" height="40">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="timer-row">
|
||||
@ -26,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-details">
|
||||
@if (Model.AvailableCryptos.Count > 1)
|
||||
@if(Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div class="currency-selection">
|
||||
<div class="single-item-order__left">
|
||||
@ -36,7 +43,7 @@
|
||||
</div>
|
||||
<div class="single-item-order__right">
|
||||
<div class="payment__currencies">
|
||||
@foreach (var crypto in Model.AvailableCryptos)
|
||||
@foreach(var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
|
||||
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
|
||||
@ -101,7 +108,7 @@
|
||||
<div class="payment-tabs__tab" id="copy-tab">
|
||||
<span>{{$t("Copy")}}</span>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
@if(Model.AllowCoinConversion)
|
||||
{
|
||||
<div class="payment-tabs__tab" id="altcoins-tab">
|
||||
<span>{{$t("Conversion")}}</span>
|
||||
@ -142,7 +149,7 @@
|
||||
</div>
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" />
|
||||
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
</qrcode>
|
||||
</div>
|
||||
@ -193,10 +200,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.AllowCoinConversion)
|
||||
@if(Model.AllowCoinConversion)
|
||||
{
|
||||
<div id="altcoins" class="bp-view payment manual-flow">
|
||||
<div v-if="srvModel.paymentMethodId != 'BTC_LightningLike'">
|
||||
<div v-if="srvModel.isLightning">
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_Lightning")}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_BodyTop", srvModel)}}
|
||||
@ -217,13 +231,6 @@
|
||||
</a>*@
|
||||
</center>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="manual__step-two__instructions">
|
||||
<span>
|
||||
{{$t("ConversionTab_Lightning")}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -279,7 +286,7 @@
|
||||
<div class="expired__text">
|
||||
{{$t("InvoiceExpired_Body_3")}}
|
||||
</div>
|
||||
<div class="expired__text">
|
||||
<div class="expired__text expired__text__smaller">
|
||||
<span class="expired__text__bullet">{{$t("Invoice ID")}}</span>:
|
||||
{{srvModel.invoiceId}}
|
||||
<br />
|
||||
|
@ -20,6 +20,12 @@
|
||||
</script>
|
||||
|
||||
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
|
||||
|
||||
|
||||
@if(Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||
}
|
||||
</head>
|
||||
<body style="background: #E4E4E4">
|
||||
<noscript>
|
||||
@ -83,6 +89,9 @@
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div style="margin-top: 10px; text-align: right;" class="form-text small text-muted">
|
||||
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -98,11 +107,12 @@
|
||||
resources: {
|
||||
'en-US': { translation: locales_en },
|
||||
'de-DE': { translation: locales_de },
|
||||
//'es-ES': { translation: locales_es },
|
||||
//'ja-JP': { translation: locales_ja },
|
||||
'es-ES': { translation: locales_es },
|
||||
'ja-JP': { translation: locales_ja },
|
||||
'fr-FR': { translation: locales_fr },
|
||||
'pt-BR': { translation: locales_pt_br },
|
||||
'nl': { translation: locales_nl }
|
||||
'nl': { translation: locales_nl },
|
||||
'cs-CZ': { translation: locales_cs }
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
|
||||
}
|
||||
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
||||
<div class="row">
|
||||
@ -32,17 +33,13 @@
|
||||
<div id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||
<span>A ledger wallet is detected, which account do you want to use?</span>
|
||||
<ul>
|
||||
@for(int i = 0; i < 4; i++)
|
||||
{
|
||||
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
|
||||
}
|
||||
@for(int i = 0; i < 4; i++)
|
||||
{
|
||||
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationSchemeFormat"></label>
|
||||
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span>BTCPay format memo</span>
|
||||
<table class="table">
|
||||
@ -90,7 +87,6 @@
|
||||
</div>
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<input type="hidden" asp-for="DerivationSchemeFormat" />
|
||||
<div class="form-group">
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
@ -110,6 +106,16 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<h5>Wrong addresses?</h5>
|
||||
<span>Help us to find the correct settings by telling us the first address of your wallet</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="HintAddress"></label>
|
||||
<input asp-for="HintAddress" class="form-control" />
|
||||
<span asp-validation-for="HintAddress" class="text-danger"></span>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
}
|
||||
</form>
|
||||
|
54
BTCPayServer/Views/Stores/CheckoutExperience.cshtml
Normal file
54
BTCPayServer/Views/Stores/CheckoutExperience.cshtml
Normal file
@ -0,0 +1,54 @@
|
||||
@model CheckoutExperienceViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Checkout experience";
|
||||
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Checkout);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomLogo"></label>
|
||||
<input asp-for="CustomLogo" class="form-control" />
|
||||
<span asp-validation-for="CustomLogo" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSS"></label>
|
||||
<input asp-for="CustomCSS" class="form-control" />
|
||||
<span asp-validation-for="CustomCSS" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultCryptoCurrency"></label>
|
||||
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultLang"></label>
|
||||
<select asp-for="DefaultLang" asp-items="Model.Languages" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AllowCoinConversion"></label>
|
||||
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="LightningMaxValue"></label>
|
||||
<input asp-for="LightningMaxValue" class="form-control" />
|
||||
<span asp-validation-for="LightningMaxValue" class="text-danger"></span>
|
||||
<p class="form-text text-muted">Example: 5.50 USD</p>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -11,13 +11,14 @@ namespace BTCPayServer.Views.Stores
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
public static string Index => "Index";
|
||||
|
||||
public static string Checkout => "Checkout experience";
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
public static string Users => "Users";
|
||||
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
||||
|
||||
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
|
@ -17,7 +17,8 @@
|
||||
<div class="col-md-8">
|
||||
<div class="form-group">
|
||||
<h5>Users</h5>
|
||||
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)</span>
|
||||
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)<br />
|
||||
Note that the user must have a registered account on this BTCPay Server.</span>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<form method="post">
|
||||
|
@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@ -30,14 +30,6 @@
|
||||
<input asp-for="StoreWebsite" class="form-control" />
|
||||
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultCryptoCurrency"></label>
|
||||
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DefaultLang"></label>
|
||||
<select asp-for="DefaultLang" asp-items="Model.Languages" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
@ -74,10 +66,6 @@
|
||||
</select>
|
||||
<span asp-validation-for="SpeedPolicy" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AllowCoinConversion"></label>
|
||||
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h5>Derivation Scheme</h5>
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice on chain.</span>
|
||||
@ -94,19 +82,19 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var scheme in Model.DerivationSchemes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Crypto</td>
|
||||
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
|
||||
<td style="text-align:right">
|
||||
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
{
|
||||
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
|
||||
}
|
||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Crypto</td>
|
||||
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
|
||||
<td style="text-align:right">
|
||||
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
{
|
||||
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
|
||||
}
|
||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -116,7 +104,7 @@
|
||||
<h5>Lightning nodes (Experimental)</h5>
|
||||
<p>
|
||||
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices.<br /></span>
|
||||
<span>This is experimental and not advised for production so keep in mind:</span>
|
||||
<span>This is experimental and not advised for production.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -130,13 +118,13 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var scheme in Model.LightningNodes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.CryptoCode</td>
|
||||
<td>@scheme.Address</td>
|
||||
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
|
||||
</tr>
|
||||
}
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.CryptoCode</td>
|
||||
<td>@scheme.Address</td>
|
||||
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -2,7 +2,8 @@
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
|
||||
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">General settings</a></li>
|
||||
<li class="@StoreNavPages.CheckoutNavClass(ViewContext)"><a asp-action="CheckoutExperience">Checkout experience</a></li>
|
||||
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
|
||||
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
|
||||
</ul>
|
||||
|
@ -9550,8 +9550,8 @@ strong {
|
||||
}
|
||||
|
||||
.expired__body {
|
||||
padding: 14px 10px;
|
||||
padding-top: 8px;
|
||||
padding: 0px 8px 0px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.expired__header {
|
||||
@ -9562,10 +9562,14 @@ strong {
|
||||
.expired__text {
|
||||
margin-top: 20px;
|
||||
font-weight: 100;
|
||||
font-size: 14.5px;
|
||||
font-size: 14px;
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.expired__text .expired__text__smaller {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.expired__text__bullet {
|
||||
font-weight: 500;
|
||||
}
|
||||
@ -9879,6 +9883,17 @@ strong {
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.qr_currency_icon {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Warning Page */
|
||||
.manual__step-two--warning {
|
||||
display: block;
|
||||
|
@ -62,7 +62,7 @@ function onDataCallback(jsonData) {
|
||||
}
|
||||
|
||||
// restoring qr code view only when currency is switched
|
||||
if (jsonData.paymentMethodId == srvModel.paymentMethodId) {
|
||||
if (jsonData.paymentMethodId === srvModel.paymentMethodId) {
|
||||
$(".payment__currencies").show();
|
||||
$(".payment__spinner").hide();
|
||||
}
|
||||
@ -72,7 +72,7 @@ function onDataCallback(jsonData) {
|
||||
}
|
||||
|
||||
function changeCurrency(currency) {
|
||||
if (srvModel.paymentMethodId != currency) {
|
||||
if (srvModel.paymentMethodId !== currency) {
|
||||
$(".payment__currencies").hide();
|
||||
$(".payment__spinner").show();
|
||||
srvModel.paymentMethodId = currency;
|
||||
|
48
BTCPayServer/wwwroot/js/checkout/langs/cs.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/cs.js
Normal file
@ -0,0 +1,48 @@
|
||||
const locales_cs = {
|
||||
nested: {
|
||||
lang: 'Jazyk'
|
||||
},
|
||||
"Awaiting Payment...": "Očekávám platbu...",
|
||||
"Pay with": "Zaplatit pomocí",
|
||||
"Contact and Refund Email": "Kontaktní email",
|
||||
"Contact_Body": "Prosímte poskytněte vaši emailovou adresu. Kontaktujeme vás v případě, že se objeví problému s vaší platbou.",
|
||||
"Your email": "Váš email",
|
||||
"Continue": "Pokračovat",
|
||||
"Please enter a valid email address": "Prosíme vložte platnou emailovou adresu",
|
||||
"Order Amount": "Cena objednávky",
|
||||
"Network Cost": "Síťové náklady",
|
||||
"Already Paid": "Již zaplaceno",
|
||||
"Due": "Zbývá",
|
||||
// Tabs
|
||||
"Scan": "Skenovat",
|
||||
"Copy": "Kopírovat",
|
||||
"Conversion": "Konverze",
|
||||
// Scan tab
|
||||
"Open in wallet": "Otevřít v peněžence",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "K dokončení platby, prosíme pošlete {{btcDue}} {{cryptoCode}} na adresu níže.",
|
||||
"Amount": "Částka",
|
||||
"Address": "Adresa",
|
||||
"Copied": "Zkopírováno",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Můžete zaplatit {{btcDue}} {{cryptoCode}} i pomocí altcoinů které přímo nepodporuje obchodník.",
|
||||
"ConversionTab_BodyDesc": "Tato služba je poskytována třetí stranou. Prosíme mějte na paměti, že nemáme žádnou kontrolu nad tím, jak poskytovatelé budou nakládat s vašimi prostředky. Faktura bude označena jako zaplacena, pouze když jsou prostředky obdrženy v {{cryptoCode}} Blockchainu.",
|
||||
"Shapeshift_Button_Text": "Zaplatit pomocí Altcoinů",
|
||||
"ConversionTab_Lightning": "Pro platby Lightning Network nejsou dostupní žádní poskytovatelé konverzí.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "Faktura brzy vyprší...",
|
||||
"Invoice expired": "Faktura vypršela",
|
||||
"What happened?": "Co se stalo?",
|
||||
"InvoiceExpired_Body_1": "Tato faktura již vypršela. Faktura je platná pouze {{maxTimeMinutes}} minut. \
|
||||
Můžete se vrátit do {{storeName}}, pokud chcete svojí objednávku založit znovu.",
|
||||
"InvoiceExpired_Body_2": "Pokud jste se pokoušeli poslat platbu, nebyla zatím zaznamenána v Bitcoinové síti. Zatím jsme neobdrželi vaše prostředky.",
|
||||
"InvoiceExpired_Body_3": "Pokud nebude transakce přijata Bitcoinovou sítí, vaše prostředky bude opět použitelné ve vaší peněžence. V závislosti na vaší peněžence toto může trvat 48-72 hodin.",
|
||||
"Invoice ID": "ID Faktury",
|
||||
"Order ID": "ID Objednávky",
|
||||
"Return to StoreName": "Vrátit se na {{storeName}}",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "Faktura byla zaplacena",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "Tato faktura byla archivována",
|
||||
"Archived_Body": "Prosíme kontaktujte prodejce pro informace o objednávce a případnou pomoc"
|
||||
};
|
@ -28,7 +28,7 @@ const locales_de = {
|
||||
"ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.",
|
||||
"ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst bezahlt, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.",
|
||||
"Shapeshift_Button_Text": "Bezahlen mit Altcoins",
|
||||
"ConversionTab_Lightning": "Für BTC Lightning Network-Zahlungen sind keine Conversion-Anbieter verfügbar.",
|
||||
"ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Conversion-Anbieter verfügbar.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "Die Rechnung läuft bald ab...",
|
||||
"Invoice expired": "Die Rechnung ist abgelaufen",
|
||||
|
@ -28,7 +28,7 @@ const locales_en = {
|
||||
"ConversionTab_BodyTop": "You can pay {{btcDue}} {{cryptoCode}} using altcoins other than the ones merchant directly supports.",
|
||||
"ConversionTab_BodyDesc": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain.",
|
||||
"Shapeshift_Button_Text": "Pay with Altcoins",
|
||||
"ConversionTab_Lightning": "No conversion providers available for BTC Lightning Network payments.",
|
||||
"ConversionTab_Lightning": "No conversion providers available for Lightning Network payments.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "Invoice expiring soon...",
|
||||
"Invoice expired": "Invoice expired",
|
||||
|
@ -1,2 +1,47 @@
|
||||
const locales_es = {
|
||||
};
|
||||
nested: {
|
||||
lang: 'Lenguaje'
|
||||
},
|
||||
"Awaiting Payment...": "En espera de pago...",
|
||||
"Pay with": "Pagar con",
|
||||
"Contact and Refund Email": "Contacto y correo electrónico de reembolso",
|
||||
"Contact_Body": "Por favor provea una dirección de correo electrónico a continuación. Nos pondremos en contacto con usted en esta dirección si hay un problema con su pago.",
|
||||
"Your email": "Tu correo electrónico",
|
||||
"Continue": "Continuar",
|
||||
"Please enter a valid email address": "Por favor entre un correo electrónico valido",
|
||||
"Order Amount": "Total de el pedido",
|
||||
"Network Cost": "Costo de la red",
|
||||
"Already Paid": "Ya pagado",
|
||||
"Due": "Debido",
|
||||
// Tabs
|
||||
"Scan": "Escaniar",
|
||||
"Copy": "Copiar",
|
||||
"Conversion": "Conversión",
|
||||
// Scan tab
|
||||
"Open in wallet": "Abrir en billetera",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "Para completar su pago, envíe {{btcDue}} {{cryptoCode}} a la dirección siguiente.",
|
||||
"Amount": "Cantidad",
|
||||
"Address": "Direccón",
|
||||
"Copied": "Copiado",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Puede pagar {{btcDue}} {{cryptoCode}} usando altcoins que no sean los que el comerciante soporta directamente.",
|
||||
"ConversionTab_BodyDesc": "Este servicio es proveído por terceros. Tenga en cuenta que no tenemos control sobre cómo los proveedores enviarán sus fondos. La factura solo se marcará como abonada una vez que se reciban los fondos en el bloque de cadenas de {{cryptoCode}} .",
|
||||
"Shapeshift_Button_Text": "Pagar con Altcoins",
|
||||
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "La factura expira pronto...",
|
||||
"Invoice expired": "La factura expiro",
|
||||
"What happened?": "¿Qué sucedió?",
|
||||
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \ Puede volver a {{storeName}} si desea volver a enviar su pago.",
|
||||
"InvoiceExpired_Body_2": "Si intentó enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido sus fondos.",
|
||||
"InvoiceExpired_Body_3": "Si la transacción no es aceptada por la red de Bitcoin, los fondos se podrán gastar nuevamente en su billetera. Dependiendo de su billetera, esto puede tomar 48-72 horas.",
|
||||
"Invoice ID": "ID de factura",
|
||||
"Order ID": "ID de pedido",
|
||||
"Return to StoreName": "Regresar a {{storeName}}",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "Esta factura ha sido pagada",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "Esta factura ha sido archivada",
|
||||
"Archived_Body": "Por favor, comuníquese con la tienda para obtener información de su pedido o asistencia"
|
||||
};
|
||||
|
@ -30,13 +30,13 @@ const locales_fr = {
|
||||
"Shapeshift_Button_Text": "Payer avec une crypto-monnaie alternative",
|
||||
"ConversionTab_Lightning": "Pas de fournisseur disponible pour les paiements sur le Lightning Network.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "La facture va bientôt expirée...",
|
||||
"Invoice expiring soon...": "La facture va bientôt expirer...",
|
||||
"Invoice expired": "Facture expiré",
|
||||
"What happened?": "Que s'est t'il passé?",
|
||||
"InvoiceExpired_Body_1": "La facture a expirée. Une facture est seulement valide pour {{maxTimeMinutes}} minutes. \
|
||||
Vous pouvez revenir sur {{storeName}} si vous voulez resoumettre votre paiement.",
|
||||
"InvoiceExpired_Body_2": "Si vous avez essayé d'envoyer un paiement, il n'a pas encore été accepté par la blockchain. Nous n'avons pas encore reçu vos fonds.",
|
||||
"InvoiceExpired_Body_3": "Si votre transaction n'a pas été accepté par la blockchain, vos fonds reviendront et dans votre portefueille. Selon votre portefueille, cela peut prendre entre 48 et 72 heures.",
|
||||
"InvoiceExpired_Body_3": "Si votre transaction n'a pas été accepté par la blockchain, vos fonds reviendront dans votre portefueille. Selon votre portefueille, cela peut prendre entre 48 et 72 heures.",
|
||||
"Invoice ID": "Numéro de facture",
|
||||
"Order ID": "Numéro de commande",
|
||||
"Return to StoreName": "Retourner sur {{storeName}}",
|
||||
|
@ -1,2 +1,48 @@
|
||||
const locales_ja = {
|
||||
const locales_ja = {
|
||||
nested: {
|
||||
lang: 'Language'
|
||||
},
|
||||
"Awaiting Payment...": "お支払いをお待ちしております…",
|
||||
"Pay with": "お支払い方法",
|
||||
"Contact and Refund Email": "問題発生時の連絡先",
|
||||
"Contact_Body": "決済においては、何か問題が発生したらこちらのメールアドレスに対してご連絡差し上げることもございますので、ご記入ください",
|
||||
"Your email": "ご自分のメールアドレス",
|
||||
"Continue": "続ける",
|
||||
"Please enter a valid email address": "正常なメールアドレスをご記入ください",
|
||||
"Order Amount": "注文金額",
|
||||
"Network Cost": "ネットワーク手数料",
|
||||
"Already Paid": "支払い済み金額",
|
||||
"Due": "未払い金額",
|
||||
// Tabs
|
||||
"Scan": "スキャン",
|
||||
"Copy": "コピー",
|
||||
"Conversion": "変換",
|
||||
// Scan tab
|
||||
"Open in wallet": "ウォレットで開く",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "決済をするために、下記のアドレスに {{btcDue}} {{cryptoCode}} をお送りください",
|
||||
"Amount": "金額",
|
||||
"Address": "アドレス",
|
||||
"Copied": "コピーしました",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "代わりに、お店が受け付けていなくても {{btcDue}} {{cryptoCode}} での支払いもできます。",
|
||||
"ConversionTab_BodyDesc": "ただし、この変換は第三者サービスによるものですので、お店が受け付けている通貨で着金するまでの間の処理に関しては何の保証もいたしません。変換後に受付中の通貨 ({{cryptoCode}}) がお店に着金してから支払い済みとなりますのでご了承ください。",
|
||||
"Shapeshift_Button_Text": "他の仮想通貨で支払う",
|
||||
"ConversionTab_Lightning": "ライトニングのペイメントでは現在変換サービスが存在しないためご利用いただけません。ご了承ください。",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "お支払いの期限が迫っています...",
|
||||
"Invoice expired": "お支払いの期限が切れました",
|
||||
"What happened?": "え!?ナニコレ!?",
|
||||
"InvoiceExpired_Body_1": "当件のお支払いの有効期限が過ぎてしまいました。最大 {{maxTimeMinutes}} 分以内に支払うことが義務付けられています。 \
|
||||
まだお支払いのご希望の場合 {{storeName}} に一旦戻っていただき、もう一度お支払いの手続きを最初からやり直してみてください。",
|
||||
"InvoiceExpired_Body_2": "送金手続きを完了したつもりでも、ネットワークにて取り込まれて処理されるまでは決済となりません。現時点ではまだ着金しておりません。",
|
||||
"InvoiceExpired_Body_3": "ネットワークにて取り込まれなかった送金はいずれ送金元のウォレットに戻りますが、ウォレットソフトによっては2〜3日かかる場合もございますのでご了承ください。",
|
||||
"Invoice ID": "お支払い ID",
|
||||
"Order ID": "ご注文 ID",
|
||||
"Return to StoreName": "{{storeName}} に戻る",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "お支払いが完了しました",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "お支払いをアーカイブしました",
|
||||
"Archived_Body": "ご注文に関わる詳細などでお困りの場合はお店の担当窓口へお問い合わせください。"
|
||||
};
|
||||
|
@ -36,7 +36,7 @@ const locales_nl = {
|
||||
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is geldig voor {{maxTimeMinutes}} minuten. \
|
||||
Je kan terug komen naar {{storeName}} indien je nog eens je betaling wilt proberen uit te voeren.",
|
||||
"InvoiceExpired_Body_2": "Indien je een betaling uitvoerde, werd deze nog niet aanvaard door de blockchain. We hebben je fondsen nog niet ontvangen.",
|
||||
"InvoiceExpired_Body_3": "Si votre transaction n'a pas été accepté par la blockchain, vos fonds reviendront et dans votre portefueille. Selon votre portefueille, cela peut prendre entre 48 et 72 heures. Indien je transactie niet door de blockchain werd aanvaard, zullen je fondsen terug in wallet verschijnen. Volgens de wallet, kan dit 48 to 72 uren duren.",
|
||||
"InvoiceExpired_Body_3": "Indien je transactie niet door de blockchain werd aanvaard, zullen je fondsen terug in wallet verschijnen. Volgens de wallet, kan dit 48 to 72 uren duren.",
|
||||
"Invoice ID": "Factuurnummer",
|
||||
"Order ID": "Bestllingsnummer",
|
||||
"Return to StoreName": "Terug naar {{storeName}}",
|
||||
|
@ -28,7 +28,7 @@ const locales_pt_br = {
|
||||
"ConversionTab_BodyTop": "Você pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
|
||||
"ConversionTab_BodyDesc": "Esse serviço é oferecido por terceiros. Por favor, tenha em mente que não temos nenhum controle sobre como seus fundos serão utilizados. A fatura apenas será marcada como paga quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
|
||||
"Shapeshift_Button_Text": "Pague com Altcoins",
|
||||
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network de BTC.",
|
||||
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "A fatura está vencendo...",
|
||||
"Invoice expired": "Fatura vencida",
|
||||
|
@ -1,3 +1,6 @@
|
||||
$ver = [regex]::Match((Get-Content BTCPayServer\BTCPayServer.csproj), '<Version>([^<]+)<').Groups[1].Value
|
||||
git tag -a "v$ver" -m "$ver"
|
||||
git push --tags
|
||||
git push --tags
|
||||
git tag -d "stable"
|
||||
git tag -a "stable" -m "stable"
|
||||
git push --tags --force
|
||||
|
Reference in New Issue
Block a user