Compare commits

...

95 Commits

Author SHA1 Message Date
07d0b98a23 Update language 2019-01-31 19:33:07 +09:00
c7c0f01010 bump 2019-01-31 19:24:36 +09:00
cf6b17250a Can set lightning network as default payment method (close #290) 2019-01-31 19:07:38 +09:00
90503a490c Add dots to make derivation examples clearer (#561) 2019-01-31 17:00:15 +09:00
ebdd53b99b fix unfairly long dropdown in ledger account selection (#574)
Closes #570
2019-01-31 16:56:39 +09:00
51a5d2e812 Refactor XFrames Attribute & simplify pos settings page (#576)
* Enable better error when invoice cannot be created on crowdfund

Closes #572

* Allow all public apps in iframe

* cleanup pos page dev info
2019-01-31 16:56:21 +09:00
2ad509d56a Update Readme.md (#577)
* Update readme

* Update README.md

* add apps link

* fix broken link
2019-01-31 16:55:27 +09:00
1a98bfba36 Fix formatting of currencies in Invoice detail page 2019-01-30 19:18:44 +09:00
d05bb6c60e Properly format currencies in Invoice list 2019-01-30 19:01:18 +09:00
ed81b6a6aa bump 2019-01-30 15:52:31 +09:00
264914588f fix bitpay API not having CORS 2019-01-30 14:57:10 +09:00
05df43b426 fix bitpay API not having CORS 2019-01-30 14:36:26 +09:00
0334a4e176 bump 2019-01-30 13:46:55 +09:00
38dca425da Fix repetitive IPN for lightning network payments (https://github.com/btcpayserver/btcpayserver/issues/564) 2019-01-30 13:40:08 +09:00
82d4a79dd4 Fix potential crash if the current host is an IP instead of DNS name, might fix https://github.com/btcpayserver/btcpayserver/issues/543 2019-01-30 12:52:34 +09:00
6725be8145 Remove warnings 2019-01-29 18:35:27 +09:00
f5b693f01b Disable quadricagx tests because exchange is down 2019-01-29 18:34:30 +09:00
f09f23e570 Enable better error when invoice cannot be created on crowdfund (#575)
Closes #572
2019-01-29 18:32:44 +09:00
4f4d05b8cd Make sure CORS is enabled on Bitpay's API 2019-01-29 18:20:53 +09:00
0c5b5ff49c Add link to no stores error (#558)
* add link

* safer status message

* refactor

* small view cleanup
2019-01-29 16:44:46 +09:00
a815fad3f1 Put back the list of ledger accounts to 5. 2019-01-29 13:06:43 +09:00
d8b1c7c10a Fix broken lightning payments on Checkout page 2019-01-28 18:50:26 +09:00
02e1aea80c add warning for third parties (#562)
* add warning for third parties

* Update UpdateCoinSwitchSettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateChangellySettings.cshtml

* Update UpdateCoinSwitchSettings.cshtml
2019-01-28 17:40:23 +09:00
1892f7e0f4 rename field 2019-01-28 17:10:51 +09:00
b7b50349a7 Convert Ledger account list to dropdown and add more accounts to list (#560) 2019-01-28 17:07:01 +09:00
02d227ee02 Fix connection to checkout backend (bad links) 2019-01-28 16:24:11 +09:00
47f8938b89 Catch websocket connection issues 2019-01-28 15:12:40 +09:00
4945a640a7 Use PaymentHash of a lightning payment as PaymentId 2019-01-27 13:06:55 +09:00
0136977359 update translations 2019-01-26 21:23:41 +09:00
0acd3e20b0 bump 2019-01-26 20:58:15 +09:00
30bdfeee37 Enhance PosData Viewer & add cart to posdata in POS app (#559) 2019-01-26 13:26:49 +09:00
7ea665d884 Merge pull request #557 from Kukks/master
Fix close invoice button for modal invoices #555
2019-01-25 20:48:45 +09:00
073edcfb12 Merge remote-tracking branch 'btcpayserver/master' 2019-01-25 12:41:20 +01:00
a645366a25 Fix close invoice button for modal invoices #555 2019-01-25 12:41:15 +01:00
12aa0b7abd Merge pull request #556 from ChekaZ/master
Support Bitcoinplus
2019-01-25 16:09:39 +09:00
3f98a50410 Support Bitcoinplus 2019-01-25 01:03:04 +01:00
24c8c076d5 Add taxIncluded field in invoice 2019-01-24 20:53:29 +09:00
37e6931d33 Improve help 2019-01-23 17:44:03 +09:00
86493568e9 Fix external services parsing 2019-01-23 13:31:00 +09:00
bb51436ae3 Accept absolute url for external services 2019-01-23 13:17:36 +09:00
854a55ac1a Merge branch 'store-level-email' 2019-01-22 21:39:55 +09:00
cfb4b080d3 Emails on store level 2019-01-22 21:38:39 +09:00
00aa2e4e17 Merge pull request #546 from britttttk/fix/message
Fix delete user message
2019-01-21 17:11:24 +09:00
69c67d99f6 Fix message for delete user 2019-01-20 21:19:01 -07:00
65596ec8c1 fix delete user message 2019-01-20 21:12:20 -07:00
49643cb00e Make CanScheduleBackgroundTasks more robust 2019-01-19 21:19:15 +09:00
35b0faee57 Merge pull request #541 from Horndev/patch-2
Improve exception messages in server configuration parsing.
2019-01-19 20:49:08 +09:00
88ef4d69b2 Improve help and exception messages.
Improve the messages passed to users and in exceptions when parsing the BTCPayServerOptions configuration.
2019-01-18 10:23:47 -04:00
575b6ca222 Improve error messages when the store has no payment method configured 2019-01-18 19:15:31 +09:00
b5a0e844d2 Cann GetInvoicesTotal in parallel 2019-01-17 23:40:47 +09:00
2642e11ce2 Fix amount format in wallet send 2019-01-17 23:37:39 +09:00
b4fe655efe Merge pull request #534 from sipsorcery/fixpaging
Small improvement to the paging buttons on the list invoices page
2019-01-17 11:14:31 +09:00
ffb761909a Merge pull request #535 from dalijolijo/master
Change default exchange for Bitcore
2019-01-17 11:10:55 +09:00
b443e1ac6e Change default exchange for Bitcore 2019-01-16 22:04:24 +00:00
a4792f54a7 Added bootstrap paging buttons to the invoice list page and fixed paging buttons. 2019-01-16 21:33:04 +01:00
686ae029e0 bump 2019-01-16 23:49:40 +09:00
f3fd2e7d0f Update translations 2019-01-16 23:08:48 +09:00
7efd9ba0a5 Fix ledger on firefox 2019-01-16 23:07:22 +09:00
1c2a6bb8a1 Delete unused code 2019-01-16 19:35:29 +09:00
7bcf1cbdd5 Remove references to hangfire 2019-01-16 19:30:03 +09:00
2aaa2544bd Do not send mail synchronously in InvoiceNotificationManager 2019-01-16 19:21:02 +09:00
d85f03ba20 Remove HangFire dependency 2019-01-16 19:15:09 +09:00
cfb51a6be4 Merge pull request #531 from Kukks/patch-2
fix merge bug
2019-01-16 15:58:29 +09:00
c9d778c94b Bump nbitcoin and nbxplorer 2019-01-16 15:16:41 +09:00
fd62f882de fix merge bug 2019-01-15 18:18:41 +01:00
adc050f190 Trim destination address 2019-01-16 01:19:37 +09:00
2d551b9fc5 bump 2019-01-16 00:13:03 +09:00
884acdde32 Disabled POLIS and Bitcoin because default exchange (cryptopedia) is down 2019-01-16 00:08:17 +09:00
8f896de794 Merge pull request #516 from Kukks/feature/crowdfund
Crowdfund Bug fixes
2019-01-15 23:57:29 +09:00
5e4e26d2fd Merge pull request #529 from Kukks/bugfix/app-proper-redirect
fix app redirect to app instead of root url
2019-01-15 23:54:54 +09:00
ae688e6615 Merge pull request #530 from Horndev/patch-1
Grammatical corrections in exception messages
2019-01-15 23:54:20 +09:00
c4c812bdf6 Remove cryptopia from directly queried exchanges 2019-01-15 23:53:32 +09:00
e620fc0283 Add expert mode to BTCPay with No Change UTXO option 2019-01-15 23:50:45 +09:00
c333902468 Round up invoice price 2019-01-15 22:12:29 +09:00
4c83ecd06a Remove unused code 2019-01-15 21:56:33 +09:00
b28a547dc4 Grammatical corrections in exception messages
Fixed a few grammatical errors in LightningLikePaymentHandler.
2019-01-15 12:21:31 +00:00
6bc17e05bd add ids for better styling possibilities 2019-01-15 13:12:19 +01:00
0903350d30 add more log 2019-01-15 12:12:17 +01:00
6c0f19b457 Merge branch 'master' into feature/crowdfund 2019-01-15 09:48:16 +01:00
e119dc823f fix app redirect to app instead of root url 2019-01-15 09:46:07 +01:00
43295c9c57 Merge pull request #528 from bolatovumar/master
Update .NET Core SDK version in documentation
2019-01-15 16:42:40 +09:00
ded8b54042 Merge pull request #524 from Kukks/escapedstorename
Fix Store name character escaping on paid invoice #522
2019-01-15 16:41:51 +09:00
50a3178d51 Update .NET Core SDK version in documentation
Address #523
2019-01-14 16:49:12 -08:00
393c226032 fix escaped store name in return 2019-01-14 09:32:22 +01:00
f2630df387 dispose streamer properly 2019-01-14 08:21:27 +01:00
abcd2c1750 add padding when disqus enabled 2019-01-14 08:01:07 +01:00
cc95f3b5b5 fix exponent numbers in contribution amounts 2019-01-14 07:53:03 +01:00
a08ee93b43 fix issue with perk ordering 2019-01-14 07:45:21 +01:00
4b90f873d5 Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund 2019-01-11 10:52:31 +01:00
419ab8e0b1 add loader and fix perk badge zindex 2019-01-11 10:52:21 +01:00
c95ef27998 bump 2019-01-11 00:17:58 +09:00
63dfd93834 Merge pull request #511 from Kukks/feature/crowdfund
crowdfund: change text
2019-01-11 00:17:37 +09:00
57610881de change text 2019-01-10 16:12:00 +01:00
7469faf296 Merge pull request #510 from Kukks/feature/crowdfund
fix notif number
2019-01-10 23:58:57 +09:00
55a884a559 fix js number 2019-01-10 15:57:30 +01:00
114 changed files with 1979 additions and 802 deletions

View File

@ -5,6 +5,7 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Crowdfund;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Hubs;

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Tests
{
public class MockDelay : IDelay
{
class WaitObj
{
public DateTimeOffset Expiration;
public TaskCompletionSource<bool> CTS;
}
List<WaitObj> waits = new List<WaitObj>();
DateTimeOffset _Now = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public async Task Wait(TimeSpan delay, CancellationToken cancellation)
{
WaitObj w = new WaitObj();
w.Expiration = _Now + delay;
w.CTS = new TaskCompletionSource<bool>();
using (cancellation.Register(() =>
{
w.CTS.TrySetCanceled();
}))
{
lock (waits)
{
waits.Add(w);
}
await w.CTS.Task;
}
}
public void Advance(TimeSpan time)
{
_Now += time;
lock (waits)
{
foreach (var wait in waits.ToArray())
{
if (_Now >= wait.Expiration)
{
wait.CTS.TrySetResult(true);
waits.Remove(wait);
}
}
}
}
public void AdvanceMilliseconds(long milli)
{
Advance(TimeSpan.FromMilliseconds(milli));
}
public override string ToString()
{
return _Now.Millisecond.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -1549,23 +1549,96 @@ donation:
}
}
[Fact]
[Trait("Fast", "Fast")]
public void CanScheduleBackgroundTasks()
{
BackgroundJobClient client = new BackgroundJobClient();
MockDelay mockDelay = new MockDelay();
client.Delay = mockDelay;
bool[] jobs = new bool[4];
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Logs.Tester.LogInformation("Start Job[0] in 5 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[0]"); jobs[0] = true; }, TimeSpan.FromSeconds(5.0));
Logs.Tester.LogInformation("Start Job[1] in 2 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[1]"); jobs[1] = true; }, TimeSpan.FromSeconds(2.0));
Logs.Tester.LogInformation("Start Job[2] fails in 6 sec");
client.Schedule(async () => { jobs[2] = true; throw new Exception("Job[2]"); }, TimeSpan.FromSeconds(6.0));
Logs.Tester.LogInformation("Start Job[3] starts in in 7 sec");
client.Schedule(async () => { Logs.Tester.LogInformation("Job[3]"); jobs[3] = true; }, TimeSpan.FromSeconds(7.0));
Assert.True(new[] { false, false, false, false }.SequenceEqual(jobs));
CancellationTokenSource cts = new CancellationTokenSource();
var processing = client.ProcessJobs(cts.Token);
Assert.Equal(4, client.GetExecutingCount());
var waitJobsFinish = client.WaitAllRunning(default);
mockDelay.Advance(TimeSpan.FromSeconds(2.0));
Assert.True(new[] { false, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(3.0));
Assert.True(new[] { true, true, false, false }.SequenceEqual(jobs));
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, false }.SequenceEqual(jobs));
Assert.Equal(1, client.GetExecutingCount());
Assert.False(waitJobsFinish.Wait(100));
Assert.False(waitJobsFinish.IsCompletedSuccessfully);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Assert.True(new[] { true, true, true, true }.SequenceEqual(jobs));
Assert.True(waitJobsFinish.Wait(100));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.Equal(0, client.GetExecutingCount());
bool jobExecuted = false;
Logs.Tester.LogInformation("This job will be cancelled");
client.Schedule(async () => { jobExecuted = true; }, TimeSpan.FromSeconds(1.0));
mockDelay.Advance(TimeSpan.FromSeconds(0.5));
Assert.False(jobExecuted);
Thread.Sleep(100);
Assert.Equal(1, client.GetExecutingCount());
waitJobsFinish = client.WaitAllRunning(default);
Assert.False(waitJobsFinish.Wait(100));
cts.Cancel();
Assert.True(waitJobsFinish.Wait(1000));
Assert.True(waitJobsFinish.IsCompletedSuccessfully);
Assert.True(!waitJobsFinish.IsFaulted);
Assert.False(jobExecuted);
mockDelay.Advance(TimeSpan.FromSeconds(1.0));
Thread.Sleep(100); // Make sure it get cancelled
Assert.False(jobExecuted);
Assert.Equal(0, client.GetExecutingCount());
Assert.True(processing.IsCanceled);
Assert.True(client.WaitAllRunning(default).Wait(100));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
}
[Fact]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
testCases.ForEach(tuple =>
@ -1588,18 +1661,16 @@ donation:
var controller = tester.PayTester.GetController<InvoiceController>(null);
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
new List<(string input, Dictionary<string, object> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
{ (null, new Dictionary<string, object>())},
{("", new Dictionary<string, object>())},
{("{}", new Dictionary<string, object>())},
{("non-json-content", new Dictionary<string, object>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, object>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, object>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, object>(){ {"key", "True"}})},
{("{ invalidjson file here}", new Dictionary<string, object>(){ {String.Empty, "{ invalidjson file here}"}})}
};
var tasks = new List<Task>();
@ -1856,6 +1927,50 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateStrangeInvoice()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000012m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.00000001m, invoice1.Price);
Assert.Equal(0.00000002m, invoice2.Price);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = -0.1m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.0m, invoice.Price);
}
}
[Fact]
[Trait("Integration", "Integration")]
public void InvoiceFlowThroughDifferentStatesCorrectly()
@ -1869,6 +1984,7 @@ donation:
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0m,
TaxIncluded = 1000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
@ -1898,6 +2014,8 @@ donation:
});
invoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
Assert.Equal(1000.0m, invoice.TaxIncluded);
Assert.Equal(5000.0m, invoice.Price);
Assert.Equal(Money.Coins(0), invoice.BtcPaid);
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
@ -2020,19 +2138,20 @@ donation:
}
}
[Fact]
[Trait("Integration", "Integration")]
public void CheckQuadrigacxRateProvider()
{
var quadri = new QuadrigacxRateProvider();
var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
Assert.NotEmpty(rates);
Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
}
//[Fact]
//[Trait("Integration", "Integration")]
// 29 january, the exchange is down
//public void CheckQuadrigacxRateProvider()
//{
// var quadri = new QuadrigacxRateProvider();
// var rates = quadri.GetRatesAsync().GetAwaiter().GetResult();
// Assert.NotEmpty(rates);
// Assert.NotEqual(0.0m, rates.First().BidAsk.Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_CAD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("BTC_USD")).Bid);
// Assert.NotEqual(0.0m, rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_CAD")).Bid);
// Assert.Null(rates.GetRate(QuadrigacxRateProvider.QuadrigacxName, CurrencyPair.Parse("LTC_USD")));
//}
[Fact]
[Trait("Integration", "Integration")]
@ -2046,6 +2165,9 @@ donation:
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
result.Fetcher.InvalidateCache();
@ -2194,6 +2316,42 @@ donation:
Assert.Throws<InvalidOperationException>(() => fetch.GetRatesAsync().GetAwaiter().GetResult());
}
[Fact]
[Trait("Fast", "Fast")]
public void CheckParseStatusMessageModel()
{
var legacyStatus = "Error: some bad shit happened";
var parsed = new StatusMessageModel(legacyStatus);
Assert.Equal(legacyStatus, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Error, parsed.Severity);
var legacyStatus2 = "Some normal shit happened";
parsed = new StatusMessageModel(legacyStatus2);
Assert.Equal(legacyStatus2, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
var newStatus = new StatusMessageModel()
{
Html = "<a href='xxx'>something new</a>",
Severity = StatusMessageModel.StatusSeverity.Info
};
parsed = new StatusMessageModel(newStatus.ToString());
Assert.Null(parsed.Message);
Assert.Equal(newStatus.Html, parsed.Html);
Assert.Equal(StatusMessageModel.StatusSeverity.Info, parsed.Severity);
var newStatus2 = new StatusMessageModel()
{
Message = "something new",
Severity = StatusMessageModel.StatusSeverity.Success
};
parsed = new StatusMessageModel(newStatus2.ToString());
Assert.Null(parsed.Html);
Assert.Equal(newStatus2.Message, parsed.Message);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, parsed.Severity);
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:2.0.0.2
image: nicolasdorier/nbxplorer:2.0.0.8
restart: unless-stopped
ports:
- "32838:32838"

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoinplus()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("XBC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Bitcoinplus",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://chainz.cryptoid.info/xbc/tx.dws?{0}" : "https://chainz.cryptoid.info/xbc/tx.dws?{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoinplus",
DefaultRateRules = new[]
{
"XBC_X = XBC_BTC * BTC_X",
"XBC_BTC = cryptopia(XBC_BTC)"
},
CryptoImagePath = "imlegacy/bitcoinplus.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("65'") : new KeyPath("1'")
});
}
}
}

