Compare commits

..

19 Commits

Author SHA1 Message Date
cded2548f5 bump 2018-10-12 13:32:04 +09:00
dcc859a86a Disable export to JSON 2018-10-12 13:17:38 +09:00
9bec38559f Nepali Translation (#311)
* Nepali Language

* Delete Checkout.cshtml

* Delete LanguageService.cs

* add np.js

* Match Language File Style
2018-10-12 10:11:03 +09:00
2856454d41 [langs-fr] Correction and some minor improvement (#316) 2018-10-12 10:10:42 +09:00
b3c4fc4003 Added Russian (#312) 2018-10-12 10:10:20 +09:00
c2bbc04c4c Various bugfixes (#308)
* NotifyEmail field on Invoice, sending email when triggered

* Styling invoices page

* Exporting Invoices in JSON

* Recoding based on feedback

* Fixing image breaking responsive layout on mobile

* Reducing amount of data sent in email notification

* Turning bundling on by default
2018-10-12 10:09:13 +09:00
db40c7bc32 Solving the new version of btcpayserver caused btcpay-python not to create an order problem (#327) 2018-10-11 23:50:28 +09:00
60707fdbd1 Add simplified chinese translation (#326)
* Add Chinese Simplified translation

* Add Chinese simplified translation
2018-10-11 14:25:16 +09:00
f05614f4da bump 2018-10-11 00:51:43 +09:00
a10c382bd4 [Tests] return WalletId when registering scheme 2018-10-10 00:13:37 +09:00
da2fb876cb Can pass pre filled amount and address to Send Wallet 2018-10-09 23:48:14 +09:00
3c58bff803 Comparable WalletId 2018-10-09 23:44:32 +09:00
a28814bc0e Fix RateRules crash if dups 2018-10-09 23:43:03 +09:00
3cff8261ae Set the id for apps only to 20 bytes, and output the Id in the controller so we can use it in tests 2018-10-09 23:38:56 +09:00
b16e8f7b76 Move GetAppDataIfOwner inside AppHelper 2018-10-09 23:34:51 +09:00
57ab001c0c [Tests] Pass Port to the fake http context 2018-10-09 23:32:47 +09:00
3d85dace38 Move currency display method into CurrencyNameTable 2018-10-09 23:30:26 +09:00
7a04c2974f Center QR Authenticator QR Code 2018-10-09 23:30:26 +09:00
d459839bf7 Displaying Node Info as QR code for Lightning payments (#318)
* Styling elements required for Node info

* Allowing switching QR between bolt11 and node info for lightning

* Equal width for Bolt11 and Node info buttons

* Certain languages were too verbose for display of "Pay with"

Fixes: #317
2018-10-08 07:19:55 +09:00
40 changed files with 583 additions and 178 deletions

View File

@ -201,7 +201,7 @@ namespace BTCPayServer.Tests
public T GetController<T>(string userId = null, string storeId = null) where T : Controller
{
var context = new DefaultHttpContext();
context.Request.Host = new HostString("127.0.0.1");
context.Request.Host = new HostString("127.0.0.1", Port);
context.Request.Scheme = "http";
context.Request.Protocol = "http";
if (userId != null)

View File

@ -10,6 +10,19 @@ namespace BTCPayServer.Tests
{
public class RateRulesTest
{
[Fact]
public void SecondDuplicatedRuleIsIgnored()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("DOGE_X = 1.1");
builder.AppendLine("DOGE_X = 1.2");
Assert.True(RateRules.TryParse(builder.ToString(), out var rules));
var rule = rules.GetRuleFor(new CurrencyPair("DOGE", "BTC"));
rule.Reevaluate();
Assert.True(!rule.HasError);
Assert.Equal(1.1m, rule.BidAsk.Ask);
}
[Fact]
public void CanParseRateRules()
{

View File

@ -73,11 +73,11 @@ namespace BTCPayServer.Tests
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
public WalletId RegisterDerivationScheme(string crytoCode)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string cryptoCode)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
@ -92,6 +92,8 @@ namespace BTCPayServer.Tests
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }

View File

@ -332,7 +332,7 @@ namespace BTCPayServer.Tests
(0.1m, "$0.10 (USD)"),
})
{
var actual = InvoiceController.FormatCurrency(test.Item1, "USD", new CurrencyNameTable());
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD");
Assert.Equal(test.Item2, actual);
}
}
@ -1359,6 +1359,7 @@ namespace BTCPayServer.Tests
Assert.Single(appList.Apps);
Assert.Empty(appList2.Apps);
Assert.Equal("test", appList.Apps[0].AppName);
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
Assert.True(appList.Apps[0].IsOwner);
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);

View File

@ -63,7 +63,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.3.1
image: nicolasdorier/nbxplorer:1.0.3.3
ports:
- "32838:32838"
expose:

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.108</Version>
<Version>1.0.2.110</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>

View File

@ -40,6 +40,7 @@ namespace BTCPayServer.Controllers
[TempData]
public string StatusMessage { get; set; }
public string CreatedAppId { get; set; }
public async Task<IActionResult> ListApps()
{
@ -104,7 +105,7 @@ namespace BTCPayServer.Controllers
StatusMessage = "Error: You are not owner of this store";
return RedirectToAction(nameof(ListApps));
}
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32));
var id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20));
using (var ctx = _ContextFactory.CreateContext())
{
var appData = new AppData() { Id = id };
@ -115,7 +116,7 @@ namespace BTCPayServer.Controllers
await ctx.SaveChangesAsync();
}
StatusMessage = "App successfully created";
CreatedAppId = id;
if (appType == AppType.PointOfSale)
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
return RedirectToAction(nameof(ListApps));
@ -136,21 +137,9 @@ namespace BTCPayServer.Controllers
});
}
private async Task<AppData> GetOwnedApp(string appId, AppType? type = null)
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
{
var userId = GetUserId();
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
}
private async Task<StoreData[]> GetOwnedStores()

