Compare commits

...

45 Commits

Author SHA1 Message Date
29af07b3f9 Make sure we do not return outdated rates 2018-08-23 13:47:56 +09:00
758436a428 bump 2018-08-23 12:16:43 +09:00
e0cadb4f62 Do not send not found if invoices does not belong to logged on user 2018-08-23 12:13:27 +09:00
013dfa1b61 Properly escape attributes, make the preview an actual form rather than an image 2018-08-23 11:55:29 +09:00
e0f1c50534 Making Currency a textbox instead of dropdown 2018-08-23 11:17:54 +09:00
d50dc2e68e ServerIPN is an email 2018-08-23 11:12:25 +09:00
8b5b18c97e Make sure no trailing slash bug 2018-08-23 11:11:39 +09:00
f7383b4cc8 Fixing error on CheckoutExperience if no crypto is set 2018-08-23 11:08:53 +09:00
1bc32285ba Merge branch 'master' of https://github.com/rockstardev/btcpayserver into rockstardev-master 2018-08-23 11:01:48 +09:00
f12114f9aa Poll and cache rates in parallel 2018-08-23 00:24:33 +09:00
2b6faa8d20 Migrating generator to work on store path 2018-08-22 14:05:12 +02:00
45b7df6ac9 Removing traces of PayButton being an app... shhhh... 2018-08-22 14:04:59 +02:00
5a43ce2719 Transfering Pay Button from App directly to Store 2018-08-22 13:57:54 +02:00
fe31dc8606 Setting titles for new pill navigation 2018-08-22 13:56:55 +02:00
f0615482d9 Refactoring pill navigation to use new subnav code with enums 2018-08-22 13:50:29 +02:00
03c47e6f7d Merge remote-tracking branch 'source/master' 2018-08-22 13:01:34 +02:00
4111b8a5a3 Maintaining AppId reference 2018-08-22 12:59:55 +02:00
5b5a2e8c25 Reorganizing Javascript code and references 2018-08-22 11:10:46 +02:00
9ec0c23c52 Folder for pay buttons, donate button 2018-08-22 10:59:24 +02:00
9a5034c13c URL of image for pay button 2018-08-22 10:52:17 +02:00
b1fcf4524a Separate app for PayButton 2018-08-22 10:26:49 +02:00
87d384dba5 Decouple RateProviderFactory with RateFetcher 2018-08-22 16:53:40 +09:00
4f5a8f7953 Use direct provider for Kraken, update packages 2018-08-21 15:59:57 +09:00
8728356698 Use HttpClientFactory for coinaverage 2018-08-21 14:33:13 +09:00
9c30476fc8 Making BTCPayServer a bit faster when creating invoices 2018-08-21 13:54:52 +09:00
09beb57eaf Fixing csproj 2018-08-17 16:27:37 +02:00
76a36d1829 Merge remote-tracking branch 'source/master'
# Conflicts:
#	BTCPayServer/BTCPayServer.csproj
2018-08-17 13:35:56 +02:00
0d4036efa2 Dynamically determining site url 2018-08-17 13:33:47 +02:00
cb4562aad5 Model validation attributes added for email and url 2018-08-17 13:26:33 +02:00
0084d4766b Server side validation of PayButton POST 2018-08-17 13:21:00 +02:00
74ddcfa01e Handling payment button post and providing test form 2018-08-17 12:38:03 +02:00
ed36fba0d7 Defining input names for validation errors 2018-08-17 10:30:49 +02:00
af015d435b Integrating Clipboard.js and implemeting copy code 2018-08-16 22:22:15 +02:00
ec59980e6f Validation and conditional rendering of Generated Code section 2018-08-16 21:55:24 +02:00
f0f4247c5d Field validation 2018-08-15 23:58:39 +02:00
b562094956 Currency dropdown as part of page model 2018-08-14 23:47:41 +02:00
4afd55c441 Rendering button code on load from srvModel 2018-08-14 23:47:28 +02:00
d7404f418d Returning intialized model and inputchange on all attribs 2018-08-14 19:40:57 +02:00
893410911c Button width influenced by generator 2018-08-14 18:59:59 +02:00
556b581b6a Updating display of generated HTML 2018-08-14 14:57:46 +02:00
1685ccaca8 Fixing success message for DNS 2018-08-13 17:04:37 +09:00
522abcfdfd Fix setting up DNS name 2018-08-13 16:48:10 +09:00
ed1827ff28 Fix not showing ssh service if no LND 2018-08-13 15:10:59 +09:00
57daf27fdd Completion of UI for rest of Pay Button gneerator 2018-08-11 13:34:52 +02:00
e698d90e3c Pay Button page foundation 2018-08-10 20:26:51 +02:00
55 changed files with 9144 additions and 473 deletions

View File

@ -120,36 +120,67 @@ namespace BTCPayServer.Tests
.Build();
_Host.Start();
InvoiceRepository = (InvoiceRepository)_Host.Services.GetService(typeof(InvoiceRepository));
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var rateProvider = (BTCPayRateProviderFactory)_Host.Services.GetService(typeof(BTCPayRateProviderFactory));
rateProvider.DirectProviders.Clear();
StoreRepository = (StoreRepository)_Host.Services.GetService(typeof(StoreRepository));
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
if (MockRates)
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.DirectProviders.Add("coinaverage", coinAverageMock);
var rateProvider = (RateProviderFactory)_Host.Services.GetService(typeof(RateProviderFactory));
rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
rateProvider.Providers.Add("coinaverage", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
rateProvider.Providers.Add("bittrex", bittrex);
}
}
public string HostName
@ -177,7 +208,7 @@ namespace BTCPayServer.Tests
{
context.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, Policies.CookieAuthentication));
}
if(storeId != null)
if (storeId != null)
{
context.SetStoreData(GetService<StoreRepository>().FindStore(storeId, userId).GetAwaiter().GetResult());
}

View File

