Compare commits

...

33 Commits

Author SHA1 Message Date
556082c4c9 fix json serialization 2018-05-26 18:29:57 +09:00
6a46d02fc6 Add dummy policies field 2018-05-26 18:26:02 +09:00
d75e5b8b12 Merge pull request #129 from cronos-polis/master
Polis Integration
2018-05-26 14:50:32 +09:00
a97ef2eee8 MinerFee matching Bitpay API 2018-05-25 22:49:49 +09:00
be33ebc168 fix typo 2018-05-25 17:35:01 +09:00
789193a0c8 fix typo 2018-05-25 12:55:29 +09:00
01792cf299 Merge pull request #190 from yashbhutwala/fix_typo
Fix spelling of lightning
2018-05-25 11:41:46 +09:00
ff9265f721 Fix spelling of lightning 2018-05-24 16:26:01 -04:00
8d61314852 Use FullNotification and improve instructions 2018-05-25 00:02:24 +09:00
1ce6ae8727 fix typo 2018-05-24 23:58:24 +09:00
dec5dbc0d2 Ability to pass fields to POS app #181 2018-05-24 23:54:48 +09:00
4e32dad1ea Can solve inverses at exchange level 2018-05-23 19:29:01 +09:00
127ca7582f Make sure inverse rules have priority 2018-05-23 19:13:12 +09:00
b98993f84b fix typo 2018-05-23 11:16:19 +09:00
e35f074b66 Better label for Rate Multiplier 2018-05-23 02:43:22 +09:00
ba3d13d56c Make sure the rate of the merchant is using the ask of a divided exchange 2018-05-23 02:18:38 +09:00
ead67887ab Enable SSL was ignored 2018-05-23 00:59:50 +09:00
437f27f107 Merge pull request #188 from andres-pinilla/master
Better spanish translation (tu)
2018-05-22 14:11:25 +09:00
8d41a8e98d Better spanish translation (tú) 2018-05-21 23:15:04 -05:00
f22c8a72cd Rebase 2018-05-16 11:21:57 -05:00
f81ca1888d Merge remote-tracking branch 'upstream/master' 2018-05-14 11:23:54 -05:00
ed02e0f4d6 Merge pull request #1 from zeusthealmighty/patch-1
KeyPath Updated
2018-05-14 11:23:08 -05:00
0a83f21af5 KeyPath Updated 2018-05-14 10:04:12 -05:00
7fdf19ca22 Remove cryptopia from CoinAverage 2018-05-11 11:07:42 -05:00
4e776adb03 Merge remote-tracking branch 'upstream/master' 2018-05-11 11:06:51 -05:00
d102c142b9 Typo 2018-05-11 10:46:49 -05:00
f7989541b9 Change Polis Rates - Add Cryptopia 2018-05-11 10:26:08 -05:00
e1dfbfe3b0 Rebase 2018-05-11 10:20:23 -05:00
7c3ddf904c Merge remote-tracking branch 'upstream/master' 2018-05-04 09:47:03 -05:00
25208915eb Merge remote-tracking branch 'upstream/master' 2018-04-30 10:29:30 -05:00
52e0845fc5 Merge remote-tracking branch 'upstream/master' 2018-04-21 09:59:46 -05:00
daf1a0a4bc Revert to origin csproj 2018-04-17 11:19:28 -05:00
bc8978182e Add Polis 2018-04-17 11:13:50 -05:00
28 changed files with 459 additions and 173 deletions

View File