View File

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"BTX_X = BTX_BTC * BTC_X",
"BTX_BTC = cryptopia(BTX_BTC)"
"BTX_BTC = hitbtc(BTX_BTC)"
},
CryptoImagePath = "imlegacy/bitcore.svg",
LightningImagePath = "imlegacy/bitcore-lightning.svg",

View File

@ -52,10 +52,13 @@ namespace BTCPayServer
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();
InitViacoin();
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.41</Version>
<Version>1.0.3.53</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,11 +33,9 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.4" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="Hangfire" Version="1.6.20" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
<PackageReference Include="LedgerWallet" Version="2.0.0.3" />
<PackageReference Include="Meziantou.AspNetCore.BundleTagHelpers" Version="2.0.0" />
@ -47,10 +45,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="NBitcoin" Version="4.1.1.73" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="NBitcoin" Version="4.1.1.78" />
<PackageReference Include="NBitpayClient" Version="1.0.0.31" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.1" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0.3" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />

View File

@ -110,7 +110,7 @@ namespace BTCPayServer.Configuration
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
@ -118,7 +118,7 @@ namespace BTCPayServer.Configuration
}
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning will work but use an deprecated format, please replace it by '{connectionString.ToString()}'");
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
@ -177,10 +177,11 @@ namespace BTCPayServer.Configuration
var services = conf.GetOrDefault<string>("externalservices", null);
if (services != null)
{
foreach (var service in services.Split(new[] { ';', ',' })
.Select(p => p.Split(':'))
.Where(p => p.Length == 2)
.Select(p => (Name: p[0], Link: p[1])))
foreach (var service in services.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => (p, SeparatorIndex: p.IndexOf(':', StringComparison.OrdinalIgnoreCase)))
.Where(p => p.SeparatorIndex != -1)
.Select(p => (Name: p.p.Substring(0, p.SeparatorIndex),
Link: p.p.Substring(p.SeparatorIndex + 1))))
{
ExternalServices.AddOrReplace(service.Name, service.Link);
}
@ -235,7 +236,7 @@ namespace BTCPayServer.Configuration
RootPath = "/" + RootPath;
var old = conf.GetOrDefault<Uri>("internallightningnode", null);
if (old != null)
throw new ConfigException($"internallightningnode should not be used anymore, use btclightning instead");
throw new ConfigException($"internallightningnode is deprecated and should not be used anymore, use btclightning instead");
LogFile = GetDebugLog(conf);
if (!string.IsNullOrEmpty(LogFile))

View File

@ -2,6 +2,7 @@
using BTCPayServer.Filters;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using NBitcoin.DataEncoders;
using NBitpayClient;
@ -14,6 +15,7 @@ namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[BitpayAPIConstraint(true)]
[EnableCors(CorsPolicies.All)]
public class AccessTokenController : Controller
{
TokenRepository _TokenRepository;

View File

@ -28,7 +28,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
@ -40,14 +40,14 @@ namespace BTCPayServer.Controllers
RoleManager<IdentityRole> roleManager,
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
SettingsRepository settingsRepository,
Configuration.BTCPayServerOptions options)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_Options = options;
@ -286,7 +286,8 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
RegisteredUserId = user.Id;
await _emailSender.SendEmailConfirmationAsync(model.Email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(model.Email, callbackUrl);
if (!policies.RequiresConfirmedEmail)
{
if(logon)
@ -446,8 +447,9 @@ namespace BTCPayServer.Controllers
// visit https://go.microsoft.com/fwlink/?LinkID=532713
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme);
await _emailSender.SendEmailAsync(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
_EmailSenderFactory.GetEmailSender().SendEmail(model.Email, "Reset Password",
$"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
return RedirectToAction(nameof(ForgotPasswordConfirmation));
}

View File

@ -85,6 +85,7 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
Id = appId,
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,

View File

@ -73,7 +73,12 @@ namespace BTCPayServer.Controllers
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must have created at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var vm = new CreateAppViewModel();
@ -88,7 +93,12 @@ namespace BTCPayServer.Controllers
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
if (stores.Length == 0)
{
StatusMessage = "Error: You must own at least one store";
StatusMessage = new StatusMessageModel()
{
Html =
$"Error: You must have created at least one store. <a href='{(Url.Action("CreateStore", "UserStores"))}'>Create store</a>",
Severity = StatusMessageModel.StatusSeverity.Error
}.ToString();
return RedirectToAction(nameof(ListApps));
}
var selectedStore = vm.SelectedStore;

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Crowdfund;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Hubs;
@ -19,10 +20,12 @@ using BTCPayServer.Services.Rates;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using YamlDotNet.RepresentationModel;
using static BTCPayServer.Controllers.AppsController;
@ -47,7 +50,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewPointOfSale(string appId)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
@ -88,7 +91,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
@ -117,7 +120,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(null)]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request)
@ -153,10 +156,11 @@ namespace BTCPayServer.Controllers
var store = await _AppsHelper.GetStore(app);
var title = settings.Title;
var price = request.Amount;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
var choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
@ -172,35 +176,44 @@ namespace BTCPayServer.Controllers
}
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
try
{
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl,
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
{
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
}, store, HttpContext.Request.GetAbsoluteRoot());
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}, store, HttpContext.Request.GetAbsoluteRoot());
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice",
new {invoiceId = invoice.Data.Id});
}
else
{
return Ok(invoice.Data.Id);
}
}
else
catch (BitpayHttpException e)
{
return Ok(invoice.Data.Id);
return BadRequest(e.Message);
}
}
[HttpPost]
[Route("/apps/{appId}/pos")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
public async Task<IActionResult> ViewPointOfSale(string appId,
@ -209,7 +222,8 @@ namespace BTCPayServer.Controllers
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey)
string choiceKey,
string posData = null)
{
var app = await _AppsHelper.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
@ -225,10 +239,11 @@ namespace BTCPayServer.Controllers
}
string title = null;
var price = 0.0m;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppsHelper.Parse(settings.Template, settings.Currency);
var choice = choices.FirstOrDefault(c => c.Id == choiceKey);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
@ -247,15 +262,16 @@ namespace BTCPayServer.Controllers
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
var invoice = await _InvoiceController.CreateInvoiceCore(new NBitpayClient.Invoice()
{
ItemCode = choiceKey ?? string.Empty,
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId,
NotificationURL = notificationUrl,
RedirectURL = redirectUrl,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData
}, store, HttpContext.Request.GetAbsoluteRoot());
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}

View File