@ -14,6 +14,7 @@ using Xunit;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Tests.Logging;
namespace BTCPayServer.Tests
{

View File

@ -398,6 +398,7 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSetLightningServer()
{
using (var tester = ServerTester.Create())
@ -546,18 +547,21 @@ namespace BTCPayServer.Tests
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCLightning()
{
ProcessLightningPayment(LightningConnectionType.CLightning);
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentCharge()
{
ProcessLightningPayment(LightningConnectionType.Charge);
}
[Fact]
[Trait("Flaky", "Flaky")]
public void CanSendLightningPaymentLnd()
{
ProcessLightningPayment(LightningConnectionType.LndREST);
@ -943,8 +947,8 @@ namespace BTCPayServer.Tests
user.RegisterDerivationScheme("BTC");
List<decimal> rates = new List<decimal>();
rates.Add(CreateInvoice(tester, user, "coinaverage"));
var bitflyer = CreateInvoice(tester, user, "bitflyer");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer");
var bitflyer = CreateInvoice(tester, user, "bitflyer", "JPY");
var bitflyer2 = CreateInvoice(tester, user, "bitflyer", "JPY");
Assert.Equal(bitflyer, bitflyer2); // Should be equal because cache
rates.Add(bitflyer);
@ -955,7 +959,7 @@ namespace BTCPayServer.Tests
}
}
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange)
private static decimal CreateInvoice(ServerTester tester, TestAccount user, string exchange, string currency = "USD")
{
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -964,7 +968,7 @@ namespace BTCPayServer.Tests
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
Currency = "USD",
Currency = currency,
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
@ -978,12 +982,10 @@ namespace BTCPayServer.Tests
{
using (var tester = ServerTester.Create())
{
tester.PayTester.MockRates = false;
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
@ -994,7 +996,7 @@ namespace BTCPayServer.Tests
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(Money.Coins(1.0m), invoice1.BtcPrice);
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
@ -1013,11 +1015,9 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
var expectedRate = 5000.0m * 0.6m;
var expectedCoins = invoice2.Price / expectedRate;
Assert.True(invoice2.BtcPrice.Almost(Money.Coins(expectedCoins), 0.00001m));
}
}
@ -1132,7 +1132,7 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);";
"X_X = coinaverage(X_X);";
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
@ -1666,15 +1666,17 @@ namespace BTCPayServer.Tests
[Fact]
public void CanQueryDirectProviders()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
foreach (var result in factory
.DirectProviders
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync()))
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
Assert.NotEmpty(exchangeRates.ByExchange[result.ExpectedName]);
@ -1687,21 +1689,23 @@ namespace BTCPayServer.Tests
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync().GetAwaiter().GetResult();
}
[Fact]
public void CanGetRateCryptoCurrenciesByDefault()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var factory = CreateBTCPayRateFactory(provider);
var factory = CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var pairs =
provider.GetAll()
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
.ToHashSet();
var rules = new StoreBlob().GetDefaultRateRules(provider);
var result = factory.FetchRates(pairs, rules);
var result = fetcher.FetchRates(pairs, rules);
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
@ -1709,46 +1713,93 @@ namespace BTCPayServer.Tests
}
}
private static BTCPayRateProviderFactory CreateBTCPayRateFactory(BTCPayNetworkProvider provider)
private static RateProviderFactory CreateBTCPayRateFactory()
{
return new BTCPayRateProviderFactory(new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) }, provider, new CoinAverageSettings());
return new RateProviderFactory(CreateMemoryCache(), null, new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
{
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync()
{
Hit = true;
var rates = new ExchangeRates();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates);
}
public void AssertHit()
{
Assert.True(Hit, "Should have hit the provider");
Hit = false;
}
public void AssertNotHit()
{
Assert.False(Hit, "Should have not hit the provider");
Hit = false;
}
}
[Fact]
public void CheckRatesProvider()
{
var provider = new BTCPayNetworkProvider(NetworkType.Mainnet);
var coinAverage = new CoinAverageRateProvider();
var rates = coinAverage.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(rates.GetRate("coinaverage", new CurrencyPair("BTC", "JPY")));
var ratesBitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/"))).GetRatesAsync().GetAwaiter().GetResult();
Assert.NotNull(ratesBitpay.GetRate("bitpay", new CurrencyPair("BTC", "JPY")));
var spy = new SpyRateProvider();
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory(provider);
factory.CacheSpan = TimeSpan.FromSeconds(10);
var factory = CreateBTCPayRateFactory();
factory.Providers.Clear();
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetcher = new RateFetcher(factory);
Thread.Sleep(11000);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid);
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = factory.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.False(fetchedRate.Cached);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider(spy);
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.RefreshRate = TimeSpan.FromSeconds(1.0);
Thread.Sleep(1020);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules).GetAwaiter().GetResult();
spy.AssertNotHit();
fetch.ValidatyTime = TimeSpan.FromSeconds(1.0);
fetch.UpdateIfNecessary().GetAwaiter().GetResult();
spy.AssertHit();
fetch.GetRatesAsync().GetAwaiter().GetResult();
Thread.Sleep(1000);
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)

View File

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

View File

@ -2,9 +2,12 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.90</Version>
<Version>1.0.2.96</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
@ -31,22 +34,22 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.4.1" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.19" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.2" />
<PackageReference Include="LedgerWallet" Version="2.0.0" />
<PackageReference Include="LedgerWallet" Version="2.0.0.1" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.1.1.32" />
<PackageReference Include="NBitcoin" Version="4.1.1.45" />
<PackageReference Include="NBitpayClient" Version="1.0.0.29" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.15" />
<PackageReference Include="NBXplorer.Client" Version="1.0.2.18" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.16" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.17" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.1.0" />
<PackageReference Include="SSH.NET" Version="2016.1.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.3.0" />
@ -116,7 +119,16 @@
</ItemGroup>
<ItemGroup>
<Content Update="Views\Server\SSHService.cshtml">
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButton.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonHandle.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Stores\PayButtonTest.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\LNDGRPCServices.cshtml">

View File

@ -163,7 +163,7 @@ namespace BTCPayServer.Controllers
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Apps
.Where(us => us.Id == appId &&
.Where(us => us.Id == appId &&
us.AppType == appType.ToString())
.FirstOrDefaultAsync();
}
@ -263,14 +263,6 @@ namespace BTCPayServer.Controllers
return Redirect(invoice.Data.Url);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
private async Task UpdateAppSettings(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())

View File

@ -1,18 +1,17 @@
using System;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitcoin;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;
namespace BTCPayServer.Controllers
{
@ -24,6 +23,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> _UserManager;
CurrencyNameTable _Currencies;
InvoiceController _InvoiceController;
BTCPayNetworkProvider _NetworkProvider;
[TempData]
public string StatusMessage { get; set; }
@ -32,12 +32,14 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
ApplicationDbContextFactory contextFactory,
CurrencyNameTable currencies,
InvoiceController invoiceController)
InvoiceController invoiceController,
BTCPayNetworkProvider networkProvider)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_ContextFactory = contextFactory;
_Currencies = currencies;
_NetworkProvider = networkProvider;
}
public async Task<IActionResult> ListApps()
{
@ -199,5 +201,13 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
private async Task<StoreData> GetStore(AppData app)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.Stores.FirstOrDefaultAsync(s => s.Id == app.StoreDataId);
}
}
}
}

View File

@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery()
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true,
IncludeEvents = true

View File