View File

@ -181,5 +181,22 @@ namespace BTCPayServer.Controllers
{
return _Currencies.GetCurrencyData(currency, useFallback);
}
public async Task<AppData> GetAppDataIfOwner(string userId, string appId, AppType? type = null)
{
if (userId == null || appId == null)
return null;
using (var ctx = _ContextFactory.CreateContext())
{
var app = await ctx.UserStore
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
.SelectMany(us => us.StoreData.Apps.Where(a => a.Id == appId))
.FirstOrDefaultAsync();
if (app == null)
return null;
if (type != null && type.Value.ToString() != app.AppType)
return null;
return app;
}
}
}
}

View File

@ -57,7 +57,8 @@ namespace BTCPayServer.Controllers
MonitoringDate = invoice.MonitoringExpiration,
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency, _CurrencyNameTable),
Fiat = _CurrencyNameTable.DisplayFormatCurrency((decimal)dto.Price, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
@ -228,7 +229,7 @@ namespace BTCPayServer.Controllers
if (!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c=> paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
.FirstOrDefault();
if (paymentMethodTemp == null)
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
@ -307,7 +308,7 @@ namespace BTCPayServer.Controllers
private string GetDisplayName(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
network.DisplayName : network.DisplayName + " (via Lightning)";
network.DisplayName : network.DisplayName + " (Lightning)";
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
@ -323,39 +324,12 @@ namespace BTCPayServer.Controllers
if (cryptoCode == productInformation.Currency)
return null;
return FormatCurrency(productInformation.Price, productInformation.Currency, _CurrencyNameTable);
return _CurrencyNameTable.DisplayFormatCurrency(productInformation.Price, productInformation.Currency);
}
private string ExchangeRate(PaymentMethod paymentMethod)
{
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency, _CurrencyNameTable);
}
public static string FormatCurrency(decimal price, string currency, CurrencyNameTable currencies)
{
var provider = currencies.GetNumberFormatInfo(currency, true);
var currencyData = currencies.GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(price, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - price) / price) < 0.001m)
{
price = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return price.ToString("C", provider);
else
return price.ToString("C", provider) + $" ({currency})";
return _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate, currency);
}
[HttpGet]
@ -432,23 +406,17 @@ namespace BTCPayServer.Controllers
[BitpayAPIConstraint(false)]
public async Task<IActionResult> ListInvoices(string searchTerm = null, int skip = 0, int count = 50)
{
var model = new InvoicesModel();
var filterString = new SearchString(searchTerm);
foreach (var invoice in await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var model = new InvoicesModel
{
TextSearch = filterString.TextSearch,
Count = count,
SearchTerm = searchTerm,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
}))
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
foreach (var invoice in list)
{
model.SearchTerm = searchTerm;
model.Invoices.Add(new InvoiceModel()
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
@ -460,12 +428,29 @@ namespace BTCPayServer.Controllers
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}"
});
}
model.Skip = skip;
model.Count = count;
model.StatusMessage = StatusMessage;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
: r,
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
ExceptionStatus = filterString.Filters.ContainsKey("exceptionstatus") ? filterString.Filters["exceptionstatus"].ToArray() : null,
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
});
return list;
}
[HttpGet]
[Route("invoices/create")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
@ -528,6 +513,7 @@ namespace BTCPayServer.Controllers
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationEmail = model.NotificationEmail,
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,

View File

@ -83,6 +83,7 @@ namespace BTCPayServer.Controllers
entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications;
entity.ExtendedNotifications = invoice.ExtendedNotifications;
entity.NotificationURL = notificationUri?.AbsoluteUri;
entity.NotificationEmail = invoice.NotificationEmail;
entity.BuyerInformation = Map<Invoice, BuyerInformation>(invoice);
entity.PaymentTolerance = storeBlob.PaymentTolerance;
//Another way of passing buyer info to support

View File

@ -51,7 +51,7 @@ namespace BTCPayServer.Controllers
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
BuyerEmail = model.NotifyEmail,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true

View File

@ -99,9 +99,17 @@ namespace BTCPayServer.Controllers
vm.Confirmation = false;
return View(vm);
}
var storeBlob = store.GetStoreBlob();
var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId);
var willBeExcluded = !vm.Enabled;
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
var showAddress = // Show addresses if:
// - If the user is testing the hint address in confirmation screen
(vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) ||
// - The user is setting a new derivation scheme
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()) ||
// - The user is clicking on continue without changing anything
(!vm.Confirmation && willBeExcluded == wasExcluded);
if (!showAddress)
{
@ -110,9 +118,7 @@ namespace BTCPayServer.Controllers
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
store.SetStoreBlob(storeBlob);
}
catch