@ -129,19 +129,19 @@ namespace BTCPayServer.Tests
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
Value = 5000m
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
Value = 4500m
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
Value = 500m
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
}

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
namespace BTCPayServer.Tests
{
@ -94,12 +95,12 @@ namespace BTCPayServer.Tests
Assert.Equal(test.ExpectedExchangeRates, string.Join(',', rule.ExchangeRates.OfType<object>().ToArray()));
}
var rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_CAD"));
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), 5000);
rule2.ExchangeRates.SetRate("bittrex", CurrencyPair.Parse("DOGE_BTC"), new BidAsk(5000m));
rule2.Reevaluate();
Assert.True(rule2.HasError);
Assert.Equal("5000 * ERR_RATE_UNAVAILABLE(coinbase, BTC_CAD) * 1.1", rule2.ToString(true));
Assert.Equal("bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", rule2.ToString(false));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 2000.4m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(2000.4m));
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
@ -116,7 +117,7 @@ namespace BTCPayServer.Tests
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
@ -124,10 +125,10 @@ namespace BTCPayServer.Tests
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), 1000m);
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal(( 1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
////////
// Make sure kraken is not converted to CurrencyPair
@ -135,8 +136,44 @@ namespace BTCPayServer.Tests
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), 1000m);
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
// Make sure can handle pairs
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("BTC_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.Value.Value);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.Value.Value);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();
builder.AppendLine("EUR_CDNT = 10");
builder.AppendLine("CDNT_BTC = CDNT_EUR * EUR_BTC;");
builder.AppendLine("CDNT_X = CDNT_BTC * BTC_X;");
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("CDNT_EUR"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / 10", rule2.ToString(false));
// Make sure an inverse can be solved on an exchange
builder = new StringBuilder();
builder.AppendLine("X_X = coinaverage(X_X);");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
}
}
}

View File

@ -185,99 +185,6 @@ namespace BTCPayServer.Tests
HttpClient _Http = new HttpClient();
class MockHttpRequest : HttpRequest
{
Uri serverUri;
public MockHttpRequest(Uri serverUri)
{
this.serverUri = serverUri;
}
public override HttpContext HttpContext => throw new NotImplementedException();
public override string Method
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Scheme
{
get => serverUri.Scheme;
set => throw new NotImplementedException();
}
public override bool IsHttps
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override HostString Host
{
get => new HostString(serverUri.Host, serverUri.Port);
set => throw new NotImplementedException();
}
public override PathString PathBase
{
get => "";
set => throw new NotImplementedException();
}
public override PathString Path
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override QueryString QueryString
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IQueryCollection Query
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string Protocol
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override IHeaderDictionary Headers => throw new NotImplementedException();
public override IRequestCookieCollection Cookies
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override long? ContentLength
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ContentType
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Stream Body
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override bool HasFormContentType => throw new NotImplementedException();
public override IFormCollection Form
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}
public BTCPayServerTester PayTester
{
get; set;

View File

@ -1262,7 +1262,7 @@ namespace BTCPayServer.Tests
Assert.Equal("orange", vmview.Items[1].Title);
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
var invoice = user.BitPay.GetInvoices().First();
Assert.Equal(10.00m, invoice.Price);
Assert.Equal("CAD", invoice.Currency);
@ -1325,6 +1325,8 @@ namespace BTCPayServer.Tests
var repo = tester.PayTester.GetService<InvoiceRepository>();
var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext();
Assert.Equal(0, invoice.CryptoInfo[0].TxCount);
Assert.True(invoice.MinerFees.ContainsKey("BTC"));
Assert.Equal(100m, invoice.MinerFees["BTC"].SatoshiPerBytes);
Eventually(() =>
{
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
@ -1464,10 +1466,10 @@ namespace BTCPayServer.Tests
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Value);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Value);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
@ -1492,7 +1494,7 @@ namespace BTCPayServer.Tests
e => (e.CurrencyPair == new CurrencyPair("BTC", "USD") ||
e.CurrencyPair == new CurrencyPair("BTC", "EUR") ||
e.CurrencyPair == new CurrencyPair("BTC", "USDT"))
&& e.Value > 1.0m // 1BTC will always be more than 1USD
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitPolis()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("POLIS");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1997'") : new KeyPath("1'")
});
}
}
}

View File

@ -50,6 +50,7 @@ namespace BTCPayServer
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitPolis();
}
/// <summary>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.24</Version>
<Version>1.0.2.30</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -40,8 +40,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.7" />
<PackageReference Include="NBitpayClient" Version="1.0.0.23" />
<PackageReference Include="NBitcoin" Version="4.1.1.8" />
<PackageReference Include="NBitpayClient" Version="1.0.0.25" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.8" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />

View File

@ -77,6 +77,7 @@ namespace BTCPayServer.Controllers
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,

