Compare commits

...

46 Commits

Author SHA1 Message Date
b7c58c2083 Fix bug of authentication caused by previous refactoring on authentication 2018-06-06 14:46:41 +09:00
cd75fd6842 bump 2018-06-05 12:53:55 +09:00
370951a3bd make sure postgres DB is created with C locale 2018-06-05 12:51:37 +09:00
2c08b0137b Update NBitpayClient 2018-06-05 12:29:45 +09:00
1eee31e9f1 Fix rate bug: Sometimes coinaverage is sending null bid and ask 2018-06-04 12:01:30 +09:00
01cf579530 Use proper custom authentication handler for bitpay 2018-06-04 12:00:03 +09:00
f72705935a Merge pull request #201 from ChekaZ/master
Support Feathercoin
2018-06-04 01:45:07 +09:00
a29ab6b6b0 Support Feathercoin 2018-06-03 14:30:43 +02:00
4784518235 update link 2018-06-01 13:21:56 +09:00
0697b8bf86 update images 2018-05-31 23:54:03 +09:00
5050b59014 bump 2018-05-31 18:41:33 +09:00
665cf4c3b1 Updating BTCPayServer to .NET Core 2.1 2018-05-31 18:41:03 +09:00
98e81ab0fd Merge branch 'rockstardev-master' 2018-05-29 12:27:55 +09:00
6ce70237fc Merge branch 'master' of github.com:btcpayserver/btcpayserver 2018-05-29 12:27:45 +09:00
4f23fc99a1 Renaming method to better communicate function 2018-05-29 12:27:04 +09:00
d7fccae452 Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-29 12:27:04 +09:00
d7a5021ed2 Updating Invoice details to show local date time 2018-05-29 12:27:03 +09:00
63ec832667 Support for localizing datetimes base on browser's timezone 2018-05-29 12:27:03 +09:00
8d95b9fa04 Renaming method to better communicate function 2018-05-28 09:36:49 -05:00
b497d1871e Merge pull request #195 from andres-pinilla/patch-3
Updated es.js
2018-05-28 10:50:30 +09:00
c7cd029482 Update es.js
Various strings updated.
2018-05-27 20:36:32 -05:00
68f2cba60d Generalizing TimeSpan to Time Ago, reverting invoice list to ago 2018-05-26 09:42:55 -05:00
5c4200b036 Updating Invoice details to show local date time 2018-05-26 09:35:42 -05:00
bc06114023 Support for localizing datetimes base on browser's timezone 2018-05-26 09:32:20 -05:00
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
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
38 changed files with 490 additions and 271 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
</ItemGroup>

View File

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

View File

@ -343,6 +343,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
@ -772,6 +773,22 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
// Test request pairing code client side
var storeController = user.GetController<StoresController>();
storeController.CreateToken(new CreateTokenViewModel()
{
Facade = Facade.Merchant.ToString(),
Label = "test2",
StoreId = user.StoreId
}).GetAwaiter().GetResult();
Assert.NotNull(storeController.GeneratedPairingCode);
var bitpay = new Bitpay(new Key(), tester.PayTester.ServerUri);
bitpay.AuthorizeClient(new PairingCode(storeController.GeneratedPairingCode)).Wait();
Assert.True(bitpay.TestAccess(Facade.Merchant));
Assert.True(bitpay.TestAccess(Facade.PointOfSale));
// Can generate API Key
var repo = tester.PayTester.GetService<TokenRepository>();
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
@ -1325,6 +1342,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()

View File