View File

@ -32,17 +32,19 @@ namespace BTCPayServer.Controllers
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public class WalletsController : Controller
public partial class WalletsController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
public StoreRepository Repository { get; }
public BTCPayNetworkProvider NetworkProvider { get; }
public ExplorerClientProvider ExplorerClientProvider { get; }
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
RateFetcher _RateProvider;
public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -56,13 +58,13 @@ namespace BTCPayServer.Controllers
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
_Repo = repo;
_RateProvider = rateProvider;
_NetworkProvider = networkProvider;
Repository = repo;
RateFetcher = rateProvider;
NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
_explorerProvider = explorerProvider;
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
@ -70,10 +72,10 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var stores = await Repository.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.SelectMany(s => s.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
@ -111,7 +113,7 @@ namespace BTCPayServer.Controllers
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
@ -120,7 +122,7 @@ namespace BTCPayServer.Controllers
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
foreach (var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
@ -139,29 +141,33 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
var store = await Repository.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
var rateRules = store.GetStoreBlob().GetRateRules(NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
model.CryptoCurrency = walletId.CryptoCode;
WalletModel model = new WalletModel()
{
DefaultAddress = defaultDestination,
DefaultAmount = defaultAmount,
ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase),
CryptoCurrency = walletId.CryptoCode
};
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
var result = await RateFetcher.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
@ -173,7 +179,7 @@ namespace BTCPayServer.Controllers
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch(Exception ex) { model.RateError = ex.Message; }
catch (Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
@ -187,7 +193,7 @@ namespace BTCPayServer.Controllers
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch(ArgumentException) { }
catch (ArgumentException) { }
return null;
}
@ -197,7 +203,7 @@ namespace BTCPayServer.Controllers
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(_NetworkProvider)
.GetSupportedPaymentMethods(NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
@ -257,7 +263,7 @@ namespace BTCPayServer.Controllers
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
network = NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
@ -403,7 +409,7 @@ namespace BTCPayServer.Controllers
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _explorerProvider.GetExplorerClient(network);
var explorer = ExplorerClientProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{

View File

@ -20,6 +20,7 @@ using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.HostedServices
{
@ -52,24 +53,44 @@ namespace BTCPayServer.HostedServices
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
EventAggregator eventAggregator,
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger)
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
@ -203,7 +224,7 @@ namespace BTCPayServer.HostedServices
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
@ -264,15 +285,15 @@ namespace BTCPayServer.HostedServices
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
if (t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
if (t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
if (t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
@ -289,7 +310,7 @@ namespace BTCPayServer.HostedServices
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
if (executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);

View File

@ -83,14 +83,14 @@ namespace BTCPayServer.Hosting
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
@ -106,7 +106,7 @@ namespace BTCPayServer.Hosting
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
( httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;

View File

@ -53,6 +53,12 @@ namespace BTCPayServer.Models.InvoicingModels
get; set;
}
[EmailAddress]
public string NotificationEmail
{
get; set;
}
[Uri]
public string NotificationUrl
{

View File

@ -142,5 +142,6 @@ namespace BTCPayServer.Models.InvoicingModels
public AddressModel[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
}
}

View File

@ -15,6 +15,9 @@ namespace BTCPayServer.Models.WalletViewModels
get;
set;
}
public string DefaultAddress { get; set; }
public string DefaultAmount { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

View File

@ -5,7 +5,7 @@
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_BUNDLEJSCSS": "true",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
@ -17,4 +17,4 @@
"applicationUrl": "http://127.0.0.1:14142/"
}
}
}
}

View File

@ -77,7 +77,7 @@ namespace BTCPayServer.Rating
if (CurrencyPair.TryParse(id.Identifier.ValueText, out var currencyPair))
{
expression = expression.WithTriviaFrom(expression);
ExpressionsByPair.Add(currencyPair, (expression, id));
ExpressionsByPair.TryAdd(currencyPair, (expression, id));
}
}
base.VisitAssignmentExpression(node);

View File

@ -210,14 +210,14 @@ namespace BTCPayServer.Security
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;

View File

@ -283,6 +283,11 @@ namespace BTCPayServer.Services.Invoices
get;
set;
}
public string NotificationEmail
{
get;
set;
}
public string NotificationURL
{
get;

View File

@ -29,9 +29,12 @@ namespace BTCPayServer.Services
new Language("pt-PT", "Portuguese"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("np-NP", "नेपाली"),
new Language("cs-CZ", "Česky"),
new Language("is-IS", "Íslenska"),
new Language("hr-HR", "Croatian"),
new Language("ru-RU", "русский"),
new Language("zh-SP", "中文(简体)"),
};
}
}