@ -49,7 +49,7 @@ namespace BTCPayServer.Controllers
{
InvoiceRepository _InvoiceRepository;
ContentSecurityPolicies _CSP;
BTCPayRateProviderFactory _RateProvider;
RateFetcher _RateProvider;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
private CurrencyNameTable _CurrencyNameTable;
@ -62,7 +62,7 @@ namespace BTCPayServer.Controllers
InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
BTCPayWalletProvider walletProvider,
@ -195,12 +195,9 @@ namespace BTCPayServer.Controllers
var allRateRuleErrors = string.Join(", ", rateResult.Errors.ToArray());
logs.Write($"{pair.Key}: Rate rule error ({allRateRuleErrors})");
}
if (rateResult.ExchangeExceptions.Count != 0)
foreach (var ex in rateResult.ExchangeExceptions)
{
foreach (var ex in rateResult.ExchangeExceptions)
{
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
logs.Write($"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})");
}
}).ToArray());
}
@ -210,6 +207,7 @@ namespace BTCPayServer.Controllers
try
{
var storeBlob = store.GetStoreBlob();
var preparePayment = handler.PreparePayment(supportedPaymentMethod, store, network);
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.BidAsk == null)
{
@ -220,7 +218,7 @@ namespace BTCPayServer.Controllers
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
paymentMethod.SetPaymentMethodDetails(paymentDetails);

View File

@ -15,12 +15,12 @@ namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
BTCPayRateProviderFactory _RateProviderFactory;
RateFetcher _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
StoreRepository _StoreRepo;
public RateController(
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)

View File

@ -33,14 +33,14 @@ namespace BTCPayServer.Controllers
private UserManager<ApplicationUser> _UserManager;
SettingsRepository _SettingsRepository;
private readonly NBXplorerDashboard _dashBoard;
private BTCPayRateProviderFactory _RateProviderFactory;
private RateFetcher _RateProviderFactory;
private StoreRepository _StoreRepository;
LightningConfigurationProvider _LnConfigProvider;
BTCPayServerOptions _Options;
public ServerController(UserManager<ApplicationUser> userManager,
Configuration.BTCPayServerOptions options,
BTCPayRateProviderFactory rateProviderFactory,
RateFetcher rateProviderFactory,
SettingsRepository settingsRepository,
NBXplorerDashboard dashBoard,
LightningConfigurationProvider lnConfigProvider,
@ -180,6 +180,8 @@ namespace BTCPayServer.Controllers
return View(vm);
}
vm.DNSDomain = vm.DNSDomain.Trim().ToLowerInvariant();
if (vm.DNSDomain.Equals(this.Request.Host.Host, StringComparison.OrdinalIgnoreCase))
return View(vm);
if (IPAddress.TryParse(vm.DNSDomain, out var unused))
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"This should be a domain name");
@ -200,12 +202,13 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
if (this.Request.Host.Port != null)
builder.Port = this.Request.Host.Port.Value;
builder.Path = "runid";
builder.Query = $"expected={RunId}";
var response = await client.GetAsync(builder.Uri);
if (!response.IsSuccessStatusCode)
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
var hasCommonAddress = addresses2.GetAwaiter().GetResult().Select(c => c.ToString()).Any(s => addressesSet.Contains(s));
if (!hasCommonAddress)
{
ModelState.AddModelError(nameof(vm.DNSDomain), $"Invalid host ({vm.DNSDomain} is not pointing to this BTCPay instance)");
return View(vm);

View File

@ -1,8 +1,13 @@
using BTCPayServer.Authentication;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Authentication;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Rating;
using BTCPayServer.Security;
@ -11,21 +16,13 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
{
@ -35,7 +32,7 @@ namespace BTCPayServer.Controllers
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
BTCPayRateProviderFactory _RateFactory;
RateFetcher _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
IServiceProvider serviceProvider,
@ -47,11 +44,12 @@ namespace BTCPayServer.Controllers
AccessTokenController tokenController,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
BTCPayRateProviderFactory rateFactory,
RateFetcher rateFactory,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
IHostingEnvironment env,
InvoiceController invoiceController)
{
_RateFactory = rateFactory;
_Repo = repo;
@ -67,6 +65,7 @@ namespace BTCPayServer.Controllers
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
_InvoiceController = invoiceController;
}
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
@ -81,6 +80,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
IHostingEnvironment _Env;
InvoiceController _InvoiceController;
[TempData]
public string StatusMessage
@ -521,7 +521,7 @@ namespace BTCPayServer.Controllers
private CoinAverageExchange[] GetSupportedExchanges()
{
return _RateFactory.GetSupportedExchanges()
return _RateFactory.RateProviderFactory.GetSupportedExchanges()
.Select(c => c.Value)
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)
.ToArray();
@ -767,5 +767,65 @@ namespace BTCPayServer.Controllers
return null;
return _UserManager.GetUserId(User);
}
// TODO: Need to have talk about how architect default currency implementation
// For now we have also hardcoded USD for Store creation and then Invoice creation
const string DEFAULT_CURRENCY = "USD";
[Route("{storeId}/paybutton")]
public IActionResult PayButton()
{
var store = StoreData;
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{
Price = 10,
Currency = DEFAULT_CURRENCY,
ButtonSize = 2,
UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.png",
StoreId = store.Id
};
return View(model);
}
[HttpPost]
[Route("{storeId}/pay")]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> PayButtonHandle(string storeId, [FromForm]PayButtonViewModel model)
{
var store = StoreData;
// TODO: extract validation to model
if (model.Price <= 0)
ModelState.AddModelError("Price", "Price must be greater than 0");
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
BuyerEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
}
[HttpGet]
[Route("{storeId}/paybuttontest")]
public IActionResult PayButtonTest(string storeId)
{
return View();
}
}
}

View File

@ -42,7 +42,7 @@ namespace BTCPayServer.Controllers
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
BTCPayRateProviderFactory _RateProvider;
RateFetcher _RateProvider;
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
@ -50,7 +50,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
BTCPayRateProviderFactory rateProvider,
RateFetcher rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)

View File

@ -199,7 +199,7 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{

View File

@ -18,9 +18,9 @@ namespace BTCPayServer.HostedServices
{
private SettingsRepository _SettingsRepository;
private CoinAverageSettings _coinAverageSettings;
BTCPayRateProviderFactory _RateProviderFactory;
RateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo,
BTCPayRateProviderFactory rateProviderFactory,
RateProviderFactory rateProviderFactory,
CoinAverageSettings coinAverageSettings)
{
this._SettingsRepository = repo;
@ -33,16 +33,41 @@ namespace BTCPayServer.HostedServices
return new[]
{
CreateLoopTask(RefreshCoinAverageSupportedExchanges),
CreateLoopTask(RefreshCoinAverageSettings)
CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates)
};
}
async Task RefreshRates()
{
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(Cancellation))
{
timeout.CancelAfter(TimeSpan.FromSeconds(20.0));
try
{
await Task.WhenAll(_RateProviderFactory.Providers
.Select(p => (Fetcher: p.Value as BackgroundFetcherRateProvider, ExchangeName: p.Key)).Where(p => p.Fetcher != null)
.Select(p => p.Fetcher.UpdateIfNecessary().ContinueWith(t =>
{
if (t.Result.Exception != null)
{
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
}
}, TaskScheduler.Default))
.ToArray()).WithCancellation(timeout.Token);
}
catch (OperationCanceledException) when (timeout.IsCancellationRequested)
{
}
}
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
}
async Task RefreshCoinAverageSupportedExchanges()
{
await new SynchronizationContextRemover();
var tickers = await new CoinAverageRateProvider() { Authenticator = _coinAverageSettings }.GetExchangeTickersAsync();
var exchanges = new CoinAverageExchanges();
foreach(var item in tickers
foreach (var item in tickers
.Exchanges
.Select(c => new CoinAverageExchange(c.Name, c.DisplayName)))
{
@ -54,7 +79,6 @@ namespace BTCPayServer.HostedServices
async Task RefreshCoinAverageSettings()
{
await new SynchronizationContextRemover();
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))

View File

@ -53,6 +53,7 @@ namespace BTCPayServer.Hosting
var factory = provider.GetRequiredService<ApplicationDbContextFactory>();
factory.ConfigureBuilder(o);
});
services.AddHttpClient();
services.TryAddSingleton<SettingsRepository>();
services.TryAddSingleton<InvoicePaymentNotification>();
services.TryAddSingleton<BTCPayServerOptions>(o => o.GetRequiredService<IOptions<BTCPayServerOptions>>().Value);
@ -137,7 +138,8 @@ namespace BTCPayServer.Hosting
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
});
services.TryAddSingleton<BTCPayRateProviderFactory>();
services.TryAddSingleton<RateProviderFactory>();
services.TryAddSingleton<RateFetcher>();
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<AccessTokenController>();