View File

@ -17,6 +17,8 @@ using YamlDotNet.RepresentationModel;
using System.IO;
using BTCPayServer.Services.Rates;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
namespace BTCPayServer.Controllers
{
@ -57,9 +59,50 @@ namespace BTCPayServer.Controllers
var app = await GetOwnedApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template
};
if (HttpContext?.Request != null)
{
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash() + $"apps/{appId}/pos";
var encoder = HtmlEncoder.Default;
if (settings.ShowCustomAmount)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"amount\" value=\"100\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example1 = builder.ToString();
}
try
{
var items = Parse(settings.Template, settings.Currency);
var builder = new StringBuilder();
builder.AppendLine($"<form method=\"POST\" action=\"{encoder.Encode(appUrl)}\">");
builder.AppendLine($" <input type=\"hidden\" name=\"email\" value=\"customer@example.com\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"orderId\" value=\"CustomOrderId\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"notificationUrl\" value=\"https://example.com/callbacks\" />");
builder.AppendLine($" <input type=\"hidden\" name=\"redirectUrl\" value=\"https://example.com/thanksyou\" />");
builder.AppendLine($" <button type=\"submit\" name=\"choiceKey\" value=\"{items[0].Id}\">Buy now</button>");
builder.AppendLine($"</form>");
vm.Example2 = builder.ToString();
}
catch { }
vm.InvoiceUrl = appUrl + "invoices/SkdsDghkdP3D3qkj7bLq3";
}
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
return View(vm);
}
[HttpPost]
[Route("{appId}/settings/pos")]
@ -104,6 +147,7 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
var currency = _Currencies.GetCurrencyData(settings.Currency, false);
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
return View(new ViewPointOfSaleViewModel()
{
Title = settings.Title,
@ -163,7 +207,13 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{appId}/pos")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> ViewPointOfSale(string appId, decimal amount, string choiceKey)
public async Task<IActionResult> ViewPointOfSale(string appId,
decimal amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
{
var app = await GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -173,7 +223,7 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
@ -190,16 +240,22 @@ namespace BTCPayServer.Controllers
}
else
{
if (!settings.ShowCustomAmount)
return NotFound();
price = amount;
title = settings.Title;
}
var store = await GetStore(app);
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}

View File

@ -109,6 +109,9 @@ namespace BTCPayServer.Controllers
}
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
entity.Status = "new";
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);

View File

@ -20,5 +20,9 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }
public string InvoiceUrl { get; internal set; }
}
}

View File

@ -235,7 +235,7 @@ namespace BTCPayServer.Models
public long AmountPaid { get; set; }
[JsonProperty("minerFees")]
public long MinerFees { get; set; }
public Dictionary<string, NBitpayClient.MinerFeeInfo> MinerFees { get; set; }
[JsonProperty("exchangeRates")]
public Dictionary<string, Dictionary<string, decimal>> ExchangeRates { get; set; }

View File