View File

@ -101,6 +101,40 @@ namespace BTCPayServer.Services.Rates
currencyProviders.TryAdd(code, number);
}
/// <summary>
/// Format a currency like "0.004 $ (USD)", round to significant divisibility
/// </summary>
/// <param name="value">The value</param>
/// <param name="currency">Currency code</param>
/// <param name="threeLetterSuffix">Add three letter suffix (like USD)</param>
/// <returns></returns>
public string DisplayFormatCurrency(decimal value, string currency, bool threeLetterSuffix = true)
{
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();
provider.CurrencyDecimalDigits = divisibility;
}
if (currencyData.Crypto)
return value.ToString("C", provider);
else
return value.ToString("C", provider) + $" ({currency})";
}
Dictionary<string, CurrencyData> _Currencies;
static CurrencyData[] LoadCurrency()
@ -135,13 +169,16 @@ namespace BTCPayServer.Services.Rates
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
dico.TryAdd(network.CryptoCode, new CurrencyData()
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = 8,
Name = network.CryptoCode,
Crypto = true
});
}))
{
dico[network.CryptoCode].Crypto = true;
}
}
return dico.Values.ToArray();
@ -150,9 +187,9 @@ namespace BTCPayServer.Services.Rates
public CurrencyData GetCurrencyData(string currency, bool useFallback)
{
CurrencyData result;
if(!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
if (!_Currencies.TryGetValue(currency.ToUpperInvariant(), out result))
{
if(useFallback)
if (useFallback)
{
var usd = GetCurrencyData("USD", false);
result = new CurrencyData()

View File

@ -57,14 +57,14 @@
<div class="container text-center">
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-4 text-center">
<div class="col-md-2 text-center">
</div>
<div class="col-md-4 text-center">
<div class="col-md-8 text-center">
<a href="https://www.youtube.com/channel/UCpG9WL6TJuoNfFVkaDMp9ug" target="_blank">
<img src="~/img/youtube.png" height="225" width="400" />
<img src="~/img/youtube.png" class="img-fluid" />
</a>
</div>
<div class="col-md-6 text-center">
<div class="col-md-2 text-center">
</div>
</div>
</div>

View File

@ -188,9 +188,15 @@
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null }">
<div class="btnGroupLnd">
<button onclick="lndToggleBolt11()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 0 }">{{$t("BOLT 11 Invoice")}}</button>
<button onclick="lndToggleNode()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 1 }">{{$t("Node Info")}}</button>
</div>
</div>
<div class="payment__scan">
<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 v-bind:val="scanDisplayQr" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">

View File

@ -70,7 +70,7 @@
{{$t("nested.lang")}} >>
*@
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
@foreach(var lang in langService.GetLanguages())
@foreach (var lang in langService.GetLanguages())
{
<option value="@lang.Code">@lang.DisplayName</option>
}
@ -117,9 +117,12 @@
'pt': { translation: locales_pt },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'np': { translation: locales_np },
'cs-CZ': { translation: locales_cs },
'is-IS': { translation: locales_is },
'hr-HR': { translation: locales_hr }
'hr-HR': { translation: locales_hr },
'ru-RU': { translation: locales_ru },
'zh-SP': { translation: locales_zh_sp }
},
});
@ -151,6 +154,8 @@
},
data: {
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false
}
});

View File

@ -44,6 +44,11 @@
<input asp-for="BuyerEmail" class="form-control" />
<span asp-validation-for="BuyerEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationEmail" class="control-label"></label>
<input asp-for="NotificationEmail" class="form-control" />
<span asp-validation-for="NotificationEmail" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="NotificationUrl" class="control-label"></label>
<input asp-for="NotificationUrl" class="form-control" />

View File