View File

@ -15,6 +15,7 @@ namespace BTCPayServer.Models.AppViewModels
public string AppName { get; set; }
public string AppType { get; set; }
public bool IsOwner { get; set; }
public string UpdateAction { get { return "Update" + AppType; } }
public string ViewAction { get { return "View" + AppType; } }
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels
{
public class PayButtonViewModel
{
public decimal Price { get; set; }
[Required]
public string Currency { get; set; }
public string CheckoutDesc { get; set; }
public string OrderId { get; set; }
public int ButtonSize { get; set; }
[Url]
public string ServerIpn { get; set; }
[Url]
public string BrowserRedirect { get; set; }
[EmailAddress]
public string NotifyEmail { get; set; }
public string StoreId { get; set; }
// Data that influences Pay Button UI, but not invoice creation
public string UrlRoot { get; set; }
public List<string> CurrencyDropdown { get; set; }
public string PayButtonImageUrl { get; set; }
}
}

View File

@ -27,16 +27,30 @@ namespace BTCPayServer.Payments.Bitcoin
_WalletProvider = walletProvider;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
class Prepare
{
public Task<FeeRate> GetFeeRate;
public Task<BitcoinAddress> ReserveAddress;
}
public override object PreparePayment(DerivationStrategy supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
return new Prepare()
{
GetFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync(),
ReserveAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase)
};
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(DerivationStrategy supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
if (!_ExplorerProvider.IsAvailable(network))
throw new PaymentMethodUnavailableException($"Full node not available");
var getFeeRate = _FeeRateProviderFactory.CreateFeeProvider(network).GetFeeRateAsync();
var getAddress = _WalletProvider.GetWallet(network).ReserveAddressAsync(supportedPaymentMethod.DerivationStrategyBase);
var prepare = (Prepare)preparePaymentObject;
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = await getFeeRate;
onchainMethod.FeeRate = await prepare.GetFeeRate;
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
onchainMethod.DepositAddress = (await getAddress).ToString();
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
return onchainMethod;
}
}

View File

@ -20,23 +20,46 @@ namespace BTCPayServer.Payments
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
/// <summary>
/// This method called before the rate have been fetched
/// </summary>
/// <param name="supportedPaymentMethod"></param>
/// <param name="store"></param>
/// <param name="network"></param>
/// <returns></returns>
object PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network);
}
public interface IPaymentMethodHandler<T> : IPaymentMethodHandler where T : ISupportedPaymentMethod
{
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
}
public abstract class PaymentMethodHandlerBase<T> : IPaymentMethodHandler<T> where T : ISupportedPaymentMethod
{
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network);
public abstract Task<IPaymentMethodDetails> CreatePaymentMethodDetails(T supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject);
public virtual object PreparePayment(T supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
return null;
}
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, BTCPayNetwork network)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, store, network);
return PreparePayment(method, store, network);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}
Task<IPaymentMethodDetails> IPaymentMethodHandler.CreatePaymentMethodDetails(ISupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
if (supportedPaymentMethod is T method)
{
return CreatePaymentMethodDetails(method, paymentMethod, store, network, preparePaymentObject);
}
throw new NotSupportedException("Invalid supportedPaymentMethod");
}

View File

@ -26,7 +26,7 @@ namespace BTCPayServer.Payments.Lightning
_LightningClientFactory = lightningClientFactory;
_Dashboard = dashboard;
}
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network)
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
{
var storeBlob = store.GetStoreBlob();
var test = Test(supportedPaymentMethod, network);

View File

@ -221,6 +221,16 @@ namespace BTCPayServer.Rating
}
public class ExchangeRate
{
public ExchangeRate()
{
}
public ExchangeRate(string exchange, CurrencyPair currencyPair, BidAsk bidAsk)
{
this.Exchange = exchange;
this.CurrencyPair = currencyPair;
this.BidAsk = bidAsk;
}
public string Exchange { get; set; }
public CurrencyPair CurrencyPair { get; set; }
public BidAsk BidAsk { get; set; }

View File

@ -1,215 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class ExchangeException
{
public Exception Exception { get; set; }
public string ExchangeName { get; set; }
}
public class RateResult
{
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public BidAsk BidAsk { get; set; }
public bool Cached { get; internal set; }
}
public class BTCPayRateProviderFactory
{
class QueryRateResult
{
public bool CachedResult { get; set; }
public List<ExchangeException> Exceptions { get; set; }
public ExchangeRates ExchangeRates { get; set; }
}
IMemoryCache _Cache;
private IOptions<MemoryCacheOptions> _CacheOptions;
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
CoinAverageSettings _CoinAverageSettings;
public BTCPayRateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
BTCPayNetworkProvider btcpayNetworkProvider,
CoinAverageSettings coinAverageSettings)
{
if (cacheOptions == null)
throw new ArgumentNullException(nameof(cacheOptions));
_CoinAverageSettings = coinAverageSettings;
_Cache = new MemoryCache(cacheOptions);
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
this.btcpayNetworkProvider = btcpayNetworkProvider;
InitExchanges();
}
public bool UseCoinAverageAsFallback { get; set; } = true;
private void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
DirectProviders.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
DirectProviders.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
DirectProviders.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), false));
DirectProviders.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
DirectProviders.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
DirectProviders.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
DirectProviders.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
DirectProviders.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, Authenticator = _CoinAverageSettings });
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("kraken", new ExchangeSharpRateProvider("kraken", new ExchangeKrakenAPI(), true));
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
}
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> DirectProviders
{
get
{
return _DirectProviders;
}
}
BTCPayNetworkProvider btcpayNetworkProvider;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
_Cache = new MemoryCache(_CacheOptions);
}
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
{
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
}
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{
var dependentQueries = new List<Task<QueryRateResult>>();
foreach (var requiredExchange in i.RateRule.ExchangeRates)
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = QueryRates(requiredExchange.Exchange);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);
}
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
}
return fetchingRates;
}
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{
var result = new RateResult();
result.Cached = true;
foreach (var queryAsync in dependentQueries)
{
var query = await queryAsync;
if (!query.CachedResult)
result.Cached = false;
result.ExchangeExceptions.AddRange(query.Exceptions);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);
return result;
}
private async Task<QueryRateResult> QueryRates(string exchangeName)
{
List<IRateProvider> providers = new List<IRateProvider>();
if (DirectProviders.TryGetValue(exchangeName, out var directProvider))
providers.Add(directProvider);
if (_CoinAverageSettings.AvailableExchanges.ContainsKey(exchangeName))
{
providers.Add(new CoinAverageRateProvider()
{
Exchange = exchangeName,
Authenticator = _CoinAverageSettings
});
}
var fallback = new FallbackRateProvider(providers.ToArray());
var cached = new CachedRateProvider(exchangeName, fallback, _Cache)
{
CacheSpan = CacheSpan
};
var value = await cached.GetRatesAsync();
return new QueryRateResult()
{
CachedResult = !fallback.Used,
ExchangeRates = value,
Exceptions = fallback.Exceptions
.Select(c => new ExchangeException() { Exception = c, ExchangeName = exchangeName }).ToList()
};
}
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class BackgroundFetcherRateProvider : IRateProvider
{
public class LatestFetch
{
public ExchangeRates Latest;
public DateTimeOffset Timestamp;
public DateTimeOffset Expiration;
public Exception Exception;
internal ExchangeRates GetResult()
{
if(Expiration < DateTimeOffset.UtcNow)
{
if(Exception != null)
{
ExceptionDispatchInfo.Capture(Exception).Throw();
}
else
{
throw new InvalidOperationException("The rate has expired");
}
}
return Latest;
}
internal void CopyFrom(LatestFetch previous)
{
Latest = previous.Latest;
Timestamp = previous.Timestamp;
Expiration = previous.Expiration;
Exception = previous.Exception;
}
}
IRateProvider _Inner;
public BackgroundFetcherRateProvider(IRateProvider inner)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
_Inner = inner;
}
public TimeSpan RefreshRate { get; set; } = TimeSpan.FromSeconds(30);
public TimeSpan ValidatyTime { get; set; } = TimeSpan.FromMinutes(10);
public DateTimeOffset NextUpdate
{
get
{
var latest = _Latest;
if (latest == null || latest.Exception != null)
return DateTimeOffset.UtcNow;
return latest.Timestamp + RefreshRate;
}
}
public async Task<LatestFetch> UpdateIfNecessary()
{
if (NextUpdate <= DateTimeOffset.UtcNow)
{
try
{
await Fetch();
}
catch { } // Exception is inside _Latest
return _Latest;
}
return _Latest;
}
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync()
{
return (_Latest ?? (await Fetch())).GetResult();
}
private async Task<LatestFetch> Fetch()
{
var previous = _Latest;
var fetch = new LatestFetch();
try
{
var rates = await _Inner.GetRatesAsync();
fetch.Latest = rates;
fetch.Expiration = DateTimeOffset.UtcNow + ValidatyTime;
}
catch (Exception ex)
{
if(previous != null)
{
fetch.CopyFrom(previous);
}
fetch.Exception = ex;
}
fetch.Timestamp = DateTimeOffset.UtcNow;
_Latest = fetch;
fetch.GetResult(); // Will throw if not valid
return fetch;
}
public void InvalidateCache()
{
_Latest = null;
}
}
}