@ -44,7 +44,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by ...")]
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
{

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
@ -44,6 +45,9 @@ namespace BTCPayServer.Models
public class PairingCodeResponse
{
[JsonProperty(PropertyName = "policies")]
public JArray Policies { get; set; }
[JsonProperty(PropertyName = "pairingCode")]
public string PairingCode
{

View File

@ -41,9 +41,9 @@ namespace BTCPayServer.Rating
}
else
{
if (rate.Value.HasValue)
if (rate.BidAsk != null)
{
_AllRates[key].Value = rate.Value;
_AllRates[key].BidAsk = rate.BidAsk;
}
}
}
@ -58,39 +58,175 @@ namespace BTCPayServer.Rating
return GetEnumerator();
}
public void SetRate(string exchangeName, CurrencyPair currencyPair, decimal value)
public void SetRate(string exchangeName, CurrencyPair currencyPair, BidAsk bidAsk)
{
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
rate.Value = value;
if(rate != null)
{
rate.BidAsk = bidAsk;
}
var invPair = currencyPair.Inverse();
var invRate = rates.FirstOrDefault(r => r.CurrencyPair == invPair);
if (invRate != null)
{
invRate.BidAsk = bidAsk?.Inverse();
}
}
}
public decimal? GetRate(string exchangeName, CurrencyPair currencyPair)
public BidAsk GetRate(string exchangeName, CurrencyPair currencyPair)
{
if (currencyPair.Left == currencyPair.Right)
return 1.0m;
return BidAsk.One;
if (ByExchange.TryGetValue(exchangeName, out var rates))
{
var rate = rates.FirstOrDefault(r => r.CurrencyPair == currencyPair);
if (rate != null)
return rate.Value;
return rate.BidAsk;
}
return null;
}
}
public class BidAsk
{
private readonly static BidAsk _One = new BidAsk(1.0m);
public static BidAsk One
{
get
{
return _One;
}
}
private readonly static BidAsk _Zero = new BidAsk(0.0m);
public static BidAsk Zero
{
get
{
return _Zero;
}
}
public BidAsk(decimal bid, decimal ask)
{
if (bid > ask)
throw new ArgumentException("the bid should be lower than ask", nameof(bid));
_Ask = ask;
_Bid = bid;
}
public BidAsk(decimal v) : this(v, v)
{
}
private readonly decimal _Bid;
public decimal Bid
{
get
{
return _Bid;
}
}
private readonly decimal _Ask;
public decimal Ask
{
get
{
return _Ask;
}
}
public BidAsk Inverse()
{
return new BidAsk(1.0m / Ask, 1.0m / Bid);
}
public static BidAsk operator +(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid + b.Bid, a.Ask + b.Ask);
}
public static BidAsk operator +(BidAsk a)
{
return new BidAsk(a.Bid, a.Ask);
}
public static BidAsk operator -(BidAsk a)
{
return new BidAsk(-a.Bid, -a.Ask);
}
public static BidAsk operator *(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid * b.Bid, a.Ask * b.Ask);
}
public static BidAsk operator /(BidAsk a, BidAsk b)
{
// This one is tricky.
// BTC_EUR = (6000, 6100)
// Implicit rule give
// EUR_BTC = 1 / BTC_EUR
// Or
// EUR_BTC = (1, 1) / BTC_EUR
// Naive calculation would give us ( 1/6000, 1/6100) = (0.000166, 0.000163)
// However, this is an invalid BidAsk!!! because 0.000166 > 0.000163
// So instead, we need to calculate (1/6100, 1/6000)
return new BidAsk(a.Bid / b.Ask, a.Ask / b.Bid);
}
public static BidAsk operator -(BidAsk a, BidAsk b)
{
return new BidAsk(a.Bid - b.Bid, a.Ask - b.Ask);
}
public override bool Equals(object obj)
{
BidAsk item = obj as BidAsk;
if (item == null)
return false;
return Bid == item.Bid && Ask == item.Ask;
}
public static bool operator ==(BidAsk a, BidAsk b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.Bid == b.Bid && a.Ask == b.Ask;
}
public static bool operator !=(BidAsk a, BidAsk b)
{
return !(a == b);
}
public override int GetHashCode()
{
return ToString().GetHashCode(StringComparison.InvariantCulture);
}
public override string ToString()
{
if (Bid == Ask)
return Bid.ToString(CultureInfo.InvariantCulture);
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
}
}
public class ExchangeRate
{
public string Exchange { get; set; }
public CurrencyPair CurrencyPair { get; set; }
public decimal? Value { get; set; }
public BidAsk BidAsk { get; set; }
public override string ToString()
{
if (Value == null)
if (BidAsk == null)
return $"{Exchange}({CurrencyPair})";
return $"{Exchange}({CurrencyPair}) == {Value.Value.ToString(CultureInfo.InvariantCulture)}";
return $"{Exchange}({CurrencyPair}) == {BidAsk.ToString()}";
}
}
}

View File