@ -46,7 +46,7 @@ services:
- lightning-charged
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.2.6
image: nicolasdorier/nbxplorer:1.0.2.8
ports:
- "32838:32838"
expose:
@ -89,7 +89,7 @@ services:
- "bitcoin_datadir:/data"
customer_lightningd:
image: nicolasdorier/clightning:0.0.0.16-dev
image: nicolasdorier/clightning:0.0.0.20-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |
@ -112,7 +112,7 @@ services:
- bitcoind
lightning-charged:
image: shesek/lightning-charge:0.3.9
image: shesek/lightning-charge:0.3.12
environment:
NETWORK: regtest
API_TOKEN: foiewnccewuify
@ -131,7 +131,7 @@ services:
- merchant_lightningd
merchant_lightningd:
image: nicolasdorier/clightning:0.0.0.14-dev
image: nicolasdorier/clightning:0.0.0.20-dev
environment:
EXPOSE_TCP: "true"
LIGHTNINGD_OPT: |

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 InitFeathercoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("FTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://explorer.feathercoin.com/tx/{0}" : "https://explorer.feathercoin.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "feathercoin",
DefaultRateRules = new[]
{
"FTC_X = FTC_BTC * BTC_X",
"FTC_BTC = bittrex(FTC_BTC)"
},
CryptoImagePath = "imlegacy/feathercoin.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("8'") : new KeyPath("1'")
});
}
}
}

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,8 @@ namespace BTCPayServer
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitPolis();
InitFeathercoin();
}
/// <summary>

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.27</Version>
<Version>1.0.2.33</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -37,24 +37,24 @@
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<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.10" />
<PackageReference Include="NBitpayClient" Version="1.0.0.28" />
<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" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1-rc1" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.10" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.14" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0-rc1-final" PrivateAssets="All" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version=" 2.1.0" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" Version="4.3.1" />
</ItemGroup>

View File