@ -12,8 +12,8 @@ using NBitpayClient;
namespace BTCPayServer.Controllers
{
[EnableCors("BitpayAPI")]
[BitpayAPIConstraint]
[EnableCors(CorsPolicies.All)]
[Authorize(Policies.CanCreateInvoice.Key, AuthenticationSchemes = Policies.BitpayAuthentication)]
public class InvoiceControllerAPI : Controller
{

View File

@ -21,6 +21,7 @@ using BTCPayServer.Services.Invoices.Export;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
@ -64,6 +65,7 @@ namespace BTCPayServer.Controllers
OrderId = invoice.OrderId,
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(dto.Price, dto.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.TaxIncluded, dto.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
@ -80,9 +82,9 @@ namespace BTCPayServer.Controllers
var paymentMethodId = data.GetId();
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.PaymentMethod = ToString(paymentMethodId);
cryptoPayment.Due = $"{accounting.Due} {paymentMethodId.CryptoCode}";
cryptoPayment.Paid = $"{accounting.CryptoPaid} {paymentMethodId.CryptoCode}";
cryptoPayment.Overpaid = $"{accounting.OverpaidHelper} {paymentMethodId.CryptoCode}";
cryptoPayment.Due = _CurrencyNameTable.DisplayFormatCurrency(accounting.Due.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if (onchainMethod != null)
@ -188,7 +190,7 @@ namespace BTCPayServer.Controllers
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
@ -211,31 +213,29 @@ namespace BTCPayServer.Controllers
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
if (invoice == null)
return null;
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
bool isDefaultPaymentId = false;
if (paymentMethodId == null)
{
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider);
isDefaultPaymentId = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (network == null && isDefaultCrypto)
if (network == null && isDefaultPaymentId)
{
network = _NetworkProvider.GetAll().FirstOrDefault();
paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
paymentMethodIdStr = paymentMethodId.ToString();
}
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if (!isDefaultCrypto)
if (!isDefaultPaymentId)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider)
.Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode)
@ -244,7 +244,6 @@ namespace BTCPayServer.Controllers
paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
paymentMethodIdStr = paymentMethodId.ToString();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
@ -278,7 +277,6 @@ namespace BTCPayServer.Controllers
PaymentMethodName = GetDisplayName(paymentMethodId, network),
CryptoImage = GetImage(paymentMethodId, network),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en",
@ -369,9 +367,12 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/{invoiceId}/status")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
var model = await GetInvoiceModel(invoiceId, paymentMethodId == null ? null : PaymentMethodId.Parse(paymentMethodId));
if (model == null)
return NotFound();
return Json(model);
@ -379,6 +380,10 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
[Route("i/{invoiceId}/{paymentMethodId}/status/ws")]
[Route("invoice/{invoiceId}/status/ws")]
[Route("invoice/{invoiceId}/{paymentMethodId}/status")]
[Route("invoice/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
@ -424,6 +429,7 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")]
[Route("invoice/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)
{
if (!ModelState.IsValid)
@ -447,8 +453,12 @@ namespace BTCPayServer.Controllers
Count = count,
StatusMessage = StatusMessage
};
var list = await ListInvoicesProcess(searchTerm, skip, count);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
var counting = _InvoiceRepository.GetInvoicesTotal(invoiceQuery);
invoiceQuery.Count = count;
invoiceQuery.Skip = skip;
var list = await _InvoiceRepository.GetInvoices(invoiceQuery);
foreach (var invoice in list)
{
var state = invoice.GetInvoiceState();
@ -460,22 +470,21 @@ namespace BTCPayServer.Controllers
InvoiceId = invoice.Id,
OrderId = invoice.OrderId ?? string.Empty,
RedirectUrl = invoice.RedirectURL ?? string.Empty,
AmountCurrency = $"{invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture)} {invoice.ProductInformation.Currency}",
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.ProductInformation.Price, invoice.ProductInformation.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete()
});
}
model.Total = await counting;
return View(model);
}
private async Task<InvoiceEntity[]> ListInvoicesProcess(string searchTerm = null, int skip = 0, int count = 50)
private InvoiceQuery GetInvoiceQuery(string searchTerm = null)
{
var filterString = new SearchString(searchTerm);
var list = await _InvoiceRepository.GetInvoices(new InvoiceQuery()
var invoiceQuery = new InvoiceQuery()
{
TextSearch = filterString.TextSearch,
Count = count,
Skip = skip,
UserId = GetUserId(),
Unusual = !filterString.Filters.ContainsKey("unusual") ? null
: !bool.TryParse(filterString.Filters["unusual"].First(), out var r) ? (bool?)null
@ -485,9 +494,8 @@ namespace BTCPayServer.Controllers
StoreId = filterString.Filters.ContainsKey("storeid") ? filterString.Filters["storeid"].ToArray() : null,
ItemCode = filterString.Filters.ContainsKey("itemcode") ? filterString.Filters["itemcode"].ToArray() : null,
OrderId = filterString.Filters.ContainsKey("orderid") ? filterString.Filters["orderid"].ToArray() : null
});
return list;
};
return invoiceQuery;
}
[HttpGet]
@ -497,7 +505,10 @@ namespace BTCPayServer.Controllers
{
var model = new InvoiceExport(_NetworkProvider, _CurrencyNameTable);
var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue);
InvoiceQuery invoiceQuery = GetInvoiceQuery(searchTerm);
invoiceQuery.Count = int.MaxValue;
invoiceQuery.Skip = 0;
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
var res = model.Process(invoices, format);
var cd = new ContentDisposition
@ -675,9 +686,9 @@ namespace BTCPayServer.Controllers
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
public static Dictionary<string, object> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
var result = new Dictionary<string,object>();
if (string.IsNullOrEmpty(posData))
{
return result;
@ -685,7 +696,6 @@ namespace BTCPayServer.Controllers
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
@ -693,7 +703,14 @@ namespace BTCPayServer.Controllers
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count(); i++)
{
result.Add($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.Add(item.Key, ParsePosData(item.Value.ToString()));
break;
default:
result.Add(item.Key, item.Value.ToString());

View File

@ -71,7 +71,6 @@ namespace BTCPayServer.Controllers
{
InvoiceTime = DateTimeOffset.UtcNow
};
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
@ -95,7 +94,20 @@ namespace BTCPayServer.Controllers
throw new BitpayHttpException(400, "Invalid email");
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded);
invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price);
entity.ProductInformation = Map<Invoice, ProductInformation>(invoice);
entity.RedirectURL = invoice.RedirectURL ?? store.StoreWebsite;
if (!Uri.IsWellFormedUriString(entity.RedirectURL, UriKind.Absolute))
entity.RedirectURL = null;
@ -147,7 +159,7 @@ namespace BTCPayServer.Controllers
if (supported.Count == 0)
{
StringBuilder errors = new StringBuilder();
errors.AppendLine("No payment method available for this store");
errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)");
foreach (var error in logs.ToList())
{
errors.AppendLine(error.ToString());

View File

@ -31,7 +31,7 @@ namespace BTCPayServer.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
@ -44,7 +44,7 @@ namespace BTCPayServer.Controllers
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
EmailSenderFactory emailSenderFactory,
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
@ -54,7 +54,7 @@ namespace BTCPayServer.Controllers
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
@ -156,8 +156,7 @@ namespace BTCPayServer.Controllers
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
_EmailSenderFactory.GetEmailSender().SendEmailConfirmation(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(Index));
}

View File

@ -12,11 +12,13 @@ using BTCPayServer.Rating;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authorization;
using BTCPayServer.Authentication;
using Microsoft.AspNetCore.Cors;
namespace BTCPayServer.Controllers
{
[Authorize(AuthenticationSchemes = Security.Policies.BitpayAuthentication)]
[AllowAnonymous]
[EnableCors(CorsPolicies.All)]
public class RateController : Controller
{
RateFetcher _RateProviderFactory;
@ -139,9 +141,9 @@ namespace BTCPayServer.Controllers
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
var defaultPaymentId = store.GetDefaultPaymentId(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultPaymentId.CryptoCode);
if (string.IsNullOrEmpty(currencyPairs))
{

View File

@ -208,8 +208,8 @@ namespace BTCPayServer.Controllers
{
builder.Scheme = this.Request.Scheme;
builder.Host = vm.DNSDomain;
var addresses1 = Dns.GetHostAddressesAsync(this.Request.Host.Host);
var addresses2 = Dns.GetHostAddressesAsync(vm.DNSDomain);
var addresses1 = GetAddressAsync(this.Request.Host.Host);
var addresses2 = GetAddressAsync(vm.DNSDomain);
await Task.WhenAll(addresses1, addresses2);
var addressesSet = addresses1.GetAwaiter().GetResult().Select(c => c.ToString()).ToHashSet();
@ -253,6 +253,13 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Maintenance));
}
private Task<IPAddress[]> GetAddressAsync(string domainOrIP)
{
if (IPAddress.TryParse(domainOrIP, out var ip))
return Task.FromResult(new[] { ip });
return Dns.GetHostAddressesAsync(domainOrIP);
}
public static string RunId = Encoders.Hex.EncodeData(NBitcoin.RandomUtils.GetBytes(32));
[HttpGet]
[Route("runid")]
@ -465,7 +472,7 @@ namespace BTCPayServer.Controllers
result.ExternalServices.Add(new ServicesViewModel.ExternalService()
{
Name = externalService.Key,
Link = this.Request.GetRelativePath(externalService.Value)
Link = this.Request.GetRelativePathOrAbsolute(externalService.Value)
});
}
if(_Options.SSHSettings != null)

View File

@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Services.Mails;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[Route("{storeId}/emails")]
public IActionResult Emails()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var data = store.GetStoreBlob().EmailSettings ?? new EmailSettings();
return View(new EmailsViewModel() { Settings = data });
}
[Route("{storeId}/emails")]
[HttpPost]
public async Task<IActionResult> Emails(string storeId, EmailsViewModel model, string command)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
if (command == "Test")
{
try
{
if (!model.Settings.IsComplete())
{
model.StatusMessage = "Error: Required fields missing";
return View(model);
}
var client = model.Settings.CreateSmtpClient();
await client.SendMailAsync(model.Settings.From, model.TestEmail, "BTCPay test", "BTCPay test");
model.StatusMessage = "Email sent to " + model.TestEmail + ", please, verify you received it";
}
catch (Exception ex)
{
model.StatusMessage = "Error: " + ex.Message;
}
return View(model);
}
else // if(command == "Save")
{
var storeBlob = store.GetStoreBlob();
storeBlob.EmailSettings = model.Settings;
store.SetStoreBlob(storeBlob);
await _Repo.UpdateStore(store);
StatusMessage = "Email settings modified";
return RedirectToAction(nameof(UpdateStore), new {
storeId});
}
}
}
}

View File

@ -96,6 +96,11 @@ namespace BTCPayServer.Controllers
{
get; set;
}
[TempData]
public bool StoreNotConfigured
{
get; set;
}
[HttpGet]
[Route("{storeId}/users")]
@ -167,7 +172,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel()
{
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove access to {user.Email}?",
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
}
@ -326,7 +331,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
vm.SetCryptoCurrencies(_NetworkProvider, StoreData, StoreData.GetDefaultPaymentId(_NetworkProvider));
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
@ -360,12 +365,13 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod);
if (StoreData.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId)
{
needUpdate = true;
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
StoreData.SetDefaultPaymentId(defaultPaymentMethodId);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetCryptoCurrencies(_NetworkProvider, StoreData, defaultPaymentMethodId);
model.SetLanguages(_LangService, model.DefaultLang);
if (!ModelState.IsValid)
@ -567,6 +573,7 @@ namespace BTCPayServer.Controllers
var model = new TokensViewModel();
var tokens = await _TokenRepository.GetTokensByStoreIdAsync(StoreData.Id);
model.StatusMessage = StatusMessage;
model.StoreNotConfigured = StoreNotConfigured;
model.Tokens = tokens.Select(t => new TokenViewModel()
{
Facade = t.Facade,
@ -794,6 +801,10 @@ namespace BTCPayServer.Controllers
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{
var excludeFilter = store.GetStoreBlob().GetExcludedPaymentMethods();
StoreNotConfigured = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(p => !excludeFilter.Match(p.PaymentId))
.Count() == 0;
StatusMessage = "Pairing is successful";
if (pairingResult == PairingResult.Partial)
StatusMessage = "Server initiated pairing code: " + pairingCode;

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string defaultDestination = null, string defaultAmount = null)
WalletId walletId, string defaultDestination = null, string defaultAmount = null, bool advancedMode = false)
{
if (walletId?.StoreId == null)
return NotFound();
@ -195,6 +195,7 @@ namespace BTCPayServer.Controllers
}
catch (Exception ex) { model.RateError = ex.Message; }
}
model.AdvancedMode = advancedMode;
return View(model);
}
@ -202,7 +203,7 @@ namespace BTCPayServer.Controllers
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm)
WalletId walletId, WalletSendModel vm, string command = null)
{
if (walletId?.StoreId == null)
return NotFound();
@ -212,6 +213,14 @@ namespace BTCPayServer.Controllers
var network = this.NetworkProvider.GetNetwork(walletId?.CryptoCode);
if (network == null)
return NotFound();
if (command == "noob" || command == "expert")
{
ModelState.Clear();
vm.AdvancedMode = command == "expert";
return View(vm);
}
var destination = ParseDestination(vm.Destination, network.NBitcoinNetwork);
if (destination == null)
ModelState.AddModelError(nameof(vm.Destination), "Invalid address");
@ -231,7 +240,8 @@ namespace BTCPayServer.Controllers
Destination = vm.Destination,
Amount = vm.Amount.Value,
SubstractFees = vm.SubstractFees,
FeeSatoshiPerByte = vm.FeeSatoshiPerByte
FeeSatoshiPerByte = vm.FeeSatoshiPerByte,
NoChange = vm.NoChange
});
}
@ -403,6 +413,7 @@ namespace BTCPayServer.Controllers
// getxpub
int account = 0,
// sendtoaddress
bool noChange = false,
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
@ -436,7 +447,7 @@ namespace BTCPayServer.Controllers
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
destinationAddress = BitcoinAddress.Create(destination.Trim(), network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
@ -487,24 +498,16 @@ namespace BTCPayServer.Controllers
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
var keypaths = new Dictionary<Script, KeyPath>();
List<Coin> availableCoins = new List<Coin>();
foreach (var c in await wallet.GetUnspentCoins(derivationScheme))
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins.Add(c.Coin);
}
var changeAddress = await change;
var storeBlob = storeData.GetStoreBlob();
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
@ -520,10 +523,25 @@ namespace BTCPayServer.Controllers
storeData.SetStoreBlob(storeBlob);
await Repository.UpdateStore(storeData);
}
retry:
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
builder.AddCoins(availableCoins);
foreach (var element in send)
{
@ -531,6 +549,7 @@ namespace BTCPayServer.Controllers
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
@ -547,13 +566,15 @@ namespace BTCPayServer.Controllers
}
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
var hasChange = unsigned.Outputs.Any(o => o.ScriptPubKey == changeAddress.Item1.ScriptPubKey);
if (noChange && hasChange)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
availableCoins = builder.FindSpentCoins(unsigned).Cast<Coin>().ToList();
amountBTC = builder.FindSpentCoins(unsigned).Select(c => c.TxOut.Value).Sum();
subsctractFeesValue = true;
goto retry;
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();

View File

@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels;
@ -35,20 +36,30 @@ namespace BTCPayServer.Hubs
{
model.RedirectToCheckout = false;
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
switch (result)
try
{
case OkObjectResult okObjectResult:
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
break;
case ObjectResult objectResult:
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
break;
default:
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
break;
var result =
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
switch (result)
{
case OkObjectResult okObjectResult:
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
break;
case ObjectResult objectResult:
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
break;
default:
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
break;
}
}
catch (Exception)
{
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
}
}
}

View File

@ -2,11 +2,11 @@ using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Hubs;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
@ -16,14 +16,11 @@ using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using NBitcoin;
using YamlDotNet.Core;
namespace BTCPayServer.Hubs
namespace BTCPayServer.Crowdfund
{
public class
CrowdfundHubStreamer
public class CrowdfundHubStreamer: IDisposable
{
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
private readonly EventAggregator _EventAggregator;
@ -37,7 +34,9 @@ namespace BTCPayServer.Hubs
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
private List<IEventAggregatorSubscription> _Subscriptions;
public CrowdfundHubStreamer(EventAggregator eventAggregator,
IHubContext<CrowdfundHub> hubContext,
IMemoryCache memoryCache,
@ -116,13 +115,15 @@ namespace BTCPayServer.Hubs
private void SubscribeToEvents()
{
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent);
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
_Subscriptions = new List<IEventAggregatorSubscription>()
{
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
InvalidateCacheForApp(updated.AppId);
});
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent),
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
{
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
InvalidateCacheForApp(updated.AppId);
})
};
}
private string GetCacheKey(string appId)
@ -152,7 +153,7 @@ namespace BTCPayServer.Hubs
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
} );
_Logger.LogInformation($"App {quickLookup.appId}: Received Payment");
InvalidateCacheForApp(quickLookup.appId);
break;
case InvoiceEvent.Created:
@ -361,5 +362,10 @@ namespace BTCPayServer.Hubs
StartDate = startDate
});
}
public void Dispose()
{
_Subscriptions.ForEach(subscription => subscription.Dispose());
}
}
}