@ -87,6 +87,10 @@
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
@ -167,14 +171,14 @@
<th class="text-right">Rate</th>
<th class="text-right">Paid</th>
<th class="text-right">Due</th>
@if(Model.StatusException == "paidOver")
@if (Model.StatusException == "paidOver")
{
<th class="text-right">Overpaid</th>
}
</tr>
</thead>
<tbody>
@foreach(var payment in Model.CryptoPayments)
@foreach (var payment in Model.CryptoPayments)
{
<tr>
<td>@payment.PaymentMethod</td>
@ -182,7 +186,7 @@
<td class="text-right">@payment.Rate</td>
<td class="text-right">@payment.Paid</td>
<td class="text-right">@payment.Due</td>
@if(Model.StatusException == "paidOver")
@if (Model.StatusException == "paidOver")
{
<td class="text-right">@payment.Overpaid</td>
}
@ -192,7 +196,7 @@
</table>
</div>
</div>
@if(Model.OnChainPayments.Count > 0)
@if (Model.OnChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
@ -207,7 +211,7 @@
</tr>
</thead>
<tbody>
@foreach(var payment in Model.OnChainPayments)
@foreach (var payment in Model.OnChainPayments)
{
var replaced = payment.Replaced ? "class='linethrough'" : "";
<tr @replaced>
@ -226,7 +230,7 @@
</div>
</div>
}
@if(Model.OffChainPayments.Count > 0)
@if (Model.OffChainPayments.Count > 0)
{
<div class="row">
<div class="col-md-12">
@ -239,7 +243,7 @@
</tr>
</thead>
<tbody>
@foreach(var payment in Model.OffChainPayments)
@foreach (var payment in Model.OffChainPayments)
{
<tr>
<td>@payment.Crypto</td>
@ -262,7 +266,7 @@
</tr>
</thead>
<tbody>
@foreach(var address in Model.Addresses)
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight-bold" : "";
<tr>
@ -286,7 +290,7 @@
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
@foreach (var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp.ToBrowserDate()</td>

View File

@ -32,10 +32,19 @@
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed status:complete</code>.
</p>
</div>
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-4">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
</div>
<div class="col-lg-8">
<div class="form-group">
<form asp-action="SearchInvoice" method="post">
<form asp-action="SearchInvoice" method="post" style="float:right;">
<div class="input-group">
<input asp-for="SearchTerm" class="form-control" />
<input asp-for="SearchTerm" class="form-control" style="width:300px;" />
<span class="input-group-btn">
<button type="submit" class="btn btn-primary" title="Search invoice">
<span class="fa fa-search"></span> Search
@ -50,7 +59,6 @@
</div>
<div class="row">
<a asp-action="CreateInvoice" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new invoice</a>
<table class="table table-sm table-responsive-md">
<thead>
<tr>
@ -63,12 +71,12 @@
</tr>
</thead>
<tbody>
@foreach(var invoice in Model.Invoices)
@foreach (var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
@if(invoice.RedirectUrl != string.Empty)
@if (invoice.RedirectUrl != string.Empty)
{
<a href="@invoice.RedirectUrl">@invoice.OrderId</a>
}
@ -78,7 +86,7 @@
}
</td>
<td>@invoice.InvoiceId</td>
@if(invoice.Status == "paid")
@if (invoice.Status == "paid")
{
<td>
<div class="btn-group">
@ -97,7 +105,7 @@
}
<td style="text-align:right">@invoice.AmountCurrency</td>
<td style="text-align:right">
@if(invoice.ShowCheckout)
@if (invoice.ShowCheckout)
{
<a asp-action="Checkout" asp-route-invoiceId="@invoice.InvoiceId">Checkout</a> <span>-</span>
}<a asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId">Details</a>
@ -107,7 +115,7 @@
</tbody>
</table>
<span>
@if(Model.Skip != 0)
@if (Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
{

View File

@ -56,5 +56,6 @@
width: 150,
height: 150
});
$("#qrCode > img").css({ "margin": "auto" });
</script>
}

View File

@ -11,6 +11,8 @@ namespace BTCPayServer
static readonly Regex _WalletStoreRegex = new Regex("^S-([a-zA-Z0-9]{30,60})-([a-zA-Z]{2,5})$");
public static bool TryParse(string str, out WalletId walletId)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
walletId = null;
WalletId w = new WalletId();
var match = _WalletStoreRegex.Match(str);
@ -32,8 +34,37 @@ namespace BTCPayServer
}
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public override bool Equals(object obj)
{
WalletId item = obj as WalletId;
if (item == null)
return false;
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
}
public static bool operator ==(WalletId a, WalletId b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(WalletId a, WalletId b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode(StringComparison.Ordinal);
}
public override string ToString()
{
if (StoreId == null || CryptoCode == null)
return "";
return $"S-{StoreId}-{CryptoCode.ToUpperInvariant()}";
}
}

View File

@ -9236,7 +9236,7 @@ strong {
}
.bp-view.scan {
padding-top: .9em !important;
padding-top: 0em !important;
}
.bp-view:not(.active) {
@ -9376,11 +9376,64 @@ strong {
border-bottom: 1px dotted;
}
.wrapBtnGroup {
text-align: center;
margin: -2px 0px 12px;
padding: 0 15px;
}
.btnGroupLnd {
margin: 0 auto;
text-align: center;
width: inherit;
display: table;
background-color: #fff;
color: #000;
font-size: 0px;
width:100%;
}
.btnGroupLnd button:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-right: 0px;
}
.btnGroupLnd button:last-child {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.btnGroupLnd button {
display: table-cell;
border: solid 1px #24725B;
padding: 6px 9px;
margin: 0px;
font-size: 12px;
width: 50%;
}
.btnGroupLnd button:hover {
background-color: #eeffee;
}
.btnGroupLnd button.active {
background-color: #329F80;
border-color: #329F80;
color: #fff;
}
.btnGroupLnd button.active:hover {
background-color: #24725B;
border-color: #24725B;
color: #fff;
}
.payment__details__instruction__open-wallet {
display: block;
margin-left: auto;
margin-right: auto;
margin: 15px 15px 18px;
margin: 0px 15px 18px;
text-align: center;
}

View File

@ -54,6 +54,7 @@
.clickable_underline {
border-bottom: 1px dotted #ccc;
font-size: 13px;
}
.payment__currencies:hover .clickable_underline {

View File

@ -20,6 +20,17 @@ function resetTabsSlider() {
closePaymentMethodDialog(null);
}
function changeCurrency(currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
checkoutCtrl.scanDisplayQr = "";
srvModel.paymentMethodId = currency;
fetchStatus();
}
return false;
}
function onDataCallback(jsonData) {
// extender properties used
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
@ -66,20 +77,26 @@ function onDataCallback(jsonData) {
$(".payment__spinner").hide();
}
if (checkoutCtrl.scanDisplayQr === "") {
checkoutCtrl.scanDisplayQr = jsonData.invoiceBitcoinUrlQR;
}
if (jsonData.isLightning && checkoutCtrl.lndModel === null) {
var lndModel = {
toggle: 0
};
checkoutCtrl.lndModel = lndModel;
}
if (!jsonData.isLightning) {
checkoutCtrl.lndModel = null;
}
// updating ui
checkoutCtrl.srvModel = jsonData;
}
function changeCurrency(currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
srvModel.paymentMethodId = currency;
fetchStatus();
}
return false;
}
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.paymentMethodId + "/status";
$.ajax({
@ -93,6 +110,16 @@ function fetchStatus() {
});
}
function lndToggleBolt11() {
checkoutCtrl.lndModel.toggle = 0;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.invoiceBitcoinUrlQR;
}
function lndToggleNode() {
checkoutCtrl.lndModel.toggle = 1;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.peerInfo;
}
// private methods
$(document).ready(function () {
// initialize
@ -117,7 +144,7 @@ $(document).ready(function () {
progressStart(srvModel.maxTimeSeconds); // Progress bar
if (srvModel.requiresRefundEmail && !validateEmail(srvModel.customerEmail))
emailForm(); // Email form Display
showEmailForm();
else
hideEmailForm();
}
@ -133,7 +160,7 @@ $(document).ready(function () {
}
// Email Form
// Setup Email mode
function emailForm() {
function showEmailForm() {
$(".modal-dialog").addClass("enter-purchaser-email");
$("#emailAddressForm .action-button").click(function () {

View File

@ -8,11 +8,11 @@ const locales_fr = {
"Contact_Body": "Merci de renseigner l'adresse email ci-dessous. Nous vous contacterons à cette adresse si il y a un problème avec votre paiement.",
"Your email": "Votre email",
"Continue": "Continuer",
"Please enter a valid email address": "Merci de rentrer une addrese email valide",
"Please enter a valid email address": "Merci de saisir une addrese email valide",
"Order Amount": "Montant de la commande",
"Network Cost": "Coût réseau",
"Already Paid": "Déjà payé",
"Due": "Dûe",
"Due": "Reste à payer",
// Tabs
"Scan": "Scanner",
"Copy": "Copier",
@ -25,18 +25,18 @@ const locales_fr = {
"Address": "Adresse",
"Copied": "Copié",
// Conversion tab
"ConversionTab_BodyTop": "Vous pouvez payer {{btcDue}} {{cryptoCode}} en utilisant d'autre crypto-monnaies alternatives non supportées directement par le marchant.",
"ConversionTab_BodyDesc": "Ce service est fournis par un tiers partie. Cependant, nous n'avons aucun controle la façon dont sera traité vos fonds. La facture sera considérée payée seulement quand les fonds seront reçus sur la blockchain {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Payer avec une crypto-monnaie alternative",
"ConversionTab_Lightning": "Pas de fournisseur disponible pour les paiements sur le Lightning Network.",
"ConversionTab_BodyTop": "Vous pouvez payer {{btcDue}} {{cryptoCode}} en utilisant d'autres crypto-monnaies alternatives non supportées directement par le marchand.",
"ConversionTab_BodyDesc": "Ce service est fourni par un tiers. Nous n'avons aucun contrôle sur la façon dont seront traités vos fonds. La facture sera considérée payée seulement quand les fonds seront reçus sur la blockchain {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Payer en altcoins",
"ConversionTab_Lightning": "Le service de conversion n'est pas disponible pour les paiements sur le Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La facture va bientôt expirer...",
"Invoice expired": "Facture expirée",
"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 dans votre portefueille. Selon votre portefueille, cela peut prendre entre 48 et 72 heures.",
"What happened?": "Que s'est-il passé ?",
"InvoiceExpired_Body_1": "La facture a expiré. Une facture est valide seulement {{maxTimeMinutes}} minutes. \
Si vous voulez soumettre à nouveau votre paiement, vous pouvez revenir sur {{storeName}} .",
"InvoiceExpired_Body_2": "Si vous avez envoyé un paiement, ce dernier n'a pas encore été inscrit dans la blockchain. Nous n'avons pas reçu vos fonds.",
"InvoiceExpired_Body_3": "Si votre transaction n'est pas inscrite dans la blockchain, vos fonds reviendront dans votre portefeuille. Selon votre portefeuille, 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}}",
@ -44,10 +44,10 @@ Vous pouvez revenir sur {{storeName}} si vous voulez resoumettre votre paiement.
"This invoice has been paid": "Cette facture a été payée",
// Invoice archived
"This invoice has been archived": "Cette facture a été archivée",
"Archived_Body": "Merci de contacter le marchand pour plus d'assistance ou d'information sur cette commande.",
"Archived_Body": "Merci de contacter le marchand pour obtenir de l'aide ou des informations sur cette commande.",
// Lightning
"BOLT 11 Invoice": "Facture BOLT 11",
"Node Info": "Information du noeud",
"Node Info": "Informations sur le nœud",
//
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions"

View File

@ -0,0 +1,55 @@
const locales_np = {
nested: {
lang: 'भाषा'
},
"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": "तपाईं एक व्यापारी को सीधा समर्थन भन्दा Altcoins अन्य को उपयोग को {{btcDue}} {{cryptoCode}} भुगतान गर्न सक्छन्",
"ConversionTab_BodyDesc": "यो सेवा तेस्रो पार्टी द्वारा प्रदान गरिएको छ। कृपया ध्यान राख्नुहोस् कि हामीसँग कुनै नियन्त्रण छैन कि कसरी प्रदायकहरूले तपाईंको रकम अगाडि बढ्नेछन्। रकम प्राप्त {{cryptoCode}} Blockchain भएको बेलामा इनभ्वाइस मात्र भुक्तानी चिन्ह लगाइनेछ",
"Shapeshift_Button_Text": "तिर्नुहोस् Altcoins",
"ConversionTab_Lightning": "Lightning Network भुक्तानीको लागि कुनै परिवर्तन प्रदायकहरू उपलब्ध छैनन्",
// Invoice expired
"Invoice expiring soon...": "चलानीको म्याद सकियो",
"Invoice expired": "इनभ्वाइस समाप्त भयो",
"What happened?": "के भयो",
"InvoiceExpired_Body_1": "यो चलानी समाप्त भएको छ। इनभ्वाइस {{maxTimeMinutes}} मिनेटको लागि मात्र वैध छ। \
तपाइँ आफ्नो भुक्तानी पुन: पेश गर्न चाहानुहुन्छ यदि तपाइँ {{ storeName }} भुक्तानी पठाउन प्रयास गर्नुभयो भने।",
"InvoiceExpired_Body_2": "भने यो अझै नेटवर्कद्वारा स्वीकार गरिएको छैन। हामीले अझै सम्म तपाईंको रकम प्राप्त गरेका छैनौ।",
"InvoiceExpired_Body_3": "यदि लेनदेन नेटवर्क द्वारा स्वीकार गरेन भने, रकम तपाईंको वालेटमा फेरि खर्चयोग्य हुनेछ। तपाईंको वालेटको आधारमा, यो 48-72 घण्टा लाग्न सक्छ",
"Invoice ID": "चलानी आईडी",
"Order ID": "अर्डर आईडी",
"Return to StoreName": "यसलाई फर्काउनुहोस् {{storeName}}",
// Invoice paid
"This invoice has been paid": "इनभ्वाइस भुक्तानी गरिएको छ",
// Invoice archived
"This invoice has been archived": "इनभ्वाइस संग्रहीत गरिएको छ",
"Archived_Body": "कृपया स्टोर जानकारी वा सहयोगको लागि स्टोरलाई सम्पर्क गर्नुहोस्",
// Lightning
"BOLT 11 Invoice": "BOLT 11 इनभ्वाइस",
"Node Info": "नोड जानकारी",
//
"txCount": "{{count}} लेनदेन",
"txCount_plural": "{{count}} लेनदेन"
};

View File

@ -0,0 +1,54 @@
const locales_ru = {
nested: {
lang: 'язык'
},
"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}} используя Altcoins отличные от предпочитаемых продавцом.",
"ConversionTab_BodyDesc": "Эта услуга предоставляется третьим лицом. Пожалуйста, имейте в виду, что мы не контролируем, каким образом провайдеры переедут ваши фондовые средства. Счет будет закрыт только после {{cryptoCode}} Blockchain получения средств.",
"Shapeshift_Button_Text": "заплатить Altcoins",
"ConversionTab_Lightning": "Возможность конвертации Lightning Network платежей отсутствует.",
// Invoice expired
"Invoice expiring soon...": "Время выставленного счета вскоре истечёт...",
"Invoice expired": "Срок действия выставленного счета истек",
"What happened?": "Что случилось?",
"InvoiceExpired_Body_1": "Срок действия этого счета истек. Счет действителен только {{maxTimeMinutes}} минут. \
вы можете посетить {{storeName}} опять, если захотите оплатить.",
"InvoiceExpired_Body_2": "ваша оплата пока еще не принята системой. Мы еще не получили ваши фонды.",
"InvoiceExpired_Body_3": "если транзакция не пройдёт Ваши фонды будут возвращены в ваш кошелек.в зависимости от Вашего кошелек это может занять от 48 до 72 часов.",
"Invoice ID": "Порядковый номер выставленного счета",
"Order ID": "Порядковый номер заказа",
"Return to StoreName": "возвратить в {{storeName}}",
// Invoice paid
"This invoice has been paid": "Этот выставленный счет был оплачен",
// Invoice archived
"This invoice has been archived": "Этот счет был заархивирован.",
"Archived_Body": "ожалуйста, обратитесь в магазин для получения информации о заказе или помощи",
// Lightning
"BOLT 11 Invoice": "счет BOLT 11",
"Node Info": "Информация об устройстве",
//
"txCount": "{{count}} Сделка",
"txCount_plural": "{{count}} операции"
};

View File

@ -0,0 +1,54 @@
const locales_zh_sp = {
nested: {
lang: '语言'
},
"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": "您也可以使用商家支持以外的其他altcoins支付{{btcDue}} {{cryptoCode}}。",
"ConversionTab_BodyDesc": "请您注意,这项服务由第三方应用提供,所以我们无法直接获悉和操控第三方应用的具体兑换过程。订单只有在支付款被{{cryptocode}}的区块链接收之后,才会显示已支付。",
"Shapeshift_Button_Text": "Altcoins支付",
"ConversionTab_Lightning": "闪电网络不支持其他altcoins兑换支付。",
// Invoice expired
"Invoice expiring soon...": "订单即将过期...",
"Invoice expired": "订单已过期 ",
"What happened?": "发生了什么?",
"InvoiceExpired_Body_1": "您的订单已过期。一个订单只在{{maxTimeMinutes}}分钟内有效。 \
如果您想要再次支付,可返回{{storeName}}。",
"InvoiceExpired_Body_2": "如果您已支付,但付款没有被网络确认接收。这说明我方目前还未收到您的支付款。",
"InvoiceExpired_Body_3": "如果您的支付最终被网络拒绝支付款将被返回您的钱包。根据您使用的钱包不同可能需要48-72小时。",
"Invoice ID": "订单号",
"Order ID": "订货号",
"Return to StoreName": "返回{{storeName}}。",
// Invoice paid
"This invoice has been paid": "此订单已支付",
// Invoice archived
"This invoice has been archived": "此订单已归档",
"Archived_Body": "请联系商家获得更多的订单信息或帮助",
// Lightning
"BOLT 11 Invoice": "BOLT 11 订单",
"Node Info": "节点信息(Node Info)",
//
"txCount": "{{count}}笔交易",
"txCount_plural": "{{count}}笔交易"
};

View File

@ -15,7 +15,12 @@ $(function () {
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();
if (srvModel.defaultAddress !== null) {
$("#destination-textbox").val(srvModel.defaultAddress);
}
if (srvModel.defaultAmount !== null) {
$("#amount-textbox").val(srvModel.defaultAmount);
}
function WriteAlert(type, message) {
$("#walletAlert").removeClass("alert-danger");
$("#walletAlert").removeClass("alert-warning");
@ -31,9 +36,9 @@ $(function () {
$("#" + prefix + "-error").css("display", "none");
$("#" + prefix + "-success").css("display", "none");
$("#" + prefix+"-" + type).css("display", "block");
$("#" + prefix + "-" + type).css("display", "block");
$("." + prefix +"-label").text(message);
$("." + prefix + "-label").text(message);
}
$("#sendform").on("submit", function (elem) {
@ -133,8 +138,7 @@ $(function () {
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
.catch(function (reason) {
if (reason.name === "TransportError")
reason = "Are you running the ledger app with version equals or above 1.2.4?";
Write('hw', 'error', reason);