@ -12,7 +12,7 @@ using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
[BitpayAPIConstraint]
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;
@ -30,6 +30,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("tokens")]
[AllowAnonymous]
public async Task<DataWrapper<List<PairingCodeResponse>>> Tokens([FromBody] TokenRequest request)
{
PairingCodeEntity pairingEntity = null;
@ -53,7 +54,7 @@ namespace BTCPayServer.Controllers
else
{
var sin = this.User.GetSIN() ?? request.Id;
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
if (string.IsNullOrEmpty(sin) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(sin))
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
pairingEntity = await _TokenRepository.GetPairingAsync(request.PairingCode);
@ -77,6 +78,7 @@ namespace BTCPayServer.Controllers
{
new PairingCodeResponse()
{
Policies = new Newtonsoft.Json.Linq.JArray(),
PairingCode = pairingEntity.Id,
PairingExpiration = pairingEntity.Expiration,
DateCreated = pairingEntity.CreatedTime,

View File

@ -101,7 +101,7 @@ namespace BTCPayServer.Controllers
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\": \"BCH\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
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]

View File

@ -20,7 +20,7 @@ namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[Authorize(Policies.CanUseStore.Key)]
[Authorize(Policies.CanUseStore.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{
private InvoiceController _InvoiceController;

View File

@ -425,7 +425,7 @@ namespace BTCPayServer.Controllers
{
Status = invoice.Status + (invoice.ExceptionStatus == null ? string.Empty : $" ({invoice.ExceptionStatus})"),
ShowCheckout = invoice.Status == "new",
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
Date = invoice.InvoiceTime,
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,

View File

@ -6,6 +6,11 @@ using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.PostgreSql;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Metadata;
namespace BTCPayServer.Data
{
@ -31,12 +36,56 @@ namespace BTCPayServer.Data
return new ApplicationDbContext(builder.Options);
}
class CustomNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
public CustomNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies) : base(dependencies)
{
}
protected override void Generate(NpgsqlCreateDatabaseOperation operation, IModel model, MigrationCommandListBuilder builder)
{
builder
.Append("CREATE DATABASE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name));
// POSTGRES gotcha: Indexed Text column (even if PK) are not used if we are not using C locale
builder
.Append(" TEMPLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("template0"));
builder
.Append(" LC_CTYPE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" LC_COLLATE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("C"));
builder
.Append(" ENCODING ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier("UTF8"));
if (operation.Tablespace != null)
{
builder
.Append(" TABLESPACE ")
.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Tablespace));
}
builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);
EndStatement(builder, suppressTransaction: true);
}
}
public void ConfigureBuilder(DbContextOptionsBuilder builder)
{
if (_Type == DatabaseType.Sqlite)
builder.UseSqlite(_ConnectionString);
else if (_Type == DatabaseType.Postgres)
builder.UseNpgsql(_ConnectionString);
builder
.UseNpgsql(_ConnectionString)
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)

View File

@ -36,28 +36,6 @@ namespace BTCPayServer
{
public static class Extensions
{
public static string Prettify(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
public static string PrettyPrint(this TimeSpan expiration)
{
StringBuilder builder = new StringBuilder();

View File

@ -117,7 +117,6 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.AddTransient<IConfigureOptions<MvcOptions>, BitpayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
@ -137,6 +136,7 @@ namespace BTCPayServer.Hosting
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));
BitpayAuthentication.AddAuthentication(services);
services.AddBundles();
services.AddTransient<BundleOptions>(provider =>

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

@ -33,10 +33,7 @@ namespace BTCPayServer.Models.InvoicingModels
public class InvoiceModel
{
public string Date
{
get; set;
}
public DateTimeOffset Date { get; set; }
public string OrderId { get; set; }
public string RedirectUrl { get; set; }

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

@ -0,0 +1,242 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Authentication;
using System.Text.Encodings.Web;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Security
{
public class BitpayAuthentication
{
public class BitpayAuthOptions : AuthenticationSchemeOptions
{
}
class BitpayAuthHandler : AuthenticationHandler<BitpayAuthOptions>
{
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayAuthHandler(
TokenRepository tokenRepository,
StoreRepository storeRepository,
IOptionsMonitor<BitpayAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
_TokenRepository = tokenRepository;
_StoreRepository = storeRepository;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (Context.Request.HttpContext.GetIsBitpayAPI())
{
var bitpayAuth = Context.Request.HttpContext.GetBitpayAuth();
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(Context.Request.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!Context.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(Context.Request.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)Context.User.Identity);
identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
Context.Request.HttpContext.SetStoreData(store);
return AuthenticateResult.Success(new AuthenticationTicket(Context.User, Policies.BitpayAuthentication));
}
else if (failedAuth)
{
return AuthenticateResult.Fail("Invalid credentials");
}
}
return AuthenticateResult.NoResult();
}
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return null;
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return storeId;
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if (expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
path == "/invoices" &&
httpContext.Request.Method == "GET")
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{
bitpayAuth = bitpayAuth ?? new Action<BitpayAuthOptions>((o) =>{ });
services.AddAuthentication().AddScheme<BitpayAuthOptions, BitpayAuthHandler>(Policies.BitpayAuthentication, bitpayAuth);
}
}
}

View File

@ -1,196 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBitpayClient.Extensions;
using Newtonsoft.Json.Linq;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Http.Internal;
namespace BTCPayServer.Security
{
public class BitpayClaimsFilter : IAsyncAuthorizationFilter, IConfigureOptions<MvcOptions>
{
UserManager<ApplicationUser> _UserManager;
StoreRepository _StoreRepository;
TokenRepository _TokenRepository;
public BitpayClaimsFilter(
UserManager<ApplicationUser> userManager,
TokenRepository tokenRepository,
StoreRepository storeRepository)
{
_UserManager = userManager;
_StoreRepository = storeRepository;
_TokenRepository = tokenRepository;
}
void IConfigureOptions<MvcOptions>.Configure(MvcOptions options)
{
options.Filters.Add(typeof(BitpayClaimsFilter));
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var principal = context.HttpContext.User;
if (context.HttpContext.GetIsBitpayAPI())
{
var bitpayAuth = context.HttpContext.GetBitpayAuth();
string storeId = null;
var failedAuth = false;
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
{
storeId = await CheckBitId(context.HttpContext, bitpayAuth.Signature, bitpayAuth.Id);
if (!context.HttpContext.User.Claims.Any(c => c.Type == Claims.SIN))
{
Logs.PayServer.LogDebug("BitId signature check failed");
failedAuth = true;
}
}
else if (!string.IsNullOrEmpty(bitpayAuth.Authorization))
{
storeId = await CheckLegacyAPIKey(context.HttpContext, bitpayAuth.Authorization);
if (storeId == null)
{
Logs.PayServer.LogDebug("API key check failed");
failedAuth = true;
}
}
if (storeId != null)
{
var identity = ((ClaimsIdentity)context.HttpContext.User.Identity);
identity.AddClaim(new Claim(Policies.CanUseStore.Key, storeId));
var store = await _StoreRepository.FindStore(storeId);
context.HttpContext.SetStoreData(store);
}
else if (failedAuth)
{
throw new BitpayHttpException(401, "Invalid credentials");
}
}
}
private async Task<string> CheckBitId(HttpContext httpContext, string sig, string id)
{
httpContext.Request.EnableRewind();
string storeId = null;
string body = string.Empty;
if (httpContext.Request.ContentLength != 0 && httpContext.Request.Body != null)
{
using (StreamReader reader = new StreamReader(httpContext.Request.Body, Encoding.UTF8, true, 1024, true))
{
body = reader.ReadToEnd();
}
httpContext.Request.Body.Position = 0;
}
var url = httpContext.Request.GetEncodedUrl();
try
{
var key = new PubKey(id);
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var sin = key.GetBitIDSIN();
var identity = ((ClaimsIdentity)httpContext.User.Identity);
identity.AddClaim(new Claim(Claims.SIN, sin));
string token = null;
if (httpContext.Request.Query.TryGetValue("token", out var tokenValues))
{
token = tokenValues[0];
}
if (token == null && !String.IsNullOrEmpty(body) && httpContext.Request.Method == "POST")
{
try
{
token = JObject.Parse(body)?.Property("token")?.Value?.Value<string>();
}
catch { }
}
if (token != null)
{
var bitToken = await GetTokenPermissionAsync(sin, token);
if (bitToken == null)
{
return null;
}
storeId = bitToken.StoreId;
}
}
}
catch (FormatException) { }
return storeId;
}
private async Task<string> CheckLegacyAPIKey(HttpContext httpContext, string auth)
{
var splitted = auth.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (splitted.Length != 2 || !splitted[0].Equals("Basic", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string apiKey = null;
try
{
apiKey = Encoders.ASCII.EncodeData(Encoders.Base64.DecodeData(splitted[1]));
}
catch
{
return null;
}
return await _TokenRepository.GetStoreIdFromAPIKey(apiKey);
}
private async Task<BitTokenEntity> GetTokenPermissionAsync(string sin, string expectedToken)
{
var actualTokens = (await _TokenRepository.GetTokens(sin)).ToArray();
actualTokens = actualTokens.SelectMany(t => GetCompatibleTokens(t)).ToArray();
var actualToken = actualTokens.FirstOrDefault(a => a.Value.Equals(expectedToken, StringComparison.Ordinal));
if (expectedToken == null || actualToken == null)
{
Logs.PayServer.LogDebug($"No token found for facade {Facade.Merchant} for SIN {sin}");
return null;
}
return actualToken;
}
private IEnumerable<BitTokenEntity> GetCompatibleTokens(BitTokenEntity token)
{
if (token.Facade == Facade.Merchant.ToString())
{
yield return token.Clone(Facade.User);
yield return token.Clone(Facade.PointOfSale);
}
if (token.Facade == Facade.PointOfSale.ToString())
{
yield return token.Clone(Facade.User);
}
yield return token;
}
}
}

View File

@ -8,6 +8,7 @@ namespace BTCPayServer.Security
{
public static class Policies
{
public const string BitpayAuthentication = "Bitpay.Auth";
public const string CookieAuthentication = "Identity.Application";
public static AuthorizationOptions AddBTCPayPolicies(this AuthorizationOptions options)
{

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

@ -85,7 +85,8 @@ namespace BTCPayServer.Services.Rates
{
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) ||
if (bid == null || ask == null ||
!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)

View File

@ -57,7 +57,7 @@
<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 the <code>GET</code> request to the invoice's url with <code>Content-Type: application/json</code></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>

View File

@ -53,15 +53,15 @@
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate</td>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate</td>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate</td>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Transaction speed</th>
@ -289,7 +289,7 @@
@foreach(var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>
<td>@evt.Timestamp.ToBrowserDate()</td>
<td>@evt.Message</td>
</tr>
}

View File

@ -66,7 +66,7 @@
@foreach(var invoice in Model.Invoices)
{
<tr>
<td>@invoice.Date</td>
<td>@invoice.Date.ToTimeAgo()</td>
<td>
@if(invoice.RedirectUrl != string.Empty)
{

View File

@ -40,7 +40,7 @@
<table class="table table-sm">
<tr>
<th>Quota period</th>
<td>@Model.RateLimits.TotalPeriod.Prettify()</td>
<td>@Model.RateLimits.TotalPeriod.TimeString()</td>
</tr>
<tr>
<th>Requests quota</th>
@ -48,7 +48,7 @@
</tr>
<tr>
<th>Quota reset in</th>
<td>@Model.RateLimits.CounterReset.Prettify()</td>
<td>@Model.RateLimits.CounterReset.TimeString()</td>
</tr>
</table>
}

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

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Views
@ -22,5 +24,39 @@ namespace BTCPayServer.Views
var activePage = (T)viewData[ACTIVE_PAGE_KEY];
return page.Equals(activePage) ? "active" : null;
}
public static HtmlString ToBrowserDate(this DateTimeOffset date)
{
var hello = date.ToString("o", CultureInfo.InvariantCulture);
return new HtmlString($"<span class='localizeDate'>{hello}</span>");
}
public static string ToTimeAgo(this DateTimeOffset date)
{
var formatted = (DateTimeOffset.UtcNow - date).TimeString() + " ago";
return formatted;
}
public static string TimeString(this TimeSpan timeSpan)
{
if (timeSpan.TotalMinutes < 1)
{
return $"{(int)timeSpan.TotalSeconds} second{Plural((int)timeSpan.TotalSeconds)}";
}
if (timeSpan.TotalHours < 1)
{
return $"{(int)timeSpan.TotalMinutes} minute{Plural((int)timeSpan.TotalMinutes)}";
}
if (timeSpan.Days < 1)
{
return $"{(int)timeSpan.TotalHours} hour{Plural((int)timeSpan.TotalHours)}";
}
return $"{(int)timeSpan.TotalDays} day{Plural((int)timeSpan.TotalDays)}";
}
private static string Plural(int totalDays)
{
return totalDays > 1 ? "s" : string.Empty;
}
}
}

View File

@ -18,7 +18,8 @@
"wwwroot/vendor/jquery-easing/jquery.easing.js",
"wwwroot/vendor/scrollreveal/scrollreveal.min.js",
"wwwroot/vendor/magnific-popup/jquery.magnific-popup.js",
"wwwroot/vendor/bootstrap4-creativestart/*.js"
"wwwroot/vendor/bootstrap4-creativestart/*.js",
"wwwroot/main/**/*.js"
]
},
{

View File

@ -5,7 +5,7 @@ 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 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.",
"Contact_Body": "Por favor indica una dirección de correo electrónico a continuación. Nos pondremos en contacto contigo en esta dirección si hay algún problema con tu pago.",
"Your email": "Tu correo electrónico",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor ingresa un correo electrónico válido",
@ -18,15 +18,15 @@ const locales_es = {
"Copy": "Copiar",
"Conversion": "Conversión",
// Scan tab
"Open in wallet": "Abrir en billetera",
"Open in wallet": "Abrir en la billetera",
// Copy tab
"CompletePay_Body": "Para completar tu pago, envía {{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": "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}} .",
"ConversionTab_BodyTop": "Puedes pagar {{btcDue}} {{cryptoCode}} usando Altcoins que este comercio no soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros envían los fondos. La factura solo se marcará como pagada una vez 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
@ -36,8 +36,8 @@ const locales_es = {
"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",
"Invoice ID": "ID de la factura",
"Order ID": "ID del pedido",
"Return to StoreName": "Regresar a {{storeName}}",
// Invoice paid
"This invoice has been paid": "Esta factura ha sido pagada",

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,9 @@
$(function () {
$(".localizeDate").each(function (index) {
var serverDate = $(this).text();
var localDate = new Date(serverDate);
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
$(this).text(dateString);
});
});

View File

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.1.300-rc1-sdk-alpine3.7 AS builder
FROM microsoft/dotnet:2.1.300-sdk-alpine3.7 AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
# Cache some dependencies
@ -6,7 +6,7 @@ RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
FROM microsoft/dotnet:2.1.0-rc1-aspnetcore-runtime-alpine3.7
FROM microsoft/dotnet:2.1.0-aspnetcore-runtime-alpine3.7
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false
RUN apk add --no-cache icu-libs

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>

View File

@ -30,7 +30,7 @@ You can also checkout [The Merchants Guide to accepting Bitcoin directly with no
While the documentation advise using docker-compose, you may want to build yourself outside of development purpose.
First install .NET Core SDK 2.1 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core/sdk-2.1.300-rc1).
First install .NET Core SDK 2.1 as specified by [Microsoft website](https://www.microsoft.com/net/download/dotnet-core).
On Powershell:
```