View File

@ -3,8 +3,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
using JetBrains.Annotations;
@ -98,15 +96,5 @@ namespace BTCPayServer.Data
else if (_Type == DatabaseType.MySQL)
builder.UseMySql(_ConnectionString);
}
public void ConfigureHangfireBuilder(IGlobalConfiguration builder)
{
builder.UseMemoryStorage();
//We always use memory storage because of incompatibilities with the latest postgres in 2.1
//if (_Type == DatabaseType.Sqlite)
// builder.UseMemoryStorage(); //Sqlite provider does not support multiple workers
//else if (_Type == DatabaseType.Postgres)
// builder.UsePostgreSqlStorage(_ConnectionString);
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
{
@ -194,7 +195,7 @@ namespace BTCPayServer.Data
get;
set;
}
[Obsolete("Use GetDefaultCrypto instead")]
[Obsolete("Use GetDefaultPaymentId instead")]
public string DefaultCrypto { get; set; }
public List<PairedSINData> PairedSINs { get; set; }
public IEnumerable<APIKeyData> APIKeys { get; set; }
@ -203,13 +204,14 @@ namespace BTCPayServer.Data
public List<Claim> AdditionalClaims { get; set; } = new List<Claim>();
#pragma warning disable CS0618
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
public PaymentMethodId GetDefaultPaymentId(BTCPayNetworkProvider networkProvider = null)
{
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.CryptoCode).FirstOrDefault() ?? "BTC");
var str = DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).Select(p => p.PaymentId.ToString()).FirstOrDefault() ?? "BTC");
return PaymentMethodId.Parse(str);
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
public void SetDefaultPaymentId(PaymentMethodId defaultPaymentId)
{
DefaultCrypto = defaultCryptoCurrency;
DefaultCrypto = defaultPaymentId.ToString();
}
#pragma warning restore CS0618
@ -403,6 +405,8 @@ namespace BTCPayServer.Data
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
public EmailSettings EmailSettings { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@ -10,9 +10,9 @@ namespace BTCPayServer.Services
{
public static class EmailSenderExtensions
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
public static void SendEmailConfirmation(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
emailSender.SendEmail(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

@ -12,13 +12,32 @@ namespace BTCPayServer.Filters
{
Value = value;
}
public string Value
public XFrameOptionsAttribute(XFrameOptions type, string allowFrom = null)
{
get; set;
switch (type)
{
case XFrameOptions.Deny:
Value = "deny";
break;
case XFrameOptions.SameOrigin:
Value = "deny";
break;
case XFrameOptions.AllowFrom:
Value = $"allow-from {allowFrom}";
break;
case XFrameOptions.AllowAll:
Value = "allow-all";
break;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public string Value { get; set; }
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
@ -28,5 +47,13 @@ namespace BTCPayServer.Filters
context.HttpContext.Response.SetHeaderOnStarting("X-Frame-Options", Value);
}
}
public enum XFrameOptions
{
Deny,
SameOrigin,
AllowFrom,
AllowAll
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using Microsoft.Extensions.Hosting;
using NicolasDorier.RateLimits;
namespace BTCPayServer.HostedServices
{
public class BackgroundJobSchedulerHostedService : IHostedService
{
public BackgroundJobSchedulerHostedService(IBackgroundJobClient backgroundJobClient)
{
BackgroundJobClient = (BackgroundJobClient)backgroundJobClient;
}
public BackgroundJobClient BackgroundJobClient { get; }
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Stop = new CancellationTokenSource();
_Loop = BackgroundJobClient.ProcessJobs(_Stop.Token);
return Task.CompletedTask;
}
CancellationTokenSource _Stop;
public async Task StopAsync(CancellationToken cancellationToken)
{
_Stop.Cancel();
try
{
await _Loop;
}
catch (OperationCanceledException)
{
}
await BackgroundJobClient.WaitAllRunning(cancellationToken);
}
}
public class BackgroundJobClient : IBackgroundJobClient
{
class BackgroundJob
{
public Func<Task> Action;
public TimeSpan Delay;
public IDelay DelayImplementation;
public BackgroundJob(Func<Task> action, TimeSpan delay, IDelay delayImplementation)
{
this.Action = action;
this.Delay = delay;
this.DelayImplementation = delayImplementation;
}
public async Task Run(CancellationToken cancellationToken)
{
await DelayImplementation.Wait(Delay, cancellationToken);
await Action();
}
}
public IDelay Delay { get; set; } = TaskDelay.Instance;
public int GetExecutingCount()
{
lock (_Processing)
{
return _Processing.Count();
}
}
private Channel<BackgroundJob> _Jobs = Channel.CreateUnbounded<BackgroundJob>();
HashSet<Task> _Processing = new HashSet<Task>();
public void Schedule(Func<Task> action, TimeSpan delay)
{
_Jobs.Writer.TryWrite(new BackgroundJob(action, delay, Delay));
}
public async Task WaitAllRunning(CancellationToken cancellationToken)
{
Task[] processing = null;
lock (_Processing)
{
processing = _Processing.ToArray();
}
try
{
await Task.WhenAll(processing).WithCancellation(cancellationToken);
}
catch (Exception) when (cancellationToken.IsCancellationRequested)
{
throw;
}
}
public async Task ProcessJobs(CancellationToken cancellationToken)
{
while (await _Jobs.Reader.WaitToReadAsync(cancellationToken))
{
if (_Jobs.Reader.TryRead(out var job))
{
var processing = job.Run(cancellationToken);
lock (_Processing)
{
_Processing.Add(processing);
}
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
processing.ContinueWith(t =>
{
if (t.IsFaulted)
{
Logs.PayServer.LogWarning(t.Exception, "Unhandled exception while job running");
}
lock (_Processing)
{
_Processing.Remove(processing);
}
}, default, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}
}
}
}
}

View File

@ -1,10 +1,7 @@
using Hangfire;
using Hangfire.Common;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Hangfire.Annotations;
using System.Reflection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -21,6 +18,7 @@ using NBXplorer;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Payments;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
@ -35,25 +33,17 @@ namespace BTCPayServer.HostedServices
get; set;
}
public InvoiceEntity Invoice
public InvoicePaymentNotificationEventWrapper Notification
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
{
get; set;
}
IBackgroundJobClient _JobClient;
EventAggregator _EventAggregator;
InvoiceRepository _InvoiceRepository;
BTCPayNetworkProvider _NetworkProvider;
IEmailSender _EmailSender;
private readonly EmailSenderFactory _EmailSenderFactory;
public InvoiceNotificationManager(
IBackgroundJobClient jobClient,
@ -61,102 +51,114 @@ namespace BTCPayServer.HostedServices
InvoiceRepository invoiceRepository,
BTCPayNetworkProvider networkProvider,
ILogger<InvoiceNotificationManager> logger,
IEmailSender emailSender)
EmailSenderFactory emailSenderFactory)
{
Logger = logger as ILogger ?? NullLogger.Instance;
_JobClient = jobClient;
_EventAggregator = eventAggregator;
_InvoiceRepository = invoiceRepository;
_NetworkProvider = networkProvider;
_EmailSender = emailSender;
_EmailSenderFactory = emailSenderFactory;
}
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
void Notify(InvoiceEntity invoice, InvoiceEvent invoiceEvent, bool extendedNotification)
{
var dto = invoice.EntityToDTO(_NetworkProvider);
var notification = new InvoicePaymentNotificationEventWrapper()
{
Data = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
},
Event = new InvoicePaymentNotificationEvent()
{
Code = invoiceEvent.EventCode,
Name = invoiceEvent.Name
},
ExtendedNotification = extendedNotification,
NotificationURL = invoice.NotificationURL
};
// For lightning network payments, paid, confirmed and completed come all at once.
// So despite the event is "paid" or "confirmed" the Status of the invoice is technically complete
// This confuse loggers who think their endpoint get duplicated events
// So here, we just override the status expressed by the notification
if (invoiceEvent.Name == InvoiceEvent.Confirmed)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Confirmed);
}
if (invoiceEvent.Name == InvoiceEvent.PaidInFull)
{
notification.Data.Status = InvoiceState.ToString(InvoiceStatus.Paid);
}
//////////////////
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Data.Rate = dto.Rate;
notification.Data.Url = dto.Url;
notification.Data.BTCDue = dto.BTCDue;
notification.Data.BTCPaid = dto.BTCPaid;
notification.Data.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
CancellationTokenSource cts = new CancellationTokenSource(10000);
if (!String.IsNullOrEmpty(invoice.NotificationEmail))
{
// just extracting most important data for email body, merchant should query API back for full invoice based on Invoice.Id
var ipn = new
{
invoice.Id,
invoice.Status,
invoice.StoreId
};
// TODO: Consider adding info on ItemDesc and payment info (amount)
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(notification);
_EmailSenderFactory.GetEmailSender(invoice.StoreId).SendEmail(
invoice.NotificationEmail,
$"BtcPayServer Invoice Notification - ${invoice.StoreId}",
emailBody);
var emailBody = NBitcoin.JsonConverters.Serializer.ToString(ipn);
await _EmailSender.SendEmailAsync(
invoice.NotificationEmail, $"BtcPayServer Invoice Notification - ${invoice.StoreId}", emailBody);
}
try
{
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
var response = await SendNotification(invoice, eventCode, name, cts.Token);
response.EnsureSuccessStatusCode();
if (string.IsNullOrEmpty(invoice.NotificationURL) || !Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute))
return;
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = ex.Message
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
ConcurrentDictionary<string, string> _Executing = new ConcurrentDictionary<string, string>();
public async Task NotifyHttp(string invoiceData)
{
var job = NBitcoin.JsonConverters.Serializer.ToObject<ScheduledJob>(invoiceData);
var jobId = GetHttpJobId(job.Invoice);
if (!_Executing.TryAdd(jobId, jobId))
return; //For some reason, Hangfire fire the job several time
Logger.LogInformation("Running " + jobId);
bool reschedule = false;
var aggregatorEvent = new InvoiceIPNEvent(job.Notification.Data.Id, job.Notification.Event.Code, job.Notification.Event.Name);
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
HttpResponseMessage response = await SendNotification(job.Notification, cts.Token);
reschedule = !response.IsSuccessStatusCode;
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
});
aggregatorEvent.Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null;
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
aggregatorEvent.Error = "Timeout";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
reschedule = true;
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
catch (Exception ex)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
@ -166,21 +168,16 @@ namespace BTCPayServer.HostedServices
ex = ex.InnerException;
}
string message = String.Join(',', messages.ToArray());
Logger.LogInformation("Job " + jobId + " threw exception " + message);
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = $"Unexpected error: {message}"
});
aggregatorEvent.Error = $"Unexpected error: {message}";
_EventAggregator.Publish<InvoiceIPNEvent>(aggregatorEvent);
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
finally { cts?.Dispose(); }
job.TryCount++;
if (job.TryCount < MaxTry && reschedule)
{
Logger.LogInformation("Rescheduling " + jobId + " in 10 minutes, remaining try " + (MaxTry - job.TryCount));
invoiceData = NBitcoin.JsonConverters.Serializer.ToString(job);
_JobClient.Schedule(() => NotifyHttp(invoiceData), TimeSpan.FromMinutes(10.0));
}
@ -199,64 +196,35 @@ namespace BTCPayServer.HostedServices
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
[JsonProperty("extendedNotification")]
public bool ExtendedNotification { get; set; }
[JsonProperty(PropertyName = "notificationURL")]
public string NotificationURL { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoicePaymentNotificationEventWrapper notification, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
var dto = invoice.EntityToDTO(_NetworkProvider);
InvoicePaymentNotification notification = new InvoicePaymentNotification()
{
Id = dto.Id,
Currency = dto.Currency,
CurrentTime = dto.CurrentTime,
ExceptionStatus = dto.ExceptionStatus,
ExpirationTime = dto.ExpirationTime,
InvoiceTime = dto.InvoiceTime,
PosData = dto.PosData,
Price = dto.Price,
Status = dto.Status,
BuyerFields = invoice.RefundMail == null ? null : new Newtonsoft.Json.Linq.JObject() { new JProperty("buyerEmail", invoice.RefundMail) },
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates,
var notificationString = NBitcoin.JsonConverters.Serializer.ToString(notification);
var jobj = JObject.Parse(notificationString);
};
// We keep backward compatibility with bitpay by passing BTC info to the notification
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
if (notification.ExtendedNotification)
{
#pragma warning disable CS0618
notification.Rate = dto.Rate;
notification.Url = dto.Url;
notification.BTCDue = dto.BTCDue;
notification.BTCPaid = dto.BTCPaid;
notification.BTCPrice = dto.BTCPrice;
#pragma warning restore CS0618
}
string notificationString = null;
if (eventCode.HasValue)
{
var wrapper = new InvoicePaymentNotificationEventWrapper();
wrapper.Data = notification;
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
notificationString = JsonConvert.SerializeObject(wrapper);
jobj.Remove("extendedNotification");
jobj.Remove("notificationURL");
notificationString = jobj.ToString();
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
notificationString = jobj["data"].ToString();
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.RequestUri = new Uri(notification.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
var response = await Enqueue(notification.Data.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
@ -320,11 +288,6 @@ namespace BTCPayServer.HostedServices
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
{
return $"{invoice.Id}-{invoice.Status}-HTTP";
}
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
@ -350,19 +313,18 @@ namespace BTCPayServer.HostedServices
e.Name == InvoiceEvent.Completed ||
e.Name == InvoiceEvent.ExpiredPaidPartial
)
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (e.Name == "invoice_confirmed")
if (e.Name == InvoiceEvent.Confirmed)
{
tasks.Add(Notify(invoice));
Notify(invoice, e, false);
}
if (invoice.ExtendedNotifications)
{
tasks.Add(Notify(invoice, e.EventCode, e.Name));
Notify(invoice, e, true);
}
await Task.WhenAll(tasks.ToArray());
}));

View File

@ -11,7 +11,6 @@ using BTCPayServer.Logging;
using System.Threading;
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using Hangfire;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Controllers;
using BTCPayServer.Events;

View File