@ -18,6 +18,7 @@ namespace BTCPayServer.Rating
UnsupportedOperator,
MissingArgument,
DivideByZero,
InvalidNegative,
PreprocessError,
RateUnavailable,
InvalidExchangeName,
@ -139,7 +140,7 @@ namespace BTCPayServer.Rating
}
return new RateRule(this, currencyPair, candidate);
}
public ExpressionSyntax FindBestCandidate(CurrencyPair p)
{
var invP = p.Inverse();
@ -147,9 +148,9 @@ namespace BTCPayServer.Rating
foreach (var pair in new[]
{
(Pair: p, Priority: 0, Inverse: false),
(Pair: new CurrencyPair(p.Left, "X"), Priority: 1, Inverse: false),
(Pair: new CurrencyPair("X", p.Right), Priority: 1, Inverse: false),
(Pair: invP, Priority: 2, Inverse: true),
(Pair: invP, Priority: 1, Inverse: true),
(Pair: new CurrencyPair(p.Left, "X"), Priority: 2, Inverse: false),
(Pair: new CurrencyPair("X", p.Right), Priority: 2, Inverse: false),
(Pair: new CurrencyPair(invP.Left, "X"), Priority: 3, Inverse: true),
(Pair: new CurrencyPair("X", invP.Right), Priority: 3, Inverse: true),
(Pair: new CurrencyPair("X", "X"), Priority: 4, Inverse: false)
@ -216,8 +217,7 @@ namespace BTCPayServer.Rating
}
else
{
var token = SyntaxFactory.ParseToken(rate.Value.ToString(CultureInfo.InvariantCulture));
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, token);
return RateRules.CreateExpression(rate.ToString());
}
}
}
@ -225,7 +225,7 @@ namespace BTCPayServer.Rating
class CalculateWalker : CSharpSyntaxWalker
{
public Stack<decimal> Values = new Stack<decimal>();
public Stack<BidAsk> Values = new Stack<BidAsk>();
public List<RateRulesErrors> Errors = new List<RateRulesErrors>();
public override void VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node)
@ -254,7 +254,15 @@ namespace BTCPayServer.Rating
switch (node.Kind())
{
case SyntaxKind.UnaryMinusExpression:
Values.Push(-Values.Pop());
var v = Values.Pop();
if(v.Bid == v.Ask)
{
Values.Push(-v);
}
else
{
Errors.Add(RateRulesErrors.InvalidNegative);
}
break;
case SyntaxKind.UnaryPlusExpression:
Values.Push(+Values.Pop());
@ -299,7 +307,7 @@ namespace BTCPayServer.Rating
Values.Push(a * b);
break;
case SyntaxKind.DivideExpression:
if (b == decimal.Zero)
if (a.Ask == decimal.Zero || b.Ask == decimal.Zero)
{
Errors.Add(RateRulesErrors.DivideByZero);
}
@ -309,19 +317,48 @@ namespace BTCPayServer.Rating
}
break;
case SyntaxKind.SubtractExpression:
Values.Push(a - b);
if (b.Bid == b.Ask)
{
Values.Push(a - b);
}
else
{
Errors.Add(RateRulesErrors.InvalidNegative);
}
break;
default:
throw new NotSupportedException("Should never happen");
}
}
Stack<decimal> _TupleValues = null;
public override void VisitTupleExpression(TupleExpressionSyntax node)
{
_TupleValues = new Stack<decimal>();
base.VisitTupleExpression(node);
if(_TupleValues.Count != 2)
{
Errors.Add(RateRulesErrors.MissingArgument);
}
else
{
var ask = _TupleValues.Pop();
var bid = _TupleValues.Pop();
Values.Push(new BidAsk(bid, ask));
}
_TupleValues = null;
}
public override void VisitLiteralExpression(LiteralExpressionSyntax node)
{
switch (node.Kind())
{
case SyntaxKind.NumericLiteralExpression:
Values.Push(decimal.Parse(node.ToString(), CultureInfo.InvariantCulture));
var v = decimal.Parse(node.ToString(), CultureInfo.InvariantCulture);
if (_TupleValues == null)
Values.Push(new BidAsk(v));
else
_TupleValues.Push(v);
break;
}
}
@ -487,7 +524,7 @@ namespace BTCPayServer.Rating
Errors.AddRange(calculate.Errors);
return false;
}
_Value = calculate.Values.Pop();
_Value = calculate.Values.Pop().Bid;
_EvaluatedNode = result;
return true;
}