View File

@ -38,7 +38,7 @@ namespace BTCPayServer.Services.Rates
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
public Task<ExchangeRates> GetRatesAsync()
{

View File

@ -56,6 +56,19 @@ namespace BTCPayServer.Services.Rates
{
}
public HttpClient HttpClient
{
get
{
return _LocalClient ?? _Client;
}
set
{
_LocalClient = null;
}
}
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
public string Exchange { get; set; } = CoinAverageName;
@ -107,7 +120,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
using (resp)
{
@ -150,7 +163,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
@ -162,7 +175,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetRateLimitsResponse();
@ -193,7 +206,7 @@ namespace BTCPayServer.Services.Rates
{
await auth.AddHeader(request);
}
var resp = await _Client.SendAsync(request);
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetExchangeTickersResponse();

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -43,17 +44,17 @@ namespace BTCPayServer.Services.Rates
}
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
HashSet<string> notFoundSymbols = new HashSet<string>();
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
private ExchangeRate CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
{
if (notFoundSymbols.Contains(ticker.Key))
if (notFoundSymbols.ContainsKey(ticker.Key))
return null;
try
{
var tickerName = _ExchangeAPI.ExchangeSymbolToGlobalSymbol(ticker.Key);
if (!CurrencyPair.TryParse(tickerName, out var pair))
{
notFoundSymbols.Add(ticker.Key);
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
return null;
}
if(ReverseCurrencyPair)
@ -66,7 +67,7 @@ namespace BTCPayServer.Services.Rates
}
catch (ArgumentException)
{
notFoundSymbols.Add(ticker.Key);
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
return null;
}
}

View File

@ -9,7 +9,6 @@ namespace BTCPayServer.Services.Rates
public class FallbackRateProvider : IRateProvider
{
IRateProvider[] _Providers;
public bool Used { get; set; }
public FallbackRateProvider(IRateProvider[] providers)
{
if (providers == null)
@ -19,7 +18,6 @@ namespace BTCPayServer.Services.Rates
public async Task<ExchangeRates> GetRatesAsync()
{
Used = true;
foreach (var p in _Providers)
{
try

View File

@ -0,0 +1,135 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider
{
public KrakenExchangeRateProvider()
{
_Helper = new ExchangeKrakenAPI();
}
ExchangeKrakenAPI _Helper;
public HttpClient HttpClient
{
get
{
return _LocalClient ?? _Client;
}
set
{
_LocalClient = null;
}
}
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
string[] _Symbols = Array.Empty<string>();
DateTimeOffset? _LastSymbolUpdate = null;
public async Task<ExchangeRates> GetRatesAsync()
{
var result = new ExchangeRates();
var symbols = await GetSymbolsAsync();
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList);
JToken apiTickers = await MakeJsonRequestAsync<JToken>("/0/public/Ticker", null, new Dictionary<string, object> { { "pair", csvPairsList } });
var tickers = new List<KeyValuePair<string, ExchangeTicker>>();
foreach (string symbol in symbols)
{
var ticker = ConvertToExchangeTicker(symbol, apiTickers[symbol]);
if (ticker != null)
{
try
{
var global = _Helper.ExchangeSymbolToGlobalSymbol(symbol);
if (CurrencyPair.TryParse(global, out var pair))
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
else
notFoundSymbols.TryAdd(symbol, symbol);
}
catch (ArgumentException)
{
notFoundSymbols.TryAdd(symbol, symbol);
}
}
}
return result;
}
private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker)
{
if (ticker == null)
return null;
decimal last = ticker["c"][0].ConvertInvariant<decimal>();
return new ExchangeTicker
{
Ask = ticker["a"][0].ConvertInvariant<decimal>(),
Bid = ticker["b"][0].ConvertInvariant<decimal>(),
Last = last,
Volume = new ExchangeVolume
{
BaseVolume = ticker["v"][1].ConvertInvariant<decimal>(),
BaseSymbol = symbol,
ConvertedVolume = ticker["v"][1].ConvertInvariant<decimal>() * last,
ConvertedSymbol = symbol,
Timestamp = DateTime.UtcNow
}
};
}
private async Task<string[]> GetSymbolsAsync()
{
if (_LastSymbolUpdate != null && DateTimeOffset.UtcNow - _LastSymbolUpdate.Value < TimeSpan.FromDays(0.5))
{
return _Symbols;
}
else
{
JToken json = await MakeJsonRequestAsync<JToken>("/0/public/AssetPairs");
var symbols = (from prop in json.Children<JProperty>() where !prop.Name.Contains(".d", StringComparison.OrdinalIgnoreCase) select prop.Name).ToArray();
_Symbols = symbols;
_LastSymbolUpdate = DateTimeOffset.UtcNow;
return symbols;
}
}
private async Task<T> MakeJsonRequestAsync<T>(string url, string baseUrl = null, Dictionary<string, object> payload = null, string requestMethod = null)
{
StringBuilder sb = new StringBuilder();
sb.Append("https://api.kraken.com");
;
sb.Append(url);
if (payload != null)
{
sb.Append("?");
sb.Append(String.Join('&', payload.Select(kv => $"{kv.Key}={kv.Value}").OfType<object>().ToArray()));
}
var request = new HttpRequestMessage(HttpMethod.Get, sb.ToString());
var response = await HttpClient.SendAsync(request);
string stringResult = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<T>(stringResult);
if (result is JToken json)
{
if (!(json is JArray) && json["error"] is JArray error && error.Count != 0)
{
throw new APIException(error[0].ToStringInvariant());
}
result = (T)(object)(json["result"] ?? json);
}
return result;
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Rating;
namespace BTCPayServer.Services.Rates
{
public class NullRateProvider : IRateProvider
{
private NullRateProvider()
{
}
private static readonly NullRateProvider _Instance = new NullRateProvider();
public static NullRateProvider Instance
{
get
{
return _Instance;
}
}
public Task<ExchangeRates> GetRatesAsync()
{
return Task.FromResult(new ExchangeRates());
}
}
}

View File

@ -0,0 +1,94 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using static BTCPayServer.Services.Rates.RateProviderFactory;
namespace BTCPayServer.Services.Rates
{
public class ExchangeException
{
public Exception Exception { get; set; }
public string ExchangeName { get; set; }
}
public class RateResult
{
public List<ExchangeException> ExchangeExceptions { get; set; } = new List<ExchangeException>();
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public BidAsk BidAsk { get; set; }
public TimeSpan Latency { get; internal set; }
}
public class RateFetcher
{
private readonly RateProviderFactory _rateProviderFactory;
public RateFetcher(RateProviderFactory rateProviderFactory)
{
_rateProviderFactory = rateProviderFactory;
}
public RateProviderFactory RateProviderFactory => _rateProviderFactory;
public async Task<RateResult> FetchRate(CurrencyPair pair, RateRules rules)
{
return await FetchRates(new HashSet<CurrencyPair>(new[] { pair }), rules).First().Value;
}
public Dictionary<CurrencyPair, Task<RateResult>> FetchRates(HashSet<CurrencyPair> pairs, RateRules rules)
{
if (rules == null)
throw new ArgumentNullException(nameof(rules));
var fetchingRates = new Dictionary<CurrencyPair, Task<RateResult>>();
var fetchingExchanges = new Dictionary<string, Task<QueryRateResult>>();
var consolidatedRates = new ExchangeRates();
foreach (var i in pairs.Select(p => (Pair: p, RateRule: rules.GetRuleFor(p))))
{
var dependentQueries = new List<Task<QueryRateResult>>();
foreach (var requiredExchange in i.RateRule.ExchangeRates)
{
if (!fetchingExchanges.TryGetValue(requiredExchange.Exchange, out var fetching))
{
fetching = _rateProviderFactory.QueryRates(requiredExchange.Exchange);
fetchingExchanges.Add(requiredExchange.Exchange, fetching);
}
dependentQueries.Add(fetching);
}
fetchingRates.Add(i.Pair, GetRuleValue(dependentQueries, i.RateRule));
}
return fetchingRates;
}
private async Task<RateResult> GetRuleValue(List<Task<QueryRateResult>> dependentQueries, RateRule rateRule)
{
var result = new RateResult();
foreach (var queryAsync in dependentQueries)
{
var query = await queryAsync;
result.Latency = query.Latency;
if (query.Exception != null)
result.ExchangeExceptions.Add(query.Exception);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);
return result;
}
}
}

View File

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using ExchangeSharp;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Rates
{
public class RateProviderFactory
{
class WrapperRateProvider : IRateProvider
{
private readonly IRateProvider _inner;
public Exception Exception { get; private set; }
public TimeSpan Latency { get; set; }
public WrapperRateProvider(IRateProvider inner)
{
_inner = inner;
}
public Task<ExchangeRates> GetRatesAsync()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
{
return _inner.GetRatesAsync();
}
catch (Exception ex)
{
Exception = ex;
return Task.FromResult(new ExchangeRates());
}
finally
{
Latency = DateTimeOffset.UtcNow - now;
}
}
}
public class QueryRateResult
{
public TimeSpan Latency { get; set; }
public ExchangeRates ExchangeRates { get; set; }
public ExchangeException Exception { get; internal set; }
}
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
IHttpClientFactory httpClientFactory,
CoinAverageSettings coinAverageSettings)
{
_httpClientFactory = httpClientFactory;
_CoinAverageSettings = coinAverageSettings;
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
}
private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
var cache = new MemoryCache(_CacheOptions);
foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null))
{
provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinAverageRateProvider.CoinAverageName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
c.RefreshRate = CacheSpan;
}
CoinAverageSettings _CoinAverageSettings;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> Providers
{
get
{
return _DirectProviders;
}
}
private void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add("bitpay", new BitpayRateProvider(new NBitpayClient.Bitpay(new NBitcoin.Key(), new Uri("https://bitpay.com/"))));
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient(), Authenticator = _CoinAverageSettings });
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient() });
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gdax", new ExchangeSharpRateProvider("gdax", new ExchangeGdaxAPI()));
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
foreach (var provider in Providers.ToArray())
{
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
prov.RefreshRate = provider.Key == CoinAverageRateProvider.CoinAverageName ? CacheSpan : TimeSpan.FromMinutes(1.0);
Providers[provider.Key] = prov;
}
var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Key))
{
var coinAverage = new CoinAverageRateProvider()
{
Exchange = supportedExchange.Key,
HttpClient = _httpClientFactory?.CreateClient(),
Authenticator = _CoinAverageSettings
};
var cached = new CachedRateProvider(supportedExchange.Key, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Key, cached);
}
}
}
public CoinAverageExchanges GetSupportedExchanges()
{
CoinAverageExchanges exchanges = new CoinAverageExchanges();
foreach (var exchange in _CoinAverageSettings.AvailableExchanges)
{
exchanges.Add(exchange.Value);
}
// Add other exchanges supported here
exchanges.Add(new CoinAverageExchange(CoinAverageRateProvider.CoinAverageName, "Coin Average"));
exchanges.Add(new CoinAverageExchange("cryptopia", "Cryptopia"));
return exchanges;
}
public async Task<QueryRateResult> QueryRates(string exchangeName)
{
Providers.TryGetValue(exchangeName, out var directProvider);
directProvider = directProvider ?? NullRateProvider.Instance;
var wrapper = new WrapperRateProvider(directProvider);
var value = await wrapper.GetRatesAsync();
return new QueryRateResult()
{
Latency = wrapper.Latency,
ExchangeRates = value,
Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
};
}
}
}