@ -38,6 +38,7 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Crowdfund;
using BTCPayServer.Hubs;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
@ -116,7 +117,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
Fallback = new FeeRate(100, 1),
Fallback = new FeeRate(100L, 1),
BlockTarget = 20
});
@ -144,6 +145,8 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
services.TryAddSingleton<ExplorerClientProvider>();
@ -162,7 +165,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddSingleton<EmailSenderFactory>();
// bundling
services.AddAuthorization(o => Policies.AddBTCPayPolicies(o));

View File

@ -81,32 +81,35 @@ namespace BTCPayServer.Hosting
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
var method = httpContext.Request.Method;
isJson = method == "OPTIONS" ? true : isJson;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
(method == "POST" || method == "OPTIONS") &&
isJson)
return true;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
(method == "GET" || method == "OPTIONS"))
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(method == "GET" || method == "OPTIONS") &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
(method == "GET" || method == "OPTIONS"))
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
(method == "GET" || method == "POST" || method == "OPTIONS"))
return true;
return false;

View File

@ -19,7 +19,6 @@ using BTCPayServer.Models;
using Microsoft.AspNetCore.Identity;
using BTCPayServer.Data;
using Microsoft.Extensions.Logging;
using Hangfire;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
@ -27,11 +26,8 @@ using BTCPayServer.Controllers;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Mails;
using Microsoft.Extensions.Configuration;
using Hangfire.AspNetCore;
using BTCPayServer.Configuration;
using System.IO;
using Hangfire.Dashboard;
using Hangfire.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Threading;
using Microsoft.Extensions.Options;
@ -46,18 +42,6 @@ namespace BTCPayServer.Hosting
{
public class Startup
{
class NeedRole : IDashboardAuthorizationFilter
{
string _Role;
public NeedRole(string role)
{
_Role = role;
}
public bool Authorize([NotNull] DashboardContext context)
{
return context.GetHttpContext().User.IsInRole(_Role);
}
}
public Startup(IConfiguration conf, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
Configuration = conf;
@ -108,21 +92,6 @@ namespace BTCPayServer.Hosting
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
});
services.AddHangfire((o) =>
{
var scope = AspNetCoreJobActivator.Current.BeginScope(null);
var options = (ApplicationDbContextFactory)scope.Resolve(typeof(ApplicationDbContextFactory));
options.ConfigureHangfireBuilder(o);
});
services.AddCors(o =>
{
o.AddPolicy("BitpayAPI", b =>
{
b.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin();
});
});
// If the HTTPS certificate path is not set this logic will NOT be used and the default Kestrel binding logic will be.
string httpsCertificateFilePath = Configuration.GetOrDefault<string>("HttpsCertificateFilePath", null);
bool useDefaultCertificate = Configuration.GetOrDefault<bool>("HttpsUseDefaultCertificate", false);
@ -193,12 +162,6 @@ namespace BTCPayServer.Hosting
app.UsePayServer();
app.UseStaticFiles();
app.UseAuthentication();
app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions()
{
AppPath = options.GetRootUri(),
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
});
app.UseSignalR(route =>
{
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");

33
BTCPayServer/IDelay.cs Normal file
View File

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer
{
public interface IDelay
{
Task Wait(TimeSpan delay, CancellationToken cancellationToken);
}
public class TaskDelay : IDelay
{
TaskDelay()
{
}
private static readonly TaskDelay _Instance = new TaskDelay();
public static TaskDelay Instance
{
get
{
return _Instance;
}
}
public Task Wait(TimeSpan delay, CancellationToken cancellationToken)
{
return Task.Delay(delay, cancellationToken);
}
}
}

View File

@ -41,5 +41,7 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]
public string CustomCSSLink { get; set; }
public string Id { get; set; }
}
}

View File

@ -90,6 +90,12 @@ namespace BTCPayServer.Models
get; set;
}
[JsonProperty("taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
//"currency":"USD"
[JsonProperty("currency")]
public string Currency

View File

@ -105,6 +105,7 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TaxIncluded { get; set; }
public BuyerInformation BuyerInformation
{
get;
@ -143,6 +144,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
public Dictionary<string, object> PosData { get; set; }
}
}

View File

@ -15,6 +15,10 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public int Total
{
get; set;
}
public string SearchTerm
{
get; set;

View File

@ -24,7 +24,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool IsModal { get; set; }
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
public string BtcAddress { get; set; }
public string BtcDue { get; set; }

View File

@ -0,0 +1,80 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models
{
public class StatusMessageModel
{
public StatusMessageModel()
{
}
public StatusMessageModel(string s)
{
if (string.IsNullOrEmpty(s))
return;
try
{
if (s.StartsWith("{", StringComparison.InvariantCultureIgnoreCase) &&
s.EndsWith("}", StringComparison.InvariantCultureIgnoreCase))
{
var model = JObject.Parse(s).ToObject<StatusMessageModel>();
Html = model.Html;
Message = model.Message;
Severity = model.Severity;
}
else
{
ParseNonJsonStatus(s);
}
}
catch (Exception)
{
ParseNonJsonStatus(s);
}
}
public string Message { get; set; }
public string Html { get; set; }
public StatusSeverity Severity { get; set; }
public string SeverityCSS
{
get
{
switch (Severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public override string ToString()
{
return JObject.FromObject(this).ToString(Formatting.None);
}
private void ParseNonJsonStatus(string s)
{
Message = s;
Severity = s.StartsWith("Error", StringComparison.InvariantCultureIgnoreCase)
? StatusSeverity.Error
: StatusSeverity.Success;
}
public enum StatusSeverity
{
Info,
Error,
Success
}
}
}

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -19,8 +20,8 @@ namespace BTCPayServer.Models.StoreViewModels
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default the default payment method on checkout")]
public string DefaultPaymentMethod { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
@ -48,12 +49,25 @@ namespace BTCPayServer.Models.StoreViewModels
public string HtmlTitle { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
public void SetCryptoCurrencies(BTCPayNetworkProvider networks, Data.StoreData storeData, PaymentMethodId paymentMethodId)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
var paymentIds = storeData.GetSupportedPaymentMethods(networks).Select(o => o.PaymentId)
.OrderByDescending(a => a.CryptoCode == "BTC")
.ThenBy(a => a.CryptoCode)
.ThenBy(a => a.PaymentType == PaymentTypes.LightningLike ? 1 : 0);
var choices = paymentIds.Select(o => new Format() { Name = GetDisplayName(networks, o), Value = o.ToString() }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == paymentMethodId?.ToString()) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
DefaultPaymentMethod = chosen.Value;
}
private string GetDisplayName(BTCPayNetworkProvider networks, PaymentMethodId paymentMethodId)
{
var display = networks.GetNetwork(paymentMethodId.CryptoCode)?.DisplayName ?? paymentMethodId.CryptoCode;
return paymentMethodId.PaymentType == PaymentTypes.BTCLike ?
display : $"{display} (Lightning)";
}
public void SetLanguages(LanguageService langService, string defaultLang)

View File

@ -72,5 +72,6 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "API Key")]
public string ApiKey { get; set; }
public string EncodedApiKey { get; set; }
public bool StoreNotConfigured { get; set; }
}
}

View File

@ -11,5 +11,6 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SubstractFees { get; set; }
public decimal Amount { get; set; }
public string Destination { get; set; }
public bool NoChange { get; set; }
}
}

View File

@ -28,6 +28,10 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Fee rate (satoshi per byte)")]
[Required]
public int FeeSatoshiPerByte { get; set; }
[Display(Name = "Make sure no change UTXO is created")]
public bool NoChange { get; set; }
public bool AdvancedMode { get; set; }
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }

View File

@ -7,6 +7,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Lightning.JsonConverters;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Lightning
@ -16,17 +17,21 @@ namespace BTCPayServer.Payments.Lightning
[JsonConverter(typeof(LightMoneyJsonConverter))]
public LightMoney Amount { get; set; }
public string BOLT11 { get; set; }
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
public uint256 PaymentHash { get; set; }
public string GetDestination(BTCPayNetwork network)
{
return GetPaymentId();
return BOLT11;
}
public decimal NetworkFee { get; set; }
public string GetPaymentId()
{
return BOLT11;
// Legacy, some old payments don't have the PaymentHash set
return PaymentHash?.ToString() ?? BOLT11;
}
public PaymentTypes GetPaymentType()

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely maner");
}
catch (Exception ex)
{
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
throw new PaymentMethodUnavailableException($"The lightning node did not reply in a timely manner");
}
catch (Exception ex)
{
@ -115,7 +115,7 @@ namespace BTCPayServer.Payments.Lightning
}
if (address == null)
throw new PaymentMethodUnavailableException($"DNS did not resolved {nodeInfo.Host}");
throw new PaymentMethodUnavailableException($"DNS did not resolve {nodeInfo.Host}");
using (var tcp = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp))
{

View File

@ -191,6 +191,7 @@ namespace BTCPayServer.Payments.Lightning
var payment = await _InvoiceRepository.AddPayment(listenedInvoice.InvoiceId, notification.PaidAt.Value, new LightningLikePaymentData()
{
BOLT11 = notification.BOLT11,
PaymentHash = BOLT11PaymentRequest.Parse(notification.BOLT11, network.NBitcoinNetwork).PaymentHash,
Amount = notification.Amount
}, network, accounted: true);
if (payment != null)

View File

@ -198,44 +198,6 @@ namespace BTCPayServer.Security
}
yield return token;
}
private bool IsBitpayAPI(HttpContext httpContext, bool bitpayAuth)
{
if (!httpContext.Request.Path.HasValue)
return false;
var isJson = (httpContext.Request.ContentType ?? string.Empty).StartsWith("application/json", StringComparison.OrdinalIgnoreCase);
var path = httpContext.Request.Path.Value;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "POST" &&
isJson)
return true;
if (
bitpayAuth &&
(path == "/invoices" || path == "/invoices/") &&
httpContext.Request.Method == "GET")
return true;
if (
path.StartsWith("/invoices/", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET" &&
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;
if (
path.Equals("/tokens", StringComparison.Ordinal) &&
(httpContext.Request.Method == "GET" || httpContext.Request.Method == "POST"))
return true;
return false;
}
}
internal static void AddAuthentication(IServiceCollection services, Action<BitpayAuthOptions> bitpayAuth = null)
{

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public interface IBackgroundJobClient
{
void Schedule(Func<Task> act, TimeSpan zero);
}
}

View File

@ -91,6 +91,12 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded
{
get; set;
}
[JsonProperty(PropertyName = "currency")]
public string Currency
{

View File

@ -178,7 +178,6 @@ retry:
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
return invoice;
}
@ -420,91 +419,108 @@ retry:
return entity;
}
private IQueryable<Data.InvoiceData> GetInvoiceQuery(ApplicationDbContext context, InvoiceQuery queryObject)
{
IQueryable<Data.InvoiceData> query = context.Invoices;
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
{
// Hacky way to return an empty query object. The nice way is much too elaborate:
// https://stackoverflow.com/questions/33305495/how-to-return-empty-iqueryable-in-an-async-repository-method
return query.Where(x => false);
}
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
return query;
}
public async Task<int> GetInvoicesTotal(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
var query = GetInvoiceQuery(context, queryObject);
return await query.CountAsync();
}
}
public async Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
{
using (var context = _ContextFactory.CreateContext())
{
IQueryable<Data.InvoiceData> query = context
.Invoices
.Include(o => o.Payments)
var query = GetInvoiceQuery(context, queryObject);
query = query.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if (queryObject.IncludeAddresses)
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
if (queryObject.IncludeEvents)
query = query.Include(o => o.Events);
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
{
query = query.Where(i => i.Id == queryObject.InvoiceId);
}
if (queryObject.StoreId != null && queryObject.StoreId.Length > 0)
{
var stores = queryObject.StoreId.ToHashSet();
query = query.Where(i => stores.Contains(i.StoreDataId));
}
if (queryObject.UserId != null)
{
query = query.Where(i => i.StoreData.UserStores.Any(u => u.ApplicationUserId == queryObject.UserId));
}
if (!string.IsNullOrEmpty(queryObject.TextSearch))
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
return Array.Empty<InvoiceEntity>();
query = query.Where(i => ids.Contains(i.Id));
}
if (queryObject.StartDate != null)
query = query.Where(i => queryObject.StartDate.Value <= i.Created);
if (queryObject.EndDate != null)
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
{
var statusSet = queryObject.OrderId.ToHashSet();
query = query.Where(i => statusSet.Contains(i.OrderId));
}
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
{
var statusSet = queryObject.ItemCode.ToHashSet();
query = query.Where(i => statusSet.Contains(i.ItemCode));
}
if (queryObject.Status != null && queryObject.Status.Length > 0)
{
var statusSet = queryObject.Status.ToHashSet();
query = query.Where(i => statusSet.Contains(i.Status));
}
if (queryObject.Unusual != null)
{
var unused = queryObject.Unusual.Value;
query = query.Where(i => unused == (i.Status == "invalid" || i.ExceptionStatus != null));
}
if (queryObject.ExceptionStatus != null && queryObject.ExceptionStatus.Length > 0)
{
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet();
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
}
query = query.OrderByDescending(q => q.Created);
if (queryObject.Skip != null)
query = query.Skip(queryObject.Skip.Value);
if (queryObject.Count != null)
query = query.Take(queryObject.Count.Value);
var data = await query.ToArrayAsync().ConfigureAwait(false);
return data.Select(ToEntity).ToArray();
}
}
private string NormalizeExceptionStatus(string status)

View File

