Compare commits
96 Commits
Author | SHA1 | Date | |
---|---|---|---|
271de362cb | |||
d41474ebc8 | |||
5b0b3e30f4 | |||
48a95457b6 | |||
7c0b26174f | |||
f0145142a4 | |||
2848caff2e | |||
9e05ad787f | |||
de39fa0aea | |||
94ff77f2b2 | |||
bb7dc1ed4a | |||
c5e833ee79 | |||
4397591134 | |||
986c7b94f4 | |||
a6ef7387cf | |||
95bdeacd93 | |||
07c2f6b810 | |||
8ff81f1648 | |||
c3ee43c228 | |||
d85da28ca7 | |||
042142396d | |||
fbc4ca89aa | |||
2e5d29064b | |||
ef0b8376d3 | |||
1fa1b74261 | |||
4f9e4116a2 | |||
82d8fda05f | |||
d4935263da | |||
e158d909fb | |||
de8147d5dd | |||
16f1791a9a | |||
8745c3f8c6 | |||
ec5b45cff6 | |||
1348197295 | |||
f2516854d8 | |||
062ca6e743 | |||
44b6997bb5 | |||
78b544f9ca | |||
81926b4450 | |||
a7ad71d492 | |||
18977f7265 | |||
8a88b44e98 | |||
c9e5fe42ba | |||
56dffbf514 | |||
0e1fac3773 | |||
e7c06880a8 | |||
39463a3202 | |||
36136f0f0f | |||
22e5b2869a | |||
fc3f32a4e0 | |||
3b0914e89e | |||
76cd9a7b25 | |||
0934bebf7b | |||
cd1a4c4749 | |||
8075273ec8 | |||
97b59be9bf | |||
b87ec4f3d9 | |||
3822358096 | |||
ba7e8cfe78 | |||
41978f1c59 | |||
e75e691404 | |||
a22216fd04 | |||
6900e16aa4 | |||
10c981b2a0 | |||
5f940df1b4 | |||
3f85918a0c | |||
e4299c09ea | |||
e864cf35f7 | |||
3652866660 | |||
0421004616 | |||
6936b034cb | |||
73ed4003a3 | |||
5cb8cdd511 | |||
195b5fdd1a | |||
d19b78b6cc | |||
9bbc05c3a7 | |||
7df3c86649 | |||
c6e0a923bb | |||
637fe1727b | |||
fd087bbeb8 | |||
2f515e1cc0 | |||
84cd9e570f | |||
ead97a24bd | |||
415cde1629 | |||
b438312fde | |||
79ff2cb271 | |||
ed1464c405 | |||
f85631429b | |||
5ed56d1137 | |||
d7719d25b4 | |||
6267cccc3f | |||
fd5c4021f7 | |||
18986faca8 | |||
b099f93c78 | |||
81afe397be | |||
f869c06aee |
BTCPayServer.Tests
BTCPayServer
Authentication
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Dogecoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojConfiguration
Controllers
AccessTokenController.csAppsController.PointOfSale.csInvoiceController.API.csInvoiceController.UI.csServerController.csStoresController.cs
Data
DerivationSchemeParser.csExtensions.csFilters
HostedServices
Hosting
Migrations
20180429083930_legacyapikey.Designer.cs20180429083930_legacyapikey.csApplicationDbContextModelSnapshot.cs
Models
AppViewModels
InvoicingModels
ServerViewModels
StoreViewModels
Services
BTCPayServerEnvironment.csClaims.csHardwareWalletService.cs
SynchronizationContextRemover.csInvoices
LanguageService.csRates
BTCPayRateProviderFactory.csCoinAverageRateProvider.csCoinAverageSettings.csQuadrigacxRateProvider.cs
SettingsRepository.csStores
ThemesSettings.csViews
Apps
Home
Invoice
Manage
ChangePassword.cshtmlDisable2fa.cshtmlEnableAuthenticator.cshtmlExternalLogins.cshtmlGenerateRecoveryCodes.cshtmlIndex.cshtmlManageNavPages.csResetAuthenticator.cshtmlSetPassword.cshtmlTwoFactorAuthentication.cshtml_Nav.cshtml
Server
Emails.cshtmlListUsers.cshtmlPolicies.cshtmlRates.cshtmlServerNavPages.csTheme.cshtmlUser.cshtml_Nav.cshtml
Shared
Stores
ViewsRazor.cs_ViewImports.cshtmlwwwroot
checkout/js
js/checkout/lang
main/css
vendor
bootstrap4-creativestart
bootstrap4/css
@ -72,7 +72,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chain = NBXplorerDefaultSettings.GetFolderName(NetworkType.Regtest);
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
@ -39,7 +39,7 @@ namespace BTCPayServer.Tests
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
|
@ -32,6 +32,9 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -598,8 +601,18 @@ namespace BTCPayServer.Tests
|
||||
var search = new SearchString(filter);
|
||||
Assert.Equal("storeid:abc status:abed blabhbalh", search.ToString());
|
||||
Assert.Equal("blabhbalh", search.TextSearch);
|
||||
Assert.Equal("abc", search.Filters["storeid"]);
|
||||
Assert.Equal("abed", search.Filters["status"]);
|
||||
Assert.Single(search.Filters["storeid"]);
|
||||
Assert.Single(search.Filters["status"]);
|
||||
Assert.Equal("abc", search.Filters["storeid"].First());
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
|
||||
filter = "status:abed status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("status:abed status:abed2", search.ToString());
|
||||
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
|
||||
Assert.Equal(2, search.Filters["status"].Count);
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -613,6 +626,36 @@ namespace BTCPayServer.Tests
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
Assert.True(user.BitPay.TestAccess(Facade.Merchant));
|
||||
|
||||
// Can generate API Key
|
||||
var repo = tester.PayTester.GetService<TokenRepository>();
|
||||
Assert.Empty(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
|
||||
|
||||
var apiKey = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
///////
|
||||
|
||||
// Generating a new one remove the previous
|
||||
Assert.IsType<RedirectToActionResult>(user.GetController<StoresController>().GenerateAPIKey(user.StoreId).GetAwaiter().GetResult());
|
||||
var apiKey2 = Assert.Single(repo.GetLegacyAPIKeys(user.StoreId).GetAwaiter().GetResult());
|
||||
Assert.NotEqual(apiKey, apiKey2);
|
||||
////////
|
||||
|
||||
apiKey = apiKey2;
|
||||
|
||||
// Can create an invoice with this new API Key
|
||||
HttpClient client = new HttpClient();
|
||||
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, tester.PayTester.ServerUri.AbsoluteUri + "invoices");
|
||||
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(apiKey)));
|
||||
var invoice = new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD"
|
||||
};
|
||||
message.Content = new StringContent(JsonConvert.SerializeObject(invoice), Encoding.UTF8, "application/json");
|
||||
var result = client.SendAsync(message).GetAwaiter().GetResult();
|
||||
result.EnsureSuccessStatusCode();
|
||||
/////////////////////
|
||||
}
|
||||
}
|
||||
|
||||
@ -632,8 +675,8 @@ namespace BTCPayServer.Tests
|
||||
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
|
||||
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
|
||||
rates.Add(bitflyer);
|
||||
|
||||
foreach(var rate in rates)
|
||||
|
||||
foreach (var rate in rates)
|
||||
{
|
||||
Assert.Single(rates.Where(r => r == rate));
|
||||
}
|
||||
@ -873,7 +916,7 @@ namespace BTCPayServer.Tests
|
||||
[Fact]
|
||||
public void CanParseDerivationScheme()
|
||||
{
|
||||
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
|
||||
var parser = new DerivationSchemeParser(Network.TestNet);
|
||||
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
|
||||
// Passing electrum stuff
|
||||
// Native
|
||||
@ -987,7 +1030,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, "orange").Result);
|
||||
Assert.IsType<RedirectResult>(apps.ViewPointOfSale(appId, 0, "orange").Result);
|
||||
var invoice = user.BitPay.GetInvoices().First();
|
||||
Assert.Equal(10.00, invoice.Price);
|
||||
Assert.Equal("CAD", invoice.Currency);
|
||||
@ -1054,13 +1097,13 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
var textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
StoreId = new[] { user.StoreId },
|
||||
TextSearch = invoice.OrderId
|
||||
}).GetAwaiter().GetResult();
|
||||
Assert.Single(textSearchResult);
|
||||
textSearchResult = tester.PayTester.InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
StoreId = user.StoreId,
|
||||
StoreId = new[] { user.StoreId },
|
||||
TextSearch = invoice.Id
|
||||
}).GetAwaiter().GetResult();
|
||||
|
||||
@ -1185,6 +1228,26 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckQuadrigacxRateProvider()
|
||||
{
|
||||
var quadri = new QuadrigacxRateProvider("BTC");
|
||||
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
|
||||
quadri = new QuadrigacxRateProvider("LTC");
|
||||
rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
|
||||
Assert.NotEmpty(rates);
|
||||
Assert.NotEqual(0.0m, rates.First().Value);
|
||||
Assert.NotEqual(0.0m, quadri.GetRateAsync("CAD").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("IOEW").GetAwaiter().GetResult());
|
||||
Assert.Throws<RateUnavailableException>(() => quadri.GetRateAsync("USD").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckRatesProvider()
|
||||
{
|
||||
|
@ -1,52 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
// Helper class for testing functionality and generating data needed during coding/debuging
|
||||
public class UnitTestPeusa
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
public void GeneratePubkey()
|
||||
{
|
||||
var network = Network.RegTest;
|
||||
|
||||
ExtKey masterKey = new ExtKey();
|
||||
Console.WriteLine("Master key : " + masterKey.ToString(network));
|
||||
ExtPubKey masterPubKey = masterKey.Neuter();
|
||||
|
||||
ExtPubKey pubkey = masterPubKey.Derive(0);
|
||||
Console.WriteLine("PubKey " + 0 + " : " + pubkey.ToString(network));
|
||||
}
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ services:
|
||||
- lightning-charged
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.34
|
||||
image: nicolasdorier/nbxplorer:1.0.2.2
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
@ -89,7 +89,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.3
|
||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
@ -98,6 +98,7 @@ services:
|
||||
network=regtest
|
||||
ipaddr=customer_lightningd
|
||||
log-level=debug
|
||||
dev-broadcast-interval=1000
|
||||
ports:
|
||||
- "30992:9835" # api port
|
||||
expose:
|
||||
@ -129,7 +130,7 @@ services:
|
||||
- merchant_lightningd
|
||||
|
||||
merchant_lightningd:
|
||||
image: nicolasdorier/clightning:0.0.0.5-dev
|
||||
image: nicolasdorier/clightning:0.0.0.11-dev
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_OPT: |
|
||||
|
@ -1,35 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.Authentication
|
||||
{
|
||||
public class BitIdentity : IIdentity
|
||||
{
|
||||
public BitIdentity(PubKey key)
|
||||
{
|
||||
PubKey = key;
|
||||
_Name = Encoders.Base58Check.EncodeData(Encoders.Hex.DecodeData("0f02" + key.Hash.ToString()));
|
||||
SIN = NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(key);
|
||||
}
|
||||
string _Name;
|
||||
|
||||
public string SIN
|
||||
{
|
||||
get;
|
||||
}
|
||||
public PubKey PubKey
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
public string AuthenticationType => "BitID";
|
||||
|
||||
public bool IsAuthenticated => true;
|
||||
|
||||
public string Name => _Name;
|
||||
}
|
||||
}
|
@ -33,6 +33,8 @@ namespace BTCPayServer.Authentication
|
||||
|
||||
public async Task<BitTokenEntity[]> GetTokens(string sin)
|
||||
{
|
||||
if (sin == null)
|
||||
return Array.Empty<BitTokenEntity>();
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return (await ctx.PairedSINData
|
||||
@ -43,6 +45,46 @@ namespace BTCPayServer.Authentication
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<String> GetStoreIdFromAPIKey(string apiKey)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.Id == apiKey).Select(o => o.StoreId).FirstOrDefaultAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GenerateLegacyAPIKey(string storeId)
|
||||
{
|
||||
// It is legacy support and Bitpay generate string of unknown format, trying to replicate them
|
||||
// as good as possible. The string below got generated for me.
|
||||
var chars = "ERo0vkBMOYhyU0ZHvirCplbLDIGWPdi1ok77VnW7QdE";
|
||||
var rand = new Random(Math.Abs(RandomUtils.GetInt32()));
|
||||
var generated = new char[chars.Length];
|
||||
for (int i = 0; i < generated.Length; i++)
|
||||
{
|
||||
generated[i] = chars[rand.Next(0, generated.Length)];
|
||||
}
|
||||
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
var existing = await ctx.ApiKeys.Where(o => o.StoreId == storeId).FirstOrDefaultAsync();
|
||||
if (existing != null)
|
||||
{
|
||||
ctx.ApiKeys.Remove(existing);
|
||||
}
|
||||
ctx.ApiKeys.Add(new APIKeyData() { Id = new string(generated), StoreId = storeId });
|
||||
await ctx.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string[]> GetLegacyAPIKeys(string storeId)
|
||||
{
|
||||
using (var ctx = _Factory.CreateContext())
|
||||
{
|
||||
return await ctx.ApiKeys.Where(o => o.StoreId == storeId).Select(c => c.Id).ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private BitTokenEntity CreateTokenEntity(PairedSINData data)
|
||||
{
|
||||
return new BitTokenEntity()
|
||||
|
@ -14,34 +14,28 @@ namespace BTCPayServer
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
_Settings = new Dictionary<NetworkType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { NetworkType.Mainnet, NetworkType.Testnet, NetworkType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", NBXplorerDefaultSettings.GetFolderName(chainType));
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
settings.DefaultPort = (chainType == NetworkType.Mainnet ? 23000 :
|
||||
chainType == NetworkType.Regtest ? 23002 :
|
||||
chainType == NetworkType.Testnet ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
static Dictionary<NetworkType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(NetworkType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
|
@ -20,15 +20,15 @@ namespace BTCPayServer
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/btc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,20 +12,18 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitDogecoin()
|
||||
{
|
||||
NBitcoin.Altcoins.Dogecoin.EnsureRegistered();
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DOGE");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://dogechain.info/tx/{0}" : "https://dogechain.info/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "dogecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("DOGE"),
|
||||
CryptoImagePath = "imlegacy/dogecoin.png",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("3'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,21 +12,19 @@ namespace BTCPayServer
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBitcoin.Altcoins.Litecoin.EnsureRegistered();
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = new CoinAverageRateProviderDescription("LTC"),
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
LightningImagePath = "imlegacy/ltc-lightning.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("1'")
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
|
||||
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("2'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ namespace BTCPayServer
|
||||
|
||||
BTCPayNetworkProvider(BTCPayNetworkProvider filtered, string[] cryptoCodes)
|
||||
{
|
||||
ChainType = filtered.ChainType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.ChainType);
|
||||
NetworkType = filtered.NetworkType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(filtered.NetworkType);
|
||||
_Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
cryptoCodes = cryptoCodes.Select(c => c.ToUpperInvariant()).ToArray();
|
||||
foreach (var network in filtered._Networks)
|
||||
@ -40,11 +40,11 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
public ChainType ChainType { get; set; }
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
public NetworkType NetworkType { get; private set; }
|
||||
public BTCPayNetworkProvider(NetworkType networkType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
ChainType = chainType;
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(networkType);
|
||||
NetworkType = networkType;
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
InitDogecoin();
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.84</Version>
|
||||
<Version>1.0.1.95</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@ -31,18 +31,19 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BuildBundlerMinifier" Version="2.6.375" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.0" />
|
||||
<PackageReference Include="Hangfire" Version="1.6.19" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.36" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="1.0.1" />
|
||||
<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.0.11" />
|
||||
<PackageReference Include="NBitcoin" Version="4.1.1.3" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.18" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.24" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.2.3" />
|
||||
<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" />
|
||||
@ -66,7 +67,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="wwwroot\checkout\js\core.js" />
|
||||
<None Include="wwwroot\main\js\creative.js" />
|
||||
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
|
||||
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />
|
||||
<None Include="wwwroot\vendor\font-awesome\less\animated.less" />
|
||||
|
@ -23,7 +23,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public ChainType ChainType
|
||||
public NetworkType NetworkType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -51,15 +51,15 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
NetworkType = DefaultConfiguration.GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
Logs.Configuration.LogInformation("Network: " + NetworkType.ToString());
|
||||
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType).Filter(supportedChains.ToArray());
|
||||
NetworkProvider = new BTCPayNetworkProvider(NetworkType).Filter(supportedChains.ToArray());
|
||||
foreach (var chain in supportedChains)
|
||||
{
|
||||
if (NetworkProvider.GetNetwork(chain) == null)
|
||||
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
@ -48,12 +48,12 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
@ -69,7 +69,7 @@ namespace BTCPayServer.Configuration
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
public static ChainType GetChainType(IConfiguration conf)
|
||||
public static NetworkType GetNetworkType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
@ -79,17 +79,18 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
return n.NetworkType;
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? NetworkType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? NetworkType.Testnet : NetworkType.Mainnet;
|
||||
|
||||
return net;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var networkType = GetNetworkType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(networkType);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
@ -102,7 +103,7 @@ namespace BTCPayServer.Configuration
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
foreach (var n in new BTCPayNetworkProvider(networkType).GetAll())
|
||||
{
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
@ -116,7 +117,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetNetworkType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[BitpayAPIConstraint]
|
||||
public class AccessTokenController : Controller
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
@ -23,7 +24,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("tokens")]
|
||||
public async Task<GetTokensResponse> Tokens()
|
||||
{
|
||||
var tokens = await _TokenRepository.GetTokens(this.GetBitIdentity().SIN);
|
||||
var tokens = await _TokenRepository.GetTokens(this.User.GetSIN());
|
||||
return new GetTokensResponse(tokens);
|
||||
}
|
||||
|
||||
@ -51,7 +52,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
var sin = this.GetBitIdentity(false)?.SIN ?? request.Id;
|
||||
var sin = this.User.GetSIN() ?? request.Id;
|
||||
if (string.IsNullOrEmpty(request.Id) || !NBitpayClient.Extensions.BitIdExtensions.ValidateSIN(request.Id))
|
||||
throw new BitpayHttpException(400, "'id' property is required, alternatively, use BitId");
|
||||
|
||||
|
@ -42,10 +42,12 @@ namespace BTCPayServer.Controllers
|
||||
" price: 15\n\n" +
|
||||
"tshirt:\n" +
|
||||
" price: 25";
|
||||
ShowCustomAmount = true;
|
||||
}
|
||||
public string Title { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string Template { get; set; }
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -57,7 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, Currency = settings.Currency, Template = settings.Template });
|
||||
return View(new UpdatePointOfSaleViewModel() { Title = settings.Title, ShowCustomAmount = settings.ShowCustomAmount, Currency = settings.Currency, Template = settings.Template });
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/pos")]
|
||||
@ -83,6 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
app.SetSettings(new PointOfSaleSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
ShowCustomAmount = vm.ShowCustomAmount,
|
||||
Currency = vm.Currency.ToUpperInvariant(),
|
||||
Template = vm.Template
|
||||
});
|
||||
@ -99,9 +102,13 @@ namespace BTCPayServer.Controllers
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var currency = _Currencies.GetCurrencyData(settings.Currency);
|
||||
double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility));
|
||||
return View(new ViewPointOfSaleViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||
ShowCustomAmount = settings.ShowCustomAmount,
|
||||
Items = Parse(settings.Template, settings.Currency)
|
||||
});
|
||||
}
|
||||
@ -155,23 +162,43 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpPost]
|
||||
[Route("{appId}/pos")]
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, string choiceKey)
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId, double amount, string choiceKey)
|
||||
{
|
||||
var app = await GetApp(appId, AppType.PointOfSale);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
if(string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||
}
|
||||
string title = null;
|
||||
double price = 0.0;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
var choices = Parse(settings.Template, settings.Currency);
|
||||
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
|
||||
if (choice == null)
|
||||
return NotFound();
|
||||
title = choice.Title;
|
||||
price = (double)choice.Price.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
}
|
||||
|
||||
var store = await GetStore(app);
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
|
||||
{
|
||||
ItemDesc = choice.Title,
|
||||
ItemDesc = title,
|
||||
Currency = settings.Currency,
|
||||
Price = (double)choice.Price.Value,
|
||||
Price = price,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return Redirect(invoice.Data.Url);
|
||||
}
|
||||
|
@ -22,20 +22,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private InvoiceController _InvoiceController;
|
||||
private InvoiceRepository _InvoiceRepository;
|
||||
private TokenRepository _TokenRepository;
|
||||
private StoreRepository _StoreRepository;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceControllerAPI(InvoiceController invoiceController,
|
||||
InvoiceRepository invoceRepository,
|
||||
TokenRepository tokenRepository,
|
||||
StoreRepository storeRepository,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
this._InvoiceController = invoiceController;
|
||||
this._InvoiceRepository = invoceRepository;
|
||||
this._TokenRepository = tokenRepository;
|
||||
this._StoreRepository = storeRepository;
|
||||
this._NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
@ -44,21 +38,16 @@ namespace BTCPayServer.Controllers
|
||||
[MediaTypeConstraint("application/json")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> CreateInvoice([FromBody] Invoice invoice)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, invoice.Token);
|
||||
var store = await FindStore(bitToken);
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
return await _InvoiceController.CreateInvoiceCore(invoice, HttpContext.GetStoreData(), HttpContext.Request.GetAbsoluteRoot());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/{id}")]
|
||||
public async Task<DataWrapper<InvoiceResponse>> GetInvoice(string id, string token)
|
||||
{
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(store.Id, id);
|
||||
var invoice = await _InvoiceRepository.GetInvoice(HttpContext.GetStoreData().Id, id);
|
||||
if (invoice == null)
|
||||
throw new BitpayHttpException(404, "Object not found");
|
||||
|
||||
var resp = invoice.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp);
|
||||
}
|
||||
@ -77,8 +66,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
var bitToken = await CheckTokenPermissionAsync(Facade.Merchant, token);
|
||||
var store = await FindStore(bitToken);
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
@ -87,55 +75,14 @@ namespace BTCPayServer.Controllers
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
Status = status,
|
||||
StoreId = store.Id
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
||||
|
||||
var entities = (await _InvoiceRepository.GetInvoices(query))
|
||||
.Select((o) => o.EntityToDTO(_NetworkProvider)).ToArray();
|
||||
|
||||
return DataWrapper.Create(entities);
|
||||
}
|
||||
|
||||
private async Task<BitTokenEntity> CheckTokenPermissionAsync(Facade facade, string expectedToken)
|
||||
{
|
||||
if (facade == null)
|
||||
throw new ArgumentNullException(nameof(facade));
|
||||
|
||||
var actualTokens = (await _TokenRepository.GetTokens(this.GetBitIdentity().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} for SIN {this.GetBitIdentity().SIN}");
|
||||
throw new BitpayHttpException(401, $"This endpoint does not support the `{actualTokens.Select(a => a.Facade).Concat(new[] { "user" }).FirstOrDefault()}` facade");
|
||||
}
|
||||
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 async Task<StoreData> FindStore(BitTokenEntity bitToken)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(bitToken.StoreId);
|
||||
if (store == null)
|
||||
throw new BitpayHttpException(401, "Unknown store");
|
||||
return store;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -85,54 +85,66 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
var onChainPayments = invoice
|
||||
.GetPayments()
|
||||
.Where(p => p.GetPaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
.Select<PaymentEntity, Task<object>>(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.PaymentMethod = ToString(payment.GetPaymentMethodId());
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ( (paymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (paymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
if (paymentData is Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData)
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
paymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(paymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
m.Crypto = payment.GetPaymentMethodId().CryptoCode;
|
||||
m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted)
|
||||
&& (onChainPaymentData.Legacy || invoice.MonitoringExpiration < DateTimeOffset.UtcNow)) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await ((ExplorerClientProvider)_ServiceProvider.GetService(typeof(ExplorerClientProvider))).GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(onChainPaymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
onChainPaymentData.ConfirmationCount = confirmationCount;
|
||||
payment.SetCryptoPaymentData(onChainPaymentData);
|
||||
await _InvoiceRepository.UpdatePayments(new List<PaymentEntity> { payment });
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = onChainPaymentData.ConfirmationCount;
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = onChainPaymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
var lightningPaymentData = (Payments.Lightning.LightningLikePaymentData)paymentData;
|
||||
return new InvoiceDetailsModel.OffChainPayment()
|
||||
{
|
||||
Crypto = paymentNetwork.CryptoCode,
|
||||
BOLT11 = lightningPaymentData.BOLT11
|
||||
};
|
||||
}
|
||||
if (confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
await Task.WhenAll(payments);
|
||||
await Task.WhenAll(onChainPayments);
|
||||
model.Addresses = invoice.HistoricalAddresses.Select(h => new InvoiceDetailsModel.AddressModel
|
||||
{
|
||||
Destination = h.GetAddress(),
|
||||
PaymentMethod = ToString(h.GetPaymentMethodId()),
|
||||
Current = !h.UnAssigned.HasValue
|
||||
}).ToArray();
|
||||
model.Payments = payments.Select(p => p.GetAwaiter().GetResult()).ToList();
|
||||
model.OnChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.Payment>().ToList();
|
||||
model.OffChainPayments = onChainPayments.Select(p => p.GetAwaiter().GetResult()).OfType<InvoiceDetailsModel.OffChainPayment>().ToList();
|
||||
model.StatusMessage = StatusMessage;
|
||||
return View(model);
|
||||
}
|
||||
@ -220,6 +232,7 @@ namespace BTCPayServer.Controllers
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
@ -253,7 +266,7 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
model.TimeLeft = expiration.PrettyPrint();
|
||||
return model;
|
||||
}
|
||||
|
||||
@ -272,17 +285,6 @@ namespace BTCPayServer.Controllers
|
||||
return price.ToString("C", _CurrencyNameTable.GetCurrencyProvider(currency)) + $" ({currency})";
|
||||
}
|
||||
|
||||
private string PrettyPrint(TimeSpan expiration)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
@ -365,15 +367,15 @@ namespace BTCPayServer.Controllers
|
||||
Count = count,
|
||||
Skip = skip,
|
||||
UserId = GetUserId(),
|
||||
Status = filterString.Filters.TryGet("status"),
|
||||
StoreId = filterString.Filters.TryGet("storeid")
|
||||
Status = filterString.Filters.ContainsKey("status") ? filterString.Filters["status"].ToArray() : null,
|
||||
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null
|
||||
}))
|
||||
{
|
||||
model.SearchTerm = searchTerm;
|
||||
model.Invoices.Add(new InvoiceModel()
|
||||
{
|
||||
Status = invoice.Status,
|
||||
Date = Prettify(invoice.InvoiceTime),
|
||||
Date = (DateTimeOffset.UtcNow - invoice.InvoiceTime).Prettify() + " ago",
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.OrderId ?? string.Empty,
|
||||
RedirectUrl = invoice.RedirectURL ?? string.Empty,
|
||||
@ -386,30 +388,6 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string Prettify(DateTimeOffset invoiceTime)
|
||||
{
|
||||
var ago = DateTime.UtcNow - invoiceTime;
|
||||
|
||||
if (ago.TotalMinutes < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalSeconds} second{Plural((int)ago.TotalSeconds)} ago";
|
||||
}
|
||||
if (ago.TotalHours < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalMinutes} minute{Plural((int)ago.TotalMinutes)} ago";
|
||||
}
|
||||
if (ago.Days < 1)
|
||||
{
|
||||
return $"{(int)ago.TotalHours} hour{Plural((int)ago.TotalHours)} ago";
|
||||
}
|
||||
return $"{(int)ago.TotalDays} day{Plural((int)ago.TotalDays)} ago";
|
||||
}
|
||||
|
||||
private string Plural(int totalDays)
|
||||
{
|
||||
return totalDays > 1 ? "s" : string.Empty;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("invoices/create")]
|
||||
[Authorize(AuthenticationSchemes = "Identity.Application")]
|
||||
@ -450,7 +428,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if(StatusMessage != null)
|
||||
if (StatusMessage != null)
|
||||
{
|
||||
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
@ -38,31 +39,30 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Rates()
|
||||
{
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
return View(new RatesViewModel()
|
||||
|
||||
var vm = new RatesViewModel()
|
||||
{
|
||||
CacheMinutes = rates.CacheInMinutes,
|
||||
PrivateKey = rates.PrivateKey,
|
||||
PublicKey = rates.PublicKey
|
||||
});
|
||||
|
||||
};
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
class TestCoinAverageAuthenticator : ICoinAverageAuthenticator
|
||||
private static async Task FetchRateLimits(RatesViewModel vm)
|
||||
{
|
||||
private RatesSetting settings;
|
||||
|
||||
public TestCoinAverageAuthenticator(RatesSetting settings)
|
||||
var coinAverage = GetCoinaverageService(vm, false);
|
||||
if (coinAverage != null)
|
||||
{
|
||||
this.settings = settings;
|
||||
}
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var sig = settings.GetCoinAverageSignature();
|
||||
if (sig != null)
|
||||
message.Headers.Add("X-signature", settings.GetCoinAverageSignature());
|
||||
return Task.CompletedTask;
|
||||
try
|
||||
{
|
||||
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Route("server/rates")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Rates(RatesViewModel vm)
|
||||
@ -73,24 +73,38 @@ namespace BTCPayServer.Controllers
|
||||
rates.CacheInMinutes = vm.CacheMinutes;
|
||||
try
|
||||
{
|
||||
if (rates.GetCoinAverageSignature() != null)
|
||||
{
|
||||
await new CoinAverageRateProvider("BTC")
|
||||
{ Authenticator = new TestCoinAverageAuthenticator(rates) }.TestAuthAsync();
|
||||
}
|
||||
var service = GetCoinaverageService(vm, true);
|
||||
if(service != null)
|
||||
await service.TestAuthAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FetchRateLimits(vm);
|
||||
return View(vm);
|
||||
}
|
||||
await _SettingsRepository.UpdateSetting(rates);
|
||||
((BTCPayRateProviderFactory)_RateProviderFactory).CacheSpan = TimeSpan.FromMinutes(vm.CacheMinutes);
|
||||
StatusMessage = "Rate settings successfully updated";
|
||||
return RedirectToAction(nameof(Rates));
|
||||
}
|
||||
|
||||
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
|
||||
{
|
||||
var settings = new CoinAverageSettings()
|
||||
{
|
||||
KeyPair = (vm.PublicKey, vm.PrivateKey)
|
||||
};
|
||||
if (!withAuth || settings.GetCoinAverageSignature() != null)
|
||||
{
|
||||
return new CoinAverageRateProvider("BTC")
|
||||
{ Authenticator = settings };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[Route("server/users")]
|
||||
public IActionResult ListUsers()
|
||||
{
|
||||
@ -115,6 +129,7 @@ namespace BTCPayServer.Controllers
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.Email = user.Email;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
@ -201,7 +216,22 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> Policies(PoliciesSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Policies upadated successfully";
|
||||
TempData["StatusMessage"] = "Policies updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
[Route("server/theme")]
|
||||
public async Task<IActionResult> Theme()
|
||||
{
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
return View(data);
|
||||
}
|
||||
[Route("server/theme")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Theme(ThemeSettings settings)
|
||||
{
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
TempData["StatusMessage"] = "Theme settings updated successfully";
|
||||
return View(settings);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -47,7 +48,8 @@ namespace BTCPayServer.Controllers
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
LanguageService langService,
|
||||
IHostingEnvironment env)
|
||||
IHostingEnvironment env,
|
||||
CoinAverageSettings coinAverage)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_Repo = repo;
|
||||
@ -64,7 +66,9 @@ namespace BTCPayServer.Controllers
|
||||
_ServiceProvider = serviceProvider;
|
||||
_BtcpayServerOptions = btcpayServerOptions;
|
||||
_BTCPayEnv = btcpayEnv;
|
||||
_CoinAverage = coinAverage;
|
||||
}
|
||||
CoinAverageSettings _CoinAverage;
|
||||
NBXplorerDashboard _Dashboard;
|
||||
BTCPayServerOptions _BtcpayServerOptions;
|
||||
BTCPayServerEnvironment _BTCPayEnv;
|
||||
@ -197,6 +201,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
|
||||
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
vm.RequiresRefundEmail = storeBlob.RequiresRefundEmail;
|
||||
vm.CustomCSS = storeBlob.CustomCSS?.AbsoluteUri;
|
||||
vm.CustomLogo = storeBlob.CustomLogo?.AbsoluteUri;
|
||||
return View(vm);
|
||||
@ -237,12 +242,13 @@ namespace BTCPayServer.Controllers
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
|
||||
model.SetLanguages(_LangService, model.DefaultLang);
|
||||
|
||||
if(!ModelState.IsValid)
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
blob.DefaultLang = model.DefaultLang;
|
||||
blob.AllowCoinConversion = model.AllowCoinConversion;
|
||||
blob.RequiresRefundEmail = model.RequiresRefundEmail;
|
||||
blob.LightningMaxValue = lightningMaxValue;
|
||||
blob.OnChainMinValue = onchainMinValue;
|
||||
blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute);
|
||||
@ -273,6 +279,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var vm = new StoreViewModel();
|
||||
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange);
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
@ -283,7 +290,6 @@ namespace BTCPayServer.Controllers
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -325,6 +331,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
{
|
||||
model.SetExchangeRates(GetSupportedExchanges(), model.PreferredExchange);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -371,14 +378,11 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
using (HttpClient client = new HttpClient())
|
||||
|
||||
if (!GetSupportedExchanges().Select(c => c.Name).Contains(blob.PreferredExchange, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var rate = await client.GetAsync(model.RateSource);
|
||||
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,9 +398,17 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private (String DisplayName, String Name)[] GetSupportedExchanges()
|
||||
{
|
||||
return new[] { ("Coin Average", "coinaverage") }
|
||||
.Concat(_CoinAverage.AvailableExchanges)
|
||||
.OrderBy(s => s.Item1, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
|
||||
{
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
|
||||
var parser = new DerivationSchemeParser(network.NBitcoinNetwork);
|
||||
parser.HintScriptPubKey = hint;
|
||||
return new DerivationStrategy(parser.Parse(derivationScheme), network);
|
||||
}
|
||||
@ -415,6 +427,12 @@ namespace BTCPayServer.Controllers
|
||||
SIN = t.SIN,
|
||||
Id = t.Value
|
||||
}).ToArray();
|
||||
|
||||
model.ApiKey = (await _TokenRepository.GetLegacyAPIKeys(storeId)).FirstOrDefault();
|
||||
if (model.ApiKey == null)
|
||||
model.EncodedApiKey = "*API Key*";
|
||||
else
|
||||
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
|
||||
return View(model);
|
||||
}
|
||||
|
||||
@ -513,6 +531,17 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/tokens/apikey")]
|
||||
public async Task<IActionResult> GenerateAPIKey(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
await _TokenRepository.GenerateLegacyAPIKey(storeId);
|
||||
StatusMessage = "API Key re-generated";
|
||||
return RedirectToAction(nameof(ListTokens));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-access-request")]
|
||||
|
23
BTCPayServer/Data/APIKeyData.cs
Normal file
23
BTCPayServer/Data/APIKeyData.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class APIKeyData
|
||||
{
|
||||
[MaxLength(50)]
|
||||
public string Id
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
@ -86,6 +86,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<APIKeyData> ApiKeys
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
var isConfigured = optionsBuilder.Options.Extensions.OfType<RelationalOptionsExtension>().Any();
|
||||
@ -112,6 +117,8 @@ namespace BTCPayServer.Data
|
||||
t.StoreDataId
|
||||
});
|
||||
|
||||
builder.Entity<APIKeyData>()
|
||||
.HasIndex(o => o.StoreId);
|
||||
|
||||
builder.Entity<AppData>()
|
||||
.HasOne(a => a.StoreData);
|
||||
|
@ -14,6 +14,7 @@ using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -120,7 +121,7 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
if(!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
if (!existing && supportedPaymentMethod == null && paymentMethodId.IsBTCOnChain)
|
||||
{
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
@ -214,6 +215,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
{
|
||||
@ -223,6 +225,9 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
|
@ -13,13 +13,11 @@ namespace BTCPayServer
|
||||
public class DerivationSchemeParser
|
||||
{
|
||||
public Network Network { get; set; }
|
||||
public ChainType ChainType { get; set; }
|
||||
public Script HintScriptPubKey { get; set; }
|
||||
|
||||
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
|
||||
public DerivationSchemeParser(Network expectedNetwork)
|
||||
{
|
||||
Network = expectedNetwork;
|
||||
ChainType = chainType;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
@ -78,7 +76,7 @@ namespace BTCPayServer
|
||||
if (data.Length < 4)
|
||||
continue;
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
var standardPrefix = Utils.ToBytes(Network.NetworkType == NetworkType.Mainnet ? 0x0488b21eU : 0x043587cf, false);
|
||||
for (int ii = 0; ii < 4; ii++)
|
||||
data[ii] = standardPrefix[ii];
|
||||
|
||||
|
@ -28,11 +28,46 @@ using BTCPayServer.Payments;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using BTCPayServer.Models;
|
||||
using System.Security.Claims;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
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();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
@ -100,6 +135,14 @@ namespace BTCPayServer
|
||||
request.PathBase.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetAbsoluteUri(this HttpRequest request, string redirectUrl)
|
||||
{
|
||||
bool isRelative =
|
||||
(redirectUrl.Length > 0 && redirectUrl[0] == '/')
|
||||
|| !new Uri(redirectUrl, UriKind.RelativeOrAbsolute).IsAbsoluteUri;
|
||||
return isRelative ? request.GetAbsoluteRoot() + redirectUrl : redirectUrl;
|
||||
}
|
||||
|
||||
public static IServiceCollection ConfigureBTCPayServer(this IServiceCollection services, IConfiguration conf)
|
||||
{
|
||||
services.Configure<BTCPayServerOptions>(o =>
|
||||
@ -109,12 +152,35 @@ namespace BTCPayServer
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static BitIdentity GetBitIdentity(this Controller controller, bool throws = true)
|
||||
public static string GetSIN(this ClaimsPrincipal principal)
|
||||
{
|
||||
if (!(controller.User.Identity is BitIdentity))
|
||||
return throws ? throw new UnauthorizedAccessException("no-bitid") : (BitIdentity)null;
|
||||
return (BitIdentity)controller.User.Identity;
|
||||
return principal.Claims.Where(c => c.Type == Claims.SIN).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static string GetStoreId(this ClaimsPrincipal principal)
|
||||
{
|
||||
return principal.Claims.Where(c => c.Type == Claims.OwnStore).Select(c => c.Value).FirstOrDefault();
|
||||
}
|
||||
|
||||
public static void SetIsBitpayAPI(this HttpContext ctx, bool value)
|
||||
{
|
||||
NBitcoin.Extensions.TryAdd(ctx.Items, "IsBitpayAPI", value);
|
||||
}
|
||||
|
||||
public static bool GetIsBitpayAPI(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGetValue("IsBitpayAPI", out object obj) &&
|
||||
obj is bool b && b;
|
||||
}
|
||||
|
||||
|
||||
public static StoreData GetStoreData(this HttpContext ctx)
|
||||
{
|
||||
return ctx.Items.TryGet("BTCPAY.STOREDATA") as StoreData;
|
||||
}
|
||||
public static void SetStoreData(this HttpContext ctx, StoreData storeData)
|
||||
{
|
||||
ctx.Items["BTCPAY.STOREDATA"] = storeData;
|
||||
}
|
||||
|
||||
private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
|
||||
|
@ -43,9 +43,7 @@ namespace BTCPayServer.Filters
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var hasVersion = context.RouteContext.HttpContext.Request.Headers["x-accept-version"].Where(h => h == "2.0.0").Any();
|
||||
var hasIdentity = context.RouteContext.HttpContext.Request.Headers["x-identity"].Any();
|
||||
return (hasVersion || hasIdentity) == IsBitpayAPI;
|
||||
return context.RouteContext.HttpContext.GetIsBitpayAPI() == IsBitpayAPI;
|
||||
}
|
||||
}
|
||||
|
||||
|
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
66
BTCPayServer/HostedServices/BaseAsyncService.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public abstract class BaseAsyncService : IHostedService
|
||||
{
|
||||
private CancellationTokenSource _Cts;
|
||||
protected Task[] _Tasks;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts = new CancellationTokenSource();
|
||||
_Tasks = InitializeTasks();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal abstract Task[] InitializeTasks();
|
||||
|
||||
protected CancellationToken Cancellation
|
||||
{
|
||||
get { return _Cts.Token; }
|
||||
}
|
||||
|
||||
protected async Task CreateLoopTask(Func<Task> act, [CallerMemberName]string caller = null)
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogWarning(ex, caller + " failed");
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), _Cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.IsCancellationRequested) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAll(_Tasks);
|
||||
}
|
||||
}
|
||||
}
|
74
BTCPayServer/HostedServices/CssThemeManager.cs
Normal file
74
BTCPayServer/HostedServices/CssThemeManager.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class CssThemeManager
|
||||
{
|
||||
public void Update(ThemeSettings data)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(data.BootstrapCssUri))
|
||||
_bootstrapUri = "/vendor/bootstrap4/css/bootstrap.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_bootstrapUri = data.BootstrapCssUri;
|
||||
|
||||
|
||||
if (String.IsNullOrWhiteSpace(data.CreativeStartCssUri))
|
||||
_creativeStartUri = "/vendor/bootstrap4-creativestart/creative.css?v=" + DateTime.Now.Ticks;
|
||||
else
|
||||
_creativeStartUri = data.CreativeStartCssUri;
|
||||
}
|
||||
|
||||
private string _bootstrapUri;
|
||||
public string BootstrapUri
|
||||
{
|
||||
get { return _bootstrapUri; }
|
||||
}
|
||||
|
||||
private string _creativeStartUri;
|
||||
public string CreativeStartUri
|
||||
{
|
||||
get { return _creativeStartUri; }
|
||||
}
|
||||
}
|
||||
|
||||
public class CssThemeManagerHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private CssThemeManager _CssThemeManager;
|
||||
|
||||
public CssThemeManagerHostedService(SettingsRepository settingsRepository, CssThemeManager cssThemeManager)
|
||||
{
|
||||
_SettingsRepository = settingsRepository;
|
||||
_CssThemeManager = cssThemeManager;
|
||||
}
|
||||
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(ListenForThemeChanges)
|
||||
};
|
||||
}
|
||||
|
||||
async Task ListenForThemeChanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var data = (await _SettingsRepository.GetSettingAsync<ThemeSettings>()) ?? new ThemeSettings();
|
||||
_CssThemeManager.Update(data);
|
||||
|
||||
await _SettingsRepository.WaitSettingsChanged<ThemeSettings>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
@ -184,8 +184,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
if(status != null && error == null)
|
||||
{
|
||||
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
|
||||
if(status.NetworkType != _Network.NBitcoinNetwork.NetworkType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.NetworkType}, expected: {_Network.NBitcoinNetwork.NetworkType})";
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
|
@ -8,48 +8,60 @@ using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class RatesHostedService : IHostedService
|
||||
public class RatesHostedService : BaseAsyncService
|
||||
{
|
||||
private SettingsRepository _SettingsRepository;
|
||||
private IRateProviderFactory _RateProviderFactory;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
private CoinAverageSettings _coinAverageSettings;
|
||||
public RatesHostedService(SettingsRepository repo,
|
||||
CoinAverageSettings coinAverageSettings,
|
||||
IRateProviderFactory rateProviderFactory)
|
||||
{
|
||||
this._SettingsRepository = repo;
|
||||
_RateProviderFactory = rateProviderFactory;
|
||||
}
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
return Task.CompletedTask;
|
||||
_coinAverageSettings = coinAverageSettings;
|
||||
}
|
||||
|
||||
async void Init()
|
||||
internal override Task[] InitializeTasks()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
|
||||
CreateLoopTask(RefreshCoinAverageSettings)
|
||||
};
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSupportedExchanges()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var tickers = await new CoinAverageRateProvider("BTC") { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
|
||||
_coinAverageSettings.AvailableExchanges = tickers
|
||||
.Exchanges
|
||||
.Select(c => (c.DisplayName, c.Name))
|
||||
.ToArray();
|
||||
await Task.Delay(TimeSpan.FromHours(5), Cancellation);
|
||||
}
|
||||
|
||||
async Task RefreshCoinAverageSettings()
|
||||
{
|
||||
await new SynchronizationContextRemover();
|
||||
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
|
||||
|
||||
//string[] availableExchanges = null;
|
||||
//// So we don't run this in testing
|
||||
//if(_RateProviderFactory is BTCPayRateProviderFactory)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// await new CoinAverageRateProvider("BTC").GetExchangeTickersAsync();
|
||||
// }
|
||||
// catch(Exception ex)
|
||||
// {
|
||||
// Logs.PayServer.LogWarning(ex, "Failed to get exchange tickers");
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
|
||||
{
|
||||
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
_coinAverageSettings.KeyPair = null;
|
||||
}
|
||||
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,7 +109,8 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, BTCPayCoinAverageAuthenticator>();
|
||||
services.TryAddSingleton<CoinAverageSettings>();
|
||||
services.TryAddSingleton<ICoinAverageAuthenticator, CoinAverageSettingsAuthenticator>();
|
||||
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
@ -146,6 +147,9 @@ namespace BTCPayServer.Hosting
|
||||
BlockTarget = 20
|
||||
});
|
||||
|
||||
services.AddSingleton<CssThemeManager>();
|
||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<DerivationStrategy>, Payments.Bitcoin.BitcoinLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
|
||||
@ -160,7 +164,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().NetworkType == NetworkType.Mainnet)
|
||||
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
|
@ -23,20 +23,28 @@ using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Net.WebSockets;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Services;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class BTCPayMiddleware
|
||||
{
|
||||
TokenRepository _TokenRepository;
|
||||
StoreRepository _StoreRepository;
|
||||
RequestDelegate _Next;
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayMiddleware(RequestDelegate next,
|
||||
TokenRepository tokenRepo,
|
||||
StoreRepository storeRepo,
|
||||
BTCPayServerOptions options)
|
||||
{
|
||||
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
|
||||
_StoreRepository = storeRepo;
|
||||
_Next = next ?? throw new ArgumentNullException(nameof(next));
|
||||
_Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
@ -45,42 +53,48 @@ namespace BTCPayServer.Hosting
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
RewriteHostIfNeeded(httpContext);
|
||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||
var sig = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||
var id = values.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(sig) && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
httpContext.Request.EnableRewind();
|
||||
|
||||
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 bitid = new BitIdentity(key);
|
||||
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
|
||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
|
||||
}
|
||||
}
|
||||
catch (FormatException) { }
|
||||
if (!(httpContext.User.Identity is BitIdentity))
|
||||
Logs.PayServer.LogDebug("BitId signature check failed");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
|
||||
var isBitpayAPI = IsBitpayAPI(httpContext, isBitpayAuth);
|
||||
httpContext.SetIsBitpayAPI(isBitpayAPI);
|
||||
if (isBitpayAPI)
|
||||
{
|
||||
|
||||
string storeId = null;
|
||||
var failedAuth = false;
|
||||
if (!string.IsNullOrEmpty(bitpayAuth.Signature) && !string.IsNullOrEmpty(bitpayAuth.Id))
|
||||
{
|
||||
storeId = await CheckBitId(httpContext, bitpayAuth.Signature, bitpayAuth.Id);
|
||||
if (!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(httpContext, bitpayAuth.Authorization);
|
||||
if (storeId == null)
|
||||
{
|
||||
Logs.PayServer.LogDebug("API key check failed");
|
||||
failedAuth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var identity = ((ClaimsIdentity)httpContext.User.Identity);
|
||||
identity.AddClaim(new Claim(Claims.OwnStore, storeId));
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
httpContext.SetStoreData(store);
|
||||
}
|
||||
else if (failedAuth)
|
||||
{
|
||||
throw new BitpayHttpException(401, "Can't access to store");
|
||||
}
|
||||
}
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
@ -100,6 +114,55 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
}
|
||||
|
||||
private static (string Signature, String Id, String Authorization) GetBitpayAuth(HttpContext httpContext, out bool hasBitpayAuth)
|
||||
{
|
||||
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
|
||||
var sig = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("x-identity", out values);
|
||||
var id = values.FirstOrDefault();
|
||||
httpContext.Request.Headers.TryGetValue("Authorization", out values);
|
||||
var auth = values.FirstOrDefault();
|
||||
hasBitpayAuth = auth != null || (sig != null && id != null);
|
||||
return (sig, id, auth);
|
||||
}
|
||||
|
||||
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
|
||||
{
|
||||
if (!httpContext.Request.Path.HasValue)
|
||||
return false;
|
||||
|
||||
var path = httpContext.Request.Path.Value;
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "POST" &&
|
||||
(httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path == "/invoices" &&
|
||||
httpContext.Request.Method == "GET")
|
||||
return true;
|
||||
|
||||
if (
|
||||
bitpayAuth &&
|
||||
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
|
||||
httpContext.Request.Method == "GET")
|
||||
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;
|
||||
}
|
||||
|
||||
private void RewriteHostIfNeeded(HttpContext httpContext)
|
||||
{
|
||||
string reverseProxyScheme = null;
|
||||
@ -132,7 +195,7 @@ namespace BTCPayServer.Hosting
|
||||
httpContext.Request.Scheme = reverseProxyScheme;
|
||||
}
|
||||
else
|
||||
{
|
||||
{
|
||||
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
|
||||
}
|
||||
if (_Options.ExternalUrl.IsDefaultPort)
|
||||
@ -192,5 +255,109 @@ namespace BTCPayServer.Hosting
|
||||
await writer.FlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
553
BTCPayServer/Migrations/20180429083930_legacyapikey.Designer.cs
generated
Normal file
@ -0,0 +1,553 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180429083930_legacyapikey")]
|
||||
partial class legacyapikey
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("AppType");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Settings");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Apps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("Apps")
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
35
BTCPayServer/Migrations/20180429083930_legacyapikey.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class legacyapikey : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApiKeys",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<string>(maxLength: 50, nullable: false),
|
||||
StoreId = table.Column<string>(maxLength: 50, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApiKeys", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApiKeys_StoreId",
|
||||
table: "ApiKeys",
|
||||
column: "StoreId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApiKeys");
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ namespace BTCPayServer.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
@ -36,6 +36,22 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.Property<string>("StoreId")
|
||||
.HasMaxLength(50);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreId");
|
||||
|
||||
b.ToTable("ApiKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
|
@ -17,5 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
[Required]
|
||||
[MaxLength(5000)]
|
||||
public string Template { get; set; }
|
||||
|
||||
[Display(Name = "User can input custom amount")]
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public ItemPrice Price { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
public bool ShowCustomAmount { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Item[] Items { get; set; }
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
public class Payment
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Crypto { get; set; }
|
||||
public string Confirmations
|
||||
{
|
||||
get; set;
|
||||
@ -72,7 +72,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get; set;
|
||||
} = new List<CryptoPayment>();
|
||||
|
||||
public List<Payment> Payments { get; set; } = new List<Payment>();
|
||||
public List<Payment> OnChainPayments { get; set; } = new List<Payment>();
|
||||
public List<OffChainPayment> OffChainPayments { get; set; } = new List<OffChainPayment>();
|
||||
public class OffChainPayment
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
public string BOLT11 { get; set; }
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
|
@ -24,6 +24,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string BtcAddress { get; set; }
|
||||
public string BtcDue { get; set; }
|
||||
public string CustomerEmail { get; set; }
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
public int ExpirationSeconds { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string MerchantRefLink { get; set; }
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
@ -14,5 +15,6 @@ namespace BTCPayServer.Models.ServerViewModels
|
||||
[Display(Name = "Cache the rates for ... minutes")]
|
||||
[Range(0, 60)]
|
||||
public int CacheMinutes { get; set; }
|
||||
public GetRateLimitsResponse RateLimits { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,12 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[MaxLength(20)]
|
||||
public string LightningMaxValue { get; set; }
|
||||
|
||||
[Display(Name = "Requires a refund email")]
|
||||
public bool RequiresRefundEmail
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Do not propose on chain payment if the value of the invoice is below...")]
|
||||
[MaxLength(20)]
|
||||
public string OnChainMinValue { get; set; }
|
||||
|
@ -12,6 +12,11 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public class DerivationScheme
|
||||
{
|
||||
public string Crypto { get; set; }
|
||||
@ -44,6 +49,17 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
public void SetExchangeRates((String DisplayName, String Name)[] supportedList, string preferredExchange)
|
||||
{
|
||||
var defaultStore = preferredExchange ?? "coinaverage";
|
||||
var choices = supportedList.Select(o => new Format() { Name = o.DisplayName, Value = o.Name }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Value == defaultStore) ?? choices.FirstOrDefault();
|
||||
Exchanges = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
PreferredExchange = chosen.Value;
|
||||
}
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
|
@ -68,5 +68,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "API Key")]
|
||||
public string ApiKey { get; set; }
|
||||
public string EncodedApiKey { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1111,4 +1111,17 @@ namespace BTCPayServer
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
public static class MultiValueDictionaryExtensions
|
||||
{
|
||||
public static MultiValueDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
|
||||
{
|
||||
var dictionary = new MultiValueDictionary<TKey, TValue>();
|
||||
foreach(var item in collection)
|
||||
{
|
||||
dictionary.Add(keySelector(item), valueSelector(item));
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace BTCPayServer
|
||||
.Select(t => t.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(kv[0].ToLowerInvariant(), kv[1]))
|
||||
.ToDictionary(o => o.Key, o => o.Value);
|
||||
.ToMultiValueDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
foreach(var filter in splitted)
|
||||
{
|
||||
@ -38,8 +38,8 @@ namespace BTCPayServer
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> Filters { get; private set; }
|
||||
|
||||
public MultiValueDictionary<string, string> Filters { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using NBXplorer;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -20,13 +21,13 @@ namespace BTCPayServer.Services
|
||||
Build = "Release";
|
||||
#endif
|
||||
Environment = env;
|
||||
ChainType = provider.NBXplorerNetworkProvider.ChainType;
|
||||
NetworkType = provider.NetworkType;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ChainType ChainType { get; set; }
|
||||
public NetworkType NetworkType { get; set; }
|
||||
public string Version
|
||||
{
|
||||
get; set;
|
||||
@ -40,7 +41,7 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
get
|
||||
{
|
||||
return ChainType == ChainType.Regtest && Environment.IsDevelopment();
|
||||
return NetworkType == NetworkType.Regtest && Environment.IsDevelopment();
|
||||
}
|
||||
}
|
||||
public override string ToString()
|
||||
|
15
BTCPayServer/Services/Claims.cs
Normal file
15
BTCPayServer/Services/Claims.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class Claims
|
||||
{
|
||||
public const string SIN = "BITID_SIN";
|
||||
|
||||
public const string OwnStore = "BTCPAY_OWN_STORE";
|
||||
}
|
||||
}
|
@ -99,10 +99,14 @@ namespace BTCPayServer.Services
|
||||
try
|
||||
{
|
||||
var pubKey = await ledger.GetWalletPubKeyAsync(account);
|
||||
if (pubKey.Address.Network != network.NBitcoinNetwork)
|
||||
try
|
||||
{
|
||||
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
|
||||
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
|
||||
pubKey.GetAddress(network.NBitcoinNetwork);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (network.NBitcoinNetwork.NetworkType == NetworkType.Mainnet)
|
||||
throw new Exception($"The opened ledger app does not seems to support {network.NBitcoinNetwork.Name}.");
|
||||
}
|
||||
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
|
||||
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||
@ -195,6 +199,7 @@ namespace BTCPayServer.Services
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.StandardTransactionPolicy.MinRelayTxFee = minTxRelayFee;
|
||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
|
@ -399,9 +399,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
query = query.Where(i => i.Id == queryObject.InvoiceId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(queryObject.StoreId))
|
||||
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
|
||||
{
|
||||
query = query.Where(i => i.StoreDataId == queryObject.StoreId);
|
||||
var stores = queryObject.StoreId.ToHashSet();
|
||||
query = query.Where(i => stores.Contains(i.StoreDataId));
|
||||
}
|
||||
|
||||
if (queryObject.UserId != null)
|
||||
@ -429,8 +430,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (queryObject.OrderId != null)
|
||||
query = query.Where(i => i.OrderId == queryObject.OrderId);
|
||||
|
||||
if (queryObject.Status != null)
|
||||
query = query.Where(i => i.Status == queryObject.Status);
|
||||
if (queryObject.Status != null && queryObject.Status.Length > 0)
|
||||
{
|
||||
var statusSet = queryObject.Status.ToHashSet();
|
||||
query = query.Where(i => statusSet.Contains(i.Status));
|
||||
}
|
||||
|
||||
query = query.OrderByDescending(q => q.Created);
|
||||
|
||||
@ -568,7 +572,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public class InvoiceQuery
|
||||
{
|
||||
public string StoreId
|
||||
public string[] StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -610,7 +614,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Status
|
||||
public string[] Status
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
@ -26,10 +26,12 @@ namespace BTCPayServer.Services
|
||||
new Language("ja-JP", "日本語"),
|
||||
new Language("fr-FR", "Français"),
|
||||
new Language("es-ES", "Spanish"),
|
||||
new Language("pt-PT", "Portuguese"),
|
||||
new Language("pt-BR", "Portuguese (Brazil)"),
|
||||
new Language("nl-NL", "Dutch"),
|
||||
new Language("cs-CZ", "Česky"),
|
||||
new Language("is-IS", "Íslenska"),
|
||||
new Language("hr-HR", "Croatian"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -65,9 +65,17 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private IRateProvider CreateExchangeRateProvider(BTCPayNetwork network, string exchange)
|
||||
{
|
||||
List<IRateProvider> providers = new List<IRateProvider>();
|
||||
|
||||
if(exchange == "quadrigacx")
|
||||
{
|
||||
providers.Add(new QuadrigacxRateProvider(network.CryptoCode));
|
||||
}
|
||||
|
||||
var coinAverage = new CoinAverageRateProviderDescription(network.CryptoCode).CreateRateProvider(serviceProvider);
|
||||
coinAverage.Exchange = exchange;
|
||||
return coinAverage;
|
||||
providers.Add(coinAverage);
|
||||
return new FallbackRateProvider(providers.ToArray());
|
||||
}
|
||||
|
||||
private CachedRateProvider CreateCachedRateProvider(BTCPayNetwork network, IRateProvider rateProvider, string additionalScope)
|
||||
|
@ -59,41 +59,11 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
public class RatesSetting
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int CacheInMinutes { get; set; } = 15;
|
||||
|
||||
public string GetCoinAverageSignature()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PublicKey) || string.IsNullOrEmpty(PrivateKey))
|
||||
return null;
|
||||
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
|
||||
var payload = timestamp + "." + PublicKey;
|
||||
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
|
||||
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
|
||||
return payload + "." + digestValueHex;
|
||||
}
|
||||
}
|
||||
public class BTCPayCoinAverageAuthenticator : ICoinAverageAuthenticator
|
||||
{
|
||||
private SettingsRepository settingsRepo;
|
||||
|
||||
public BTCPayCoinAverageAuthenticator(SettingsRepository settingsRepo)
|
||||
{
|
||||
this.settingsRepo = settingsRepo;
|
||||
}
|
||||
public async Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var settings = (await settingsRepo.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
|
||||
var signature = settings.GetCoinAverageSignature();
|
||||
if (signature != null)
|
||||
{
|
||||
message.Headers.Add("X-signature", signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICoinAverageAuthenticator
|
||||
@ -200,9 +170,45 @@ namespace BTCPayServer.Services.Rates
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await _Client.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
var response = new GetRateLimitsResponse();
|
||||
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
|
||||
var totalPeriod = jobj["total_period"].Value<string>();
|
||||
if (totalPeriod == "24h")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromHours(24);
|
||||
}
|
||||
else if (totalPeriod == "30d")
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromDays(30);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
|
||||
}
|
||||
response.RequestsLeft = jobj["requests_left"].Value<int>();
|
||||
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<GetExchangeTickersResponse> GetExchangeTickersAsync()
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/symbols/exchanges/ticker");
|
||||
var auth = Authenticator;
|
||||
if (auth != null)
|
||||
{
|
||||
await auth.AddHeader(request);
|
||||
}
|
||||
var resp = await _Client.SendAsync(request);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
@ -221,4 +227,12 @@ namespace BTCPayServer.Services.Rates
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public class GetRateLimitsResponse
|
||||
{
|
||||
public TimeSpan CounterReset { get; set; }
|
||||
public int RequestsLeft { get; set; }
|
||||
public int RequestsPerPeriod { get; set; }
|
||||
public TimeSpan TotalPeriod { get; set; }
|
||||
}
|
||||
}
|
||||
|
119
BTCPayServer/Services/Rates/CoinAverageSettings.cs
Normal file
119
BTCPayServer/Services/Rates/CoinAverageSettings.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
|
||||
{
|
||||
CoinAverageSettings _Settings;
|
||||
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
|
||||
{
|
||||
_Settings = settings;
|
||||
}
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
return _Settings.AddHeader(message);
|
||||
}
|
||||
}
|
||||
public class CoinAverageSettings : ICoinAverageAuthenticator
|
||||
{
|
||||
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
|
||||
public (String DisplayName, String Name)[] AvailableExchanges { get; set; } = Array.Empty<(String DisplayName, String Name)>();
|
||||
|
||||
public CoinAverageSettings()
|
||||
{
|
||||
//GENERATED BY:
|
||||
//StringBuilder b = new StringBuilder();
|
||||
//b.AppendLine("_coinAverageSettings.AvailableExchanges = new[] {");
|
||||
//foreach (var availableExchange in _coinAverageSettings.AvailableExchanges)
|
||||
//{
|
||||
// b.AppendLine($"(DisplayName: \"{availableExchange.DisplayName}\", Name: \"{availableExchange.Name}\"),");
|
||||
//}
|
||||
//b.AppendLine("}.ToArray()");
|
||||
|
||||
AvailableExchanges = new[] {
|
||||
(DisplayName: "BitBargain", Name: "bitbargain"),
|
||||
(DisplayName: "Tidex", Name: "tidex"),
|
||||
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
|
||||
(DisplayName: "EtherDelta", Name: "etherdelta"),
|
||||
(DisplayName: "Kraken", Name: "kraken"),
|
||||
(DisplayName: "BitBay", Name: "bitbay"),
|
||||
(DisplayName: "Independent Reserve", Name: "independentreserve"),
|
||||
(DisplayName: "Exmoney", Name: "exmoney"),
|
||||
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
|
||||
(DisplayName: "Huobi", Name: "huobi"),
|
||||
(DisplayName: "GDAX", Name: "gdax"),
|
||||
(DisplayName: "Coincheck", Name: "coincheck"),
|
||||
(DisplayName: "Bittylicious", Name: "bittylicious"),
|
||||
(DisplayName: "Gemini", Name: "gemini"),
|
||||
(DisplayName: "QuadrigaCX", Name: "quadrigacx"),
|
||||
(DisplayName: "Bit2C", Name: "bit2c"),
|
||||
(DisplayName: "Luno", Name: "luno"),
|
||||
(DisplayName: "Negocie Coins", Name: "negociecoins"),
|
||||
(DisplayName: "FYB-SE", Name: "fybse"),
|
||||
(DisplayName: "Hitbtc", Name: "hitbtc"),
|
||||
(DisplayName: "Bitex.la", Name: "bitex"),
|
||||
(DisplayName: "Korbit", Name: "korbit"),
|
||||
(DisplayName: "itBit", Name: "itbit"),
|
||||
(DisplayName: "Okex", Name: "okex"),
|
||||
(DisplayName: "Bitsquare", Name: "bitsquare"),
|
||||
(DisplayName: "Bitfinex", Name: "bitfinex"),
|
||||
(DisplayName: "CoinMate", Name: "coinmate"),
|
||||
(DisplayName: "Bitstamp", Name: "bitstamp"),
|
||||
(DisplayName: "Cryptonit", Name: "cryptonit"),
|
||||
(DisplayName: "Foxbit", Name: "foxbit"),
|
||||
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
|
||||
(DisplayName: "Poloniex", Name: "poloniex"),
|
||||
(DisplayName: "Bit-Z", Name: "bitz"),
|
||||
(DisplayName: "Liqui", Name: "liqui"),
|
||||
(DisplayName: "BitKonan", Name: "bitkonan"),
|
||||
(DisplayName: "Kucoin", Name: "kucoin"),
|
||||
(DisplayName: "Binance", Name: "binance"),
|
||||
(DisplayName: "Rock Trading", Name: "rocktrading"),
|
||||
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
|
||||
(DisplayName: "Coinsecure", Name: "coinsecure"),
|
||||
(DisplayName: "Coinfloor", Name: "coinfloor"),
|
||||
(DisplayName: "bitFlyer", Name: "bitflyer"),
|
||||
(DisplayName: "BTCTurk", Name: "btcturk"),
|
||||
(DisplayName: "Bittrex", Name: "bittrex"),
|
||||
(DisplayName: "CampBX", Name: "campbx"),
|
||||
(DisplayName: "Zaif", Name: "zaif"),
|
||||
(DisplayName: "FYB-SG", Name: "fybsg"),
|
||||
(DisplayName: "Quoine", Name: "quoine"),
|
||||
(DisplayName: "BTC Markets", Name: "btcmarkets"),
|
||||
(DisplayName: "Bitso", Name: "bitso"),
|
||||
}.ToArray();
|
||||
}
|
||||
|
||||
public Task AddHeader(HttpRequestMessage message)
|
||||
{
|
||||
var signature = GetCoinAverageSignature();
|
||||
if (signature != null)
|
||||
{
|
||||
message.Headers.Add("X-signature", signature);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetCoinAverageSignature()
|
||||
{
|
||||
var keyPair = KeyPair;
|
||||
if (!keyPair.HasValue)
|
||||
return null;
|
||||
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
|
||||
return null;
|
||||
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
|
||||
var payload = timestamp + "." + keyPair.Value.PublicKey;
|
||||
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
|
||||
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
|
||||
return payload + "." + digestValueHex;
|
||||
}
|
||||
}
|
||||
}
|
65
BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs
Normal file
65
BTCPayServer/Services/Rates/QuadrigacxRateProvider.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class QuadrigacxRateProvider : IRateProvider
|
||||
{
|
||||
public QuadrigacxRateProvider(string crypto)
|
||||
{
|
||||
CryptoCode = crypto;
|
||||
}
|
||||
public string CryptoCode { get; set; }
|
||||
static HttpClient _Client = new HttpClient();
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return await GetRatesAsyncCore(CryptoCode, currency);
|
||||
}
|
||||
|
||||
private async Task<decimal> GetRatesAsyncCore(string cryptoCode, string currency)
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book={cryptoCode.ToLowerInvariant()}_{currency.ToLowerInvariant()}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
if (!TryToDecimal(rates, out var result))
|
||||
throw new RateUnavailableException(currency);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool TryToDecimal(JObject p, out decimal v)
|
||||
{
|
||||
v = 0.0m;
|
||||
JToken token = p.Property("bid")?.Value;
|
||||
if (token == null)
|
||||
return false;
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
var response = await _Client.GetAsync($"https://api.quadrigacx.com/v2/ticker?book=all");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await response.Content.ReadAsStringAsync());
|
||||
|
||||
List<Rate> result = new List<Rate>();
|
||||
foreach (var prop in rates.Properties())
|
||||
{
|
||||
var rate = new Rate();
|
||||
var splitted = prop.Name.Split('_');
|
||||
var crypto = splitted[0].ToUpperInvariant();
|
||||
if (crypto != CryptoCode)
|
||||
continue;
|
||||
rate.Currency = splitted[1].ToUpperInvariant();
|
||||
TryToDecimal((JObject)prop.Value, out var v);
|
||||
rate.Value = v;
|
||||
result.Add(rate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using BTCPayServer.Models;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
@ -51,6 +52,22 @@ namespace BTCPayServer.Services
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyCollection<TaskCompletionSource<bool>> value;
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
if(_Subscriptions.TryGetValue(typeof(T), out value))
|
||||
{
|
||||
_Subscriptions.Remove(typeof(T));
|
||||
}
|
||||
}
|
||||
if(value != null)
|
||||
{
|
||||
foreach(var v in value)
|
||||
{
|
||||
v.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T Deserialize<T>(string value)
|
||||
@ -62,5 +79,35 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
return JsonConvert.SerializeObject(obj);
|
||||
}
|
||||
|
||||
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
|
||||
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
using (cancellation.Register(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
tcs.TrySetCanceled();
|
||||
}
|
||||
catch { }
|
||||
}))
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Add(typeof(T), tcs);
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
tcs.Task.ContinueWith(_ =>
|
||||
{
|
||||
lock (_Subscriptions)
|
||||
{
|
||||
_Subscriptions.Remove(typeof(T), tcs);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ namespace BTCPayServer.Services.Stores
|
||||
|
||||
public async Task<StoreData> FindStore(string storeId)
|
||||
{
|
||||
if (storeId == null)
|
||||
return null;
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.FindAsync<StoreData>(storeId).ConfigureAwait(false);
|
||||
|
17
BTCPayServer/Services/ThemesSettings.cs
Normal file
17
BTCPayServer/Services/ThemesSettings.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class ThemeSettings
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public string BootstrapCssUri { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public string CreativeStartCssUri { get; set; }
|
||||
}
|
||||
}
|
37
BTCPayServer/SynchronizationContextRemover.cs
Normal file
37
BTCPayServer/SynchronizationContextRemover.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public struct SynchronizationContextRemover : INotifyCompletion
|
||||
{
|
||||
public bool IsCompleted => SynchronizationContext.Current == null;
|
||||
|
||||
public void OnCompleted(Action continuation)
|
||||
{
|
||||
var prev = SynchronizationContext.Current;
|
||||
try
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(null);
|
||||
continuation();
|
||||
}
|
||||
finally
|
||||
{
|
||||
SynchronizationContext.SetSynchronizationContext(prev);
|
||||
}
|
||||
}
|
||||
|
||||
public SynchronizationContextRemover GetAwaiter()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void GetResult()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -29,6 +29,10 @@
|
||||
<input asp-for="Currency" class="form-control" />
|
||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ShowCustomAmount"></label>
|
||||
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Template" class="control-label"></label>*
|
||||
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
|
||||
|
@ -15,31 +15,35 @@
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
@foreach (var item in Model.Items)
|
||||
@for(int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
<div class="col-sm-4 mb-3">
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 3) ? "col-sm-4 mb-3" : "col align-self-center";
|
||||
var item = Model.Items[i];
|
||||
<div class="@className">
|
||||
<h3>@item.Title</h3>
|
||||
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">Buy for @item.Price.Formatted</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
@*<div class="row mt-4">
|
||||
@if(Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4 offset-md-4 col-sm-6 offset-sm-3">
|
||||
<h3>Something else</h3>
|
||||
<form data-buy>
|
||||
<form method="post" data-buy>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.00001" name="amount" placeholder="undefined (optional)"><div class="input-group-append">
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="amount"><div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">Pay</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>*@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<script src="~/vendor/jquery/jquery.js"></script>
|
||||
|
@ -6,8 +6,9 @@
|
||||
|
||||
<header class="masthead">
|
||||
<div class="header-content">
|
||||
<div class="header-content-inner">
|
||||
<h1 id="homeHeading" style="padding-bottom:30px;">Welcome to BTCPay Server</h1>
|
||||
<div class="header-content-inner text-white">
|
||||
<h1>Welcome to BTCPay Server</h1>
|
||||
<hr />
|
||||
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business. The API is compatible with Bitpay service to allow seamless migration.</p>
|
||||
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Getting started</a>
|
||||
</div>
|
||||
@ -28,21 +29,21 @@
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/lock-logo.png" />
|
||||
<h3>Secure</h3>
|
||||
<h3 class="text-dark">Secure</h3>
|
||||
<p class="text-muted">The payment server does not need to know your private keys, so your money can't be stolen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/qr-logo.png" />
|
||||
<h3>Easy</h3>
|
||||
<h3 class="text-dark">Easy</h3>
|
||||
<p class="text-muted">A user-friendly Bitcoin checkout page for your customers.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-6 text-center">
|
||||
<div class="service-box">
|
||||
<img src="~/img/money-logo.png" />
|
||||
<h3>Visibility</h3>
|
||||
<h3 class="text-dark">Visibility</h3>
|
||||
<p class="text-muted">Manage, generate reports, and search for your invoices easily.</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +53,7 @@
|
||||
|
||||
|
||||
|
||||
<div class="call-to-action bg-dark">
|
||||
<div class="call-to-action bg-dark text-white">
|
||||
<div class="container text-center">
|
||||
<h2>Video tutorials</h2>
|
||||
<div class="row">
|
||||
@ -102,12 +103,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="call-to-action bg-dark">
|
||||
<div class="call-to-action bg-dark text-white">
|
||||
<div class="container text-center">
|
||||
<h2>Donate</h2>
|
||||
<p>Donation to this address will be reinvested into the development of this tool</p>
|
||||
<img src="~/img/donation.jpg">
|
||||
<p style="font-size:10px">3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
|
||||
<p><img src="~/img/donation.jpg"></p>
|
||||
<p>3BpfdkF93GwFRWdrAN3SNsRAsi6d158YQi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -117,7 +118,10 @@
|
||||
<div class="col-lg-8 mx-auto text-center">
|
||||
<h2 class="section-heading">Let's Get In Touch!</h2>
|
||||
<hr class="primary">
|
||||
<p>An open source project is nothing without its community, come and get in touch with us!</p>
|
||||
<p>
|
||||
An open source project is nothing without its community<br />
|
||||
Come and get in touch with us!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
@ -110,10 +110,12 @@
|
||||
'es-ES': { translation: locales_es },
|
||||
'ja-JP': { translation: locales_ja },
|
||||
'fr-FR': { translation: locales_fr },
|
||||
'pt': { translation: locales_pt },
|
||||
'pt-BR': { translation: locales_pt_br },
|
||||
'nl': { translation: locales_nl },
|
||||
'cs-CZ': { translation: locales_cs },
|
||||
'is-IS': { translation: locales_is }
|
||||
'is-IS': { translation: locales_is },
|
||||
'hr-HR': { translation: locales_hr }
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -4,14 +4,21 @@
|
||||
}
|
||||
|
||||
<style type="text/css">
|
||||
.overflowbox {
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.linethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.money {
|
||||
text-align: right;
|
||||
|
||||
.smMaxWidth {
|
||||
max-width: 200px;
|
||||
}
|
||||
@@media (min-width: 768px) {
|
||||
.smMaxWidth {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.firstCol {
|
||||
width: 140px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -28,14 +35,13 @@
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
<p>Invoice details</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Store</th>
|
||||
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
|
||||
@ -93,10 +99,9 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Buyer information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Name
|
||||
<th>
|
||||
<th>Name</th>
|
||||
<td>@Model.BuyerInformation.BuyerName</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -134,7 +139,7 @@
|
||||
</table>
|
||||
|
||||
<h3>Product information</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@Model.ProductInformation.ItemCode</td>
|
||||
@ -153,77 +158,109 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Paid summary</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Payment method</th>
|
||||
<th>Address</th>
|
||||
<th class="money">Rate</th>
|
||||
<th class="money">Paid</th>
|
||||
<th class="money">Due</th>
|
||||
<th class="text-right">Rate</th>
|
||||
<th class="text-right">Paid</th>
|
||||
<th class="text-right">Due</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
<td>@payment.Address</td>
|
||||
<td class="money">@payment.Rate</td>
|
||||
<td class="money">@payment.Paid</td>
|
||||
<td class="money">@payment.Due</td>
|
||||
</tr>
|
||||
}
|
||||
@foreach (var payment in Model.CryptoPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.PaymentMethod</td>
|
||||
<td>@payment.Address</td>
|
||||
<td class="text-right">@payment.Rate</td>
|
||||
<td class="text-right">@payment.Paid</td>
|
||||
<td class="text-right">@payment.Due</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Payments</h3>
|
||||
<table class="table table-sm">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th style="text-align:right;">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var payment in Model.Payments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "text-decoration: line-through;" : "";
|
||||
<tr>
|
||||
<td style="@replaced">@payment.PaymentMethod</td>
|
||||
<td style="@replaced">@payment.DepositAddress</td>
|
||||
<td style="@replaced"><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td style="text-align:right;">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@if (Model.OnChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>On-Chain payments</h3>
|
||||
<table class="table table-sm table-responsive-lg">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Crypto</th>
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-right">Confirmations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OnChainPayments)
|
||||
{
|
||||
var replaced = payment.Replaced ? "class='linethrough'" : "";
|
||||
<tr @replaced>
|
||||
<td>@payment.Crypto</td>
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td class="smMaxWidth text-truncate">
|
||||
<a href="@payment.TransactionLink" target="_blank">
|
||||
@payment.TransactionId
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">@payment.Confirmations</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (Model.OffChainPayments.Count > 0)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Off-Chain payments</h3>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th class="firstCol">Crypto</th>
|
||||
<th>BOLT11</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var payment in Model.OffChainPayments)
|
||||
{
|
||||
<tr>
|
||||
<td>@payment.Crypto</td>
|
||||
<td class="smMaxWidth text-truncate">@payment.BOLT11</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Addresses</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="white-space:nowrap;">Payment method</th>
|
||||
<th class="firstCol">Payment method</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var address in Model.Addresses)
|
||||
@foreach (var address in Model.Addresses)
|
||||
{
|
||||
var current = address.Current ? "font-weight: bold;" : "";
|
||||
<tr>
|
||||
<td style="width:100px;@current">@address.PaymentMethod</td>
|
||||
<td style="max-width:100px;overflow:hidden;@current">@address.Destination</td>
|
||||
</tr>
|
||||
}
|
||||
var current = address.Current ? "font-weight-bold" : "";
|
||||
<tr>
|
||||
<td>@address.PaymentMethod</td>
|
||||
<td class="smMaxWidth text-truncate @current">@address.Destination</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -232,7 +269,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Events</h3>
|
||||
<table class="table table-sm">
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
@ -240,7 +277,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var evt in Model.Events)
|
||||
@foreach (var evt in Model.Events)
|
||||
{
|
||||
<tr>
|
||||
<td>@evt.Timestamp</td>
|
||||
|
@ -26,6 +26,9 @@
|
||||
<li><b>storeid:id</b> for filtering a specific store</li>
|
||||
<li><b>status:(expired|invalid|complete|confirmed|paid|new)</b> for filtering a specific status</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you want two confirmed and complete invoices, duplicate the filter: `status:confirmed status:complete`.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<form asp-action="SearchInvoice" method="post">
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model ChangePasswordViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Change password";
|
||||
ViewData.AddActivePage(ManageNavPages.ChangePassword);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Change password");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,6 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "Disable two-factor authentication (2FA)";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Disable two-factor authentication (2FA)");
|
||||
}
|
||||
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model EnableAuthenticatorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Enable authenticator";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Enable authenticator");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model ExternalLoginsViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Manage your external logins";
|
||||
ViewData.AddActivePage(ManageNavPages.ExternalLogins);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ExternalLogins, "Manage your external logins");
|
||||
}
|
||||
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model GenerateRecoveryCodesViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Recovery codes";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Recovery codes");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model IndexViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Profile";
|
||||
ViewData.AddActivePage(ManageNavPages.Index);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.Index, "Profile");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -2,41 +2,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views.Manage
|
||||
{
|
||||
public static class ManageNavPages
|
||||
public enum ManageNavPages
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
|
||||
public static string Index => "Index";
|
||||
|
||||
public static string ChangePassword => "ChangePassword";
|
||||
|
||||
public static string ExternalLogins => "ExternalLogins";
|
||||
|
||||
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
|
||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
|
||||
|
||||
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
|
||||
|
||||
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string;
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
|
||||
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
|
||||
Index, ChangePassword, ExternalLogins, TwoFactorAuthentication, Tokens
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
@{
|
||||
ViewData["Title"] = "Reset authenticator key";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Reset authenticator key");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model SetPasswordViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Set password";
|
||||
ViewData.AddActivePage(ManageNavPages.ChangePassword);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.ChangePassword, "Set password");
|
||||
}
|
||||
|
||||
<h4>Set your password</h4>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model TwoFactorAuthenticationViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Two-factor authentication";
|
||||
ViewData.AddActivePage(ManageNavPages.TwoFactorAuthentication);
|
||||
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
|
@ -1,16 +1,15 @@
|
||||
@using BTCPayServer.Views.Manage
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@{
|
||||
var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
|
||||
}
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" asp-action="ChangePassword">Password</a>
|
||||
@if(hasExternalLogins)
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.Index)" asp-action="Index">Profile</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ChangePassword)" asp-action="ChangePassword">Password</a>
|
||||
@if (hasExternalLogins)
|
||||
{
|
||||
<a class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-action="ExternalLogins">External logins</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.ExternalLogins)" asp-action="ExternalLogins">External logins</a>
|
||||
}
|
||||
<a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-action="TwoFactorAuthentication">Two-factor authentication</a>
|
||||
</div>
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model EmailsViewModel
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Emails;
|
||||
ViewData.AddActivePage(ServerNavPages.Emails);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Emails);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model UsersViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Users";
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,19 +1,18 @@
|
||||
@model BTCPayServer.Services.PoliciesSettings
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Policies;
|
||||
ViewData.AddActivePage(ServerNavPages.Policies);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Policies);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-lg-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-lg-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresConfirmedEmail"></label>
|
||||
|
@ -1,7 +1,6 @@
|
||||
@model RatesViewModel
|
||||
@{
|
||||
ViewData["Title"] = ServerNavPages.Rates;
|
||||
ViewData.AddActivePage(ServerNavPages.Rates);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Rates);
|
||||
}
|
||||
|
||||
|
||||
@ -28,14 +27,31 @@
|
||||
<input asp-for="PublicKey" style="width:50%;" class="form-control" placeholder="Public key" />
|
||||
<label class="sr-only" asp-for="PrivateKey"></label>
|
||||
<input asp-for="PrivateKey" style="width:50%;" class="form-control" placeholder="Private key" />
|
||||
<span asp-validation-for="PrivateKey" class="text-danger"></span>
|
||||
<p class="form-text text-muted">You can find the information on <a href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
|
||||
<p class="form-text text-muted">You can find the information on <a target="_blank" href="https://bitcoinaverage.com/en/apikeys">bitcoinaverage api key page</a></p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CacheMinutes"></label>
|
||||
<input asp-for="CacheMinutes" class="form-control" />
|
||||
<span asp-validation-for="CacheMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
@if(Model.RateLimits != null)
|
||||
{
|
||||
<h5>Current Bitcoin Average Quotas:</h5>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<th>Quota period</th>
|
||||
<td>@Model.RateLimits.TotalPeriod.Prettify()</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Requests quota</th>
|
||||
<td>@Model.RateLimits.RequestsLeft/@Model.RateLimits.RequestsPerPeriod</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Quota reset in</th>
|
||||
<td>@Model.RateLimits.CounterReset.Prettify()</td>
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -2,37 +2,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views.Server
|
||||
{
|
||||
public static class ServerNavPages
|
||||
public enum ServerNavPages
|
||||
{
|
||||
public static string ActivePageKey => "ActivePage";
|
||||
public static string Index => "Index";
|
||||
|
||||
|
||||
public static string Users => "Users";
|
||||
public static string Rates => "Rates";
|
||||
public static string Emails => "Email server";
|
||||
public static string Policies => "Policies";
|
||||
public static string Hangfire => "Hangfire";
|
||||
|
||||
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
|
||||
public static string EmailsNavClass(ViewContext viewContext) => PageNavClass(viewContext, Emails);
|
||||
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
|
||||
public static string PoliciesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Policies);
|
||||
public static string HangfireNavClass(ViewContext viewContext) => PageNavClass(viewContext, Hangfire);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
public static string PageNavClass(ViewContext viewContext, string page)
|
||||
{
|
||||
var activePage = viewContext.ViewData["ActivePage"] as string;
|
||||
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
|
||||
}
|
||||
|
||||
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
|
||||
Index, Users, Rates, Emails, Policies, Theme, Hangfire
|
||||
}
|
||||
}
|
||||
|
43
BTCPayServer/Views/Server/Theme.cshtml
Normal file
43
BTCPayServer/Views/Server/Theme.cshtml
Normal file
@ -0,0 +1,43 @@
|
||||
@model BTCPayServer.Services.ThemeSettings
|
||||
@{
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Theme);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", TempData["StatusMessage"])
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="BootstrapCssUri"></label>
|
||||
<input asp-for="BootstrapCssUri" class="form-control" />
|
||||
<span asp-validation-for="BootstrapCssUri" class="text-danger"></span>
|
||||
<p class="form-text text-muted">
|
||||
<a href="https://bootstrap.build/app/v4.0/" target="_blank">Build your own theme</a>
|
||||
or <a href="https://bootswatch.com/" target="_blank">pick one already made</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CreativeStartCssUri"></label>
|
||||
<input asp-for="CreativeStartCssUri" class="form-control" />
|
||||
<span asp-validation-for="CreativeStartCssUri" class="text-danger"></span>
|
||||
<p class="form-text text-muted">
|
||||
<a href="https://startbootstrap.com/template-overviews/creative/" target="_blank">Creative Start theme</a>
|
||||
is used on top of Bootstrap
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
@model UserViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Email;
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
ViewData.SetActivePageAndTitle(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<h4>Modify User - @Model.Email</h4>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
@using BTCPayServer.Views.Server
|
||||
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ServerNavPages.UsersNavClass(ViewContext)" asp-action="Users">Users</a>
|
||||
<a class="nav-link @ServerNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @ServerNavPages.EmailsNavClass(ViewContext)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ServerNavPages.PoliciesNavClass(ViewContext)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ServerNavPages.HangfireNavClass(ViewContext)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||
<div class="nav flex-column nav-pills">
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Users)" asp-action="Users">Users</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Rates)" asp-action="Rates">Rates</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Emails)" asp-action="Emails">Email server</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Policies)" asp-action="Policies">Policies</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
|
||||
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
|
||||
</div>
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
@inject RoleManager<IdentityRole> RoleManager
|
||||
@inject BTCPayServer.Services.BTCPayServerEnvironment env
|
||||
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -17,6 +19,9 @@
|
||||
<title>BTCPay Server</title>
|
||||
|
||||
@* CSS *@
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.CreativeStartUri)" rel="stylesheet" />
|
||||
|
||||
<bundle name="wwwroot/bundles/main-bundle.min.css" />
|
||||
|
||||
@* JS *@
|
||||
@ -38,9 +43,9 @@
|
||||
<div class="container">
|
||||
<a class="navbar-brand js-scroll-trigger" href="~/">
|
||||
<img src="~/img/logo.png" height="45">
|
||||
@if (env.ChainType != NBXplorer.ChainType.Main)
|
||||
@if (env.NetworkType != NBitcoin.NetworkType.Mainnet)
|
||||
{
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.ChainType.ToString()</span>
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.NetworkType.ToString()</span>
|
||||
}
|
||||
</a>
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
@ -48,25 +53,25 @@
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@if(SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if(User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if (User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@ -75,8 +80,8 @@ else
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<footer class="bg-dark">
|
||||
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
|
||||
<footer class="siteFooter bg-dark text-white">
|
||||
<div class="container text-right">@env.ToString()</div>
|
||||
</footer>
|
||||
|
||||
@if (!dashboard.IsFullySynched())
|
||||
|
@ -38,6 +38,10 @@
|
||||
<label asp-for="AllowCoinConversion"></label>
|
||||
<input asp-for="AllowCoinConversion" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RequiresRefundEmail"></label>
|
||||
<input asp-for="RequiresRefundEmail" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="LightningMaxValue"></label>
|
||||
<input asp-for="LightningMaxValue" class="form-control" />
|
||||
|
@ -5,32 +5,60 @@
|
||||
ViewData.AddActivePage(StoreNavPages.Tokens);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<p>You can allow a public key to access the API of this store</p>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>SIN</th>
|
||||
<th>Facade</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var token in Model.Tokens)
|
||||
{
|
||||
<tr>
|
||||
<td>@token.Label</td>
|
||||
<td>@token.SIN</td>
|
||||
<td>@token.Facade</td>
|
||||
<td>
|
||||
<form asp-action="DeleteToken" method="post">
|
||||
<input type="hidden" name="tokenId" value="@token.Id">
|
||||
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Access token</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>Authorize a public key to access Bitpay compatible Invoice API (<a href="https://support.bitpay.com/hc/en-us/articles/115003001183-How-do-I-pair-my-client-and-create-a-token-">More information</a>)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<a asp-action="CreateToken" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new token</a>
|
||||
<table class="table table-sm table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Label</th>
|
||||
<th>SIN</th>
|
||||
<th>Facade</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var token in Model.Tokens)
|
||||
{
|
||||
<tr>
|
||||
<td>@token.Label</td>
|
||||
<td>@token.SIN</td>
|
||||
<td>@token.Facade</td>
|
||||
<td>
|
||||
<form asp-action="DeleteToken" method="post">
|
||||
<input type="hidden" name="tokenId" value="@token.Id">
|
||||
<button type="submit" class="btn btn-danger" role="button">Revoke</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Legacy API Keys</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<p>Alternatively, you can use the invoice API by including the following HTTP Header in your requests:<br /> <code>Authorization: Basic @Model.EncodedApiKey</code> </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post" asp-action="GenerateAPIKey">
|
||||
<div class="form-group">
|
||||
<label asp-for="ApiKey"></label>
|
||||
<input asp-for="ApiKey" readonly class="form-control" />
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" role="button">Create new API Key</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -36,10 +36,10 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<input asp-for="PreferredExchange" class="form-control" />
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-control"></select>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@ -81,13 +81,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var scheme in Model.DerivationSchemes)
|
||||
@foreach(var scheme in Model.DerivationSchemes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Crypto</td>
|
||||
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
|
||||
<td style="text-align:right">
|
||||
@if (!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
||||
{
|
||||
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
|
||||
}
|
||||
@ -117,7 +117,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var scheme in Model.LightningNodes)
|
||||
@foreach(var scheme in Model.LightningNodes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.CryptoCode</td>
|
||||
|
26
BTCPayServer/Views/ViewsRazor.cs
Normal file
26
BTCPayServer/Views/ViewsRazor.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace BTCPayServer.Views
|
||||
{
|
||||
public static class ViewsRazor
|
||||
{
|
||||
public const string ACTIVE_PAGE_KEY = "ActivePage";
|
||||
public static void SetActivePageAndTitle<T>(this ViewDataDictionary viewData, T activePage, string title = null)
|
||||
where T : IConvertible
|
||||
{
|
||||
viewData["Title"] = title ?? activePage.ToString();
|
||||
viewData[ACTIVE_PAGE_KEY] = activePage;
|
||||
}
|
||||
|
||||
public static string IsActivePage<T>(this ViewDataDictionary viewData, T page)
|
||||
where T : IConvertible
|
||||
{
|
||||
var activePage = (T)viewData[ACTIVE_PAGE_KEY];
|
||||
return page.Equals(activePage) ? "active" : null;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using BTCPayServer
|
||||
@using BTCPayServer.Views
|
||||
@using BTCPayServer.Models
|
||||
@using BTCPayServer.Models.AccountViewModels
|
||||
@using BTCPayServer.Models.InvoicingModels
|
||||
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/main-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/bootstrap4/css/bootstrap.css",
|
||||
"wwwroot/vendor/bootstrap4-creativestart/Open-Sans.css",
|
||||
"wwwroot/vendor/magnific-popup/magnific-popup.css",
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.css",
|
||||
"wwwroot/main/**/*.css",
|
||||
@ -18,7 +18,7 @@
|
||||
"wwwroot/vendor/jquery-easing/jquery.easing.js",
|
||||
"wwwroot/vendor/scrollreveal/scrollreveal.min.js",
|
||||
"wwwroot/vendor/magnific-popup/jquery.magnific-popup.js",
|
||||
"wwwroot/main/**/js/*.js"
|
||||
"wwwroot/vendor/bootstrap4-creativestart/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -104,14 +104,11 @@ $(document).ready(function () {
|
||||
|
||||
*/
|
||||
|
||||
var display = $(".timer-row__time-left"); // Timer container
|
||||
|
||||
// check if the Document expired
|
||||
if (srvModel.expirationSeconds > 0) {
|
||||
progressStart(srvModel.maxTimeSeconds); // Progress bar
|
||||
startTimer(srvModel.expirationSeconds, display); // Timer
|
||||
|
||||
if (!validateEmail(srvModel.customerEmail))
|
||||
if (srvModel.requiresRefundEmail && !validateEmail(srvModel.customerEmail))
|
||||
emailForm(); // Email form Display
|
||||
else
|
||||
hideEmailForm();
|
||||
@ -246,42 +243,21 @@ $(document).ready(function () {
|
||||
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
|
||||
});
|
||||
|
||||
// Timer Countdown
|
||||
function startTimer(duration, display) {
|
||||
var timer = duration, minutes, seconds;
|
||||
var timeout = setInterval(function () {
|
||||
minutes = parseInt(timer / 60, 10);
|
||||
seconds = parseInt(timer % 60, 10);
|
||||
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
display.text(minutes + ":" + seconds);
|
||||
|
||||
if (--timer < 0) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
// Timer Countdown && Progress bar
|
||||
function progressStart(timerMax) {
|
||||
var end = new Date(); // Setup Time Variable, should come from server
|
||||
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
|
||||
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
|
||||
var timeoutVal = Math.floor(timerMax / 100); // Timeout calc
|
||||
animateUpdate(); //Launch it
|
||||
|
||||
function updateProgress(percentage) {
|
||||
$('.timer-row__progress-bar').css("width", percentage + "%");
|
||||
}
|
||||
|
||||
function animateUpdate() {
|
||||
var now = new Date();
|
||||
var timeDiff = end.getTime() - now.getTime();
|
||||
var perc = 100 - Math.round(timeDiff / timerMax * 100);
|
||||
var status = checkoutCtrl.srvModel.status;
|
||||
|
||||
updateTimer(timeDiff / 1000);
|
||||
|
||||
if (perc === 75 && (status === "paidPartial" || status === "new")) {
|
||||
$(".timer-row").addClass("expiring-soon");
|
||||
checkoutCtrl.expiringSoon = true;
|
||||
@ -289,12 +265,34 @@ $(document).ready(function () {
|
||||
}
|
||||
if (perc <= 100) {
|
||||
updateProgress(perc);
|
||||
|
||||
var timeoutVal = 300; // Timeout calc
|
||||
setTimeout(animateUpdate, timeoutVal);
|
||||
}
|
||||
|
||||
//if (perc >= 100 && status === "expired") {
|
||||
// onDataCallback(status);
|
||||
//}
|
||||
}
|
||||
|
||||
function updateProgress(percentage) {
|
||||
$('.timer-row__progress-bar').css("width", percentage + "%");
|
||||
}
|
||||
|
||||
function updateTimer(timer) {
|
||||
var display = $(".timer-row__time-left");
|
||||
if (timer >= 0) {
|
||||
var minutes = parseInt(timer / 60, 10);
|
||||
minutes = minutes < 10 ? "0" + minutes : minutes;
|
||||
|
||||
var seconds = parseInt(timer % 60, 10);
|
||||
seconds = seconds < 10 ? "0" + seconds : seconds;
|
||||
|
||||
display.text(minutes + ":" + seconds);
|
||||
} else {
|
||||
display.text("00:00");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard Copy
|
||||
|
@ -47,5 +47,8 @@ Můžete se vrátit do {{storeName}}, pokud chcete svojí objednávku založit z
|
||||
"Archived_Body": "Prosíme kontaktujte prodejce pro informace o objednávce a případnou pomoc",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "BOLT 11 Faktura",
|
||||
"Node Info": "Info o uzlu"
|
||||
"Node Info": "Info o uzlu",
|
||||
//
|
||||
"txCount": "{{count}} transakce",
|
||||
"txCount_plural": "{{count}} transakcí"
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ const locales_de = {
|
||||
"Pay with": "Bezahlen mit",
|
||||
"Contact and Refund Email": "Kontakt und Rückerstattungs Email",
|
||||
"Contact_Body": "Bitte geben Sie unten eine E-Mail-Adresse an. Wir werden Sie unter dieser Adresse kontaktieren, falls ein Problem mit Ihrer Zahlung vorliegt.",
|
||||
"Your email": "Deine Email-Adresse",
|
||||
"Your email": "Ihre Email-Adresse",
|
||||
"Continue": "Fortsetzen",
|
||||
"Please enter a valid email address": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||
"Order Amount": "Bestellbetrag",
|
||||
@ -25,7 +25,7 @@ const locales_de = {
|
||||
"Address": "Adresse",
|
||||
"Copied": "Kopiert",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.",
|
||||
"ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit Altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.",
|
||||
"ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst als bezahlt markiert, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.",
|
||||
"Shapeshift_Button_Text": "Bezahlen mit Altcoins",
|
||||
"ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Umrechnungsanbieter verfügbar.",
|
||||
|
@ -47,5 +47,8 @@ const locales_is = {
|
||||
"Archived_Body": "Vinsamlegast hafðu samband fyrir upplýsingar eða aðstoð.",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "BOLT 11 Reikningur",
|
||||
"Node Info": "Nótu upplýsingar"
|
||||
"Node Info": "Nótu upplýsingar",
|
||||
//
|
||||
"txCount": "{{count}} reikningur",
|
||||
"txCount_plural": "{{count}} reikningar"
|
||||
};
|
||||
|
@ -47,5 +47,8 @@
|
||||
"Archived_Body": "ご注文に関わる詳細などでお困りの場合はお店の担当窓口へお問い合わせください。",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "お支払いコード",
|
||||
"Node Info": "接続情報"
|
||||
"Node Info": "接続情報",
|
||||
//
|
||||
"txCount": "取引 {{count}} 個",
|
||||
"txCount_plural": "取引 {{count}} 個"
|
||||
};
|
||||
|
@ -47,5 +47,8 @@ Je kan terug komen naar {{storeName}} indien je nog eens je betaling wilt prober
|
||||
"Archived_Body": "Bedankt om de winkel te contacteren voor bijstand met of informatie over deze bestelling",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "BOLT 11 Factuur",
|
||||
"Node Info": "Node Info"
|
||||
"Node Info": "Node Info",
|
||||
//
|
||||
"txCount": "{{count}} transactie",
|
||||
"txCount_plural": "{{count}} transacties"
|
||||
};
|
||||
|
54
BTCPayServer/wwwroot/checkout/js/langs/pt.js
Normal file
54
BTCPayServer/wwwroot/checkout/js/langs/pt.js
Normal file
@ -0,0 +1,54 @@
|
||||
const locales_pt = {
|
||||
nested: {
|
||||
lang: 'Idioma'
|
||||
},
|
||||
"Awaiting Payment...": "A Aguardar Pagamento...",
|
||||
"Pay with": "Pague com",
|
||||
"Contact and Refund Email": "E-mail de Contacto e Reembolso",
|
||||
"Contact_Body": "Por favor indique um e-mail abaixo. Entraremos em contacto para este endereço se ocorrer algum problema com o seu pagamento.",
|
||||
"Your email": "O seu e-mail",
|
||||
"Continue": "Continuar",
|
||||
"Please enter a valid email address": "Por favor introduza um e-mail válido",
|
||||
"Order Amount": "Valor da Encomenda",
|
||||
"Network Cost": "Custo da Rede",
|
||||
"Already Paid": "Já Pago",
|
||||
"Due": "Devido",
|
||||
// Tabs
|
||||
"Scan": "Digitalizar",
|
||||
"Copy": "Copiar",
|
||||
"Conversion": "Conversão",
|
||||
// Scan tab
|
||||
"Open in wallet": "Abrir na carteira",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "Para completar o seu pagamento, por favor envie {{btcDue}} {{cryptoCode}} para o endereço abaixo.",
|
||||
"Amount": "Quantia",
|
||||
"Address": "Endereço",
|
||||
"Copied": "Copiado",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
|
||||
"ConversionTab_BodyDesc": "Este serviço é oferecido por terceiros. Por favor tenha em mente que não temos qualquer controlo sobre como os seus fundos serão utilizados. A fatura será marcada como paga apenas quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
|
||||
"Shapeshift_Button_Text": "Pagar com Altcoins",
|
||||
"ConversionTab_Lightning": "Não há fornecedores de conversão disponíveis para pagamentos via Lightning Network.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "A fatura está a expirar...",
|
||||
"Invoice expired": "Fatura expirada",
|
||||
"What happened?": "O que aconteceu?",
|
||||
"InvoiceExpired_Body_1": "Esta fatura expirou. Uma fatura é válida durante {{maxTimeMinutes}} minutos. \
|
||||
Pode voltar para {{storeName}} se quiser enviar o seu pagamento novamente.",
|
||||
"InvoiceExpired_Body_2": "Se tentou enviar um pagamento, ele ainda não foi aceite pela rede Bitcoin. Nós ainda não recebemos o valor enviado.",
|
||||
"InvoiceExpired_Body_3": "Se a transação não for aceite pela rede Bitcoin, o valor voltará para sua carteira. Dependendo da sua carteira, isto pode demorar entre 48 e 72 horas.",
|
||||
"Invoice ID": "Nº da Fatura",
|
||||
"Order ID": "Nº da Encomenda",
|
||||
"Return to StoreName": "Voltar para {{storeName}}",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "Esta fatura foi paga",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "Esta fatura foi arquivada",
|
||||
"Archived_Body": "Por favor, entre em contacto com o vendedor para informações e suporte",
|
||||
// Lightning
|
||||
"BOLT 11 Invoice": "Fatura BOLT 11",
|
||||
"Node Info": "Informação do Nó",
|
||||
//
|
||||
"txCount": "{{count}} transação",
|
||||
"txCount_plural": "{{count}} transações"
|
||||
};
|
48
BTCPayServer/wwwroot/js/checkout/lang/hr.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/lang/hr.js
Normal file
@ -0,0 +1,48 @@
|
||||
const locales_hr = {
|
||||
nested: {
|
||||
lang: 'Croatian'
|
||||
},
|
||||
"Awaiting Payment...": "Čekamo uplatu...",
|
||||
"Pay with": "Plati sa",
|
||||
"Contact and Refund Email": "E-mail za kontakt i povrat sredstava",
|
||||
"Contact_Body": "Molimo upišite Vašu e-mail adresu. Kontaktirat ćemo Vas ukoliko bude potrebe.",
|
||||
"Your email": "Vaš e-mail",
|
||||
"Continue": "Dalje",
|
||||
"Please enter a valid email address": "Molimo unesite ispravnu e-mail adresu",
|
||||
"Order Amount": "Količina",
|
||||
"Network Cost": "Trošak mreže",
|
||||
"Already Paid": "Već plaćeno",
|
||||
"Due": "Rok",
|
||||
// Tabs
|
||||
"Scan": "Skeniraj",
|
||||
"Copy": "Kopiraj",
|
||||
"Conversion": "Pretvori",
|
||||
// Scan tab
|
||||
"Open in wallet": "Otvori u novčaniku",
|
||||
// Copy tab
|
||||
"CompletePay_Body": "Kako bi završili uplatu pošaljite {{btcDue}} {{cryptoCode}} na navedenu adresu",
|
||||
"Amount": "Iznos",
|
||||
"Address": "Adresa",
|
||||
"Copied": "Kopirano",
|
||||
// Conversion tab
|
||||
"ConversionTab_BodyTop": "Možete platiti {{btcDue}} {{cryptoCode}} pomoću altcoina koje prodavač ne podržava.",
|
||||
"ConversionTab_BodyDesc": "Ovu usluga pruža treća strana. Vodite računa da nemamo kontroli nad načinom kako će Vam davatelji usluge proslijediti sredstva. Vodite računa da je račun plaćen tek kada su primljena sredstva na {{cryptoCode}} Blockchainu.",
|
||||
"Shapeshift_Button_Text": "Plati s Alt-coinovima",
|
||||
"ConversionTab_Lightning": "Ne postoji treća strana koja bi konvertirala Lightning Network uplate.",
|
||||
// Invoice expired
|
||||
"Invoice expiring soon...": "Račun uskoro ističe...",
|
||||
"Invoice expired": "Račun je istekao",
|
||||
"What happened?": "Što se dogodilo",
|
||||
"InvoiceExpired_Body_1": "Račun je istekao i nije više valjan. Račun vrijedi samo {{maxTimeMinutes}} minuta. \
|
||||
Možete se vratiti na {{storeName}}, gdje možete ponovo inicirati plaćanje.",
|
||||
"InvoiceExpired_Body_2": "Ako ste pokušali poslati uplatu, ista nije registrirana na Blockchainu. Nismo još zaprimili Vašu uplatu.",
|
||||
"InvoiceExpired_Body_3": "Ako poslana sredstva na budu potvrđena na Blockchainu, sredstva će biti ponovo dostupna u Vašem novčaniku.",
|
||||
"Invoice ID": "Broj računa",
|
||||
"Order ID": "Broj narudžbe",
|
||||
"Return to StoreName": "Vrati se na {{storeName}}",
|
||||
// Invoice paid
|
||||
"This invoice has been paid": "Račun je plaćen",
|
||||
// Invoice archived
|
||||
"This invoice has been archived": "Račun je arhiviran.",
|
||||
"Archived_Body": "Kontaktirajte dućan za detalje oko narudžbe ili pomoć."
|
||||
};
|
@ -1,44 +1,16 @@
|
||||
/* Wrapping element */
|
||||
/* Set some basic padding to keep content from hitting the edges */
|
||||
.body-content {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* Carousel */
|
||||
.carousel-caption p {
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Make .svg files in the carousel display properly in older browsers */
|
||||
.carousel-inner .item img[src$=".svg"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide/rearrange for smaller screens */
|
||||
@media screen and (max-width: 767px) {
|
||||
/* Hide captions */
|
||||
.carousel-caption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 0 18px !important;
|
||||
}
|
||||
|
||||
footer {
|
||||
.siteFooter {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: 18px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
vertical-align: middle;
|
||||
font-size: 8px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user