View File

@ -83,7 +83,7 @@ namespace BTCPayServer.Services
MultiValueDictionary<Type, TaskCompletionSource<bool>> _Subscriptions = new MultiValueDictionary<Type, TaskCompletionSource<bool>>();
public async Task WaitSettingsChanged<T>(CancellationToken cancellation)
{
var tcs = new TaskCompletionSource<bool>();
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellation.Register(() =>
{
try

View File

@ -1,4 +1,5 @@
@model ListAppsViewModel
@using BTCPayServer.Services.Apps
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
}

View File

@ -13,49 +13,46 @@
</div>
<div class="row">
@if(Model.LNDServices.Count != 0)
{
<div class="col-md-8">
<div class="form-group">
<h5>LND nodes</h5>
<span>You can get access to internal LND services here. For gRPC, only BTC is supported.</span>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
@if(Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-md-8">
<div class="form-group">
<span>You can get access here to LND-gRPC or SSH services exposed by your server</span>
</div>
}
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Crypto</th>
<th>Access Type</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var lnd in Model.LNDServices)
{
<tr>
<td>@lnd.Crypto</td>
<td>@lnd.Type</td>
<td style="text-align:right">
<a asp-action="LNDGRPCServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}
@if(Model.HasSSH)
{
<tr>
<td>None</td>
<td>SSH</td>
<td style="text-align:right">
<a asp-action="SSHService">See information</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@section Scripts {

View File

@ -26,6 +26,8 @@
@* JS *@
<bundle name="wwwroot/bundles/main-bundle.min.js" />
@RenderSection("HeadScripts", required: false)
</head>
<body id="page-top">

View File

@ -3,7 +3,6 @@
ViewBag.ShowMenu = ViewBag.ShowMenu ?? true;
}
<section>
<div class="container">
<div class="row">
@ -16,7 +15,7 @@
<div>
<div class="row">
<div class="col-md-3">
@if(ViewBag.ShowMenu)
@if (ViewBag.ShowMenu)
{
@await Html.PartialAsync("_Nav")
}
@ -28,6 +27,10 @@
</div>
</div>
</section>
@section HeadScripts {
@RenderSection("HeadScripts", required: false)
}
@section Scripts {
@RenderSection("Scripts", required: false)
}

View File

@ -1,8 +1,7 @@
@model DerivationSchemeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = $"{Model.CryptoCode} Derivation scheme";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
ViewData.SetActivePageAndTitle(StoreNavPages.Index, $"{Model.CryptoCode} Derivation scheme");
}
<partial name="_StatusMessage" for="StatusMessage" />

View File

@ -1,8 +1,7 @@
@model LightningNodeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Add lightning node (Experimental)";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Add lightning node (Experimental)");
}
<h4>@ViewData["Title"]</h4>

View File

@ -1,8 +1,7 @@
@model CheckoutExperienceViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Checkout experience";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Checkout);
ViewData.SetActivePageAndTitle(StoreNavPages.Checkout, "Checkout experience");
}
<h4>@ViewData["Title"]</h4>