@ -1,48 +1,40 @@
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using Hangfire;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mail;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
// This class is used by the application to send email for account confirmation and password reset.
// For more details see https://go.microsoft.com/fwlink/?LinkID=532713
public class EmailSender : IEmailSender
public abstract class EmailSender : IEmailSender
{
IBackgroundJobClient _JobClient;
SettingsRepository _Repository;
public EmailSender(IBackgroundJobClient jobClient, SettingsRepository repository)
public EmailSender(IBackgroundJobClient jobClient)
{
if (jobClient == null)
throw new ArgumentNullException(nameof(jobClient));
_JobClient = jobClient;
_Repository = repository;
}
public async Task SendEmailAsync(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return;
_JobClient = jobClient ?? throw new ArgumentNullException(nameof(jobClient));
}
public async Task SendMailCore(string email, string subject, string message)
public void SendEmail(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>() ?? new EmailSettings();
if (!settings.IsComplete())
throw new InvalidOperationException("Email settings not configured");
var smtp = settings.CreateSmtpClient();
MailMessage mail = new MailMessage(settings.From, email, subject, message);
mail.IsBodyHtml = true;
await smtp.SendMailAsync(mail);
_JobClient.Schedule(async () =>
{
var emailSettings = await GetEmailSettings();
if (emailSettings?.IsComplete() != true)
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
var smtp = emailSettings.CreateSmtpClient();
var mail = new MailMessage(emailSettings.From, email, subject, message)
{
IsBodyHtml = true
};
await smtp.SendMailAsync(mail);
}, TimeSpan.Zero);
}
public abstract Task<EmailSettings> GetEmailSettings();
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
public class EmailSenderFactory
{
private readonly IBackgroundJobClient _JobClient;
private readonly SettingsRepository _Repository;
private readonly StoreRepository _StoreRepository;
public EmailSenderFactory(IBackgroundJobClient jobClient,
SettingsRepository repository,
StoreRepository storeRepository)
{
_JobClient = jobClient;
_Repository = repository;
_StoreRepository = storeRepository;
}
public IEmailSender GetEmailSender(string storeId = null)
{
var serverSender = new ServerEmailSender(_Repository, _JobClient);
if (string.IsNullOrEmpty(storeId))
return serverSender;
return new StoreEmailSender(_StoreRepository, serverSender, _JobClient, storeId);
}
}
}

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Mails
{
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
void SendEmail(string email, string subject, string message);
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Mails
{
class ServerEmailSender : EmailSender
{
public ServerEmailSender(SettingsRepository settingsRepository,
IBackgroundJobClient backgroundJobClient) : base(backgroundJobClient)
{
if (settingsRepository == null)
throw new ArgumentNullException(nameof(settingsRepository));
SettingsRepository = settingsRepository;
}
public SettingsRepository SettingsRepository { get; }
public override Task<EmailSettings> GetEmailSettings()
{
return SettingsRepository.GetSettingAsync<EmailSettings>();
}
}
}

View File

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Services.Mails
{
class StoreEmailSender : EmailSender
{
public StoreEmailSender(StoreRepository storeRepository,
EmailSender fallback,
IBackgroundJobClient backgroundJobClient,
string storeId) : base(backgroundJobClient)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
StoreRepository = storeRepository;
FallbackSender = fallback;
StoreId = storeId;
}
public StoreRepository StoreRepository { get; }
public EmailSender FallbackSender { get; }
public string StoreId { get; }
public override async Task<EmailSettings> GetEmailSettings()
{
var store = await StoreRepository.FindStore(StoreId);
var emailSettings = store.GetStoreBlob().EmailSettings;
if (emailSettings?.IsComplete() == true)
{
return emailSettings;
}
return await FallbackSender.GetEmailSettings();
}
}
}

View File

@ -121,15 +121,18 @@ namespace BTCPayServer.Services.Rates
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
while (true)
if (value != 0m)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
while (true)
{
value = rounded;
break;
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
divisibility++;
}
if (divisibility != provider.CurrencyDecimalDigits)
{

View File

@ -104,7 +104,8 @@ namespace BTCPayServer.Services.Rates
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitbtcAPI(), false));
// Cryptopia is often not available
Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
// Providers.Add("cryptopia", new ExchangeSharpRateProvider("cryptopia", new ExchangeCryptopiaAPI(), false));
// Handmade providers
Providers.Add(QuadrigacxRateProvider.QuadrigacxName, new QuadrigacxRateProvider());

View File

@ -1,4 +1,5 @@
@using BTCPayServer.Hubs
@using BTCPayServer.Crowdfund
@using BTCPayServer.Hubs
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@model UpdateCrowdfundViewModel
@{

View File

@ -97,34 +97,83 @@
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Host button externally</h5>
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p>
<input type="submit" class="btn btn-primary" value="Save Settings" />
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save Settings" />
<div class="accordion" id="accordian-dev-info">
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-payment-button-header">
<h2 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-payment-button" aria-expanded="true" aria-controls="accordian-dev-info-embed-payment-button">
Embed Payment Button linking to POS item
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-payment-button" class="collapse" aria-labelledby="accordian-dev-info-embed-payment-button-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<pre><code class="html">@Model.Example1</code></pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<pre><code class="html">@Model.Example2</code></pre>
}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-embed-pos-iframe-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-embed-pos-iframe" aria-expanded="false" aria-controls="accordian-dev-info-embed-pos-iframe">
Embed POS with Iframe
</button>
</h2>
</div>
<div id="accordian-dev-info-embed-pos-iframe" class="collapse" aria-labelledby="accordian-dev-info-embed-pos-iframe-header" data-parent="#accordian-dev-info">
<div class="card-body">
You can embed the POS using an iframe
@{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "AppsPublic", new {appId = Model.Id}, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>";
}
<pre><code class="html">@iframe</code></pre>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="accordian-dev-info-notification-header">
<h2 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#accordian-dev-info-notification" aria-expanded="false" aria-controls="accordian-dev-info-notification">
Notification Url Callbacks
</button>
</h2>
</div>
<div id="accordian-dev-info-notification" class="collapse" aria-labelledby="accordian-dev-info-notification-header" data-parent="#accordian-dev-info">
<div class="card-body">
<p>A <code>POST</code> callback will be sent to notification with the following form will be sent to <code>notificationUrl</code> once the enough is paid and once again once there is enough confirmations to the payment:</p>
<pre><code class="json">@Model.ExampleCallback</code></pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>You can then ship your order</li>
</ul>
</p> </div>
</div>
</div>
</div>
</div>
</form>
<a asp-action="ListApps">Back to the app list</a>
</div>
</div>
</div>

View File

@ -1,73 +1,78 @@
<div class="container p-0" id="app" v-cloak>
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
<div class="card w-100 p-0 mx-0">
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl">
<div class="d-flex justify-content-between px-2">
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl" id="crowdfund-main-image">
<div class="d-flex justify-content-between px-2" id="crowdfund-header-container">
<h1>
{{srvModel.title}}
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate">
{{srvModel.title}}
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate" id="crowdfund-header-start-date">
Starts {{startDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate">
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate" id="crowdfund-header-end-date">
Ends {{endDateRelativeTime}}
</span>
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date">
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date" id="crowdfund-header-active">
Currently Active!
</span>
</h1>
<span v-if="srvModel.targetAmount" class="mt-3">
<span class="h5">{{srvModel.targetAmount}} {{targetCurrency}}</span>
<span v-if="srvModel.resetEvery !== 'Never'" v-b-tooltip
<span v-if="srvModel.targetAmount" class="mt-3" id="crowdfund-header-target">
<span class="h5" id="crowdfund-header-target-amount">{{srvModel.targetAmount}} {{targetCurrency}}</span>
<span v-if="srvModel.resetEvery !== 'Never'"
id="crowdfund-header-target-dynamic"
v-b-tooltip
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
<span v-if="srvModel.enforceTargetAmount">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
<span v-else>Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
<span v-if="srvModel.enforceTargetAmount"
id="crowdfund-header-target-softcap">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
<span v-else
id="crowdfund-header-target-hardcap">Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
</span>
</div>
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount"
id="crowdfund-progress-bar">
<div class="progress-bar" role="progressbar"
:aria-valuenow="srvModel.info.progressPercentage"
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
aria-valuemin="0"
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% confirmed contributions'"
id="crowdfund-progress-bar-confirmed-bar"
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% contributions'"
aria-valuemax="100">
</div>
<div class="progress-bar bg-warning" role="progressbar"
id="crowdfund-progress-bar-pending-bar"
:aria-valuenow="srvModel.info.pendingProgressPercentage"
v-bind:style="{ width: srvModel.info.pendingProgressPercentage + '%' }"
v-b-tooltip :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% pending contributions'"
v-b-tooltip :title="parseFloat(srvModel.info.pendingProgressPercentage).toFixed(2) + '% contributions pending confirmation'"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
<div class="card-body">
<div class="row py-2 text-center">
<div class="col-sm border-right" id="raised-amount">
<div class="col-sm border-right" id="crowdfund-body-raised-amount">
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
<h5 class="text-muted">Raised</h5>
</div>
<div class="col-sm border-right" id="goal-raised">
<div class="col-sm border-right" id="crowdfund-body-goal-raised">
<h5>{{ percentageRaisedAmount }}%</h5>
<h5 class="text-muted">Of Goal</h5>
</div>
<div class="col-sm border-right">
<div class="col-sm border-right" id="crowdfund-body-total-contributors">
<h5>
{{srvModel.info.totalContributors}}
</h5>
<h5 class="text-muted">Contributors</h5>
</div>
<div class="col-sm" v-if="endDiff" id="campaign-dates-started">
<div class="col-sm" v-if="endDiff" id="crowdfund-body-campaign-dates-started">
<h5>
{{endDiff}}
</h5>
<h5 class="text-muted">Left</h5>
<b-tooltip target="campaign-dates-started" >
<b-tooltip target="crowdfund-body-campaign-dates-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
@ -78,13 +83,13 @@
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="startDiff" id="campaign-dates-not-started">
<div class="col-sm" v-if="startDiff" id="crowdfund-body-campaign-dates-not-started">
<h5>
{{startDiff}}
</h5>
<h5 class="text-muted">Left to start</h5>
<b-tooltip target="campaign-dates-ended" >
<b-tooltip target="crowdfund-body-campaign-dates-not-started" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
@ -95,13 +100,13 @@
</ul>
</b-tooltip>
</div>
<div class="col-sm" v-if="ended" id="campaign-dates-ended">
<div class="col-sm" v-if="ended" id="crowdfund-body-campaign-dates-not-active">
<h5>
Campaign
</h5>
<h5 >not active</h5>
<b-tooltip target="campaign-dates-not-started" >
<b-tooltip target="crowdfund-body-campaign-dates-not-active" >
<ul class="p-0">
<li v-if="startDate" class="list-unstyled">
{{started? "Started" : "Starts"}} {{startDate}}
@ -111,14 +116,10 @@
</li>
</ul>
</b-tooltip>
</div>
</div>
<b-tooltip target="raised-amount" v-if="paymentStats && paymentStats.length > 0">
<b-tooltip target="crowdfund-body-raised-amount" v-if="paymentStats && paymentStats.length > 0">
<ul class="p-0 text-uppercase">
<li v-for="stat of paymentStats" class="list-unstyled">
@ -126,37 +127,34 @@
</li>
</ul>
</b-tooltip>
<b-tooltip target="goal-raised" v-if="srvModel.resetEvery !== 'Never'">
<b-tooltip target="crowdfund-body-goal-raised" v-if="srvModel.resetEvery !== 'Never'">
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
</b-tooltip>
<div class="card-title">
<div class="card-title" id="crowdfund-body-header">
<div class="row">
<div class="col-sm-12 col-md-8 col-lg-9">
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
<div class="col-sm-12 col-md-8 col-lg-9" id="crowdfund-body-header-tagline-container">
<h2 class="text-muted" v-if="srvModel.tagline" id="crowdfund-body-header-tagline">{{srvModel.tagline}}</h2>
</div>
<div class="col-sm-12 col-md-4 col-lg-3">
<button v-if="active" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
<div class="col-sm-12 col-md-4 col-lg-3" id="crowdfund-body-header-cta-container">
<button v-if="active" id="crowdfund-body-header-cta" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
</div>
</div>
</div>
<template v-if="srvModel.disqusEnabled">
<b-tabs>
<b-tab title="Details"active>
<div class="row ">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
<div class="row mt-2">
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12">
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:loading="loading"
:in-modal="false"
:perks="perks">
@ -173,11 +171,12 @@
<template v-else>
<hr/>
<div class="row mt-2">
<div class="col-md-8 col-sm-12">
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
<div class="col-md-8 col-sm-12" id="crowdfund-body-description-container">
<div class="card-text overflow-hidden" v-html="srvModel.description" id="crowdfund-body-description"></div>
</div>
<div class="col-md-4 col-sm-12">
<div class="col-md-4 col-sm-12" id="crowdfund-body-contribution-container">
<contribute :target-currency="srvModel.targetCurrency"
:loading="loading"
:display-perks-ranking="srvModel.displayPerksRanking"
:active="active"
:in-modal="false"
@ -207,11 +206,11 @@
</div>
</div>
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
<contribute v-if="contributeModalOpen"
:target-currency="srvModel.targetCurrency"
:active="active"
:perks="srvModel.perks"
:loading="loading"
:in-modal="true">
</contribute>
</b-modal>
@ -219,11 +218,20 @@
</div>
<script type="text/x-template" id="perks-template">
<div>
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
<div class="perks-container">
<perk v-if="!perks || perks.length ===0"
:perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}"
:target-currency="targetCurrency"
:active="active"
:loading="loading"
:in-modal="inModal">
</perk>
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active" :display-perks-ranking="displayPerksRanking" :index="index"
<perk v-for="(perk, index) in perks" :perk="perk" :key="perk.id"
:target-currency="targetCurrency"
:active="active"
:display-perks-ranking="displayPerksRanking"
:index="index"
:loading="loading"
:in-modal="inModal">
</perk>
</div>
@ -276,8 +284,12 @@
<span class='input-group-text'>{{targetCurrency}}</span>
<button
class="btn btn-primary"
:disabled="!active"
v-bind:class="{ 'btn-disabled': loading}"
:disabled="!active || loading"
type="submit">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Continue
</button>
</div>
@ -297,6 +309,7 @@
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
<perks
:perks="perks"
:loading="loading"
:in-modal="inModal"
:display-perks-ranking="displayPerksRanking"
:target-currency="targetCurrency"

View File

@ -194,6 +194,7 @@
<div class="modal-footer bg-light">
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>

View File

@ -387,10 +387,10 @@
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Return to StoreName', srvModel)"></span>
</a>
<button class="action-button close-action" v-show="isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
<span v-html="$t('Close')">{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
</div>

View File

@ -155,7 +155,11 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
}
@ -178,30 +182,17 @@
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Tax included</th>
<td>@Model.TaxIncluded</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Point of Sale Data</h3>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model.PosData)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>@posDataItem.Value</td>
}
else
{
<td colspan="2">@posDataItem.Value</td>
}
</tr>
}
</table>
<partial name="PosData" model="@Model.PosData" />
</div>
</div>
}

View File