View File

@ -13,6 +13,7 @@ using NBXplorer;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using NBitpayClient;
using BTCPayServer.Payments.Bitcoin;
namespace BTCPayServer.Services.Invoices
{
@ -348,6 +349,7 @@ namespace BTCPayServer.Services.Invoices
dto.Url = ServerUrl.WithTrailingSlash() + $"invoice?id=" + Id;
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
dto.MinerFees = new Dictionary<string, MinerFeeInfo>();
foreach (var info in this.GetPaymentMethods(networkProvider))
{
var accounting = info.Calculate();
@ -381,6 +383,10 @@ namespace BTCPayServer.Services.Invoices
if (paymentId.PaymentType == PaymentTypes.BTCLike)
{
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate.GetFee(1).Satoshi;
dto.MinerFees.TryAdd(paymentId.CryptoCode, minerInfo);
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{

View File

@ -55,7 +55,7 @@ namespace BTCPayServer.Services.Mails
public SmtpClient CreateSmtpClient()
{
SmtpClient client = new SmtpClient(Server, Port.Value);
client.EnableSsl = true;
client.EnableSsl = EnableSSL;
client.UseDefaultCredentials = false;
client.Credentials = new NetworkCredential(Login, Password);
client.DeliveryMethod = SmtpDeliveryMethod.Network;

View File

@ -24,7 +24,7 @@ namespace BTCPayServer.Services.Rates
{
return new ExchangeRates((await _Bitpay.GetRatesAsync().ConfigureAwait(false))
.AllRates
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), Value = r.Value })
.Select(r => new ExchangeRate() { Exchange = BitpayName, CurrencyPair = new CurrencyPair("BTC", r.Code), BidAsk = new BidAsk(r.Value) })
.ToList());
}
}

View File

@ -54,7 +54,7 @@ namespace BTCPayServer.Services.Rates
public const string CoinAverageName = "coinaverage";
public CoinAverageRateProvider()
{
}
static HttpClient _Client = new HttpClient();
@ -69,10 +69,30 @@ namespace BTCPayServer.Services.Rates
public ICoinAverageAuthenticator Authenticator { get; set; }
private bool TryToDecimal(JProperty p, out decimal v)
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
{
JToken token = p.Value[Exchange == CoinAverageName ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
bidAsk = null;
if (Exchange == CoinAverageName)
{
JToken last = p.Value["last"];
if (!decimal.TryParse(last.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) ||
v <= 0)
return false;
bidAsk = new BidAsk(v);
return true;
}
else
{
JToken bid = p.Value["bid"];
JToken ask = p.Value["ask"];
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 > v2 ||
v1 <= 0 || v2 <= 0)
return false;
bidAsk = new BidAsk(v1, v2);
return true;
}
}
public async Task<ExchangeRates> GetRatesAsync()
@ -108,10 +128,10 @@ namespace BTCPayServer.Services.Rates
{
ExchangeRate exchangeRate = new ExchangeRate();
exchangeRate.Exchange = Exchange;
if (!TryToDecimal(prop, out decimal value))
if (!TryToBidAsk(prop, out var value))
continue;
exchangeRate.Value = value;
if(CurrencyPair.TryParse(prop.Name, out var pair))
exchangeRate.BidAsk = value;
if (CurrencyPair.TryParse(prop.Name, out var pair))
{
exchangeRate.CurrencyPair = pair;
exchangeRates.Add(exchangeRate);

View File

@ -61,7 +61,7 @@ namespace BTCPayServer.Services.Rates
var rate = new ExchangeRate();
rate.CurrencyPair = pair;
rate.Exchange = _ExchangeName;
rate.Value = ticker.Value.Bid;
rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask);
return rate;
}
catch (ArgumentException)

View File

@ -14,13 +14,19 @@ namespace BTCPayServer.Services.Rates
public const string QuadrigacxName = "quadrigacx";
static HttpClient _Client = new HttpClient();
private bool TryToDecimal(JObject p, out decimal v)
private bool TryToBidAsk(JObject p, out BidAsk v)
{
v = 0.0m;
JToken token = p.Property("bid")?.Value;
if (token == null)
v = null;
JToken bid = p.Property("bid")?.Value;
JToken ask = p.Property("ask")?.Value;
if (bid == null || ask == null)
return false;
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
if (!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 <= 0m || v2 <= 0m || v1 > v2)
return false;
v = new BidAsk(v1, v2);
return true;
}
public async Task<ExchangeRates> GetRatesAsync()
@ -37,9 +43,9 @@ namespace BTCPayServer.Services.Rates
continue;
rate.CurrencyPair = pair;
rate.Exchange = QuadrigacxName;
if (!TryToDecimal((JObject)prop.Value, out var v))
if (!TryToBidAsk((JObject)prop.Value, out var v))
continue;
rate.Value = v;
rate.BidAsk = v;
exchangeRates.Add(rate);
}
return exchangeRates;