View File

@ -1,8 +1,7 @@
@model CreateTokenViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Create a new token";
ViewData.AddActivePage(StoreNavPages.Tokens);
ViewData.SetActivePageAndTitle(StoreNavPages.Tokens, "Create a new token");
ViewBag.HidePublicKey = ViewBag.HidePublicKey ?? false;
ViewBag.ShowStores = ViewBag.ShowStores ?? false;
}

View File

@ -1,8 +1,7 @@
@model TokensViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Access Tokens";
ViewData.AddActivePage(StoreNavPages.Tokens);
ViewData.SetActivePageAndTitle(StoreNavPages.Tokens, "Access Tokens");
}
<partial name="_StatusMessage" for="StatusMessage" />

View File

@ -0,0 +1,155 @@
@model PayButtonViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Pay Button");
}
<div class="container" id="payButtonCtrl">
<div class="row">
<div class="col-lg-7">
<div class="form-row">
<div class="form-group col-md-8">
<label>Price</label>
<input name="price" type="text" class="form-control"
v-model="srvModel.price" v-on:change="inputChanges"
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
<small class="text-danger">{{ errors.first('price') }}</small>
</div>
<div class="form-group col-md-4">
<label>&nbsp;</label>
<input name="price" type="text" class="form-control"
v-model="srvModel.currency" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('currency') }">
</div>
</div>
<div class="form-group">
<label>Checkout Description</label>
<input name="checkoutDesc" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.checkoutDesc" v-on:change="inputChanges">
</div>
<div class="form-group">
<label>Order Id</label>
<input name="orderId" type="text" class="form-control" id="inputAddress" placeholder="(optional)"
v-model="srvModel.orderId" v-on:change="inputChanges">
</div>
<div class="form-group">
<label>Pay Button Image Url</label>
<input name="payButtonImageUrl" type="text" class="form-control" id="inputAddress"
v-model="srvModel.payButtonImageUrl" v-on:change="inputChanges"
v-validate="'required|url'" :class="{'is-invalid': errors.has('payButtonImageUrl') }">
<small class="text-danger">{{ errors.first('payButtonImageUrl') }}</small>
</div>
<div class="form-group">
<label>Button Size</label>
<div style="vertical-align:top; font-size:12px; display:flex;">
<button class="btn" style="width:95px;height:40px;margin-right:40px;"
v-on:click="inputChanges($event, 0)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 0) }">
146 x 40 px
</button>
<button class="btn btn-default" style="width:126px;height:46px;margin-right:40px;"
v-on:click="inputChanges($event, 1)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 1) }">
168 x 46 px
</button>
<button class="btn btn-default" style="width:146px;height:57px;"
v-on:click="inputChanges($event, 2)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 2) }">
209 x 57 px
</button>
</div>
</div>
</div>
<div class="col-lg-5">
<br />
Define parameters that define the purchase: price, currency and then optional description of purchase.
<br /><br />
Generated HTML will be displayed at the bottom of this page and all you need is to paste that HTML into your final page.
Clicking on the button will redirect customer to checkout.
</div>
</div>
<hr />
<h3>Payment Notifications</h3>
<br />
<div class="row">
<div class="col-lg-7">
<div class="form-group">
<label>Server IPN</label>
<input name="serverIpn" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.serverIpn" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
</div>
<div class="form-group">
<label>Send Email Notifications to</label>
<input name="notifyEmail" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
v-validate="'email'" :class="{'is-invalid': errors.has('notifyEmail') }">
<small class="text-danger">{{ errors.first('notifyEmail') }}</small>
</div>
<div class="form-group">
<label>Browser Redirect</label>
<input name="browserRedirect" type="text" class="form-control" placeholder="(optional)"
v-model="srvModel.browserRedirect" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('browserRedirect') }">
<small class="text-danger">{{ errors.first('browserRedirect') }}</small>
</div>
</div>
<div class="col-lg-5">
<br />
These parameters allow you to influence process after purchase. <i>Server IPN</i> is location we'll query with details.
We can also deliver email notification to specified addres.
<br /><br />
Finally <i>Browser Redirect</i> defines where BtcPayServer will redirect customer after puchase is completed.
</div>
</div>
<hr />
<h3>Generated code</h3>
<div class="row" v-show="!errors.any()">
<div class="col-lg-9">
<pre><code id="mainCode" class="html"></code></pre>
<button class="btn btn-primary" id="copyCode">
<i class="fa fa-copy"></i> Copy Code
</button>
<span class="copyLabelPopup" style="display:none;">Copied</span>
</div>
<div class="col-lg-3">
<div id="preview"></div>
</div>
</div>
<div class="row" v-show="errors.any()">
<div class="col-lg-12 text-danger">
Please fix errors shown in order for code generation to successfully execute.
</div>
</div>
<br />
</div>
@section HeadScripts {
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script src="~/vendor/vuejs/vue.js"></script>
<script src="~/vendor/vuejs-vee-validate/vee-validate.js"></script>
<script src="~/vendor/clipboard.js/clipboard.js"></script>
<script src="~/paybutton/paybutton.js"></script>
}
@section Scripts {
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
var payButtonCtrl = new Vue({
el: '#payButtonCtrl',
data: {
srvModel: srvModel
},
methods: {
inputChanges: function (event, buttonSize) {
inputChanges(event, buttonSize);
}
}
});
</script>
}