@ -142,23 +142,31 @@
}
</tbody>
</table>
<span>
@if (Model.Skip != 0)
{
<a href="@Url.Action("ListInvoices", new
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})"><<</a><span> - </span>
}
<a href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">>></a>
</span>
})">Previous</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Invoices.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Invoices.Count) ? null : "disabled")">
<a class="page-link" href="@Url.Action("ListInvoices", new
{
searchTerm = Model.SearchTerm,
skip = Model.Skip + Model.Count,
count = Model.Count,
})">Next</a>
</li>
</ul>
</nav>
</div>
</div>

View File

@ -0,0 +1,37 @@
@model Dictionary<string, object>
<table class="table table-sm table-responsive-md">
@foreach (var posDataItem in Model)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>
@if (posDataItem.Value is string)
{
@posDataItem.Value
}
else
{
<partial name="PosData" model="@posDataItem.Value"/>
}
</td>
}
else
{
<td colspan="2">
@if (posDataItem.Value is string)
{
@posDataItem.Value
}
else
{
<partial name="PosData" model="@posDataItem.Value"/>
}
</td>
}
</tr>
}
</table>

View File

@ -7,6 +7,6 @@ namespace BTCPayServer.Views.Server
{
public enum ServerNavPages
{
Index, Users, Rates, Emails, Policies, Theme, Hangfire, Services, Maintenance, Logs
Index, Users, Rates, Emails, Policies, Theme, Services, Maintenance, Logs
}
}

View File

@ -7,6 +7,5 @@
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Theme)" asp-action="Theme">Theme</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Maintenance)" asp-action="Maintenance">Maintenance</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Logs)" asp-action="Logs">Logs</a>
<a class="nav-link @ViewData.IsActivePage(ServerNavPages.Hangfire)" href="~/hangfire" target="_blank">Hangfire</a>
</div>

View File

@ -1,10 +1,17 @@
@model string
@if(!String.IsNullOrEmpty(Model))
@if(!string.IsNullOrEmpty(Model))
{
var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success";
<div class="alert alert-@statusMessageClass alert-dismissible" role="alert">
var parsedModel = new StatusMessageModel(Model);
<div class="alert alert-@parsedModel.SeverityCSS alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
@Model
@if (!string.IsNullOrEmpty(parsedModel.Message))
{
@parsedModel.Message
}
@if (!string.IsNullOrEmpty(parsedModel.Html))
{
@Html.Raw(parsedModel.Html)
}
</div>
}

View File

@ -38,12 +38,17 @@
</p>
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
<ul>
@for (int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
</ul>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="ledgerAccountsDropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Select ledger wallet account
</button>
<div class="dropdown-menu overflow-auto" style="max-height: 200px;" aria-labelledby="ledgerAccountsDropdownMenuButton">
@for (var i = 0; i < 20; i++)
{
<a class="dropdown-item ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a>
}
</div>
</div>
</div>
</div>
<div class="form-group">
@ -58,27 +63,27 @@
<tbody>
<tr>
<td>P2WPKH</td>
<td>xpub</td>
<td>xpub...</td>
</tr>
<tr>
<td>P2SH-P2WPKH</td>
<td>xpub-[p2sh]</td>
<td>xpub...-[p2sh]</td>
</tr>
<tr>
<td>P2PKH</td>
<td>xpub-[legacy]</td>
<td>xpub...-[legacy]</td>
</tr>
<tr>
<td>Multi-sig P2WSH</td>
<td>2-of-xpub1-xpub2</td>
<td>2-of-xpub1...-xpub2...</td>
</tr>
<tr>
<td>Multi-sig P2SH-P2WSH</td>
<td>2-of-xpub1-xpub2-[p2sh]</td>
<td>2-of-xpub1...-xpub2...-[p2sh]</td>
</tr>
<tr>
<td>Multi-sig P2SH</td>
<td>2-of-xpub1-xpub2-[legacy]</td>
<td>2-of-xpub1...-xpub2...-[legacy]</td>
</tr>
</tbody>
</table>

View File

@ -31,8 +31,8 @@
<span asp-validation-for="HtmlTitle" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
<label asp-for="DefaultPaymentMethod"></label>
<select asp-for="DefaultPaymentMethod" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="DefaultLang"></label>

View File

@ -0,0 +1,68 @@
@model BTCPayServer.Models.ServerViewModels.EmailsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store Email Settings");
}
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="StatusMessage" />
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form method="post">
<div class="form-group">
<label asp-for="Settings.Server"></label>
<input asp-for="Settings.Server" class="form-control" />
<span asp-validation-for="Settings.Server" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Port"></label>
<input asp-for="Settings.Port" class="form-control" />
<span asp-validation-for="Settings.Port" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.From"></label>
<input asp-for="Settings.From" class="form-control" />
<span asp-validation-for="Settings.From" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Login"></label>
<input asp-for="Settings.Login" class="form-control" />
<span asp-validation-for="Settings.Login" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.Password"></label>
<input asp-for="Settings.Password" type="password" class="form-control" />
<span asp-validation-for="Settings.Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Settings.EnableSSL"></label>
<input asp-for="Settings.EnableSSL" type="checkbox" class="form-check-inline" />
</div>
<div class="form-group">
<label asp-for="TestEmail"></label>
<input asp-for="TestEmail" class="form-control" />
<span asp-validation-for="TestEmail" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
<button type="submit" class="btn btn-primary" name="command" value="Test">Test</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
}

View File

@ -5,6 +5,13 @@
}
<partial name="_StatusMessage" for="StatusMessage" />
@if (Model.StoreNotConfigured)
{
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span>Warning: No wallet has been linked to your BTCPay Store. See <a href="https://docs.btcpayserver.org/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet" target="_blank">this link</a> for more information on how to connect your store and wallet.</span>
</div>
}
<h4>Access token</h4>
<div class="row">
<div class="col-md-8">
@ -23,7 +30,7 @@
</tr>
</thead>
<tbody>
@foreach(var token in Model.Tokens)
@foreach (var token in Model.Tokens)
{
<tr>
<td>@token.Label</td>

View File

@ -11,6 +11,9 @@
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="alert alert-warning" role="alert">
If you are enabling Changelly support, we advise that you configure the invoice expiration to a minimum of 30 minutes as it may take longer than the default 15 minutes to convert the funds.
</div>
<p>
You can obtain API keys at
<a href="https://changelly.com/?ref_id=804298eb5753" target="_blank">

View File

@ -11,8 +11,11 @@
<div class="row">
<div class="col-md-10">
<form method="post">
<div class="alert alert-warning" role="alert">
If you are enabling CoinSwitch support, we advise that you configure the invoice expiration to a minimum of 30 minutes as it may take longer than the default 15 minutes to convert the funds.
</div>
<p>
You can obtain a merchant id at
You can obtain a merchant id at
<a href="https://coinswitch.co/switch/setup/btcpay" target="_blank">
https://coinswitch.co/switch/setup/btcpay
</a>
@ -24,10 +27,10 @@
</div>
<div class="form-group">
<label asp-for="Mode"></label>
<select asp-for="Mode" asp-items="Model.Modes" class="form-control" >
<select asp-for="Mode" asp-items="Model.Modes" class="form-control">
</select>
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check"/>

View File

@ -214,7 +214,29 @@
</table>
</div>
</div>
<div class="form-group">
<div class="form-group">
<h5>Services</h5>
</div>
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Service</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
Email
</td>
<td style="text-align:right"><a asp-action="Emails" >Modify</a></td>
</tr>
</tbody>
</table>
</div>
</div>
@if(Model.CanDelete)
{
<div class="form-group">

View File

@ -33,7 +33,7 @@
<div class="form-group">
<label asp-for="Amount"></label>
<div class="input-group">
<input asp-for="Amount" class="form-control" onkeyup='updateFiatValue();' />
<input asp-for="Amount" asp-format="{0}" class="form-control" onkeyup='updateFiatValue();' />
<div class="input-group-prepend">
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
</div>
@ -55,7 +55,23 @@
<label asp-for="SubstractFees"></label>
<input asp-for="SubstractFees" class="form-check" />
</div>
<button type="submit" class="btn btn-primary">Confirm</button>
@if (Model.AdvancedMode)
{
<div class="form-group">
<label asp-for="NoChange"></label>
<a href="https://docs.btcpayserver.org/features/wallet#make-sure-no-change-utxo-is-created-expert-mode" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
<input asp-for="NoChange" class="form-check" />
</div>
}
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
@if (Model.AdvancedMode)
{
<button name="command" type="submit" value="noob" class="btn btn-secondary">Use noob mode</button>
}
else
{
<button name="command" type="submit" value="expert" class="btn btn-secondary">Use expert mode</button>
}
</form>
</div>
</div>

View File

@ -16,6 +16,7 @@
<input type="hidden" asp-for="Amount" />
<input type="hidden" asp-for="FeeSatoshiPerByte" />
<input type="hidden" asp-for="SubstractFees" />
<input type="hidden" asp-for="NoChange" />
<p>
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />

View File

@ -29,7 +29,8 @@
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
If this wallet got restored, should have received money but nothing is showing up, please <a asp-action="WalletRescan">Rescan it</a>.
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan">rescan your wallet</a>. <br />
If some transactions appear in BTCPay Server, but are missing on Electrum or another wallet, <a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-wallet#missing-payments-in-my-software-or-hardware-wallet">follow those instructions</a>.
</div>
</div>
<div class="row">

View File

@ -21,6 +21,7 @@ function Cart() {
this.updateItemsCount();
this.updateAmount();
this.updatePosData();
}
Cart.prototype.setCustomAmount = function(amount) {
@ -243,6 +244,7 @@ Cart.prototype.updateAll = function() {
this.updateSummaryTotal();
this.updateTotal();
this.updateAmount();
this.updatePosData();
}
// Update number of cart items
@ -290,6 +292,20 @@ Cart.prototype.updateTip = function(amount) {
Cart.prototype.updateAmount = function() {
$('#js-cart-amount').val(this.getTotal(true));
}
Cart.prototype.updatePosData = function() {
var result = {
cart: this.content,
customAmount: this.fromCents(this.getCustomAmount()),
discountPercentage: this.discount? parseFloat(this.discount): 0,
subTotal: this.fromCents(this.getTotalProducts()),
discountAmount: this.fromCents(this.getDiscountAmount(this.totalAmount)),
tip: this.tip? this.tip: 0,
total: this.getTotal(true)
};
console.warn(result);
$('#js-cart-posdata').val(JSON.stringify(result));
}
Cart.prototype.resetDiscount = function() {
this.setDiscount(0);
@ -644,6 +660,7 @@ $.fn.inputAmount = function(obj, type) {
obj.updateSummaryTotal();
obj.updateAmount();
obj.updatePosData();
obj.emptyCartToggle();
});
}
@ -668,4 +685,4 @@ $.fn.removeAmount = function(obj, type) {
obj.updateSummaryTotal();
obj.emptyCartToggle();
});
}
}

View File