View File

@ -38,6 +38,31 @@
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if(Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if(Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li><strong>Build the invoice's url by yourself</strong> do not trust the <code>url</code> field, this can be spoofed to use attacker's server.</li>
<li>Send a <code>GET</code> request to the invoice's url with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" />
</div>
@ -47,3 +72,10 @@
</div>
</div>
</section>
@section Scripts {
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
}

View File

@ -16,7 +16,7 @@
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<p>
<span>A connection to a lightning charge node or clightning unix socket is required to generate lignting network enabled invoices. <br /></span>
<span>A connection to a lightning charge node or clightning unix socket is required to generate lightning network enabled invoices. <br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
</p>
<ul>

View File

@ -96,7 +96,7 @@
<div class="form-group">
<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>A connection to a lightning charge node is required to generate lightning network enabled invoices.<br /></span>
<span>This is experimental and not advised for production.</span>
</p>
</div>

View File

@ -5,14 +5,14 @@ const locales_es = {
"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": "Su correo electrónico",
"Contact_Body": "Por favor provee una dirección de correo electrónico a continuación. Nos pondremos en contacto contigo en esta dirección si hay un problema con tu pago.",
"Your email": "Tu correo electrónico",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor entre un correo electrónico válido",
"Please enter a valid email address": "Por favor ingresa un correo electrónico válido",
"Order Amount": "Total del pedido",
"Network Cost": "Costo de la red",
"Already Paid": "Ya pagado",
"Due": "Debido",
"Already Paid": "Ya has pagado",
"Due": "Aún debes",
// Tabs
"Scan": "Escanear",
"Copy": "Copiar",
@ -20,22 +20,22 @@ const locales_es = {
// Scan tab
"Open in wallet": "Abrir en billetera",
// Copy tab
"CompletePay_Body": "Para completar su pago, envíe {{btcDue}} {{cryptoCode}} a la siguiente dirección.",
"CompletePay_Body": "Para completar tu pago, envía {{btcDue}} {{cryptoCode}} a la siguiente dirección.",
"Amount": "Cantidad",
"Address": "Direcció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 provisto 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 la cadena de bloques de {{cryptoCode}} .",
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando altcoins que no sean los que el comercio soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros enviarán los fondos. La factura solo se marcará como abonada una vez que se reciban los fondos en la cadena de bloques 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 expiró",
"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.",
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. Puedes regresar a {{storeName}} si deseas volver a enviar tu pago.",
"InvoiceExpired_Body_2": "Si intentaste enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido tus fondos.",
"InvoiceExpired_Body_3": "Si la transacción no es aceptada por la red de Bitcoin, los fondos se podrán gastar nuevamente en tu billetera. Dependiendo de tu billetera, esto puede tomar 48-72 horas.",
"Invoice ID": "ID de factura",
"Order ID": "ID de pedido",
"Return to StoreName": "Regresar a {{storeName}}",
@ -43,7 +43,7 @@ const locales_es = {
"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",
"Archived_Body": "Por favor, comunícate con la tienda para obtener información de tu pedido o asistencia",
// Lightning
"BOLT 11 Invoice": "Factura BOLT 11",
"Node Info": "Información del nodo",

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -5,4 +5,4 @@
<add key="aspnetcidev" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json"/>
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json"/>
</packageSources>
</configuration>
</configuration>