View File

@ -0,0 +1,20 @@
@{
var allErrors = ViewData.ModelState.Values.SelectMany(v => v.Errors.Select(b => b.ErrorMessage));
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12">
<h2>Pay Button request failed</h2>
Please fix following errors:
<ul>
@foreach (var error in allErrors)
{
<li>@error</li>
}
</ul>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,14 @@

<section>
<div class="container" id="payButtonCtrl">
<div class="row">
<form method="POST" action="http://127.0.0.1:14142/stores/4b1AnMtMNuxwG7io2dUcB3TrT3yUbgajjGfJjVfdhBDu/pay">
<input type="hidden" name="price" value="10" />
<input type="hidden" name="currency" value="USD" />
<input type="image" src="http://127.0.0.1:14142/img/paybutton/pay.png" name="submit" style="width:209px" alt="Pay with BtcPay, Self-Hosted Bitcoin Payment Processor">
</form>
</div>
</div>
</section>

View File

@ -1,8 +1,7 @@
@model BTCPayServer.Models.StoreViewModels.RatesViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Rates";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Rates);
ViewData.SetActivePageAndTitle(StoreNavPages.Rates, "Rates");
}
<h4>@ViewData["Title"]</h4>

View File

@ -7,28 +7,8 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace BTCPayServer.Views.Stores
{
public static class StoreNavPages
public enum StoreNavPages
{
public static string ActivePageKey => "ActivePage";
public static string Index => "Index";
public static string Rates => "Rates";
public static string Checkout => "Checkout experience";
public static string Tokens => "Tokens";
public static string Users => "Users";
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string CheckoutNavClass(ViewContext viewContext) => PageNavClass(viewContext, Checkout);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
public static string RatesNavClass(ViewContext viewContext) => PageNavClass(viewContext, Rates);
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;
ActivePage, Index, Rates, Checkout, Tokens, Users, PayButton
}
}

View File

@ -1,8 +1,7 @@
@model StoreUsersViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage users";
ViewData.AddActivePage(StoreNavPages.Users);
ViewData.SetActivePageAndTitle(StoreNavPages.Users, "Manage users");
}
<h4>@ViewData["Title"]</h4>

View File

@ -1,12 +1,9 @@
@model StoreViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Profile";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Profile");
}
<style type="text/css">
.smMaxWidth {
max-width: 150px;

View File

@ -1,11 +1,9 @@
@using BTCPayServer.Views.Stores
@inject SignInManager<ApplicationUser> SignInManager
<div class="nav flex-column nav-pills">
<a class="nav-link @StoreNavPages.IndexNavClass(ViewContext)" asp-action="UpdateStore">General settings</a>
<a class="nav-link @StoreNavPages.RatesNavClass(ViewContext)" asp-action="Rates">Rates</a>
<a class="nav-link @StoreNavPages.CheckoutNavClass(ViewContext)" asp-action="CheckoutExperience">Checkout experience</a>
<a class="nav-link @StoreNavPages.TokensNavClass(ViewContext)" asp-action="ListTokens">Access Tokens</a>
<a class="nav-link @StoreNavPages.UsersNavClass(ViewContext)" asp-action="StoreUsers">Users</a>
<div class="nav flex-column nav-pills">
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Index)" asp-action="UpdateStore">General settings</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Rates)" asp-action="Rates">Rates</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Checkout)" asp-action="CheckoutExperience">Checkout experience</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Tokens)" asp-action="ListTokens">Access Tokens</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.Users)" asp-action="StoreUsers">Users</a>
<a class="nav-link @ViewData.IsActivePage(StoreNavPages.PayButton)" asp-action="PayButton">Pay Button</a>
</div>

View File

@ -1 +1,2 @@
@using BTCPayServer.Views.Stores
@using BTCPayServer.Models.StoreViewModels

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,94 @@
$(function () {
inputChanges();
// Clipboard Copy
new Clipboard('#copyCode', {
text: function (trigger) {
$(".copyLabelPopup").show().delay(1000).fadeOut(500);
return inputChanges();
}
});
});
function esc(input) {
return ('' + input) /* Forces the conversion to string. */
.replace(/&/g, '&amp;') /* This MUST be the 1st replacement. */
.replace(/'/g, '&apos;') /* The 4 other predefined entities, required. */
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
/*
You may add other replacements here for HTML only
(but it's not necessary).
Or for XML, only if the named entities are defined in its DTD.
*/
;
}
Vue.use(VeeValidate);
const dictionary = {
en: {
attributes: {
price: 'Price', checkoutDesc: 'Checkout Description', orderId: 'Order Id',
serverIpn: 'Server IPN', notifyEmail: 'Send Email Notifications', browserRedirect: 'Browser Redirect',
payButtonImageUrl: "Pay Button Image Url"
}
}
};
VeeValidate.Validator.localize(dictionary);
function inputChanges(event, buttonSize) {
if (buttonSize !== null && buttonSize !== undefined) {
srvModel.buttonSize = buttonSize;
}
var html = '<form method="POST" action="' + esc(srvModel.urlRoot + 'stores/'+ srvModel.storeId + '/pay') + '">';
html += addinput("price", srvModel.price);
if (srvModel.currency) {
html += addinput("currency", srvModel.currency);
}
if (srvModel.checkoutDesc) {
html += addinput("checkoutDesc", srvModel.checkoutDesc);
}
if (srvModel.orderId) {
html += addinput("orderId", srvModel.orderId);
}
if (srvModel.serverIpn) {
html += addinput("serverIpn", srvModel.serverIpn);
}
if (srvModel.browserRedirect) {
html += addinput("browserRedirect", srvModel.browserRedirect);
}
if (srvModel.notifyEmail) {
html += addinput("notifyEmail", srvModel.notifyEmail);
}
var width = "209px";
if (srvModel.buttonSize === 0) {
width = "146px";
} else if (srvModel.buttonSize === 1) {
width = "168px";
} else if (srvModel.buttonSize === 2) {
width = "209px";
}
html += '\n <input type="image" src="' + esc(srvModel.payButtonImageUrl) + '" name="submit" style="width:' + width +
'" alt="Pay with BtcPay, Self-Hosted Bitcoin Payment Processor">';
html += '\n</form>';
$("#mainCode").text(html).html();
$("#preview").html(html);
$('pre code').each(function (i, block) {
hljs.highlightBlock(block);
});
return html;
}
function addinput(name, value) {
var html = '\n <input type="hidden" name="' + esc(name) + '" value="' + esc(value) + '" />';
return html;
}

File diff suppressed because it is too large Load Diff