@ -95,9 +95,8 @@ function onDataCallback(jsonData) {
}
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/" + srvModel.paymentMethodId + "/status";
$.ajax({
url: path,
url: window.location.pathname + "/status?invoiceId=" + srvModel.invoiceId + "&paymentMethodId=" + srvModel.paymentMethodId,
type: "GET",
cache: false
}).done(function (data) {
@ -164,11 +163,8 @@ $(document).ready(function () {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
// Push the email to a server, once the reception is confirmed move on
srvModel.customerEmail = emailAddress;
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/UpdateCustomer";
$.ajax({
url: path,
url: window.location.pathname + "/UpdateCustomer?invoiceId=" + srvModel.invoiceId,
type: "POST",
data: JSON.stringify({ Email: srvModel.customerEmail }),
contentType: "application/json; charset=utf-8"
@ -240,14 +236,22 @@ $(document).ready(function () {
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
path = path.replace("https://", "wss://");
path = path.replace("http://", "ws://");
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
ws_uri = "wss:";
} else {
ws_uri = "ws:";
}
ws_uri += "//" + loc.host;
ws_uri += loc.pathname + "/status/ws?invoiceId=" + srvModel.invoiceId;
try {
var socket = new WebSocket(path);
var socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
fetchStatus();
};
socket.onerror = function (e) {
console.error("Error while connecting to websocket for invoice notifications (callback)");
};
}
catch (e) {
console.error("Error while connecting to websocket for invoice notifications");

View File

@ -18,17 +18,17 @@ addLoadEvent(function (ev) {
Vue.use(Toasted);
Vue.component('contribute', {
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking"],
props: ["targetCurrency", "active", "perks", "inModal", "displayPerksRanking", "loading"],
template: "#contribute-template"
});
Vue.component('perks', {
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking"],
props: ["perks", "targetCurrency", "active", "inModal","displayPerksRanking", "loading"],
template: "#perks-template"
});
Vue.component('perk', {
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index"],
props: ["perk", "targetCurrency", "active", "inModal", "displayPerksRanking", "index", "loading"],
template: "#perk-template",
data: function () {
return {
@ -46,24 +46,35 @@ addLoadEvent(function (ev) {
if (e) {
e.preventDefault();
}
if(!this.active){
if(!this.active || this.loading){
return;
}
eventAggregator.$emit("contribute", {amount: this.amount, choiceKey: this.perk.id});
eventAggregator.$emit("contribute", {amount: parseFloat(this.amount), choiceKey: this.perk.id});
},
expand: function(){
if(this.canExpand){
this.expanded = true;
}
},
setAmount: function (amount) {
this.amount = (amount || 0).noExponents();
this.expanded = false;
}
},
mounted: function(){
this.amount = this.perk.price.value;
mounted: function () {
this.setAmount(this.perk.price.value);
},
watch: {
perk: function (newValue, oldValue) {
if (newValue.price.value != oldValue.price.value) {
this.setAmount(newValue.price.value);
}
}
}
});
app = new Vue({
@ -84,8 +95,9 @@ addLoadEvent(function (ev) {
active: true,
animation: true,
sound: true,
lastUpdated:""
lastUpdated:"",
loading: false,
timeoutState: 0
}
},
computed: {
@ -191,9 +203,11 @@ addLoadEvent(function (ev) {
this.active = this.started && !this.ended;
setTimeout(this.updateComputed, 1000);
},
submitModalContribute: function(e){
debugger;
this.$refs.modalContribute.onContributeFormSubmit(e);
setLoading: function(val){
this.loading = val;
if(this.timeoutState){
clearTimeout(this.timeoutState);
}
}
},
mounted: function () {
@ -207,8 +221,19 @@ addLoadEvent(function (ev) {
btcpay.showFrame();
self.contributeModalOpen = false;
self.setLoading(false);
});
eventAggregator.$on("contribute", function () {
self.setLoading(true);
self.timeoutState = setTimeout(function(){
self.setLoading(false);
},5000);
});
eventAggregator.$on("invoice-error", function(error){
self.setLoading(false);
var msg = "";
if(typeof error === "string"){
msg = error;
@ -236,6 +261,7 @@ addLoadEvent(function (ev) {
if(self.animation) {
fireworks();
}
amount = parseFloat(amount).noExponents();
if(onChain){
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
iconPack: "fontawesome",

View File

@ -52,4 +52,5 @@ canvas#fireworks {
left: -15px;
top: -15px;
padding-top: 5px;
z-index: 1
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -3,6 +3,7 @@
var amount = $("#Amount").val();
var fee = $("#FeeSatoshiPerByte").val();
var substractFee = $("#SubstractFees").val();
var noChange = $("#NoChange").val();
var loc = window.location, ws_uri;
if (loc.protocol === "https:") {
@ -48,8 +49,14 @@
args += "&amount=" + amount;
args += "&feeRate=" + fee;
args += "&substractFees=" + substractFee;
args += "&noChange=" + noChange;
WriteAlert("warning", 'Please validate the transaction on your ledger');
if (noChange === "True") {
WriteAlert("warning", 'WARNING: Because you want to make sure no change UTXO is created, you will end up sending more than the chosen amount to your destination. Please validate the transaction on your ledger');
}
else {
WriteAlert("warning", 'Please validate the transaction on your ledger');
}
var confirmButton = $("#confirm-button");
confirmButton.prop("disabled", true);

File diff suppressed because one or more lines are too long

View File

@ -45,5 +45,6 @@
"txCount": "{{count}} transakce",
"txCount_plural": "{{count}} transakcí",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

View File

@ -5,7 +5,7 @@
"lang": "Sprache",
"Awaiting Payment...": "Warten auf Zahlung...",
"Pay with": "Bezahlen mit",
"Contact and Refund Email": "Kontakt und Rückerstattungs Email",
"Contact and Refund Email": "Kontakt und Rückerstattungs E-Mail",
"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": "Ihre Email-Adresse",
"Continue": "Fortsetzen",
@ -24,16 +24,16 @@
"Copied": "Kopiert",
"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.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_CalculateAmount_Error": "Wiederholen",
"ConversionTab_LoadCurrencies_Error": "Wiederholen",
"ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Umrechnungsanbieter verfügbar.",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "Bitte eine Ausgangswährung zum Geldwechsel auswählen",
"Invoice expiring soon...": "Die Rechnung läuft bald ab...",
"Invoice expired": "Die Rechnung ist abgelaufen",
"What happened?": "Was ist passiert?",
"InvoiceExpired_Body_1": "Diese Rechnung ist abgelaufen. Eine Rechnung ist nur für {{maxTimeMinutes}} Minuten gültig. \nSie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden möchten.",
"InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_3": "Sollten Ihre Gelder zu einem späteren Zeitpunkt ankommen, werden wir entweder Ihren Auftrag bearbeiten oder Sie bezüglich der Rückerstattung kontaktieren...",
"Invoice ID": "Rechnungs ID",
"Order ID": "Auftrag ID",
"Return to StoreName": "Zurück zu {{storeName}}",
@ -42,8 +42,9 @@
"Archived_Body": "Bitte kontaktieren Sie den Shop für Bestellinformationen oder Hilfe",
"BOLT 11 Invoice": "BOLT 11 Rechnung",
"Node Info": "Netzwerkknoten Info",
"txCount": "{{count}} transaktion",
"txCount_plural": "{{count}} transaktionen",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"txCount": "{{count}} Transaktion",
"txCount_plural": "{{count}} Transaktionen",
"Pay with CoinSwitch": "Zahlen mit CoinSwitch",
"Pay with Changelly": "Zahlen mit Changelly",
"Close": "Schließen"
}

View File

@ -45,5 +45,6 @@
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

View File

@ -26,7 +26,7 @@
"ConversionTab_BodyDesc": "Este servicio es provisto por terceros. Ten en cuenta que no tenemos control sobre cómo estos terceros re-enviarán tus fondos. La factura solo se marcará como pagada una vez se reciban los fondos en la cadena de bloques de {{cryptoCode}} .",
"ConversionTab_CalculateAmount_Error": "Reintentar",
"ConversionTab_LoadCurrencies_Error": "Reintentar",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos con Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Selecciona la moneda a convertir",
"Invoice expiring soon...": "La factura expira pronto...",
"Invoice expired": "La factura expiró",
@ -45,5 +45,6 @@
"txCount": "{{count}} transacción",
"txCount_plural": "{{count}} transacciones",
"Pay with CoinSwitch": "Pagar con CoinSwitch",
"Pay with Changelly": "Pagar con Changelly"
"Pay with Changelly": "Pagar con Changelly",
"Close": "Cerrar"
}

View File

@ -45,5 +45,6 @@
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

View File

@ -45,5 +45,6 @@
"txCount": "लेनदेन",
"txCount_plural": "लेनदेनों",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

View File

@ -45,5 +45,6 @@
"txCount": "{{count}} transaction",
"txCount_plural": "{{count}} transactions",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

View File

@ -0,0 +1,50 @@
{
"NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/",
"code": "hu-HU",
"currentLanguage": "Magyar",
"lang": "Nyelv",
"Awaiting Payment...": "A fizetés még folyamatban...",
"Pay with": "Fizetési mód kiválasztása",
"Contact and Refund Email": "e-mail cím kapcsolattartáshoz és visszautaláshoz",
"Contact_Body": "Kérjük adja meg az e-mail címét. Abban az esetben ha felmerülne valami a fizetése kapcsán, ezen az e-mail címen fogjuk értesíteni.",
"Your email": "e-mail címe",
"Continue": "Tovább",
"Please enter a valid email address": "Kérjük adjon meg egy érvényes e-mail címet",
"Order Amount": "Rendelés összege",
"Network Cost": "Hálozat költsége",
"Already Paid": "Már befizetve",
"Due": "Fennálló összeg",
"Scan": "Szkennelés",
"Copy": "Másolás",
"Conversion": "Átváltás",
"Open in wallet": "Nyisd ki a pénztárcában",
"CompletePay_Body": "A fizetés végrehajtásához, kérjük küldjön {{btcDue}} {{cryptoCode}} az alábbi címre.",
"Amount": "Összeg",
"Address": "Cím",
"Copied": "Másolva",
"ConversionTab_BodyTop": "Fizethet {{btcDue}} {{cryptoCode}} olyan altcoin-okkal melyeket a kereskedő nem támogat közvetlenlül.",
"ConversionTab_BodyDesc": "Ezt a szolgáltatást harmadik fél nyújtja. Kérjük figyelembe venni, hogy nem áll módunkban befolyásolni a szolgáltatók hogyan fogják továbbítani a pénzét. A számla csak akkor lesz fizetettként jelölve, miután a tranzakciója megérkezett a {{cryptoCode}} Blockchain-re. ",
"ConversionTab_CalculateAmount_Error": "Próbáld újra",
"ConversionTab_LoadCurrencies_Error": "Próbáld újra",
"ConversionTab_Lightning": "A Lightning Network tranzakciók támogatására átváltási szolgáltató nem található",
"ConversionTab_CurrencyList_Select_Option": "Valuta kiválasztása a konvertáláshoz",
"Invoice expiring soon...": "A számla rövidesen le fog járni...",
"Invoice expired": "A számla lejárt",
"What happened?": "Mi történt?",
"InvoiceExpired_Body_1": "Ez a számla lejárt. A számla csak {{maxTimeMinutes}} percig érvényes. \nHa a fizetést újra szeretné indítani, visszaléphet a(z) {{storeName}} boltba ",
"InvoiceExpired_Body_2": "Abban az esetben ha fizetni próbált, a hálozát nem fogadta el a tanzakciót. Befizetése még nem érkezett meg.",
"InvoiceExpired_Body_3": "Ha a tranzakciója később érkezne meg hozzánk, megbízását feldolgozzuk, vagy a visszautalás megbeszélése érdekében, kapcsolatba lépünk Önnel... ",
"Invoice ID": "Számla azonosító",
"Order ID": "Megbízás azonosítója",
"Return to StoreName": "Vissza a(z) {{storeName}} boltba",
"This invoice has been paid": "A számla kifizetve",
"This invoice has been archived": "A számla archiválva",
"Archived_Body": "Rendelési információkhoz vagy ha támogatásra szorul, kérjük lépjen kapcsolatba az eladóval",
"BOLT 11 Invoice": "BOLT 11 számla",
"Node Info": "Node információ",
"txCount": "{{count}} tranzakció",
"txCount_plural": "{{count}} tranzakció",
"Pay with CoinSwitch": "Fizess CoinSwitch-csel",
"Pay with Changelly": "Fizess Changelly-vel",
"Close": "Bezár"
}

View File

@ -24,10 +24,10 @@
"Copied": "Afritað",
"ConversionTab_BodyTop": "Þú getur borgað {{btcDue}} {{cryptoCode}} með altcoins.",
"ConversionTab_BodyDesc": "Þessi þjónusta er veitt af þriðja aðila. Mundu að við höfum ekki stjórn á því hvað þeir gera við peningana. Reikningurinn verður aðeins móttekinn þegar {{cryptoCode}} greiðslan hefur verið staðfest á netinu.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_CalculateAmount_Error": "reyndu aftur",
"ConversionTab_LoadCurrencies_Error": "reyndu aftur",
"ConversionTab_Lightning": "Engir viðskiptaveitendur eru í boði fyrir Lightning Network greiðslur.",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "veldu gjaldmiðil til að breyta frá",
"Invoice expiring soon...": "Reikningurinn rennur út fljótlega...",
"Invoice expired": "Reikningurinn er útrunnin",
"What happened?": "Hvað gerðist?",
@ -44,6 +44,7 @@
"Node Info": "Nótu upplýsingar",
"txCount": "{{count}} reikningur",
"txCount_plural": "{{count}} reikningar",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with CoinSwitch": "Borga með Coinswitch",
"Pay with Changelly": "Borgar með Changelly",
"Close": "Loka"
}

View File

@ -23,27 +23,28 @@
"Address": "Indirizzo",
"Copied": "Copiato",
"ConversionTab_BodyTop": "Puoi pagare {{btcDue}} {{cryptoCode}} usando altcoin diverse da quelle che il commerciante supporta direttamente.",
"ConversionTab_BodyDesc": "Questo servizio è fornito da terze parti. Si prega di tenere presente che non abbiamo alcun controllo su come i fornitori inoltreranno i fondi. La fattura verrà contrassegnata solo dopo aver ricevuto i fondi su {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Retry",
"ConversionTab_LoadCurrencies_Error": "Retry",
"ConversionTab_BodyDesc": "Questo servizio è fornito da terze parti. Ricorda che non abbiamo alcun controllo su come tali parti inoltreranno i fondi. La fattura verrà contrassegnata come pagata solo dopo aver ricevuto i fondi su {{cryptoCode}} Blockchain.",
"ConversionTab_CalculateAmount_Error": "Riprova",
"ConversionTab_LoadCurrencies_Error": "Riprova",
"ConversionTab_Lightning": "Nessun fornitore di conversione disponibile per i pagamenti Lightning Network.",
"ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from",
"ConversionTab_CurrencyList_Select_Option": "Seleziona una valuta da convertire",
"Invoice expiring soon...": "Fattura in scadenza a breve...",
"Invoice expired": "Fattura scaduta",
"What happened?": "Cosa è successo?",
"InvoiceExpired_Body_1": "Questa fattura è scaduta. Una fattura è valida solo per {{maxTime minuti}} minuti. \nPuoi tornare a {{store name}} se desideri inviare nuovamente il pagamento.",
"InvoiceExpired_Body_2": "Se hai provato a inviare un pagamento, non è ancora stato accettato dalla rete. Non abbiamo ancora ricevuto i tuoi fondi.",
"InvoiceExpired_Body_3": "",
"InvoiceExpired_Body_1": "Questa fattura è scaduta. Una fattura è valida solo per {{maxTimeMinutes}} minuti. \nPuoi tornare a {{storeName}} se desideri inviare nuovamente il pagamento.",
"InvoiceExpired_Body_2": "Se hai provato ad inviare un pagamento, non è ancora stato accettato dalla rete. Non abbiamo ancora ricevuto i tuoi fondi.",
"InvoiceExpired_Body_3": "Se lo riceviamo in un secondo momento, processeremo comunque il tuo ordine o ti contatteremo per rimborsarti...",
"Invoice ID": "Numero della Fattura",
"Order ID": "Numero dell'Ordine",
"Return to StoreName": "Ritorna a {{storeName}}",
"This invoice has been paid": "La fattura è stata pagata",
"This invoice has been archived": "TQuesta fattura è stata pagata",
"This invoice has been archived": "Questa fattura è stata archiviata",
"Archived_Body": "Contatta il negozio per informazioni sull'ordine o per assistenza",
"BOLT 11 Invoice": "Fattura BOLT 11",
"Node Info": "Informazioni sul Nodo",
"txCount": "{{count}} transazione",
"txCount_plural": "{{count}} transazioni",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with CoinSwitch": "Paga con CoinSwitch",
"Pay with Changelly": "Paga con Changelly",
"Close": "Chiudi"
}

View File

@ -44,6 +44,7 @@
"Node Info": "接続情報",
"txCount": "取引 {{count}} 個",
"txCount_plural": "取引 {{count}} 個",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with CoinSwitch": "CoinSwitchでのお支払い",
"Pay with Changelly": "Changellyでのお支払い",
"Close": "閉じる"
}

View File

@ -45,5 +45,6 @@
"txCount": "{{count}} Транзакция",
"txCount_plural": "{{count}} Транзакциялар",
"Pay with CoinSwitch": "Pay with CoinSwitch",
"Pay with Changelly": "Pay with Changelly"
"Pay with Changelly": "Pay with Changelly",
"Close": "Close"
}

Some files were not shown because too many files have changed in this diff Show More