Compare commits

...

144 Commits

Author SHA1 Message Date
65f5a38b4a bump 2018-02-19 15:14:07 +09:00
271cbf682f fix casing 2018-02-19 15:13:45 +09:00
a634593903 Big refactoring renaming cryptoData => PaymentMethod 2018-02-19 15:09:05 +09:00
af94de93d1 Add some comments 2018-02-19 11:31:34 +09:00
35f669aa15 Isolating code of on chain specific payment in its own folder 2018-02-19 11:06:08 +09:00
4795bd8108 Add some sanity check, make sure to use CrytpoDataId everywhere 2018-02-19 03:35:19 +09:00
29aed99fd1 prevent a crash if the new property DerivationStrategies is notset at invoice level 2018-02-19 02:56:44 +09:00
aa4519ac30 Big refactoring for supporting new type of payment 2018-02-19 02:38:03 +09:00
752133b01c fix bug 2018-02-18 20:37:42 +09:00
fe0c21ba08 Make sure that IPN sent for the send invoice are sent one at a time 2018-02-18 16:09:09 +09:00
90904a6b5e bump 2018-02-18 02:42:38 +09:00
8e3f7ea68d do not block next invoices if one invoice fail processing 2018-02-18 02:40:53 +09:00
a239104a28 fix uppercase 2018-02-18 02:36:11 +09:00
3bc232e1da Further isolate bitcoin related stuff inside BitcoinLikePaymentData 2018-02-18 02:35:02 +09:00
a1ee09cd85 Further abstract payment data by encapsulating bitcoin related logic into BitcoinLikePaymentData 2018-02-18 02:19:35 +09:00
b898cc030c general code cleanup + add analyzers 2018-02-17 13:18:16 +09:00
0602353dd2 fix bug happening if only btc is supported 2018-02-17 01:55:38 +09:00
9d406923ae make sure the waitingInvoices tasks are done 2018-02-17 01:43:43 +09:00
aa8565e3cc forgot remove dev time stuff 2018-02-17 01:35:30 +09:00
5de330b1f9 Refactoring to keep coin logic out of InvoiceWatcher 2018-02-17 01:34:40 +09:00
66597aed46 hide websocket exceptions 2018-02-15 16:17:27 +09:00
3071826f06 bump 2018-02-15 15:17:41 +09:00
c3684eb064 BTCPayWallet should be singleton per cryptcode 2018-02-15 15:17:12 +09:00
335dd9e66d bump 2018-02-15 14:44:35 +09:00
cd1611dbcd make sure to not spam too much NBXplorer 2018-02-15 14:44:08 +09:00
c17793aca9 do not freeze the stores page 2018-02-15 13:33:29 +09:00
01d898b618 Caching GetCoins 2018-02-15 13:02:12 +09:00
17069c311b Remove transaction cache 2018-02-15 12:42:48 +09:00
921d072942 fix electrum format for add derivation 2018-02-15 11:47:45 +09:00
6181e8b3e4 Refactor the code to prepare the group to support of another hardware wallet 2018-02-13 16:57:40 +09:00
93fc12bb2e fix typo 2018-02-13 15:28:22 +09:00
8e73c1a2f0 error message if not using segwit 2018-02-13 13:22:37 +09:00
e97c15578d bump nbxplorer 2018-02-13 12:42:03 +09:00
fd4f4e6aff better feedback if forgot to activate browser support 2018-02-13 12:20:56 +09:00
cedf8f75e8 Small UI adjustements 2018-02-13 11:41:21 +09:00
cd0a650df4 Ledger wallet support 2018-02-13 03:27:36 +09:00
6d6b9e2ba6 disable test not passing because of bitpay 2018-02-10 22:24:50 +09:00
fd915fdc5c fix test 2018-02-10 22:21:52 +09:00
59be813fe9 bump 2018-02-10 22:10:36 +09:00
465fbdd47f Fix bug which can happen if parsing of CoinAverage decimal is on another culture 2018-02-10 22:03:33 +09:00
f220abb716 Make the address verification step mandatory 2018-02-07 21:59:16 +09:00
db46ca87d7 do not share cache between long and short profile 2018-02-01 21:34:07 +01:00
d873a1a545 Set a longer timeout for the cache for /rates, update NBXPlorer, bump 2018-02-01 21:24:13 +01:00
a464a8702b Merge pull request from Eskyee/patch-2
bootstrap.min.css deleted last line
2018-01-26 07:02:35 +01:00
63722b932a Merge pull request from Eskyee/patch-1
bootstrap.css deleted last line
2018-01-26 07:02:22 +01:00
698b3c46cd bootstrap.min.css deleted last line
deleted this line
/*# sourceMappingURL=bootstrap.min.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404
(Not Found) (bootstrap.min.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.min.css.map
2018-01-26 01:19:58 +00:00
df81051d07 bootstrap.css deleted last line
deleted this line 
/*# sourceMappingURL=bootstrap.css.map */
because of a debug error in browser's
[Error] Failed to load resource: the server responded with a status of 404 
(Not Found) (bootstrap.css.map, line 0)
https://btcpay-server-testnet.azurewebsites.net/vendor/bootstrap/css/bootstrap.css.map
2018-01-26 01:12:52 +00:00
ac70a77361 Fix with paidOver + paidLate 2018-01-24 10:37:23 +01:00
59a2432af9 Better invoice loop, fix javascript 2018-01-20 14:09:57 +09:00
ea4fa8d5d4 Mock rate provider 2018-01-20 12:30:22 +09:00
ade3eff75c unwrap rates in api/rates 2018-01-20 12:11:24 +09:00
db2a2a2b6c Fix expiration message on checkout page 2018-01-20 00:33:37 +09:00
579dcb5af8 Fix confirmation message when changing altcoin derivation scheme 2018-01-20 00:29:20 +09:00
69247dee8a Fix api/rates allow to scope by cyrptoCode and storeId 2018-01-19 18:11:43 +09:00
7b9541b8e9 Do not crash if some of the altcoins are unavailable 2018-01-19 17:39:15 +09:00
a12e4d7f64 fix typo 2018-01-19 17:14:27 +09:00
897da9b07a Better explanation for the price source 2018-01-19 17:13:29 +09:00
293525d480 do not query the rate source if the preferred exchange did not changed 2018-01-19 16:19:13 +09:00
198e810355 Store can customize rate source 2018-01-19 16:00:20 +09:00
fe25e00c94 Fix https://github.com/btcpayserver/btcpayserver/issues/38 2018-01-19 10:52:44 +09:00
8b129ab2e5 Merge pull request from lepipele/dev-lepi
Resolving problems with Vue console warnings
2018-01-19 01:41:01 +09:00
770bed54d1 bump 2018-01-19 00:52:38 +09:00
774817d4ac Add transaction speed on the invoice page 2018-01-19 00:52:17 +09:00
b8068b2ae8 Vue ignoring custom HTML5 elements
Ref: https://github.com/btcpayserver/btcpayserver/issues/34#issuecomment-358541767
2018-01-18 09:48:21 -06:00
3007a6bbc8 Upgrading Vue and linking production (min) version 2018-01-18 09:47:39 -06:00
1c0c8fece2 Change default speed to medium 2018-01-19 00:37:00 +09:00
c52eee47f0 bump 2018-01-19 00:13:40 +09:00
f88c98b9d9 fix block explorer link for mainnet 2018-01-18 23:57:41 +09:00
b0e9e10f7e Add extended notifications 2018-01-18 20:56:55 +09:00
39d47e33f6 Fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 18:53:11 +09:00
4b7b6c6327 debug 2018-01-18 18:33:26 +09:00
a59edc5e8c bump 2018-01-18 18:12:44 +09:00
5ba322ea6a Add debug messages 2018-01-18 18:12:01 +09:00
b47b4b10cb should fix https://github.com/btcpayserver/btcpayserver/issues/31 2018-01-18 17:21:29 +09:00
c88f391935 Merge branch 'master' of https://github.com/btcpayserver/btcpayserver 2018-01-18 12:45:55 +09:00
26d3178e93 Fix expiration transitioning with a delay 2018-01-18 12:45:39 +09:00
1ad27c7827 Merge pull request from thijstriemstra/patch-1
fix typos
2018-01-18 11:48:05 +09:00
4200a8eed5 add c# comment 2018-01-17 17:45:57 +01:00
daceb7af8e happy new year 2018-01-17 17:43:57 +01:00
f703b53bce fix typos 2018-01-17 17:42:06 +01:00
2762224f0f Fix parsing bug zpub LTC mainnet 2018-01-17 19:39:15 +09:00
86fc64d184 Bump 2018-01-17 16:34:24 +09:00
726cd6fd49 Add badge if not on mainnet in the top bar 2018-01-17 16:34:01 +09:00
be1c4666e0 resize videos 2018-01-17 16:28:09 +09:00
b75dfc4191 Merge branch 'lepipele-dev-lepi' 2018-01-17 16:23:21 +09:00
97815f8daf Merge branch 'dev-lepi' of https://github.com/lepipele/btcpayserver into lepipele-dev-lepi 2018-01-17 16:18:54 +09:00
af16e1db69 update docker test 2018-01-17 16:17:27 +09:00
5f6913b3a2 Can tweak the rate at store level 2018-01-17 15:59:31 +09:00
2b31af80cb Can configure invoice expiration 2018-01-17 15:14:53 +09:00
3f9889d374 Update docker, remove tx cache, use new nbxplorer 2018-01-17 15:02:53 +09:00
f8189c64a4 Non blocking modal that shows sync progress
Ref: https://forkbitpay.slack.com/archives/C7M093Z55/p1515557792000053
2018-01-16 10:37:06 -06:00
c9b5f89f17 Make sure BTCPayServer exit cleanly in case of crash during startup, see https://github.com/aspnet/Hosting/issues/1194 2018-01-15 14:42:51 +09:00
ecb82f2cc9 do not send IPN if not set 2018-01-15 00:22:40 +09:00
f340c6eb7f Add more events to invoice 2018-01-14 22:06:06 +09:00
ba0e080816 Invoices has events recorded 2018-01-14 21:48:23 +09:00
bb3d107309 Fix legacy rate not being correct for alt only payment 2018-01-14 15:26:14 +09:00
8517b222bf Add redirect url to the invoice page 2018-01-14 15:01:09 +09:00
aed32204b5 Fix crashing ManageController 2018-01-14 14:52:15 +09:00
6b4eeff3f1 add tests, do not returns bitcoinAddress field if not supported by the invoice 2018-01-13 22:01:09 +09:00
e3cc589ebb fix nbxplorer 2018-01-13 21:07:04 +09:00
4a152e8ffc fix build 2018-01-13 17:32:08 +09:00
d54a9474d1 Fixing exception thrown when invoice is paid and supporting only LTC 2018-01-13 17:23:09 +09:00
98472211fc bump 2018-01-13 12:55:05 +09:00
099c9fa1f9 Fix balance calculation when there is unconfirmed tx 2018-01-13 12:53:56 +09:00
5226b77ffc Fix bug happening when removing data of nbxplorer 2018-01-13 02:28:23 +09:00
290779ee39 bump 2018-01-13 01:48:43 +09:00
4f39a8060c Fixing bug of uncorrectly unassigned addresses 2018-01-13 01:48:19 +09:00
92caa98dfb bump 2018-01-13 01:06:18 +09:00
df7bb9e2f8 Add info about nbxplorer synching 2018-01-13 01:05:38 +09:00
02a039d695 Fixing bug when targetting testnet 2018-01-12 22:36:13 +09:00
b5e4c803aa fix bad network throwing nullreferenceexception 2018-01-12 22:08:20 +09:00
2b7c70622f bump 2018-01-12 21:55:13 +09:00
88779e7129 Make sure websockets does not throw, fix annying warning of emails 2018-01-12 18:32:46 +09:00
6beb7abfd2 fix test 2018-01-12 17:04:47 +09:00
a1ebedc0d1 Fix unit test 2018-01-12 16:54:57 +09:00
d5ad0cdb39 Fix a edge case "The instance of entity type 'HistoricalAddressInvoiceData'" 2018-01-12 16:42:10 +09:00
39fb8dbb6a better handle case when BTC is not supported by a store 2018-01-12 16:30:34 +09:00
58194cb060 Fix tests, clean code of Options 2018-01-12 16:00:31 +09:00
ef165e15bf Fix config discovery when datadir is set 2018-01-12 13:45:25 +09:00
bf871c46ec update nbxplorer 2018-01-12 13:32:27 +09:00
52331e057f update nbxplorer 2018-01-12 13:16:17 +09:00
b59021a0be Cleanup the design for multi currencies support, use a single nbxplorer instead of two 2018-01-12 11:54:57 +09:00
8596e16feb make default configuration just work with multi chains 2018-01-11 22:52:28 +09:00
223558c01d Adding transaction caching 2018-01-11 21:01:00 +09:00
3a91965187 Some refactoring improving performance, and better tests for multiple currencies 2018-01-11 17:29:48 +09:00
55d50af39d BTCWallet is single currency, introduce BTCWalletProvider 2018-01-11 14:36:12 +09:00
3ff293ab7f Fix 2018-01-10 21:14:09 +09:00
7bcf2b5472 fix slack link 2018-01-10 19:42:38 +09:00
983f34814f bump, add field to know if replaced 2018-01-10 18:40:53 +09:00
a33e20b46b fix invoice page 2018-01-10 18:38:49 +09:00
bafdcb04ed fix error if invoice is requested without existing 2018-01-10 18:33:05 +09:00
cb4468d3b3 Fixing payment in different crypto 2018-01-10 18:30:45 +09:00
de6f0008a6 bump 2018-01-10 15:43:39 +09:00
7618eacef1 Fix: altcoin payment not detected after another coin payment 2018-01-10 15:43:07 +09:00
842e083ebe bump nbxplorer 2018-01-10 12:18:34 +09:00
1c510df3fc bump 2018-01-10 02:41:33 +09:00
c80ffe396e forgot passing cancellation 2018-01-10 02:13:49 +09:00
401a31e5c2 Cleanup code 2018-01-10 02:07:42 +09:00
2df60bd121 bump 2018-01-09 23:55:37 +09:00
6d10c8a6c1 Can change crypto on checkout page 2018-01-09 23:55:08 +09:00
44898b5e23 Checkout page: Bind crypto image to client cycle view model, add logo on main QR code 2018-01-09 22:43:36 +09:00
c0f53db561 fix sync bar 2018-01-09 18:52:16 +09:00
133fb96d28 bump 2018-01-09 17:27:54 +09:00
98b7ad62af Fix order accounting 2018-01-09 17:27:26 +09:00
111 changed files with 17187 additions and 12041 deletions
BTCPayServer.Tests
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojBTCPayServer.ruleset
Configuration
Controllers
Data
Eclair
EventAggregator.cs
Events
ExplorerClientProvider.csExtensions.cs
HostedServices
Hosting
Logging
Migrations
Models
Payments
Program.cs
Properties
SearchString.cs
Services
Views
wwwroot
DockerfileLICENSEREADME.mdglobal.json

@ -4,6 +4,7 @@
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>

@ -43,21 +43,15 @@ namespace BTCPayServer.Tests
{
get; set;
}
public string CookieFile
{
get; set;
}
public Uri LTCNBXplorerUri { get; set; }
public Uri ServerUri
{
get;
set;
}
public ExtKey HDPrivateKey
{
get; set;
}
public string Postgres
{
get; set;
@ -73,27 +67,42 @@ namespace BTCPayServer.Tests
{
if (!Directory.Exists(_Directory))
Directory.CreateDirectory(_Directory);
string chain = ChainType.Regtest.ToNetwork().Name;
string chainDirectory = Path.Combine(_Directory, chain);
if (!Directory.Exists(chainDirectory))
Directory.CreateDirectory(chainDirectory);
HDPrivateKey = new ExtKey();
StringBuilder config = new StringBuilder();
config.AppendLine($"regtest=1");
config.AppendLine($"{chain.ToLowerInvariant()}=1");
config.AppendLine($"port={Port}");
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"explorer.cookiefile={CookieFile}");
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
config.AppendLine($"chains=btc,ltc");
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
config.AppendLine($"btc.explorer.cookiefile=0");
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
config.AppendLine($"ltc.explorer.cookiefile=0");
if (Postgres != null)
config.AppendLine($"postgres=" + Postgres);
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
var confPath = Path.Combine(chainDirectory, "settings.config");
File.WriteAllText(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
_Host = new WebHostBuilder()
.UseConfiguration(conf)
.ConfigureServices(s =>
{
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
var mockRates = new MockRateProviderFactory();
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
mockRates.AddMock(btc);
mockRates.AddMock(ltc);
s.AddSingleton<IRateProviderFactory>(mockRates);
s.AddLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)

@ -1,4 +1,4 @@
FROM microsoft/dotnet:2.0.0-sdk
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
WORKDIR /app
# caches restore result by copying csproj file separately
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj

@ -12,7 +12,7 @@ namespace BTCPayServer.Tests
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
{
this.parent = parent;
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
//RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
}

@ -18,6 +18,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Threading;
using BTCPayServer.Eclair;
using System.Globalization;
namespace BTCPayServer.Tests
{
@ -47,14 +48,20 @@ namespace BTCPayServer.Tests
Directory.CreateDirectory(_Directory);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
{
NBXplorerUri = ExplorerClient.Address,
LTCNBXplorerUri = LTCExplorerClient.Address,
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
};
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
PayTester.Start();
@ -102,15 +109,23 @@ namespace BTCPayServer.Tests
{
return new TestAccount(this);
}
public BTCPayNetworkProvider NetworkProvider { get; private set; }
public RPCClient ExplorerNode
{
get; set;
}
public RPCClient LTCExplorerNode
{
get; set;
}
public ExplorerClient ExplorerClient
{
get; set;
}
public ExplorerClient LTCExplorerClient { get; set; }
HttpClient _Http = new HttpClient();
@ -212,12 +227,6 @@ namespace BTCPayServer.Tests
get; set;
}
public Network Network
{
get;
set;
} = Network.RegTest;
public void Dispose()
{
if (PayTester != null)

@ -46,31 +46,55 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
await store.Pair(pairingCode.ToString(), StoreId);
}
public StoresController CreateStore()
public StoresController CreateStore(string cryptoCode = null)
{
return CreateStoreAsync().GetAwaiter().GetResult();
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
}
public async Task<StoresController> CreateStoreAsync()
public string CryptoCode { get; set; } = "BTC";
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
{
ExtKey = new ExtKey().GetWif(parent.Network);
cryptoCode = cryptoCode ?? CryptoCode;
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
var store = parent.PayTester.GetController<StoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.UpdateStore(StoreId, new StoreViewModel()
{
SpeedPolicy = SpeedPolicy.MediumSpeed
});
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
await store.UpdateStore(StoreId, vm);
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = "BTC",
CryptoCurrency = cryptoCode,
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, "Save");
return store;
}
public BTCPayNetwork SupportedNetwork { get; set; }
public void RegisterDerivationScheme(string crytoCode)
{
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
}
public async Task RegisterDerivationSchemeAsync(string crytoCode)
{
var store = parent.PayTester.GetController<StoresController>(UserId);
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
CryptoCurrency = crytoCode,
DerivationSchemeFormat = crytoCode,
DerivationScheme = derivation.ToString(),
Confirmation = true
}, "Save");
}
public DerivationStrategyBase DerivationScheme { get; set; }
private async Task RegisterAsync()

@ -24,6 +24,10 @@ using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Eclair;
using System.Collections.Generic;
using BTCPayServer.Models.StoreViewModels;
using System.Threading.Tasks;
using System.Globalization;
using BTCPayServer.Payments;
namespace BTCPayServer.Tests
{
@ -43,75 +47,85 @@ namespace BTCPayServer.Tests
entity.TxFee = Money.Coins(0.1m);
entity.Rate = 5000;
var cryptoData = entity.GetCryptoData("BTC");
Assert.NotNull(cryptoData); // Should use legacy data to build itself
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
var accounting = cryptoData.Calculate();
// Some check that handling legacy stuff does not break things
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
paymentMethod.Calculate();
Assert.NotNull(paymentMethod);
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
////////////////////
var accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(1.1m), accounting.Due);
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
Assert.Equal(Money.Coins(0.7m), accounting.Due);
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(0.6m), accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
accounting = cryptoData.Calculate();
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
entity = new InvoiceEntity();
entity.ProductInformation = new ProductInformation() { Price = 5000 };
entity.SetCryptoData(new System.Collections.Generic.Dictionary<string, CryptoData>(new KeyValuePair<string, CryptoData>[] {
new KeyValuePair<string,CryptoData>("BTC", new CryptoData()
{
Rate = 1000,
TxFee = Money.Coins(0.1m)
}),
new KeyValuePair<string,CryptoData>("LTC", new CryptoData()
{
Rate = 500,
TxFee = Money.Coins(0.01m)
})
}));
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
TxFee = Money.Coins(0.1m)
});
paymentMethods.Add(new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
TxFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
entity.Payments = new List<PaymentEntity>();
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(5.1m), accounting.Due);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
@ -120,16 +134,16 @@ namespace BTCPayServer.Tests
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
@ -139,8 +153,8 @@ namespace BTCPayServer.Tests
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
cryptoData = entity.GetCryptoData("BTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
@ -148,8 +162,8 @@ namespace BTCPayServer.Tests
Assert.Equal(accounting.Paid, accounting.TotalDue);
Assert.Equal(2, accounting.TxCount);
cryptoData = entity.GetCryptoData("LTC");
accounting = cryptoData.Calculate();
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
accounting = paymentMethod.Calculate();
Assert.Equal(Money.Zero, accounting.Due);
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
@ -270,7 +284,8 @@ namespace BTCPayServer.Tests
OrderId = "orderId",
NotificationURL = callbackServer.GetUri().AbsoluteUri,
ItemDesc = "Some description",
FullNotifications = true
FullNotifications = true,
ExtendedNotifications = true
});
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
@ -301,7 +316,7 @@ namespace BTCPayServer.Tests
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
var store2 = acc.CreateStore();
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
}
}
@ -319,24 +334,26 @@ namespace BTCPayServer.Tests
Currency = "USD"
}, Facade.Merchant);
var payment1 = Money.Coins(0.04m);
var payment2 = Money.Coins(0.08m);
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
var payment2 = invoice.BtcDue;
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
{
invoice.BitcoinAddress.ToString(),
invoice.BitcoinAddress,
payment1.ToString(),
null, //comment
null, //comment_to
false, //subtractfeefromamount
true, //replaceable
}).ResultString);
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment1, invoice.BtcPaid);
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
Assert.Equal("paid", invoice.Status);
Assert.Equal("paidOver", invoice.ExceptionStatus.ToString());
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
});
var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1));
@ -344,18 +361,17 @@ namespace BTCPayServer.Tests
{
input.ScriptSig = Script.Empty; //Strip signatures
}
var change = tx.Outputs.First(o => o.Value != payment1);
var output = tx.Outputs.First(o => o.Value == payment1);
output.Value = payment2;
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
change.Value -= (payment2 - payment1) * 2; //Add more fees
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
tester.ExplorerNode.SendRawTransaction(replaced);
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(payment2, invoice.BtcPaid);
Assert.Equal("False", invoice.ExceptionStatus.ToString());
});
}
}
@ -384,6 +400,198 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanTweakRate()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
storeController.UpdateStore(user.StoreId, vm).Wait();
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
}
}
[Fact]
public void CanHaveLTCOnlyStore()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.CryptoCode = "LTC";
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 500,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
var cashCow = tester.LTCExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
var firstPayment = Money.Coins(0.1m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("LTC", checkout.CryptoCode);
//////////////////////
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
Assert.Null(invoice.BitcoinAddress);
Assert.NotEqual(1.0, invoice.Rate);
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal("paid", invoice.Status);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Equal("paid", checkout.Status);
});
}
}
[Fact]
public void CanPayWithTwoCurrencies()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
// First we try payment with a merchant having only BTC
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
var cashCow = tester.ExplorerNode;
cashCow.Generate(2); // get some money in case
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
var firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
var controller = tester.PayTester.GetController<InvoiceController>(null);
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
Assert.Single(checkout.AvailableCryptos);
Assert.Equal("BTC", checkout.CryptoCode);
//////////////////////
// Retry now with LTC enabled
user.RegisterDerivationScheme("LTC");
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 5000.0,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
cashCow = tester.ExplorerNode;
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
firstPayment = Money.Coins(0.04m);
cashCow.SendToAddress(invoiceAddress, firstPayment);
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.True(invoice.BtcPaid == firstPayment);
});
cashCow = tester.LTCExplorerNode;
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
Assert.NotNull(ltcCryptoInfo);
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
cashCow.SendToAddress(invoiceAddress, secondPayment);
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
Eventually(() =>
{
invoice = user.BitPay.GetInvoice(invoice.Id);
Assert.Equal(Money.Zero, invoice.BtcDue);
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
Assert.Equal(Money.Zero, ltcPaid.Due);
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
Assert.Equal("paid", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
});
controller = tester.PayTester.GetController<InvoiceController>(null);
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
Assert.Equal(2, checkout.AvailableCryptos.Count);
Assert.Equal("LTC", checkout.CryptoCode);
}
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{
@ -398,8 +606,6 @@ namespace BTCPayServer.Tests
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
//RedirectURL = redirect + "redirect",
//NotificationURL = CallbackUri + "/notification",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
@ -428,11 +634,11 @@ namespace BTCPayServer.Tests
Assert.Equal("new", invoice.Status);
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
var firstPayment = Money.Coins(0.04m);
@ -466,9 +672,9 @@ namespace BTCPayServer.Tests
Assert.True(IsMapped(localInvoice, ctx));
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress.ToString());
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
Assert.NotNull(historical1.UnAssigned);
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress.ToString());
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
Assert.Null(historical2.UnAssigned);
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
secondPayment = localInvoice.BtcDue;
@ -552,17 +758,21 @@ namespace BTCPayServer.Tests
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
//Manually check that cache get hit after 10 sec
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash;
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetHash() == h) != null;
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
}
private void Eventually(Action act)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
CancellationTokenSource cts = new CancellationTokenSource(20000);
while (true)
{
try

@ -13,25 +13,27 @@ namespace BTCPayServer.Tests
{
// Unit test that generates temorary checkout Bitpay page
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
[Fact]
public void BitpayCheckout()
{
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
var url = new Uri("https://test.bitpay.com/");
var btcpay = new Bitpay(key, url);
var invoice = btcpay.CreateInvoice(new Invoice()
{
Price = 5.0,
Currency = "USD",
PosData = "posData",
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
ItemDesc = "Hello from the otherside"
}, Facade.Merchant);
// Testnet of Bitpay down
//[Fact]
//public void BitpayCheckout()
//{
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
// var url = new Uri("https://test.bitpay.com/");
// var btcpay = new Bitpay(key, url);
// var invoice = btcpay.CreateInvoice(new Invoice()
// {
// go to invoice.Url
Console.WriteLine(invoice.Url);
}
// Price = 5.0,
// Currency = "USD",
// PosData = "posData",
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
// ItemDesc = "Hello from the otherside"
// }, Facade.Merchant);
// // go to invoice.Url
// Console.WriteLine(invoice.Url);
//}
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
[Fact]

@ -1,4 +1,4 @@
version: "3"
version: "3"
# Run `docker-compose up dev` for bootstrapping your development environment
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
@ -10,17 +10,17 @@ services:
context: ..
dockerfile: BTCPayServer.Tests/Dockerfile
environment:
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://bitcoin-nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://litecoin-nbxplorer:32839/
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
TESTS_PORT: 80
TESTS_HOSTNAME: tests
expose:
- "80"
links:
- bitcoin-nbxplorer
- litecoin-nbxplorer
- nbxplorer
- postgres
extra_hosts:
- "tests:127.0.0.1"
@ -33,45 +33,32 @@ services:
regtest=1
connect=bitcoind:39388
links:
- bitcoin-nbxplorer
- litecoin-nbxplorer
- nbxplorer
- postgres
bitcoin-nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.40
nbxplorer:
image: nicolasdorier/nbxplorer:1.0.1.13
ports:
- "32838:32838"
expose:
- "32838"
environment:
NBXPLORER_NETWORK: regtest
NBXPLORER_RPCURL: http://bitcoind:43782/
NBXPLORER_RPCUSER: ceiwHEbqWI83
NBXPLORER_RPCPASSWORD: DwubwWsoo3
NBXPLORER_NODEENDPOINT: bitcoind:39388
NBXPLORER_CHAINS: "btc,ltc"
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_LTCRPCURL: http://litecoind:43782/
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- bitcoind
litecoin-nbxplorer:
image: nicolasdorier/nbxplorer:1.0.0.40
ports:
- "32839:32839"
expose:
- "32839"
environment:
NBXPLORER_NETWORK: ltc-regtest
NBXPLORER_RPCURL: http://litecoind:43782/
NBXPLORER_RPCUSER: ceiwHEbqWI83
NBXPLORER_RPCPASSWORD: DwubwWsoo3
NBXPLORER_NODEENDPOINT: litecoind:39388
NBXPLORER_BIND: 0.0.0.0:32839
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
- litecoind
- litecoind
bitcoind:
container_name: btcpayserver_dev_bitcoind
@ -85,15 +72,11 @@ services:
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
ports:
- "43782:43782"
expose:
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
litecoind:
container_name: btcpayserver_dev_litecoind
@ -107,15 +90,11 @@ services:
rpcport=43782
port=39388
whitelist=0.0.0.0/0
zmqpubrawblock=tcp://0.0.0.0:29000
zmqpubrawtx=tcp://0.0.0.0:29000
txindex=1
ports:
- "43783:43782"
expose:
- "43782" # RPC
- "39388" # P2P
- "29000" # zmq
postgres:
image: postgres:9.6.5

@ -1,12 +1,49 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer;
namespace BTCPayServer
{
public class BTCPayDefaultSettings
{
static BTCPayDefaultSettings()
{
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
{
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
chainType == ChainType.Regtest ? Network.RegTest :
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
var settings = new BTCPayDefaultSettings();
_Settings.Add(chainType, settings);
settings.ChainType = chainType;
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
chainType == ChainType.Regtest ? 23002 :
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
}
}
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
{
return _Settings[chainType];
}
public string DefaultDataDirectory { get; set; }
public string DefaultConfigurationFile { get; set; }
public ChainType ChainType { get; internal set; }
public int DefaultPort { get; set; }
}
public class BTCPayNetwork
{
public Network NBitcoinNetwork { get; set; }
@ -25,5 +62,15 @@ namespace BTCPayServer
}
public string CryptoImagePath { get; set; }
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
public BTCPayDefaultSettings DefaultSettings { get; set; }
public KeyPath CoinType { get; internal set; }
public int MaxTrackedConfirmation { get; internal set; } = 6;
public override string ToString()
{
return CryptoCode;
}
}
}

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitBitcoin()
{
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
var coinaverage = new CoinAverageRateProvider("BTC");
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
});
}
}
}

@ -0,0 +1,33 @@
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 InitLitecoin()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
var ltcRate = new CoinAverageRateProvider("LTC");
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
});
}
}
}

@ -1,93 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
namespace BTCPayServer
{
public class BTCPayNetworkProvider
public partial class BTCPayNetworkProvider
{
static BTCPayNetworkProvider()
{
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
}
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
public BTCPayNetworkProvider(Network network)
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
public NBXplorerNetworkProvider NBXplorerNetworkProvider
{
var coinaverage = new CoinAverageRateProvider("BTC");
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
var ltcRate = new CoinAverageRateProvider("LTC");
if (network == Network.Main)
get
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.Main,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "https://live.blockcypher.com/ltc/tx/{0}/",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Mainnet,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
return _NBXplorerNetworkProvider;
}
}
if (network == Network.TestNet)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.TestNet,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Testnet,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
}
if (network == Network.RegTest)
{
Add(new BTCPayNetwork()
{
CryptoCode = "BTC",
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
NBitcoinNetwork = Network.RegTest,
UriScheme = "bitcoin",
DefaultRateProvider = btcRate,
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
});
Add(new BTCPayNetwork()
{
CryptoCode = "LTC",
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Regtest,
UriScheme = "litecoin",
DefaultRateProvider = ltcRate,
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
});
}
public BTCPayNetworkProvider(ChainType chainType)
{
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
InitBitcoin();
InitLitecoin();
}
[Obsolete("To use only for legacy stuff")]

@ -2,7 +2,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.0.57</Version>
<Version>1.0.1.36</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
@ -20,23 +21,26 @@
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
<PackageReference Include="NBitpayClient" Version="1.0.0.14" />
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
<PackageReference Include="NBitcoin" Version="4.0.0.55" />
<PackageReference Include="NBitpayClient" Version="1.0.0.17" />
<PackageReference Include="DBreeze" Version="1.87.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.0.26" />
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
@ -99,4 +103,10 @@
<ItemGroup>
<Folder Include="Build\" />
</ItemGroup>
<ItemGroup>
<None Update="devtest.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
<Name Resource="MinimumRecommendedRules_Name" />
<Description Resource="MinimumRecommendedRules_Description" />
</Localization>
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1001" Action="Warning" />
<Rule Id="CA1009" Action="Warning" />
<Rule Id="CA1016" Action="Warning" />
<Rule Id="CA1033" Action="Warning" />
<Rule Id="CA1049" Action="Warning" />
<Rule Id="CA1060" Action="Warning" />
<Rule Id="CA1061" Action="Warning" />
<Rule Id="CA1063" Action="Warning" />
<Rule Id="CA1065" Action="Warning" />
<Rule Id="CA1301" Action="Warning" />
<Rule Id="CA1400" Action="Warning" />
<Rule Id="CA1401" Action="Warning" />
<Rule Id="CA1403" Action="Warning" />
<Rule Id="CA1404" Action="Warning" />
<Rule Id="CA1405" Action="Warning" />
<Rule Id="CA1410" Action="Warning" />
<Rule Id="CA1415" Action="Warning" />
<Rule Id="CA1821" Action="Warning" />
<Rule Id="CA1900" Action="Warning" />
<Rule Id="CA1901" Action="Warning" />
<Rule Id="CA2002" Action="Warning" />
<Rule Id="CA2100" Action="Warning" />
<Rule Id="CA2101" Action="Warning" />
<Rule Id="CA2108" Action="Warning" />
<Rule Id="CA2111" Action="Warning" />
<Rule Id="CA2112" Action="Warning" />
<Rule Id="CA2114" Action="Warning" />
<Rule Id="CA2116" Action="Warning" />
<Rule Id="CA2117" Action="Warning" />
<Rule Id="CA2122" Action="Warning" />
<Rule Id="CA2123" Action="Warning" />
<Rule Id="CA2124" Action="Warning" />
<Rule Id="CA2126" Action="Warning" />
<Rule Id="CA2131" Action="Warning" />
<Rule Id="CA2132" Action="Warning" />
<Rule Id="CA2133" Action="Warning" />
<Rule Id="CA2134" Action="Warning" />
<Rule Id="CA2137" Action="Warning" />
<Rule Id="CA2138" Action="Warning" />
<Rule Id="CA2140" Action="Warning" />
<Rule Id="CA2141" Action="Warning" />
<Rule Id="CA2146" Action="Warning" />
<Rule Id="CA2147" Action="Warning" />
<Rule Id="CA2149" Action="Warning" />
<Rule Id="CA2200" Action="Warning" />
<Rule Id="CA2202" Action="Warning" />
<Rule Id="CA2207" Action="Warning" />
<Rule Id="CA2212" Action="Warning" />
<Rule Id="CA2213" Action="Warning" />
<Rule Id="CA2214" Action="Warning" />
<Rule Id="CA2216" Action="Warning" />
<Rule Id="CA2220" Action="Warning" />
<Rule Id="CA2229" Action="Warning" />
<Rule Id="CA2231" Action="Warning" />
<Rule Id="CA2232" Action="Warning" />
<Rule Id="CA2235" Action="Warning" />
<Rule Id="CA2236" Action="Warning" />
<Rule Id="CA2237" Action="Warning" />
<Rule Id="CA2238" Action="Warning" />
<Rule Id="CA2240" Action="Warning" />
<Rule Id="CA2241" Action="Warning" />
<Rule Id="CA2242" Action="Warning" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
</RuleSet>

@ -13,9 +13,16 @@ using NBXplorer;
namespace BTCPayServer.Configuration
{
public class NBXplorerConnectionSetting
{
public string CryptoCode { get; internal set; }
public Uri ExplorerUri { get; internal set; }
public string CookieFile { get; internal set; }
}
public class BTCPayServerOptions
{
public Network Network
public ChainType ChainType
{
get; set;
}
@ -35,52 +42,44 @@ namespace BTCPayServer.Configuration
set;
}
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
{
get;
set;
} = new List<NBXplorerConnectionSetting>();
public void LoadArgs(IConfiguration conf)
{
var networkInfo = DefaultConfiguration.GetNetwork(conf);
Network = networkInfo?.Network;
if (Network == null)
throw new ConfigException("Invalid network");
ChainType = DefaultConfiguration.GetChainType(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
Logs.Configuration.LogInformation("Network: " + Network);
foreach (var net in new BTCPayNetworkProvider(Network).GetAll())
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.ToUpperInvariant());
var validChains = new List<string>();
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(net.NBitcoinNetwork.Name);
var explorer = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", null);
var cookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
if (explorer != null)
if (supportedChains.Contains(net.CryptoCode))
{
ExplorerFactories.Add(net.CryptoCode, (n) => CreateExplorerClient(n, explorer, cookieFile));
validChains.Add(net.CryptoCode);
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
setting.CryptoCode = net.CryptoCode;
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
NBXplorerConnectionSettings.Add(setting);
}
}
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
if(!string.IsNullOrEmpty(invalidChains))
throw new ConfigException($"Invalid chains {invalidChains}");
// Handle legacy explorer.url and explorer.cookiefile
if (ExplorerFactories.Count == 0)
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(Network.Name); // Will get BTC info
var explorer = conf.GetOrDefault<Uri>($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute));
var cookieFile = conf.GetOrDefault<string>($"explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
ExplorerFactories.Add("BTC", (n) => CreateExplorerClient(n, explorer, cookieFile));
}
//////
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
}
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBitcoinNetwork, uri);
if (!explorer.SetCookieAuth(cookieFile))
explorer.SetNoAuth();
return explorer;
}
public Dictionary<string, Func<BTCPayNetwork, ExplorerClient>> ExplorerFactories = new Dictionary<string, Func<BTCPayNetwork, ExplorerClient>>();
public string PostgresConnectionString
{
get;

@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
{
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
{
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
if (str == null)
return defaultValue;
if (typeof(T) == typeof(bool))
@ -32,12 +32,12 @@ namespace BTCPayServer.Configuration
return (T)(object)str;
else if (typeof(T) == typeof(IPEndPoint))
{
var separator = str.LastIndexOf(":");
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
if (separator == -1)
throw new FormatException();
var ip = str.Substring(0, separator);
var port = str.Substring(separator + 1);
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
}
else if (typeof(T) == typeof(int))
{

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using NBitcoin;
using System.Text;
using CommandLine;
using NBXplorer;
namespace BTCPayServer.Configuration
{
@ -17,20 +18,24 @@ namespace BTCPayServer.Configuration
{
protected override CommandLineApplication CreateCommandLineApplicationCore()
{
var provider = new BTCPayNetworkProvider(ChainType.Main);
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
CommandLineApplication app = new CommandLineApplication(true)
{
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
Name = "BTCPay"
};
app.HelpOption("-? | -h | --help");
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
foreach (var network in new BTCPayNetworkProvider(Network.Main).GetAll())
foreach (var network in provider.GetAll())
{
app.Option($"--{network.CryptoCode}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: If no explorer is specified, the default for Bitcoin will be selected)", CommandOptionType.SingleValue);
app.Option($"--{network.CryptoCode}explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
var crypto = network.CryptoCode.ToLowerInvariant();
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
}
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
return app;
@ -40,57 +45,64 @@ namespace BTCPayServer.Configuration
protected override string GetDefaultDataDir(IConfiguration conf)
{
return GetNetwork(conf).DefaultDataDirectory;
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
}
protected override string GetDefaultConfigurationFile(IConfiguration conf)
{
var network = GetNetwork(conf);
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
var dataDir = conf["datadir"];
if (dataDir == null)
return network.DefaultConfigurationFile;
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
return Path.Combine(dataDir, fileName);
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
chainDir = Path.Combine(dataDir, chainDir);
try
{
if (!Directory.Exists(chainDir))
Directory.CreateDirectory(chainDir);
}
catch { }
return Path.Combine(chainDir, fileName);
}
public static NetworkInformation GetNetwork(IConfiguration conf)
public static ChainType GetChainType(IConfiguration conf)
{
var network = conf.GetOrDefault<string>("network", null);
if (network != null)
{
var info = NetworkInformation.GetNetworkByName(network);
if (info == null)
throw new ConfigException($"Invalid network name {network}");
return info;
var n = Network.GetNetwork(network);
if (n == null)
{
throw new ConfigException($"Invalid network parameter '{network}'");
}
return n.ToChainType();
}
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
return NetworkInformation.GetNetworkByName(net.Name);
return net;
}
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
{
var network = GetNetwork(conf);
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
StringBuilder builder = new StringBuilder();
builder.AppendLine("### Global settings ###");
builder.AppendLine("#testnet=0");
builder.AppendLine("#regtest=0");
builder.AppendLine("#network=mainnet");
builder.AppendLine();
builder.AppendLine("### Server settings ###");
builder.AppendLine("#port=" + network.DefaultPort);
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
builder.AppendLine("#bind=127.0.0.1");
builder.AppendLine();
builder.AppendLine("### Database ###");
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
builder.AppendLine();
builder.AppendLine("### NBXplorer settings ###");
foreach (var n in new BTCPayNetworkProvider(network.Network).GetAll())
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
{
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(n.NBitcoinNetwork.ToString());
builder.AppendLine($"#{n.CryptoCode}.explorer.url={nbxplorer.GetDefaultExplorerUrl()}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ nbxplorer.GetDefaultCookieFile()}");
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
}
return builder.ToString();
}
@ -99,7 +111,7 @@ namespace BTCPayServer.Configuration
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
{
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
}
}
}

@ -1,83 +0,0 @@
using Microsoft.Extensions.Configuration;
using NBitcoin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration
{
public class NetworkInformation
{
static NetworkInformation()
{
_Networks = new Dictionary<string, NetworkInformation>();
foreach (var network in new[] { Network.Main, Network.TestNet, Network.RegTest })
{
NetworkInformation info = new NetworkInformation();
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
info.Network = network;
info.DefaultPort = 23002;
_Networks.Add(network.Name, info);
if (network == Network.Main)
{
info.DefaultPort = 23000;
}
if (network == Network.TestNet)
{
info.DefaultPort = 23001;
}
}
}
static Dictionary<string, NetworkInformation> _Networks;
public static NetworkInformation GetNetworkByName(string name)
{
var value = _Networks.TryGet(name);
if (value != null)
return value;
//Maybe alias ?
var network = Network.GetNetwork(name);
if (network != null)
{
value = _Networks.TryGet(network.Name);
if (value != null)
return value;
}
return null;
}
public Network Network
{
get; set;
}
public string DefaultConfigurationFile
{
get;
set;
}
public string DefaultDataDirectory
{
get;
set;
}
public int DefaultPort
{
get;
private set;
}
public override string ToString()
{
return Network.ToString();
}
public static string ToStringAll()
{
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
}
}
}

@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using BTCPayServer.Logging;
namespace BTCPayServer.Controllers
{
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly ILogger _logger;
StoreRepository storeRepository;
RoleManager<IdentityRole> _RoleManager;
SettingsRepository _SettingsRepository;
ILogger _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
StoreRepository storeRepository,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
SettingsRepository settingsRepository,
ILogger<AccountController> logger)
SettingsRepository settingsRepository)
{
this.storeRepository = storeRepository;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_RoleManager = roleManager;
_SettingsRepository = settingsRepository;
_logger = Logs.PayServer;
}
[TempData]
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load two-factor authentication user.");
}
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
if (admin.Count == 0)
{
_logger.LogInformation("Admin created.");
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
}
@ -291,7 +289,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
/// <summary>
/// <summary>
/// Test property
/// </summary>
public string RegisteredUserId

@ -9,13 +9,14 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode?}")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
{
@ -23,11 +24,12 @@ namespace BTCPayServer.Controllers
cryptoCode = "BTC";
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
return NotFound();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
PaymentRequest request = new PaymentRequest
{
DetailsVersion = 1
@ -35,7 +37,7 @@ namespace BTCPayServer.Controllers
request.Details.Expires = invoice.ExpirationTime;
request.Details.Memo = invoice.ProductInformation.ItemDesc;
request.Details.Network = network.NBitcoinNetwork;
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
request.Details.Time = DateTimeOffset.UtcNow;
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
@ -66,14 +68,18 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.IsExpired())
return NotFound();
if (cryptoCode == null)
cryptoCode = "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new Services.Invoices.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
return NotFound();
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
return NotFound();
var payment = PaymentMessage.Load(Request.Body);
var unused = _Wallet.BroadcastTransactionsAsync(network, payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}
}

@ -20,6 +20,7 @@ using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
using NBXplorer;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
@ -33,7 +34,8 @@ namespace BTCPayServer.Controllers
{
UserId = GetUserId(),
InvoiceId = invoiceId,
IncludeAddresses = true
IncludeAddresses = true,
IncludeEvents = true
})).FirstOrDefault();
if (invoice == null)
return NotFound();
@ -47,6 +49,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
Id = invoice.Id,
Status = invoice.Status,
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
RefundEmail = invoice.RefundMail,
CreatedDate = invoice.InvoiceTime,
ExpirationDate = invoice.ExpirationTime,
@ -55,36 +58,65 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation,
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
NotificationUrl = invoice.NotificationURL,
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
};
foreach (var data in invoice.GetCryptoData())
foreach (var data in invoice.GetPaymentMethods(null))
{
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
var accounting = data.Value.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
var accounting = data.Calculate();
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
cryptoPayment.Address = data.Value.DepositAddress.ToString();
cryptoPayment.Rate = FormatCurrency(data.Value);
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
if(onchainMethod != null)
{
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
}
cryptoPayment.Rate = FormatCurrency(data);
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
model.CryptoPayments.Add(cryptoPayment);
}
var payments = invoice
.Payments
.GetPayments()
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(async payment =>
{
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var m = new InvoiceDetailsModel.Payment();
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
m.CryptoCode = payment.GetCryptoCode();
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
m.TransactionId = payment.Outpoint.Hash.ToString();
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
int confirmationCount = 0;
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
{
confirmationCount = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
}
else
{
confirmationCount = paymentData.ConfirmationCount;
}
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
{
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
}
else
{
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
}
m.TransactionId = paymentData.Outpoint.Hash.ToString();
m.ReceivedTime = payment.ReceivedTime;
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
m.Replaced = !payment.Accounted;
return m;
})
.ToArray();
@ -97,75 +129,105 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("i/{invoiceId}")]
[Route("i/{invoiceId}/{cryptoCode}")]
[Route("i/{invoiceId}/{paymentMethodId}")]
[Route("invoice")]
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
[XFrameOptionsAttribute(null)]
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
{
//Keep compatibility with Bitpay
invoiceId = invoiceId ?? id;
id = invoiceId;
////
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return View(nameof(Checkout), model);
}
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (cryptoCode == null)
cryptoCode = store.GetDefaultCrypto();
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (invoice == null || network == null || !invoice.Support(network))
if (invoice == null)
return null;
var cryptoData = invoice.GetCryptoData(network);
var store = await _StoreRepository.FindStore(invoice.StoreId);
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
{
paymentMethodIdStr = store.GetDefaultCrypto();
isDefaultCrypto = true;
}
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
if (invoice == null || network == null)
return null;
if (!invoice.Support(paymentMethodId))
{
if(!isDefaultCrypto)
return null;
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
network = paymentMethodTemp.Network;
paymentMethodId = paymentMethodTemp.GetId();
}
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var currency = invoice.ProductInformation.Currency;
var accounting = cryptoData.Calculate();
var accounting = paymentMethod.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
BtcAddress = cryptoData.DepositAddress,
BtcAmount = (accounting.TotalDue - cryptoData.TxFee).ToString(),
BtcTotalDue = accounting.TotalDue.ToString(),
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
CustomerEmail = invoice.RefundMail,
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.ProductInformation.ItemDesc,
Rate = FormatCurrency(cryptoData),
Rate = FormatCurrency(paymentMethod),
MerchantRefLink = invoice.RedirectURL ?? "/",
StoreName = store.StoreName,
TxFees = cryptoData.TxFee.ToString(),
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
TxCount = accounting.TxCount,
BtcPaid = accounting.Paid.ToString(),
Status = invoice.Status,
CryptoImage = "/" + Url.Content(network.CryptoImagePath)
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv=> new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoImage = "/" + kv.Network.CryptoImagePath,
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
}).Where(c => c.CryptoImage != "/")
.ToList()
};
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = PrettyPrint(expiration);
return model;
}
private string FormatCurrency(CryptoData cryptoData)
private string FormatCurrency(PaymentMethod paymentMethod)
{
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
return FormatCurrency(cryptoData.Rate, currency);
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
return FormatCurrency(paymentMethod.Rate, currency);
}
public string FormatCurrency(decimal price, string currency)
{
@ -176,19 +238,19 @@ namespace BTCPayServer.Controllers
{
StringBuilder builder = new StringBuilder();
if (expiration.Days >= 1)
builder.Append(expiration.Days.ToString());
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
if (expiration.Hours >= 1)
builder.Append(expiration.Hours.ToString("00"));
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
return builder.ToString();
}
[HttpGet]
[Route("i/{invoiceId}/status")]
[Route("i/{invoiceId}/{cryptoCode}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
[Route("i/{invoiceId}/{paymentMethodId}/status")]
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
{
var model = await GetInvoiceModel(invoiceId, cryptoCode);
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
if (model == null)
return NotFound();
return Json(model);
@ -208,8 +270,8 @@ namespace BTCPayServer.Controllers
try
{
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
@ -220,7 +282,7 @@ namespace BTCPayServer.Controllers
finally
{
leases.Dispose();
await CloseSocket(webSocket);
await webSocket.CloseSocket();
}
return new EmptyResult();
}
@ -236,22 +298,7 @@ namespace BTCPayServer.Controllers
{
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
}
catch { await CloseSocket(webSocket); }
}
private static async Task CloseSocket(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { webSocket.Dispose(); }
catch { try { webSocket.Dispose(); } catch { } }
}
[HttpPost]
@ -385,6 +432,7 @@ namespace BTCPayServer.Controllers
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
{
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
return RedirectToAction(nameof(ListInvoices));
}

@ -39,13 +39,14 @@ using Microsoft.AspNetCore.Mvc.Routing;
using NBXplorer.DerivationStrategy;
using NBXplorer;
using BTCPayServer.HostedServices;
using BTCPayServer.Payments;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
BTCPayWalletProvider _WalletProvider;
IRateProviderFactory _RateProviders;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
public InvoiceController(InvoiceRepository invoiceRepository,
CurrencyNameTable currencyNameTable,
UserManager<ApplicationUser> userManager,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
IRateProviderFactory rateProviders,
StoreRepository storeRepository,
EventAggregator eventAggregator,
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
_UserManager = userManager;
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
@ -78,11 +79,11 @@ namespace BTCPayServer.Controllers
}
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
{
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
if (derivationStrategies.Count == 0)
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
var entity = new InvoiceEntity
{
InvoiceTime = DateTimeOffset.UtcNow
@ -94,7 +95,7 @@ namespace BTCPayServer.Controllers
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
notificationUri = null;
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
entity.OrderId = invoice.OrderId;
entity.ServerUrl = serverUrl;
@ -116,45 +117,79 @@ namespace BTCPayServer.Controllers
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
var queries = derivationStrategies
.Select(derivationStrategy =>
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
Network: derivationStrategy.Network,
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
.Where(_ => _.Wallet != null &&
_.FeeRateProvider != null &&
_.RateProvider != null)
.Select(_ =>
{
return new
{
network = derivationStrategy.Network,
getFeeRate = _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network).GetFeeRateAsync(),
getRate = _RateProviders.GetRateProvider(derivationStrategy.Network).GetRateAsync(invoice.Currency),
getAddress = _Wallet.ReserveAddressAsync(derivationStrategy)
network = _.Network,
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
};
});
var cryptoDatas = new Dictionary<string, CryptoData>();
bool legacyBTCisSet = false;
var paymentMethods = new PaymentMethodDictionary();
foreach (var q in queries)
{
CryptoData cryptoData = new CryptoData();
cryptoData.CryptoCode = q.network.CryptoCode;
cryptoData.FeeRate = (await q.getFeeRate);
cryptoData.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : cryptoData.FeeRate.GetFee(100); // assume price for 100 bytes
cryptoData.Rate = await q.getRate;
cryptoData.DepositAddress = (await q.getAddress).ToString();
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.SetId(new PaymentMethodId(q.network.CryptoCode, PaymentTypes.BTCLike));
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
onchainMethod.FeeRate = (await q.getFeeRate);
onchainMethod.TxFee = GetTxFee(storeBlob, onchainMethod.FeeRate); // assume price for 100 bytes
paymentMethod.Rate = await q.getRate;
onchainMethod.DepositAddress = (await q.getAddress);
paymentMethod.SetPaymentMethodDetails(onchainMethod);
#pragma warning disable CS0618
if (q.network.IsBTC)
{
entity.TxFee = cryptoData.TxFee;
entity.Rate = cryptoData.Rate;
entity.DepositAddress = cryptoData.DepositAddress;
legacyBTCisSet = true;
entity.TxFee = paymentMethod.TxFee;
entity.Rate = paymentMethod.Rate;
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
paymentMethods.Add(paymentMethod);
}
entity.SetCryptoData(cryptoDatas);
if (!legacyBTCisSet)
{
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
#pragma warning disable CS0618
var btc = _NetworkProvider.BTC;
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
if (feeProvider != null && rateProvider != null)
{
var gettingFee = feeProvider.GetFeeRateAsync();
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
entity.Rate = await gettingRate;
}
#pragma warning restore CS0618
}
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
var resp = entity.EntityToDTO(_NetworkProvider);
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
}
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
{
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
}
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
{
if (transactionSpeed == null)

@ -20,6 +20,7 @@ using NBitcoin;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Services.Mails;
using System.Globalization;
namespace BTCPayServer.Controllers
{
@ -33,7 +34,6 @@ namespace BTCPayServer.Controllers
private readonly ILogger _logger;
private readonly UrlEncoder _urlEncoder;
TokenRepository _TokenRepository;
private readonly BTCPayWallet _Wallet;
IHostingEnvironment _Env;
StoreRepository _StoreRepository;
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers
ILogger<ManageController> logger,
UrlEncoder urlEncoder,
TokenRepository tokenRepository,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository,
IHostingEnvironment env)
{
@ -57,7 +57,6 @@ namespace BTCPayServer.Controllers
_logger = logger;
_urlEncoder = urlEncoder;
_TokenRepository = tokenRepository;
_Wallet = wallet;
_Env = env;
_StoreRepository = storeRepository;
}
@ -436,7 +435,7 @@ namespace BTCPayServer.Controllers
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
@ -526,7 +525,7 @@ namespace BTCPayServer.Controllers
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
return string.Format(CultureInfo.InvariantCulture,
AuthenicatorUriFormat,
_urlEncoder.Encode("BTCPayServer"),
_urlEncoder.Encode(email),

@ -7,34 +7,68 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Filters;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
namespace BTCPayServer.Controllers
{
public class RateController : Controller
{
IRateProvider _RateProvider;
IRateProviderFactory _RateProviderFactory;
BTCPayNetworkProvider _NetworkProvider;
CurrencyNameTable _CurrencyNameTable;
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
StoreRepository _StoreRepo;
public RateController(
IRateProviderFactory rateProviderFactory,
BTCPayNetworkProvider networkProvider,
StoreRepository storeRepo,
CurrencyNameTable currencyNameTable)
{
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
_NetworkProvider = networkProvider;
_StoreRepo = storeRepo;
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
{
var allRates = (await _RateProvider.GetRatesAsync());
return new DataWrapper<NBitpayClient.Rate[]>
(allRates.Select(r =>
var result = await GetRates2(cryptoCode, storeId);
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
if(rates == null)
return result;
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
{
cryptoCode = cryptoCode ?? "BTC";
var network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
return NotFound();
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
if (rateProvider == null)
return NotFound();
if (storeId != null)
{
var store = await _StoreRepo.FindStore(storeId);
if (store == null)
return NotFound();
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
}
var allRates = (await rateProvider.GetRatesAsync());
return Json(allRates.Select(r =>
new NBitpayClient.Rate()
{
Code = r.Currency,
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
Value = r.Value
}).Where(n => n.Name != null).ToArray());
}
}
}

@ -2,21 +2,31 @@
using BTCPayServer.Data;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Fees;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBitpayClient;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Controllers
@ -28,27 +38,33 @@ namespace BTCPayServer.Controllers
public class StoresController : Controller
{
public StoresController(
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
IHostingEnvironment env)
{
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_TokenController = tokenController;
_Wallet = wallet;
_WalletProvider = walletProvider;
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
}
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
BTCPayWallet _Wallet;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
StoreRepository _Repo;
TokenRepository _TokenRepository;
@ -87,6 +103,208 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/wallet")]
public async Task<IActionResult> Wallet(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(storeId);
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for substract fees"); }
}
if (command == "test")
{
result = await hw.Test();
}
if (command == "getxpub")
{
result = await hw.GetExtPubKey(network);
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || !await hw.SupportDerivation(network, strategy))
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
feeRateValue,
changeAddress.Item1,
changeAddress.Item2);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
if (!directStrategy.Segwit)
return null;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
[HttpGet]
public async Task<IActionResult> ListStores()
{
@ -95,7 +313,10 @@ namespace BTCPayServer.Controllers
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
.Select(async ss => (await _Wallet.GetBalance(ss)).ToString() + " " + ss.Network.CryptoCode))
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
@ -113,6 +334,21 @@ namespace BTCPayServer.Controllers
return View(result);
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
@ -160,6 +396,9 @@ namespace BTCPayServer.Controllers
AddDerivationSchemes(store, vm);
vm.StatusMessage = StatusMessage;
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
return View(vm);
}
@ -190,15 +429,17 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
return View(vm);
}
[HttpPost]
[Route("{storeId}/derivations")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null)
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
{
selectedScheme = selectedScheme ?? "BTC";
vm.ServerUrl = GetStoreUrl(storeId);
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
@ -210,17 +451,38 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
{
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
return View(vm);
}
if (command == "Save")
DerivationStrategyBase strategy = null;
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
vm.Confirmation = false;
return View(vm);
}
if (strategy == null || vm.Confirmation)
{
try
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
await _Wallet.TrackAsync(strategy);
vm.DerivationScheme = strategy.ToString();
}
if (strategy != null)
await wallet.TrackAsync(strategy);
store.SetDerivationStrategy(network, vm.DerivationScheme);
}
catch
@ -230,29 +492,22 @@ namespace BTCPayServer.Controllers
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
try
{
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
var line = strategy.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(scheme.Network.NBitcoinNetwork).ToString()));
}
}
catch
for (int i = 0; i < 10; i++)
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
}
@ -265,6 +520,8 @@ namespace BTCPayServer.Controllers
{
return View(model);
}
if (model.PreferredExchange != null)
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
@ -297,12 +554,31 @@ namespace BTCPayServer.Controllers
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
if (store.SetStoreBlob(blob))
{
needUpdate = true;
}
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
{
using (HttpClient client = new HttpClient())
{
var rate = await client.GetAsync(model.RateSource);
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
{
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
return View(model);
}
}
}
if (needUpdate)
{
await _Repo.UpdateStore(store);
@ -315,7 +591,7 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
{
if (format == "Electrum")
{
@ -329,7 +605,7 @@ namespace BTCPayServer.Controllers
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, new string[] { });
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
@ -337,7 +613,7 @@ namespace BTCPayServer.Controllers
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBitcoinNetwork == Network.Main ? 0x0488b21eU : 0x043587cf, false);
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
@ -349,7 +625,7 @@ namespace BTCPayServer.Controllers
}
}
return DerivationStrategy.Parse(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme).ToString(), network);
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme);
}
[HttpGet]
@ -415,6 +691,7 @@ namespace BTCPayServer.Controllers
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
}
GeneratedPairingCode = pairingCode;
return RedirectToAction(nameof(RequestPairing), new
{
pairingCode = pairingCode,
@ -422,6 +699,8 @@ namespace BTCPayServer.Controllers
});
}
public string GeneratedPairingCode { get; set; }
[HttpGet]
[Route("/api-tokens")]
[Route("{storeId}/Tokens/Create")]

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using NBitcoin;
namespace BTCPayServer.Data
@ -20,28 +21,30 @@ namespace BTCPayServer.Data
#pragma warning disable CS0618
public ScriptId GetHash()
public string GetAddress()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return new ScriptId(Address);
return new ScriptId(Address.Substring(0, index));
return Address;
return Address.Substring(0, index);
}
public AddressInvoiceData SetHash(ScriptId scriptId, string cryptoCode)
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
{
Address = scriptId + "#" + cryptoCode;
Address = address + "#" + paymentMethodId?.ToString();
return this;
}
public string GetCryptoCode()
public PaymentMethodId GetpaymentMethodId()
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
if (index == -1)
return "BTC";
return Address.Substring(index + 1);
return PaymentMethodId.Parse("BTC");
/////////////////////////
return PaymentMethodId.Parse(Address.Substring(index + 1));
}
#pragma warning restore CS0618

@ -26,6 +26,11 @@ namespace BTCPayServer.Data
get; set;
}
public DbSet<InvoiceEventData> InvoiceEvents
{
get; set;
}
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
{
get; set;
@ -132,6 +137,15 @@ namespace BTCPayServer.Data
o.InvoiceDataId,
#pragma warning disable CS0618
o.Address
#pragma warning restore CS0618
});
builder.Entity<InvoiceEventData>()
.HasKey(o => new
{
o.InvoiceDataId,
#pragma warning disable CS0618
o.UniqueId
#pragma warning restore CS0618
});
}

@ -35,7 +35,7 @@ namespace BTCPayServer.Data
{
if (Address == null)
return null;
var index = Address.IndexOf("#");
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
if (index == -1)
return Address;
return Address.Substring(0, index);

@ -32,6 +32,11 @@ namespace BTCPayServer.Data
get; set;
}
public List<InvoiceEventData> Events
{
get; set;
}
public List<RefundAddressesData> RefundAddresses
{
get; set;

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Data
{
public class InvoiceEventData
{
public string InvoiceDataId
{
get; set;
}
public string UniqueId { get; internal set; }
public DateTimeOffset Timestamp
{
get; set;
}
public string Message { get; set; }
}
}

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Data
{
@ -99,10 +100,10 @@ namespace BTCPayServer.Data
if (!existing && string.IsNullOrEmpty(derivationScheme))
{
if(network.IsBTC)
if (network.IsBTC)
DerivationStrategy = null;
}
else if(!existing)
else if (!existing)
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
// This is deprecated so we don't have to set anymore
//if (network.IsBTC)
@ -173,10 +174,27 @@ namespace BTCPayServer.Data
}
}
public class RateRule
{
public RateRule()
{
RuleName = "Multiplier";
}
public string RuleName { get; set; }
public double Multiplier { get; set; }
public decimal Apply(BTCPayNetwork network, decimal rate)
{
return rate * (decimal)Multiplier;
}
}
public class StoreBlob
{
public StoreBlob()
{
InvoiceExpiration = 15;
MonitoringExpiration = 60;
}
public bool NetworkFeeDisabled
@ -190,5 +208,59 @@ namespace BTCPayServer.Data
get;
set;
}
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int InvoiceExpiration
{
get;
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule>();
RateRules.Add(new RateRule() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())
{
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
if (rateProvider is CachedRateProvider cachedRateProvider)
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
cachedRateProvider.Inner
});
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
}
else
{
rateProvider = new FallbackRateProvider(new IRateProvider[] {
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
rateProvider
});
}
}
if (RateRules == null || RateRules.Count == 0)
return rateProvider;
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
}
}
}

@ -33,7 +33,7 @@ namespace BTCPayServer.Eclair
public Task<GetInfoResponse> GetInfoAsync()
{
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", Array.Empty<object>()));
}
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
@ -104,7 +104,7 @@ namespace BTCPayServer.Eclair
public async Task<AllChannelResponse[]> AllChannelsAsync()
{
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
}
public string[] Channels()
@ -114,7 +114,7 @@ namespace BTCPayServer.Eclair
public async Task<string[]> ChannelsAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
return await SendCommandAsync<string[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
}
public void Close(string channelId)
@ -155,7 +155,7 @@ namespace BTCPayServer.Eclair
public async Task<string[]> AllNodesAsync()
{
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", Array.Empty<object>())).ConfigureAwait(false);
}
public Uri Address { get; private set; }

@ -139,7 +139,7 @@ namespace BTCPayServer.Eclair
public IEnumerable<LightMoney> Split(int parts)
{
if (parts <= 0)
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
long remain;
long result = DivRem(_MilliSatoshis, parts, out remain);
@ -431,7 +431,7 @@ namespace BTCPayServer.Eclair
/// <returns></returns>
public string ToString(bool fplus, bool trimExcessZero = true)
{
var fmt = string.Format("{{0:{0}{1}B}}",
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
(fplus ? "+" : null),
(trimExcessZero ? "2" : "11"));
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
@ -479,7 +479,7 @@ namespace BTCPayServer.Eclair
unitToUseInCalc = LightMoneyUnit.BTC;
break;
}
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
var zeros = new string('0', decPos);
var rest = new string('#', 11 - decPos);
var fmt = plus && val > 0 ? "+" : string.Empty;

@ -88,7 +88,9 @@ namespace BTCPayServer
}
}
Logs.Events.LogInformation($"New event: {evt.ToString()}");
var log = evt.ToString();
if(!String.IsNullOrEmpty(log))
Logs.Events.LogInformation(log);
foreach (var sub in actionList)
{
try

@ -2,16 +2,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceDataChangedEvent
{
public InvoiceDataChangedEvent(InvoiceEntity invoice)
{
InvoiceId = invoice.Id;
Status = invoice.Status;
ExceptionStatus = invoice.ExceptionStatus;
}
public string InvoiceId { get; set; }
public string Status { get; internal set; }
public string ExceptionStatus { get; internal set; }
public override string ToString()
{
return $"Invoice {InvoiceId} data changed";
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
{
return $"Invoice status is {Status}";
}
else
{
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
}
}
}
}

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceEvent
{
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
{
}
public InvoiceEvent(string invoiceId, int code, string name)
{
InvoiceId = invoiceId;
EventCode = code;
Name = name;
}
public string InvoiceId { get; set; }
public int EventCode { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceIPNEvent
{
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
{
InvoiceId = invoiceId;
EventCode = eventCode;
Name = name;
}
public int? EventCode { get; set; }
public string Name { get; set; }
public string InvoiceId { get; set; }
public string Error { get; set; }
public override string ToString()
{
string ipnType = "IPN";
if(EventCode.HasValue)
{
ipnType = $"IPN ({EventCode.Value} {Name})";
}
if (Error == null)
return $"{ipnType} sent for invoice {InvoiceId}";
return $"Error while sending {ipnType}: {Error}";
}
}
}

@ -2,15 +2,15 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoicePaymentEvent
public class InvoiceNeedUpdateEvent
{
public InvoicePaymentEvent(string invoiceId)
public InvoiceNeedUpdateEvent(string invoiceId)
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
InvoiceId = invoiceId;
}
@ -18,7 +18,7 @@ namespace BTCPayServer.Events
public override string ToString()
{
return $"Invoice {InvoiceId} received a payment";
return string.Empty;
}
}
}

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceNewAddressEvent
{
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
{
Address = address;
InvoiceId = invoiceId;
Network = network;
}
public string Address { get; set; }
public string InvoiceId { get; set; }
public BTCPayNetwork Network { get; set; }
public override string ToString()
{
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
}
}
}

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
namespace BTCPayServer.Events
{
public class InvoiceStatusChangedEvent
{
public InvoiceStatusChangedEvent()
{
}
public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState)
{
OldState = invoice.Status;
InvoiceId = invoice.Id;
NewState = newState;
}
public string InvoiceId { get; set; }
public string OldState { get; set; }
public string NewState { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}";
}
}
}

@ -5,18 +5,16 @@ using System.Threading.Tasks;
namespace BTCPayServer.Events
{
public class InvoiceCreatedEvent
public class InvoiceStopWatchedEvent
{
public InvoiceCreatedEvent(string id)
public InvoiceStopWatchedEvent(string invoiceId)
{
InvoiceId = id;
this.InvoiceId = invoiceId;
}
public string InvoiceId { get; set; }
public override string ToString()
{
return $"Invoice {InvoiceId} created";
return $"Invoice {InvoiceId} is not monitored anymore.";
}
}
}

@ -6,6 +6,21 @@ using BTCPayServer.HostedServices;
namespace BTCPayServer.Events
{
public class NBXplorerErrorEvent
{
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
{
Message = errorMessage;
Network = network;
}
public string Message { get; set; }
public BTCPayNetwork Network { get; set; }
public override string ToString()
{
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
}
}
public class NBXplorerStateChangedEvent
{
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)

@ -7,9 +7,10 @@ namespace BTCPayServer.Events
{
public class NewBlockEvent
{
public string CryptoCode { get; set; }
public override string ToString()
{
return "New block";
return $"{CryptoCode}: New block";
}
}
}

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Events
{
public class TxOutReceivedEvent
{
public BTCPayNetwork Network { get; set; }
public Script ScriptPubKey { get; set; }
public override string ToString()
{
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
return $"{address} received a transaction";
}
}
}

@ -1,9 +1,12 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Configuration;
using BTCPayServer.Logging;
using NBXplorer;
using BTCPayServer.HostedServices;
namespace BTCPayServer
{
@ -13,47 +16,87 @@ namespace BTCPayServer
BTCPayServerOptions _Options;
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
NBXplorerDashboard _Dashboard;
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
{
_Dashboard = dashboard;
_NetworkProviders = networkProviders;
_Options = options;
foreach (var setting in options.NBXplorerConnectionSettings)
{
var cookieFile = setting.CookieFile;
if (cookieFile.Trim() == "0" || string.IsNullOrEmpty(cookieFile.Trim()))
cookieFile = null;
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Explorer url is {(setting.ExplorerUri.AbsoluteUri ?? "not set")}");
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
if (setting.ExplorerUri != null)
{
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
}
}
}
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
{
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
if (cookieFile == null)
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
explorer.SetNoAuth();
}
if(!explorer.SetCookieAuth(cookieFile))
{
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
}
return explorer;
}
Dictionary<string, ExplorerClient> _Clients = new Dictionary<string, ExplorerClient>();
public ExplorerClient GetExplorerClient(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
if (_Options.ExplorerFactories.TryGetValue(network.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
{
return factory(network);
}
return null;
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
return client;
}
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return GetExplorerClient(network.CryptoCode);
}
public bool IsAvailable(BTCPayNetwork network)
{
return IsAvailable(network.CryptoCode);
}
public bool IsAvailable(string cryptoCode)
{
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
}
public BTCPayNetwork GetNetwork(string cryptoCode)
{
var network = _NetworkProviders.GetNetwork(cryptoCode);
if (network == null)
return null;
if (_Options.ExplorerFactories.ContainsKey(network.CryptoCode))
if (_Clients.ContainsKey(network.CryptoCode))
return network;
return null;
}
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
{
foreach(var net in _NetworkProviders.GetAll())
foreach (var net in _NetworkProviders.GetAll())
{
if(_Options.ExplorerFactories.TryGetValue(net.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
{
yield return (net, factory(net));
yield return (net, explorer);
}
}
}

@ -19,36 +19,58 @@ using System.Linq;
using System.Threading;
using BTCPayServer.Services.Wallets;
using System.IO;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using System.Net.WebSockets;
using BTCPayServer.Services.Invoices;
using NBitpayClient;
using BTCPayServer.Payments;
namespace BTCPayServer
{
public static class Extensions
{
public static string GetDefaultExplorerUrl(this NBXplorer.Configuration.NetworkInformation networkInfo)
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return $"http://127.0.0.1:{networkInfo.DefaultExplorerPort}/";
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
}
public static string GetDefaultCookieFile(this NBXplorer.Configuration.NetworkInformation networkInfo)
public static async Task CloseSocket(this WebSocket webSocket)
{
return Path.Combine(networkInfo.DefaultDataDirectory, ".cookie");
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
catch { }
finally { try { webSocket.Dispose(); } catch { } }
}
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
{
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, BTCPayNetwork network, uint256[] hashes, CancellationToken cts = default(CancellationToken))
public static bool IsCoinAverage(this string exchangeName)
{
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
}
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
{
hashes = hashes.Distinct().ToArray();
var transactions = hashes
.Select(async o => await client.GetTransactionAsync(network, o, cts))
.Select(async o => await client.GetTransactionAsync(o, cts))
.ToArray();
await Task.WhenAll(transactions).ConfigureAwait(false);
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
}
public static string WithTrailingSlash(this string str)
{
if (str.EndsWith("/"))
if (str.EndsWith("/", StringComparison.InvariantCulture))
return str;
return str + "/";
}

@ -37,6 +37,9 @@ namespace BTCPayServer.HostedServices
{
get; set;
}
public int? EventCode { get; set; }
public string Message { get; set; }
}
public ILogger Logger
@ -63,18 +66,32 @@ namespace BTCPayServer.HostedServices
_NetworkProvider = networkProvider;
}
async Task Notify(InvoiceEntity invoice)
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
{
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
await SendNotification(invoice, cts.Token);
if (string.IsNullOrEmpty(invoice.NotificationURL))
return;
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
await SendNotification(invoice, eventCode, name, cts.Token);
return;
}
catch // It fails, it is OK because we try with hangfire after
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
{
Error = "Timeout"
});
}
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
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 });
if (!string.IsNullOrEmpty(invoice.NotificationURL))
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
}
@ -93,14 +110,45 @@ namespace BTCPayServer.HostedServices
CancellationTokenSource cts = new CancellationTokenSource(10000);
try
{
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
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
});
}
catch (Exception ex)
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = "Timeout"
});
reschedule = true;
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
Logger.LogInformation("Job " + jobId + " timed out");
}
catch (Exception ex) // It fails, it is OK because we try with hangfire after
{
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
{
Error = ex.Message
});
reschedule = true;
List<string> messages = new List<string>();
while (ex != null)
{
messages.Add(ex.Message);
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}"
});
}
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
@ -115,8 +163,23 @@ namespace BTCPayServer.HostedServices
}
}
public class InvoicePaymentNotificationEvent
{
[JsonProperty("code")]
public int Code { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class InvoicePaymentNotificationEventWrapper
{
[JsonProperty("event")]
public InvoicePaymentNotificationEvent Event { get; set; }
[JsonProperty("data")]
public InvoicePaymentNotification Data { get; set; }
}
Encoding UTF8 = new UTF8Encoding(false);
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
{
var request = new HttpRequestMessage();
request.Method = HttpMethod.Post;
@ -138,8 +201,8 @@ namespace BTCPayServer.HostedServices
// 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.CryptoCode == "BTC");
if(btcCryptoInfo != null)
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
if (btcCryptoInfo != null)
{
#pragma warning disable CS0618
notification.Rate = (double)dto.Rate;
@ -149,12 +212,84 @@ namespace BTCPayServer.HostedServices
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);
}
else
{
notificationString = JsonConvert.SerializeObject(notification);
}
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
var response = await _Client.SendAsync(request, cancellation);
request.Content = new StringContent(notificationString, UTF8, "application/json");
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
return response;
}
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
/// <summary>
/// Will make sure only one callback is called at once on the same invoiceId
/// </summary>
/// <param name="id"></param>
/// <param name="sendRequest"></param>
/// <returns></returns>
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
{
Task<T> sending = null;
lock (_SendingRequestsByInvoiceId)
{
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
{
var completion = new TaskCompletionSource<T>();
sending = completion.Task;
_SendingRequestsByInvoiceId.Remove(id);
_SendingRequestsByInvoiceId.Add(id, sending);
executing.ContinueWith(_ =>
{
sendRequest()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.RanToCompletion)
{
completion.TrySetResult(t.Result);
}
if(t.Status == TaskStatus.Faulted)
{
completion.TrySetException(t.Exception);
}
if(t.Status == TaskStatus.Canceled)
{
completion.TrySetCanceled();
}
}, TaskScheduler.Default);
}, TaskScheduler.Default);
}
else
{
sending = sendRequest();
_SendingRequestsByInvoiceId.Add(id, sending);
}
sending.ContinueWith(o =>
{
lock (_SendingRequestsByInvoiceId)
{
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
if(executing2 == sending)
_SendingRequestsByInvoiceId.Remove(id);
}
}, TaskScheduler.Default);
}
return await sending;
}
int MaxTry = 6;
private static string GetHttpJobId(InvoiceEntity invoice)
@ -165,29 +300,60 @@ namespace BTCPayServer.HostedServices
CompositeDisposable leases = new CompositeDisposable();
public Task StartAsync(CancellationToken cancellationToken)
{
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
await SaveEvent(invoice.Id, e);
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
if (invoice.FullNotifications)
{
if (e.NewState == "expired" ||
e.NewState == "paid" ||
e.NewState == "invalid" ||
e.NewState == "complete"
if (e.Name == "invoice_expired" ||
e.Name == "invoice_paidInFull" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_markedInvalid" ||
e.Name == "invoice_failedToConfirm" ||
e.Name == "invoice_completed"
)
await Notify(invoice);
}
if(e.NewState == "confirmed")
if (e.Name == "invoice_confirmed")
{
await Notify(invoice);
}
if (invoice.ExtendedNotifications)
{
await Notify(invoice, e.EventCode, e.Name);
}
}));
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
{
await SaveEvent(e.InvoiceId, e);
}));
return Task.CompletedTask;
}
private Task SaveEvent(string invoiceId, object evt)
{
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();

@ -17,6 +17,7 @@ using BTCPayServer.Controllers;
using BTCPayServer.Events;
using Microsoft.AspNetCore.Hosting;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services;
namespace BTCPayServer.HostedServices
{
@ -24,13 +25,10 @@ namespace BTCPayServer.HostedServices
{
class UpdateInvoiceContext
{
public UpdateInvoiceContext()
public UpdateInvoiceContext(InvoiceEntity invoice)
{
Invoice = invoice;
}
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
public InvoiceEntity Invoice { get; set; }
public List<object> Events { get; set; } = new List<object>();
@ -45,285 +43,131 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayWallet _Wallet;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(
IHostingEnvironment env,
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWallet wallet)
EventAggregator eventAggregator)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
}
CompositeDisposable leases = new CompositeDisposable();
async Task NotifyReceived(Script scriptPubKey, BTCPayNetwork network)
{
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
if (invoice != null)
_WatchRequests.Add(invoice);
}
async Task NotifyBlock()
{
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
{
_WatchRequests.Add(invoice);
}
}
private async Task UpdateInvoice(string invoiceId)
{
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
while (true)
{
try
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
if (invoice == null)
break;
var stateBefore = invoice.Status;
var updateContext = new UpdateInvoiceContext()
{
Invoice = invoice,
KnownStates = changes
};
await UpdateInvoice(updateContext).ConfigureAwait(false);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
}
var changed = stateBefore != invoice.Status;
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
foreach (var modifiedKnownState in updateContext.ModifiedKnownStates)
{
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
break;
}
if (!changed || _Cts.Token.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (_Cts.Token.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
}
}
}
private async Task UpdateInvoice(UpdateInvoiceContext context)
{
var invoice = context.Invoice;
//Fetch unknown payments
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var getCoinsResponsesAsync = strategies
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token))
.ToArray();
await Task.WhenAll(getCoinsResponsesAsync);
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
foreach (var response in getCoinsResponses)
{
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
}
var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
bool dirtyAddress = false;
if (coins != null)
{
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
invoice.Payments.Add(payment);
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
dirtyAddress = true;
}
}
//////
var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key);
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
{
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
invoice.Status = "expired";
}
if (invoice.Status == "new" || invoice.Status == "expired")
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
foreach (var paymentMethod in allPaymentMethods.Select(c => c))
{
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
invoice.Status = "paid";
invoice.ExceptionStatus = null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired")
{
invoice.ExceptionStatus = "paidLate";
context.MarkDirty();
}
}
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
if (dirtyAddress)
{
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
}
}
if (invoice.Status == "paid")
{
var transactions = await GetPaymentsWithTransaction(network, invoice);
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
}
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1);
}
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 6);
}
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(totalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (totalConfirmed >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(network, invoice);
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
invoice.Status = "complete";
context.MarkDirty();
}
}
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
{
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
foreach (var payment in invoice.Payments)
{
TransactionResult tx;
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
{
result.Remove(payment.Outpoint);
var accounting = paymentMethod.Calculate();
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
if (network == null)
continue;
}
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
var totalPaid = payments.Select(p => p.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
if (invoice.Status == "new" || invoice.Status == "expired")
{
Confirmations = tx.Confirmations,
Transaction = tx.Transaction,
Payment = payment
};
payments.Add(accountedPayment);
foreach (var txin in tx.Transaction.Inputs)
{
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
if (totalPaid >= accounting.TotalDue)
{
//We get a double spend
var existing = spentTxIn[txin.PrevOut];
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
if (invoice.Status == "new")
{
spentTxIn[txin.PrevOut] = accountedPayment;
result.Remove(existing.Payment.Outpoint);
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
invoice.Status = "paid";
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.MarkDirty();
}
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
{
invoice.ExceptionStatus = "paidLate";
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
context.MarkDirty();
}
}
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
{
invoice.ExceptionStatus = "paidPartial";
context.MarkDirty();
}
}
// Just make sure RBF did not cancelled a payment
if (invoice.Status == "paid")
{
if (totalPaid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
{
invoice.ExceptionStatus = null;
context.MarkDirty();
}
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
{
invoice.ExceptionStatus = "paidOver";
context.MarkDirty();
}
if (totalPaid < accounting.TotalDue)
{
invoice.Status = "new";
invoice.ExceptionStatus = totalPaid == Money.Zero ? null : "paidPartial";
context.MarkDirty();
}
}
if (invoice.Status == "paid")
{
var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
var totalConfirmed = transactions.Select(t => t.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
if (// Is after the monitoring deadline
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
&&
// And not enough amount confirmed
(totalConfirmed < accounting.TotalDue))
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
invoice.Status = "invalid";
context.MarkDirty();
}
else if (totalConfirmed >= accounting.TotalDue)
{
await _InvoiceRepository.UnaffectAddress(invoice.Id);
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
invoice.Status = "confirmed";
context.MarkDirty();
}
}
if (invoice.Status == "confirmed")
{
var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
var totalConfirmed = transactions.Select(t => t.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
if (totalConfirmed >= accounting.TotalDue)
{
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
invoice.Status = "complete";
context.MarkDirty();
}
}
}
List<PaymentEntity> updated = new List<PaymentEntity>();
var accountedPayments = payments.Where(p =>
{
var accounted = result.Contains(p.Payment.Outpoint);
if (p.Payment.Accounted != accounted)
{
p.Payment.Accounted = accounted;
updated.Add(p.Payment);
}
return accounted;
}).ToArray();
await _InvoiceRepository.UpdatePayments(payments);
return accountedPayments;
}
TimeSpan _PollInterval;
@ -336,10 +180,6 @@ namespace BTCPayServer.HostedServices
set
{
_PollInterval = value;
if (_UpdatePendingInvoices != null)
{
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
}
}
}
@ -350,87 +190,138 @@ namespace BTCPayServer.HostedServices
_WatchRequests.Add(invoiceId);
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
public void Dispose()
private async Task Wait(string invoiceId)
{
_Cts.Cancel();
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
try
{
var now = DateTimeOffset.UtcNow;
if (invoice.ExpirationTime > now)
{
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
}
Watch(invoiceId);
now = DateTimeOffset.UtcNow;
if (invoice.MonitoringExpiration > now)
{
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
}
Watch(invoiceId);
}
catch when (_Cts.IsCancellationRequested)
{ }
}
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
Thread _Thread;
TaskCompletionSource<bool> _RunningTask;
Task _Loop;
Task _WaitingInvoices;
CancellationTokenSource _Cts;
Timer _UpdatePendingInvoices;
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
_Thread.Start();
_UpdatePendingInvoices = new Timer(async s =>
_Loop = StartLoop(_Cts.Token);
_WaitingInvoices = WaitPendingInvoices();
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
{
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
Watch(b.InvoiceId);
}));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
{
if (b.Name == "invoice_created")
{
_WatchRequests.Add(pending);
Watch(b.InvoiceId);
await Wait(b.InvoiceId);
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
if (b.Name == "invoice_receivedPayment")
{
Watch(b.InvoiceId);
}
}));
return Task.CompletedTask;
}
void Run()
private async Task WaitPendingInvoices()
{
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(id => Wait(id)).ToArray());
_WaitingInvoices = null;
}
async Task StartLoop(CancellationToken cancellation)
{
Logs.PayServer.LogInformation("Start watching invoices");
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
try
{
foreach (var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
{
try
int maxLoop = 5;
int loopCount = -1;
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
{
_Cts.Token.ThrowIfCancellationRequested();
var localItem = item;
// If the invoice is already updating, ignore
Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
if (updating.TryAdd(item, updateInvoice))
loopCount++;
try
{
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
if (invoice == null)
break;
var updateContext = new UpdateInvoiceContext(invoice);
await UpdateInvoice(updateContext);
if (updateContext.Dirty)
{
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
}
foreach (var evt in updateContext.Events)
{
_EventAggregator.Publish(evt, evt.GetType());
}
if (invoice.Status == "complete" ||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
{
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
break;
}
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
break;
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
Task.Delay(10000, cancellation)
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
break;
}
}
catch (Exception ex) when (!_Cts.Token.IsCancellationRequested)
{
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
_Cts.Token.WaitHandle.WaitOne(2000);
}
}
}
catch (OperationCanceledException)
catch when (cancellation.IsCancellationRequested)
{
try
{
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
}
catch (AggregateException) { }
_RunningTask.TrySetResult(true);
}
finally
{
Logs.PayServer.LogInformation("Stop watching invoices");
}
Logs.PayServer.LogInformation("Stop watching invoices");
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_UpdatePendingInvoices.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
return Task.WhenAll(waitingPendingInvoices, _Loop);
}
}
}

@ -1,204 +0,0 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
namespace BTCPayServer.HostedServices
{
public class NBXplorerListener : IHostedService
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
NBXplorerDashboard _Dashboards;
public NBXplorerListener(ExplorerClientProvider explorerClients,
NBXplorerDashboard dashboard,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Dashboards = dashboard;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
await Listen(nbxplorerEvent.Network);
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var nbxplorerState in _Dashboards.GetAll())
{
if (nbxplorerState.Status != null && nbxplorerState.Status.IsFullySynched)
{
await Listen(nbxplorerState.Network);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceCreatedEvent>(async inv =>
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
{
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
}
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}));
return Task.CompletedTask;
}
private async Task Listen(BTCPayNetwork network)
{
bool cleanup = false;
try
{
if (_Sessions.ContainsKey(network.CryptoCode))
return;
var client = _ExplorerClients.GetExplorerClient(network);
if (client == null)
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_Sessions.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
_Aggregator.Publish(new Events.NewBlockEvent());
break;
case NBXplorer.Models.NewTransactionEvent evt:
foreach (var txout in evt.Match.Outputs)
{
_Aggregator.Publish(new Events.TxOutReceivedEvent()
{
Network = network,
ScriptPubKey = txout.ScriptPubKey
});
}
break;
default:
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested) { }
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
}
finally
{
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);
}
}
}
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var strategy = GetStrategy(network.CryptoCode, invoice);
if (strategy != null)
strategies.Add(strategy);
}
return strategies;
}
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
{
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
{
if (derivationStrategy.Network.CryptoCode == cryptoCode)
{
return derivationStrategy.DerivationStrategyBase;
}
}
return null;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

@ -27,11 +27,12 @@ namespace BTCPayServer.HostedServices
public BTCPayNetwork Network { get; set; }
public NBXplorerState State { get; set; }
public StatusResult Status { get; set; }
public string Error { get; set; }
}
ConcurrentDictionary<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status)
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status, string error)
{
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status };
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status, Error = error };
_Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary);
}
@ -40,6 +41,11 @@ namespace BTCPayServer.HostedServices
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public bool IsFullySynched(string cryptoCode)
{
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
}
public IEnumerable<NBXplorerSummary> GetAll()
{
return _Summaries.Values;
@ -82,139 +88,135 @@ namespace BTCPayServer.HostedServices
BTCPayNetwork _Network;
EventAggregator _Aggregator;
ExplorerClient _Client;
Timer _Timer;
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
CancellationTokenSource _Cts;
Task _Loop;
public Task StartAsync(CancellationToken cancellationToken)
{
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_Loop = StartLoop(_Cts.Token);
return Task.CompletedTask;
}
void Callback(object state)
private async Task StartLoop(CancellationToken cancellation)
{
if (!_Idle.IsSet)
return;
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
try
{
_Idle.Reset();
CheckStatus().GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
}
finally
{
_Idle.Set();
}
}
async Task CheckStatus()
{
while (await StepAsync())
{
}
}
private async Task<bool> StepAsync()
{
var oldState = State;
StatusResult status = null;
switch (State)
{
case NBXplorerState.NotConnected:
status = await GetStatusWithTimeout();
if (status != null)
{
if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
else
{
State = NBXplorerState.Synching;
}
}
break;
case NBXplorerState.Synching:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
break;
case NBXplorerState.Ready:
status = await GetStatusWithTimeout();
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (!status.IsFullySynched)
{
State = NBXplorerState.Synching;
}
break;
}
if (oldState != State)
{
if (State == NBXplorerState.Synching)
{
SetInterval(TimeSpan.FromSeconds(10));
}
else
{
SetInterval(TimeSpan.FromMinutes(1));
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
_Dashboard.Publish(_Network, State, status);
return oldState != State;
}
private void SetInterval(TimeSpan interval)
{
try
{
_Timer.Change(0, (int)interval.TotalMilliseconds);
}
catch { }
}
private async Task<StatusResult> GetStatusWithTimeout()
{
CancellationTokenSource cts = new CancellationTokenSource();
using (cts)
{
var cancellation = cts.Token;
while (!cancellation.IsCancellationRequested)
{
try
{
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
return status;
while (await StepAsync(cancellation))
{
}
await Task.Delay(PollInterval, cancellation);
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
}
catch (OperationCanceledException) { }
catch { }
}
}
return null;
catch when (cancellation.IsCancellationRequested) { }
}
private async Task<bool> StepAsync(CancellationToken cancellation)
{
var oldState = State;
string error = null;
StatusResult status = null;
try
{
switch (State)
{
case NBXplorerState.NotConnected:
status = await _Client.GetStatusAsync(cancellation);
if (status != null)
{
if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
else
{
State = NBXplorerState.Synching;
}
}
break;
case NBXplorerState.Synching:
status = await _Client.GetStatusAsync(cancellation);
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (status.IsFullySynched)
{
State = NBXplorerState.Ready;
}
break;
case NBXplorerState.Ready:
status = await _Client.GetStatusAsync(cancellation);
if (status == null)
{
State = NBXplorerState.NotConnected;
}
else if (!status.IsFullySynched)
{
State = NBXplorerState.Synching;
}
break;
}
}
catch (Exception ex) when (!cancellation.IsCancellationRequested)
{
error = ex.Message;
}
if(status == null && error == null)
error = $"{_Network.CryptoCode}: NBXplorer does not support this cryptocurrency";
if(status != null && error == null)
{
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
}
if (error != null)
{
State = NBXplorerState.NotConnected;
status = null;
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
}
_Dashboard.Publish(_Network, State, status, error);
if (oldState != State)
{
if (State == NBXplorerState.Synching)
{
PollInterval = TimeSpan.FromSeconds(10);
}
else
{
PollInterval = TimeSpan.FromMinutes(1);
}
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
}
return oldState != State;
}
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
public NBXplorerState State { get; private set; }
public Task StopAsync(CancellationToken cancellationToken)
{
_Timer.Dispose();
_Timer = null;
_Idle.Wait();
return Task.CompletedTask;
_Cts.Cancel();
return _Loop;
}
}
}

@ -103,7 +103,7 @@ namespace BTCPayServer.Hosting
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
if (!Directory.Exists(dbpath))
Directory.CreateDirectory(dbpath);
return new InvoiceRepository(dbContext, dbpath, opts.Network);
return new InvoiceRepository(dbContext, dbpath);
});
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
@ -129,12 +129,12 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
return new BTCPayNetworkProvider(opts.Network);
return new BTCPayNetworkProvider(opts.ChainType);
});
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
@ -143,14 +143,14 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<IHostedService, NBXplorerWaiters>();
services.AddSingleton<IHostedService, NBXplorerListener>();
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
services.AddSingleton<IHostedService, InvoiceWatcher>();
services.TryAddSingleton<ExplorerClientProvider>();
services.TryAddSingleton<Bitpay>(o =>
{
if (o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main)
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
else
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
@ -205,11 +205,9 @@ namespace BTCPayServer.Hosting
act();
return;
}
catch
catch when(!cts.IsCancellationRequested)
{
if (cts.IsCancellationRequested)
throw;
Thread.Sleep(1000);
Thread.Sleep(100);
}
}
}

@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Http.Extensions;
using BTCPayServer.Controllers;
using System.Net.WebSockets;
namespace BTCPayServer.Hosting
{
@ -69,7 +70,7 @@ namespace BTCPayServer.Hosting
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
{
var bitid = new BitIdentity(key);
httpContext.User = new GenericPrincipal(bitid, new string[0]);
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
}
}
@ -82,6 +83,8 @@ namespace BTCPayServer.Hosting
{
await _Next(httpContext);
}
catch (WebSocketException)
{ }
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
@ -36,6 +37,8 @@ using System.Threading;
using Microsoft.Extensions.Options;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
namespace BTCPayServer.Hosting
{
@ -88,6 +91,15 @@ namespace BTCPayServer.Hosting
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
});
// Needed to debug U2F for ledger support
//services.Configure<KestrelServerOptions>(kestrel =>
//{
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
// {
// l.UseHttps("devtest.pfx", "toto");
// });
//});
}
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run

@ -6,13 +6,18 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class CustomConsoleLogProvider : ILoggerProvider
{
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
ConsoleLoggerProcessor _Processor;
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
{
_Processor = processor;
}
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
@ -329,7 +334,8 @@ namespace BTCPayServer.Logging
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
default(CancellationToken),
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
public virtual void EnqueueMessage(LogMessageEntry message)

@ -0,0 +1,508 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20180114123253_events")]
partial class events
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany()
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace BTCPayServer.Migrations
{
public partial class events : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "InvoiceEvents",
columns: table => new
{
InvoiceDataId = table.Column<string>(nullable: false),
UniqueId = table.Column<string>(nullable: false),
Message = table.Column<string>(nullable: true),
Timestamp = table.Column<DateTimeOffset>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
table.ForeignKey(
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
column: x => x.InvoiceDataId,
principalTable: "Invoices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "InvoiceEvents");
}
}
}

@ -1,12 +1,9 @@
// <auto-generated />
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using System;
namespace BTCPayServer.Migrations
@ -81,6 +78,21 @@ namespace BTCPayServer.Migrations
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
@ -407,6 +419,14 @@ namespace BTCPayServer.Migrations
.HasForeignKey("StoreDataId");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")

@ -37,81 +37,6 @@ namespace BTCPayServer.Models
}
}
public class InvoiceCryptoInfo
{
[JsonProperty("cryptoCode")]
public string CryptoCode { get; set; }
[JsonProperty("rate")]
public decimal Rate { get; set; }
//"exRates":{"USD":4320.02}
[JsonProperty("exRates")]
public Dictionary<string, double> ExRates
{
get; set;
}
//"btcPaid":"0.000000"
[JsonProperty("paid")]
public string Paid
{
get; set;
}
//"btcPrice":"0.001157"
[JsonProperty("price")]
public string Price
{
get; set;
}
//"btcDue":"0.001160"
/// <summary>
/// Amount of crypto remaining to pay this invoice
/// </summary>
[JsonProperty("due")]
public string Due
{
get; set;
}
[JsonProperty("paymentUrls")]
public NBitpayClient.InvoicePaymentUrls PaymentUrls
{
get; set;
}
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// Total amount of this invoice
/// </summary>
[JsonProperty("totalDue")]
public string TotalDue { get; set; }
/// <summary>
/// Total amount of network fee to pay to the invoice
/// </summary>
[JsonProperty("networkFee")]
public string NetworkFee { get; set; }
/// <summary>
/// Number of transactions required to pay
/// </summary>
[JsonProperty("txCount")]
public int TxCount { get; set; }
/// <summary>
/// Total amount of the invoice paid in this crypto
/// </summary>
[JsonProperty("cryptoPaid")]
public Money CryptoPaid { get; set; }
}
//{"facade":"pos/invoice","data":{,}}
public class InvoiceResponse
{
@ -151,7 +76,7 @@ namespace BTCPayServer.Models
}
[JsonProperty("cryptoInfo")]
public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
public List<NBitpayClient.InvoiceCryptoInfo> CryptoInfo { get; set; }
//"price":5
[JsonProperty("price")]

@ -22,7 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels
public class Payment
{
public string CryptoCode { get; set; }
public int Confirmations
public string Confirmations
{
get; set;
}
@ -48,6 +48,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public bool Replaced { get; set; }
}
public string StatusMessage
@ -70,7 +72,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
get; set;
}
public string StatusException { get; set; }
public DateTimeOffset CreatedDate
{
get; set;
@ -95,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
set;
}
public string TransactionSpeed { get; set; }
public object StoreName
{
get;
@ -110,6 +114,8 @@ namespace BTCPayServer.Models.InvoicingModels
get;
internal set;
}
public string RedirectUrl { get; set; }
public string Fiat
{
get;
@ -122,5 +128,6 @@ namespace BTCPayServer.Models.InvoicingModels
}
public HistoricalAddressInvoiceData[] Addresses { get; set; }
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
}
}

@ -7,6 +7,13 @@ namespace BTCPayServer.Models.InvoicingModels
{
public class PaymentModel
{
public class AvailableCrypto
{
public string PaymentMethodId { get; set; }
public string CryptoImage { get; set; }
public string Link { get; set; }
}
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }
@ -23,15 +30,17 @@ namespace BTCPayServer.Models.InvoicingModels
public string ItemDesc { get; set; }
public string TimeLeft { get; set; }
public string Rate { get; set; }
public string BtcAmount { get; set; }
public string TxFees { get; set; }
public string OrderAmount { get; set; }
public string InvoiceBitcoinUrl { get; set; }
public string BtcTotalDue { get; set; }
public int TxCount { get; set; }
public string BtcPaid { get; set; }
public string StoreEmail { get; set; }
public string OrderId { get; set; }
public string CryptoImage { get; set; }
public string NetworkFeeDescription { get; internal set; }
public int MaxTimeMinutes { get; internal set; }
public string PaymentType { get; internal set; }
public string PaymentMethodId { get; internal set; }
}
}

@ -48,10 +48,13 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
public bool Confirmation { get; set; }
public SelectList CryptoCurrencies { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();

@ -47,7 +47,34 @@ namespace BTCPayServer.Models.StoreViewModels
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
public string PreferredExchange { get; set; }
public string RateSource
{
get
{
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
}
}
[Display(Name = "Multiply the original rate by ...")]
[Range(0.01, 10.0)]
public double RateMultiplier
{
get;
set;
}
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
[Range(1, 60 * 24 * 31)]
public int InvoiceExpiration
{
get;
set;
}
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
[Range(10, 60 * 24 * 31)]
public int MonitoringExpiration
{

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
{
public class WalletModel
{
public string ServerUrl { get; set; }
public SelectList CryptoCurrencies { get; set; }
[Display(Name = "Crypto currency")]
public string CryptoCurrency
{
get;
set;
}
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
CryptoCurrency = chosen.Name;
}
}
}

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinLikeOnChainPaymentMethod : IPaymentMethodDetails
{
public PaymentTypes GetPaymentType()
{
return PaymentTypes.BTCLike;
}
public string GetPaymentDestination()
{
return DepositAddress?.ToString();
}
public Money GetTxFee()
{
return TxFee;
}
public void SetPaymentDestination(string newPaymentDestination)
{
if (newPaymentDestination == null)
DepositAddress = null;
else
DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network);
}
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
[JsonIgnore]
public FeeRate FeeRate { get; set; }
[JsonIgnore]
public Money TxFee { get; set; }
[JsonIgnore]
public BitcoinAddress DepositAddress { get; set; }
///////////////////////////////////////////////////////////////////////////////////////
}
}

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
namespace BTCPayServer.Payments.Bitcoin
{
public class BitcoinLikePaymentData : CryptoPaymentData
{
public PaymentTypes GetPaymentType()
{
return PaymentTypes.BTCLike;
}
public BitcoinLikePaymentData()
{
}
public BitcoinLikePaymentData(Coin coin, bool rbf)
{
Outpoint = coin.Outpoint;
Output = coin.TxOut;
ConfirmationCount = 0;
RBF = rbf;
}
[JsonIgnore]
public OutPoint Outpoint { get; set; }
[JsonIgnore]
public TxOut Output { get; set; }
public int ConfirmationCount { get; set; }
public bool RBF { get; set; }
/// <summary>
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
/// </summary>
public bool Legacy { get; set; }
public string GetPaymentId()
{
return Outpoint.ToString();
}
public string[] GetSearchTerms()
{
return new[] { Outpoint.Hash.ToString() };
}
public Money GetValue()
{
return Output.Value;
}
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
{
return ConfirmationCount >= network.MaxTrackedConfirmation;
}
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
{
if (speedPolicy == SpeedPolicy.HighSpeed)
{
return ConfirmationCount >= 1 || !RBF;
}
else if (speedPolicy == SpeedPolicy.MediumSpeed)
{
return ConfirmationCount >= 1;
}
else if (speedPolicy == SpeedPolicy.LowSpeed)
{
return ConfirmationCount >= 6;
}
return false;
}
}
}

@ -0,0 +1,407 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Logging;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NBXplorer;
using System.Collections.Concurrent;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Events;
using BTCPayServer.Services;
using BTCPayServer.Services.Wallets;
using NBitcoin;
using NBXplorer.Models;
using BTCPayServer.Payments;
using BTCPayServer.HostedServices;
namespace BTCPayServer.Payments.Bitcoin
{
public class NBXplorerListener : IHostedService
{
EventAggregator _Aggregator;
ExplorerClientProvider _ExplorerClients;
IApplicationLifetime _Lifetime;
InvoiceRepository _InvoiceRepository;
private TaskCompletionSource<bool> _RunningTask;
private CancellationTokenSource _Cts;
BTCPayWalletProvider _Wallets;
public NBXplorerListener(ExplorerClientProvider explorerClients,
BTCPayWalletProvider wallets,
InvoiceRepository invoiceRepository,
EventAggregator aggregator, IApplicationLifetime lifetime)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallets = wallets;
_InvoiceRepository = invoiceRepository;
_ExplorerClients = explorerClients;
_Aggregator = aggregator;
_Lifetime = lifetime;
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
public TimeSpan PollInterval
{
get
{
return _PollInterval;
}
set
{
_PollInterval = value;
if (_ListenPoller != null)
{
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
}
}
}
public Task StartAsync(CancellationToken cancellationToken)
{
_RunningTask = new TaskCompletionSource<bool>();
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
{
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
{
var wallet = _Wallets.GetWallet(nbxplorerEvent.Network);
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(wallet);
}
}
}));
_ListenPoller = new Timer(async s =>
{
foreach (var wallet in _Wallets.GetWallets())
{
if (_Wallets.IsAvailable(wallet.Network))
{
await Listen(wallet);
}
}
}, null, 0, (int)PollInterval.TotalMilliseconds);
leases.Add(_ListenPoller);
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
{
if (inv.Name == "invoice_created")
{
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
List<Task> listeningDerivations = new List<Task>();
foreach (var notificationSessions in _Sessions)
{
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
if (derivationStrategy != null)
{
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
}
}
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
}
}));
return Task.CompletedTask;
}
private async Task Listen(BTCPayWallet wallet)
{
var network = wallet.Network;
bool cleanup = false;
try
{
if (_Sessions.ContainsKey(network.CryptoCode))
return;
var client = _ExplorerClients.GetExplorerClient(network);
if (client == null)
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_Sessions.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
return;
}
cleanup = true;
using (session)
{
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
int paymentCount = await FindPaymentViaPolling(wallet, network);
Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
while (!_Cts.IsCancellationRequested)
{
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
switch (newEvent)
{
case NBXplorer.Models.NewBlockEvent evt:
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
.Select(invoiceId => UpdatePaymentStates(wallet, invoiceId))
.ToArray());
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
break;
case NBXplorer.Models.NewTransactionEvent evt:
wallet.InvalidateCache(evt.DerivationStrategy);
foreach (var output in evt.Outputs)
{
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
{
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
if (invoice != null)
{
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
if (!alreadyExist)
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
}
else
{
await UpdatePaymentStates(wallet, invoice.Id);
}
}
}
}
break;
default:
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
break;
}
}
}
}
catch when (_Cts.IsCancellationRequested) { }
catch (Exception ex)
{
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
}
finally
{
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);
}
}
}
}
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
{
return invoice.GetPayments()
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
}
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
.Select(p => p.Outpoint.Hash)
.ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(wallet.Network))
{
if (payment.GetpaymentMethodId().PaymentType != PaymentTypes.BTCLike)
continue;
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
continue;
var txId = tx.Transaction.GetHash();
var txConflict = conflicts.GetConflict(txId);
var accounted = txConflict == null || txConflict.IsWinner(txId);
bool updated = false;
if (accounted != payment.Accounted)
{
updated = true;
payment.Accounted = accounted;
}
if (paymentData.ConfirmationCount != tx.Confirmations)
{
if(wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
{
paymentData.ConfirmationCount = tx.Confirmations;
payment.SetCryptoPaymentData(paymentData);
updated = true;
}
}
if (updated)
updatedPaymentEntities.Add(payment);
}
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
if (updatedPaymentEntities.Count != 0)
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
return invoice;
}
class TransactionConflict
{
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
uint256 _Winner;
public bool IsWinner(uint256 txId)
{
if (_Winner == null)
{
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
{
_Winner = confirmed.Key;
}
else
{
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
_Winner = Transactions
.OrderByDescending(t => t.Value.Timestamp)
.First()
.Key;
}
}
return _Winner == txId;
}
}
class TransactionConflicts : List<TransactionConflict>
{
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
{
}
public TransactionConflict GetConflict(uint256 txId)
{
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
}
}
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
{
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
foreach (var tx in transactions)
{
var hash = tx.Transaction.GetHash();
foreach (var input in tx.Transaction.Inputs)
{
TransactionConflict conflict = new TransactionConflict();
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
{
conflict = conflictsByOutpoint[input.PrevOut];
}
if (!conflict.Transactions.ContainsKey(hash))
conflict.Transactions.Add(hash, tx);
}
}
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
}
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
{
int totalPayment = 0;
var invoices = await _InvoiceRepository.GetPendingInvoices();
foreach (var invoiceId in invoices)
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
var strategy = invoice.GetDerivationStrategy(network);
if (strategy == null)
continue;
var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
if (!invoice.Support(cryptoId))
continue;
var coins = (await wallet.GetUnspentCoins(strategy))
.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + cryptoId))
.ToArray();
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
alreadyAccounted.Add(coin.Coin.Outpoint);
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
totalPayment++;
}
}
return totalPayment;
}
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
{
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
if (paymentMethod != null &&
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
btc.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey &&
paymentMethod.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address;
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);
invoice.SetPaymentMethod(paymentMethod);
}
wallet.InvalidateCache(strategy);
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
return invoice;
}
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
{
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
{
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
var strategy = GetStrategy(network.CryptoCode, invoice);
if (strategy != null)
strategies.Add(strategy);
}
return strategies;
}
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
{
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
{
if (derivationStrategy.Network.CryptoCode == cryptoCode)
{
return derivationStrategy.DerivationStrategyBase;
}
}
return null;
}
public Task StopAsync(CancellationToken cancellationToken)
{
leases.Dispose();
_Cts.Cancel();
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
}
}
}

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
namespace BTCPayServer.Payments
{
public interface IPaymentMethodDetails
{
string GetPaymentDestination();
PaymentTypes GetPaymentType();
Money GetTxFee();
void SetPaymentDestination(string newPaymentDestination);
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments
{
public enum PaymentTypes
{
BTCLike
}
}

@ -24,8 +24,8 @@ namespace BTCPayServer
{
ServicePointManager.DefaultConnectionLimit = 100;
IWebHost host = null;
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider();
var processor = new ConsoleLoggerProcessor();
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
var loggerFactory = new LoggerFactory();
loggerFactory.AddProvider(loggerProvider);
var logger = loggerFactory.CreateLogger("Configuration");
@ -44,7 +44,8 @@ namespace BTCPayServer
.ConfigureLogging(l =>
{
l.AddFilter("Microsoft", LogLevel.Error);
l.AddProvider(new CustomConsoleLogProvider());
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
l.AddProvider(new CustomConsoleLogProvider(processor));
})
.UseStartup<Startup>()
.Build();
@ -61,13 +62,9 @@ namespace BTCPayServer
if (!string.IsNullOrEmpty(ex.Message))
Logs.Configuration.LogError(ex.Message);
}
catch (Exception exception)
{
logger.LogError("Exception thrown while running the server");
logger.LogError(exception.ToString());
}
finally
{
processor.Dispose();
if (host != null)
host.Dispose();
loggerProvider.Dispose();

@ -1,27 +1,29 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14139/",
"sslPort": 0
}
},
"profiles": {
"Default": {
"commandName": "Project"
},
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32839/",
"BTCPAY_NETWORK": "regtest",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://localhost:14142/"
}
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14139/",
"sslPort": 0
}
}
},
"profiles": {
"Default": {
"commandName": "Project",
"commandLineArgs": "--network testnet --chains ltc --ltcexplorerurl http://127.0.0.1:2727/"
},
"Docker-Regtest": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
},
"applicationUrl": "http://localhost:14142/"
}
}
}

@ -27,7 +27,7 @@ namespace BTCPayServer
{
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
{
TextSearch = TextSearch.Replace(filter, string.Empty);
TextSearch = TextSearch.Replace(filter, string.Empty, StringComparison.InvariantCulture);
}
}
TextSearch = TextSearch.Trim();

@ -5,12 +5,13 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Text;
using NBXplorer;
namespace BTCPayServer.Services
{
public class BTCPayServerEnvironment
{
public BTCPayServerEnvironment(IHostingEnvironment env)
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider)
{
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
#if DEBUG
@ -19,11 +20,13 @@ namespace BTCPayServer.Services
Build = "Release";
#endif
Environment = env;
ChainType = provider.NBXplorerNetworkProvider.ChainType;
}
public IHostingEnvironment Environment
{
get; set;
}
public ChainType ChainType { get; set; }
public string Version
{
get; set;

@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using NBitcoin;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Services
{
public class HardwareWalletException : Exception
{
public HardwareWalletException() { }
public HardwareWalletException(string message) : base(message) { }
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
}
public class HardwareWalletService
{
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
{
private readonly WebSocket webSocket;
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
{
if (webSocket == null)
throw new ArgumentNullException(nameof(webSocket));
this.webSocket = webSocket;
}
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
public async Task<byte[][]> Exchange(byte[][] apdus)
{
List<byte[]> responses = new List<byte[]>();
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
{
foreach (var apdu in apdus)
{
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
}
foreach (var apdu in apdus)
{
byte[] response = new byte[300];
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
Array.Resize(ref response, result.Count);
responses.Add(response);
}
}
return responses.ToArray();
}
}
private readonly LedgerClient _Ledger;
public LedgerClient Ledger
{
get
{
return _Ledger;
}
}
WebSocketTransport _Transport = null;
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
{
if (ledgerWallet == null)
throw new ArgumentNullException(nameof(ledgerWallet));
_Transport = new WebSocketTransport(ledgerWallet);
_Ledger = new LedgerClient(_Transport);
}
public async Task<LedgerTestResult> Test()
{
var version = await _Ledger.GetFirmwareVersionAsync();
return new LedgerTestResult() { Success = true };
}
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
{
P2SH = true,
Legacy = false
});
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
}
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
{
try
{
var pubKey = await ledger.GetWalletPubKeyAsync(account);
if (pubKey.Address.Network != network.NBitcoinNetwork)
{
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
}
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
return extpubkey;
}
catch (FormatException)
{
throw new HardwareWalletException("Unsupported ledger app");
}
}
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (!strategy.Segwit)
return false;
return await GetKeyPath(_Ledger, network, strategy) != null;
}
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
{
KeyPath foundKeyPath = null;
foreach (var account in
new[] { new KeyPath("49'"), new KeyPath("44'") }
.Select(purpose => purpose.Derive(network.CoinType))
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
{
try
{
var extpubkey = await GetExtPubKey(ledger, network, account, true);
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
{
foundKeyPath = account;
break;
}
}
catch (FormatException)
{
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
}
}
return foundKeyPath;
}
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
ReceivedCoin[] coins, BTCPayNetwork network,
(IDestination destination, Money amount, bool substractFees)[] send,
FeeRate feeRate,
IDestination changeAddress,
KeyPath changeKeyPath)
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
if (network == null)
throw new ArgumentNullException(nameof(network));
if (feeRate == null)
throw new ArgumentNullException(nameof(feeRate));
if (changeAddress == null)
throw new ArgumentNullException(nameof(changeAddress));
if (feeRate.FeePerK <= Money.Zero)
{
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
}
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");
}
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress);
builder.SendEstimatedFees(feeRate);
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach(var c in coins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
_Transport.Timeout = TimeSpan.FromMinutes(5);
var fullySigned = await Ledger.SignTransactionAsync(
usedCoins.Select(c => new SignatureRequest
{
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(),
unsigned,
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
return fullySigned;
}
}
public class LedgerTestResult
{
public bool Success { get; set; }
public string Error { get; set; }
}
public class GetXPubResult
{
public string ExtPubKey { get; set; }
}
}

@ -0,0 +1,81 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Invoices
{
public class PaymentMethodDictionary : IEnumerable<PaymentMethod>
{
Dictionary<PaymentMethodId, PaymentMethod> _Inner = new Dictionary<PaymentMethodId, PaymentMethod>();
public PaymentMethodDictionary()
{
}
public PaymentMethodDictionary(BTCPayNetworkProvider networkProvider)
{
NetworkProvider = networkProvider;
}
public BTCPayNetworkProvider NetworkProvider { get; set; }
public PaymentMethod this[PaymentMethodId index]
{
get
{
return _Inner[index];
}
}
public void Add(PaymentMethod paymentMethod)
{
_Inner.Add(paymentMethod.GetId(), paymentMethod);
}
public void Remove(PaymentMethod paymentMethod)
{
_Inner.Remove(paymentMethod.GetId());
}
public bool TryGetValue(PaymentMethodId paymentMethodId, out PaymentMethod data)
{
if (paymentMethodId == null)
throw new ArgumentNullException(nameof(paymentMethodId));
return _Inner.TryGetValue(paymentMethodId, out data);
}
public void AddOrReplace(PaymentMethod paymentMethod)
{
var key = paymentMethod.GetId();
_Inner.Remove(key);
_Inner.Add(key, paymentMethod);
}
public IEnumerator<PaymentMethod> GetEnumerator()
{
return _Inner.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public PaymentMethod TryGet(PaymentMethodId paymentMethodId)
{
if (paymentMethodId == null)
throw new ArgumentNullException(nameof(paymentMethodId));
_Inner.TryGetValue(paymentMethodId, out var value);
return value;
}
public PaymentMethod TryGet(string network, PaymentTypes paymentType)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
var id = new PaymentMethodId(network, paymentType);
return TryGet(id);
}
}
}

@ -5,12 +5,13 @@ using System;
using System.Collections.Generic;
using System.Text;
using BTCPayServer.Models;
using NBitpayClient;
using Newtonsoft.Json.Linq;
using NBitcoin.DataEncoders;
using BTCPayServer.Data;
using NBXplorer.Models;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Invoices
{
@ -121,7 +122,7 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use GetCryptoData(network).Rate instead")]
[Obsolete("Use GetPaymentMethod(network) instead")]
public decimal Rate
{
get; set;
@ -135,7 +136,7 @@ namespace BTCPayServer.Services.Invoices
get; set;
}
[Obsolete("Use GetCryptoData(network).DepositAddress instead")]
[Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")]
public string DepositAddress
{
get; set;
@ -168,6 +169,30 @@ namespace BTCPayServer.Services.Invoices
set;
}
public DerivationStrategyBase GetDerivationStrategy(BTCPayNetwork network)
{
#pragma warning disable CS0618
if (!string.IsNullOrEmpty(DerivationStrategies))
{
JObject strategies = JObject.Parse(DerivationStrategies);
#pragma warning restore CS0618
foreach (var strat in strategies.Properties())
{
if (strat.Name == network.CryptoCode)
{
return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network).DerivationStrategyBase;
}
}
}
#pragma warning disable CS0618
if (network.IsBTC && !string.IsNullOrEmpty(DerivationStrategy))
{
return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, network).DerivationStrategyBase;
}
return null;
#pragma warning restore CS0618
}
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
{
#pragma warning disable CS0618
@ -180,7 +205,7 @@ namespace BTCPayServer.Services.Invoices
var network = networks.GetNetwork(strat.Name);
if (network != null)
{
if (network == networks.BTC && btcReturned)
if (network == networks.BTC)
btcReturned = true;
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
}
@ -220,10 +245,27 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use GetPayments instead")]
public List<PaymentEntity> Payments
{
get; set;
}
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments.ToList();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
return Payments.Where(p => p.CryptoCode == cryptoCode).ToList();
}
public List<PaymentEntity> GetPayments(BTCPayNetwork network)
{
return GetPayments(network.CryptoCode);
}
#pragma warning restore CS0618
public bool Refundable
{
get;
@ -240,7 +282,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
[Obsolete("Use GetCryptoData(network).TxFee instead")]
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
public Money TxFee
{
get;
@ -262,8 +304,9 @@ namespace BTCPayServer.Services.Invoices
set;
}
[Obsolete("Use Set/GetCryptoData() instead")]
public JObject CryptoData { get; set; }
[Obsolete("Use Set/GetPaymentMethod() instead")]
[JsonProperty(PropertyName = "cryptoData")]
public JObject PaymentMethod { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public DateTimeOffset MonitoringExpiration
@ -283,6 +326,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool ExtendedNotifications { get; set; }
public List<InvoiceEventData> Events { get; internal set; }
public bool IsExpired()
{
@ -306,12 +350,13 @@ namespace BTCPayServer.Services.Invoices
Flags = new Flags() { Refundable = Refundable }
};
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
foreach (var info in this.GetCryptoData().Values)
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
foreach (var info in this.GetPaymentMethods(networkProvider, true))
{
var accounting = info.Calculate();
var cryptoInfo = new InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.CryptoCode;
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
cryptoInfo.Rate = info.Rate;
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
@ -320,20 +365,20 @@ namespace BTCPayServer.Services.Invoices
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
cryptoInfo.TxCount = accounting.TxCount;
cryptoInfo.CryptoPaid = accounting.CryptoPaid;
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
cryptoInfo.Address = info.DepositAddress;
cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
cryptoInfo.ExRates = new Dictionary<string, double>
{
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
};
var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC";
var scheme = info.Network.UriScheme;
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
@ -353,8 +398,8 @@ namespace BTCPayServer.Services.Invoices
dto.PaymentUrls = cryptoInfo.PaymentUrls;
}
#pragma warning restore CS0618
dto.CryptoInfo.Add(cryptoInfo);
if (!info.IsPhantomBTC)
dto.CryptoInfo.Add(cryptoInfo);
}
Populate(ProductInformation, dto);
@ -373,42 +418,50 @@ namespace BTCPayServer.Services.Invoices
JsonConvert.PopulateObject(str, dest);
}
internal bool Support(BTCPayNetwork network)
internal bool Support(PaymentMethodId paymentMethodId)
{
var rates = GetCryptoData();
return rates.TryGetValue(network.CryptoCode, out var data);
var rates = GetPaymentMethods(null);
return rates.TryGet(paymentMethodId) != null;
}
public CryptoData GetCryptoData(string cryptoCode)
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider)
{
GetCryptoData().TryGetValue(cryptoCode, out var data);
GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data);
return data;
}
public CryptoData GetCryptoData(BTCPayNetwork network)
public PaymentMethod GetPaymentMethod(BTCPayNetwork network, PaymentTypes paymentType, BTCPayNetworkProvider networkProvider)
{
GetCryptoData().TryGetValue(network.CryptoCode, out var data);
return data;
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
}
public Dictionary<string, CryptoData> GetCryptoData()
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false)
{
Dictionary<string, CryptoData> rates = new Dictionary<string, CryptoData>();
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
var serializer = new Serializer(Dummy);
PaymentMethod phantom = null;
#pragma warning disable CS0618
// Legacy
if (Rate != 0.0m)
if (alwaysIncludeBTC)
{
rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress });
var btcNetwork = networkProvider?.GetNetwork("BTC");
phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork };
if (btcNetwork != null || networkProvider == null)
rates.Add(phantom);
}
if (CryptoData != null)
if (PaymentMethod != null)
{
foreach (var prop in CryptoData.Properties())
foreach (var prop in PaymentMethod.Properties())
{
var r = serializer.ToObject<CryptoData>(prop.Value.ToString());
r.CryptoCode = prop.Name;
if (prop.Name == "BTC" && phantom != null)
rates.Remove(phantom);
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
r.CryptoCode = paymentMethodId.CryptoCode;
r.PaymentType = paymentMethodId.PaymentType.ToString();
r.ParentEntity = this;
rates.TryAdd(r.CryptoCode, r);
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
if(r.Network != null || networkProvider == null)
rates.Add(r);
}
}
#pragma warning restore CS0618
@ -416,23 +469,34 @@ namespace BTCPayServer.Services.Invoices
}
Network Dummy = Network.Main;
public void SetCryptoData(Dictionary<string, CryptoData> cryptoData)
public void SetPaymentMethod(PaymentMethod paymentMethod)
{
var dict = GetPaymentMethods(null);
dict.AddOrReplace(paymentMethod);
SetPaymentMethods(dict);
}
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
{
if (paymentMethods.NetworkProvider != null)
throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null");
var obj = new JObject();
var serializer = new Serializer(Dummy);
foreach (var kv in cryptoData)
{
var clone = serializer.ToObject<CryptoData>(serializer.ToString(kv.Value));
clone.CryptoCode = null;
obj.Add(new JProperty(kv.Key, JObject.Parse(serializer.ToString(clone))));
}
#pragma warning disable CS0618
CryptoData = obj;
foreach (var v in paymentMethods)
{
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
clone.CryptoCode = null;
clone.PaymentType = null;
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
}
PaymentMethod = obj;
#pragma warning restore CS0618
}
}
public class CryptoDataAccounting
public class PaymentMethodAccounting
{
/// <summary>
/// Total amount of this invoice
@ -464,24 +528,168 @@ namespace BTCPayServer.Services.Invoices
public Money NetworkFee { get; set; }
}
public class CryptoData
public class PaymentMethodId
{
public PaymentMethodId(string cryptoCode, PaymentTypes paymentType)
{
if (cryptoCode == null)
throw new ArgumentNullException(nameof(cryptoCode));
PaymentType = paymentType;
CryptoCode = cryptoCode;
}
public string CryptoCode { get; private set; }
public PaymentTypes PaymentType { get; private set; }
public override bool Equals(object obj)
{
PaymentMethodId item = obj as PaymentMethodId;
if (item == null)
return false;
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
}
public static bool operator ==(PaymentMethodId a, PaymentMethodId b)
{
if (System.Object.ReferenceEquals(a, b))
return true;
if (((object)a == null) || ((object)b == null))
return false;
return a.ToString() == b.ToString();
}
public static bool operator !=(PaymentMethodId a, PaymentMethodId b)
{
return !(a == b);
}
public override int GetHashCode()
{
#pragma warning disable CA1307 // Specify StringComparison
return ToString().GetHashCode();
#pragma warning restore CA1307 // Specify StringComparison
}
public override string ToString()
{
if (PaymentType == PaymentTypes.BTCLike)
return CryptoCode;
return CryptoCode + "_" + PaymentType.ToString();
}
public static PaymentMethodId Parse(string str)
{
var parts = str.Split('_');
return new PaymentMethodId(parts[0], parts.Length == 1 ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(parts[1]));
}
}
public class PaymentMethod
{
[JsonIgnore]
public InvoiceEntity ParentEntity { get; set; }
[JsonIgnore]
public BTCPayNetwork Network { get; set; }
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().CryptoCode instead")]
public string CryptoCode { get; set; }
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
[Obsolete("Use GetId().PaymentType instead")]
public string PaymentType { get; set; }
public PaymentMethodId GetId()
{
#pragma warning disable CS0618 // Type or member is obsolete
return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(PaymentType));
#pragma warning restore CS0618 // Type or member is obsolete
}
public void SetId(PaymentMethodId id)
{
#pragma warning disable CS0618 // Type or member is obsolete
CryptoCode = id.CryptoCode;
PaymentType = id.PaymentType.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
[JsonProperty(PropertyName = "rate")]
public decimal Rate { get; set; }
[Obsolete("Use GetPaymentMethodDetails() instead")]
[JsonProperty(PropertyName = "paymentMethod")]
public JObject PaymentMethodDetails { get; set; }
public IPaymentMethodDetails GetPaymentMethodDetails()
{
#pragma warning disable CS0618 // Type or member is obsolete
// Legacy, old code does not have PaymentMethods
if (string.IsNullOrEmpty(PaymentType) || PaymentMethodDetails == null)
{
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
FeeRate = FeeRate,
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork),
TxFee = TxFee
};
}
else
{
if (GetId().PaymentType == PaymentTypes.BTCLike)
{
var method = DeserializePaymentMethodDetails<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(PaymentMethodDetails);
method.TxFee = TxFee;
method.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
method.FeeRate = FeeRate;
return method;
}
}
throw new NotSupportedException(PaymentType);
#pragma warning restore CS0618 // Type or member is obsolete
}
private T DeserializePaymentMethodDetails<T>(JObject jobj) where T : class, IPaymentMethodDetails
{
return JsonConvert.DeserializeObject<T>(jobj.ToString());
}
public void SetPaymentMethodDetails(IPaymentMethodDetails paymentMethod)
{
#pragma warning disable CS0618 // Type or member is obsolete
// Legacy, need to fill the old fields
if (PaymentType == null)
PaymentType = paymentMethod.GetPaymentType().ToString();
else if (PaymentType != paymentMethod.GetPaymentType().ToString())
throw new InvalidOperationException("Invalid payment method affected");
if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod)
{
TxFee = bitcoinPaymentMethod.TxFee;
FeeRate = bitcoinPaymentMethod.FeeRate;
DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString();
}
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
PaymentMethodDetails = jobj;
#pragma warning restore CS0618 // Type or member is obsolete
}
[JsonProperty(PropertyName = "feeRate")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")]
public FeeRate FeeRate { get; set; }
[JsonProperty(PropertyName = "txFee")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")]
public Money TxFee { get; set; }
[JsonProperty(PropertyName = "depositAddress")]
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
public string DepositAddress { get; set; }
public CryptoDataAccounting Calculate()
[JsonIgnore]
public bool IsPhantomBTC { get; set; }
public PaymentMethodAccounting Calculate()
{
var cryptoData = ParentEntity.GetCryptoData();
var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC);
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate);
var paid = Money.Zero;
var cryptoPaid = Money.Zero;
@ -490,19 +698,22 @@ namespace BTCPayServer.Services.Invoices
bool paidEnough = totalDue <= paid;
int txCount = 0;
var payments =
ParentEntity.Payments
ParentEntity.GetPayments()
.Where(p => p.Accounted)
.OrderByDescending(p => p.ReceivedTime)
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
paidTxFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
paid += _.GetValue(cryptoData, CryptoCode);
if(!paidEnough)
totalDue += paidTxFee;
paidEnough |= totalDue <= paid;
if (CryptoCode == _.GetCryptoCode())
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetpaymentMethodId()].GetTxFee());
paid += _.GetValue(paymentMethods, GetId());
if (!paidEnough)
{
cryptoPaid += _.GetValue();
totalDue += txFee;
paidTxFee += txFee;
}
paidEnough |= totalDue <= paid;
if (GetId() == _.GetpaymentMethodId())
{
cryptoPaid += _.GetCryptoPaymentData().GetValue();
txCount++;
}
return _;
@ -512,29 +723,26 @@ namespace BTCPayServer.Services.Invoices
if (!paidEnough)
{
txCount++;
totalDue += TxFee;
totalDue += GetTxFee();
paidTxFee += GetTxFee();
}
var accounting = new CryptoDataAccounting();
var accounting = new PaymentMethodAccounting();
accounting.TotalDue = totalDue;
accounting.Paid = paid;
accounting.TxCount = txCount;
accounting.CryptoPaid = cryptoPaid;
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
accounting.NetworkFee = TxFee * txCount;
accounting.NetworkFee = paidTxFee;
return accounting;
}
}
public class AccountedPaymentEntity
{
public int Confirmations
private Money GetTxFee()
{
get;
set;
var method = GetPaymentMethodDetails();
if (method == null)
return Money.Zero;
return method.GetTxFee();
}
public PaymentEntity Payment { get; set; }
public Transaction Transaction { get; set; }
}
public class PaymentEntity
@ -543,64 +751,133 @@ namespace BTCPayServer.Services.Invoices
{
get; set;
}
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
public OutPoint Outpoint
{
get; set;
}
[Obsolete("Use GetValue() or GetScriptPubKey() instead")]
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")]
public TxOut Output
{
get; set;
}
public Script GetScriptPubKey()
{
#pragma warning disable CS0618
return Output.ScriptPubKey;
#pragma warning restore CS0618
}
public bool Accounted
{
get; set;
}
[Obsolete("Use GetCryptoCode() instead")]
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")]
public string CryptoCode
{
get;
set;
}
public Money GetValue()
[Obsolete("Use GetCryptoPaymentData() instead")]
public string CryptoPaymentData { get; set; }
[Obsolete("Use GetpaymentMethodId().PaymentType instead")]
public string CryptoPaymentDataType { get; set; }
public CryptoPaymentData GetCryptoPaymentData()
{
#pragma warning disable CS0618
return Output.Value;
if (string.IsNullOrEmpty(CryptoPaymentDataType))
{
// In case this is a payment done before this update, consider it unconfirmed with RBF for safety
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
paymentData.Outpoint = Outpoint;
paymentData.Output = Output;
paymentData.RBF = true;
paymentData.ConfirmationCount = 0;
paymentData.Legacy = true;
return paymentData;
}
if (GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
{
var paymentData = JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikePaymentData>(CryptoPaymentData);
// legacy
paymentData.Output = Output;
paymentData.Outpoint = Outpoint;
return paymentData;
}
throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
#pragma warning restore CS0618
}
public Money GetValue(Dictionary<string, CryptoData> cryptoData, string cryptoCode, Money value = null)
public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData)
{
#pragma warning disable CS0618
if (cryptoPaymentData is Payments.Bitcoin.BitcoinLikePaymentData paymentData)
{
// Legacy
Outpoint = paymentData.Outpoint;
Output = paymentData.Output;
///
}
else
throw new NotSupportedException(cryptoPaymentData.ToString());
CryptoPaymentDataType = paymentData.GetPaymentType().ToString();
CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData);
#pragma warning restore CS0618
}
public Money GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, Money value = null)
{
#pragma warning disable CS0618
value = value ?? Output.Value;
#pragma warning restore CS0618
var to = cryptoCode;
var from = GetCryptoCode();
var to = paymentMethodId;
var from = this.GetpaymentMethodId();
if (to == from)
return value;
var fromRate = cryptoData[from].Rate;
var toRate = cryptoData[to].Rate;
var fromRate = paymentMethods[from].Rate;
var toRate = paymentMethods[to].Rate;
var fiatValue = fromRate * value.ToDecimal(MoneyUnit.BTC);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return Money.Coins(otherCurrencyValue);
}
public PaymentMethodId GetpaymentMethodId()
{
#pragma warning disable CS0618 // Type or member is obsolete
return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(CryptoPaymentDataType));
#pragma warning restore CS0618 // Type or member is obsolete
}
public string GetCryptoCode()
{
#pragma warning disable CS0618
return CryptoCode ?? "BTC";
#pragma warning restore CS0618
}
}
public interface CryptoPaymentData
{
/// <summary>
/// Returns an identifier which uniquely identify the payment
/// </summary>
/// <returns>The payment id</returns>
string GetPaymentId();
/// <summary>
/// Returns terms which will be indexed and searchable in the search bar of invoice
/// </summary>
/// <returns>The search terms</returns>
string[] GetSearchTerms();
/// <summary>
/// Get value of what as been paid
/// </summary>
/// <returns>The amount paid</returns>
Money GetValue();
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
}
}

@ -1,4 +1,5 @@
using DBreeze;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;
@ -16,6 +17,8 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using System.Globalization;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
namespace BTCPayServer.Services.Invoices
{
@ -32,32 +35,18 @@ namespace BTCPayServer.Services.Invoices
}
}
Network _Network;
public Network Network
{
get
{
return _Network;
}
set
{
_Network = value;
}
}
private ApplicationDbContextFactory _ContextFactory;
private CustomThreadPool _IndexerThread;
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
{
_Engine = new DBreezeEngine(dbreezePath);
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
_Network = network;
_ContextFactory = contextFactory;
}
public async Task<bool> RemovePendingInvoice(string invoiceId)
{
Logs.PayServer.LogInformation($"Remove pending invoice {invoiceId}");
using (var ctx = _ContextFactory.CreateContext())
{
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
@ -70,12 +59,22 @@ namespace BTCPayServer.Services.Invoices
}
}
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey, string cryptoCode)
public async Task<InvoiceEntity> GetInvoiceFromScriptPubKey(Script scriptPubKey, string cryptoCode)
{
using (var db = _ContextFactory.CreateContext())
{
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString() + "#" + cryptoCode);
return result?.InvoiceDataId;
var key = scriptPubKey.Hash.ToString() + "#" + cryptoCode;
var result = await db.AddressInvoices
#pragma warning disable CS0618
.Where(a => a.Address == key)
#pragma warning restore CS0618
.Select(a => a.InvoiceData)
.Include(a => a.Payments)
.Include(a => a.RefundAddresses)
.FirstOrDefaultAsync();
if (result == null)
return null;
return ToEntity(result);
}
}
@ -105,9 +104,11 @@ namespace BTCPayServer.Services.Invoices
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
{
List<string> textSearch = new List<string>();
invoice = Clone(invoice);
invoice = Clone(invoice, null);
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
#pragma warning disable CS0618
invoice.Payments = new List<PaymentEntity>();
#pragma warning restore CS0618
invoice.StoreId = storeId;
using (var context = _ContextFactory.CreateContext())
{
@ -116,30 +117,33 @@ namespace BTCPayServer.Services.Invoices
StoreDataId = storeId,
Id = invoice.Id,
Created = invoice.InvoiceTime,
Blob = ToBytes(invoice),
Blob = ToBytes(invoice, null),
OrderId = invoice.OrderId,
Status = invoice.Status,
ItemCode = invoice.ProductInformation.ItemCode,
CustomerEmail = invoice.RefundMail
});
foreach (var cryptoData in invoice.GetCryptoData().Values)
foreach (var paymentMethod in invoice.GetPaymentMethods(networkProvider))
{
var network = networkProvider.GetNetwork(cryptoData.CryptoCode);
if (network == null)
if (paymentMethod.Network == null)
throw new InvalidOperationException("CryptoCode unsupported");
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
string address = GetDestination(paymentMethod);
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoice.Id,
CreatedTime = DateTimeOffset.UtcNow,
}.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash, network.CryptoCode));
}.Set(address, paymentMethod.GetId()));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoice.Id,
Assigned = DateTimeOffset.UtcNow
}.SetAddress(cryptoData.DepositAddress, cryptoData.CryptoCode));
textSearch.Add(cryptoData.DepositAddress);
textSearch.Add(cryptoData.Calculate().TotalDue.ToString());
}.SetAddress(paymentDestination, paymentMethod.GetId().ToString()));
textSearch.Add(paymentDestination);
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
}
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
await context.SaveChangesAsync().ConfigureAwait(false);
@ -149,8 +153,8 @@ namespace BTCPayServer.Services.Invoices
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture));
textSearch.Add(invoice.OrderId);
textSearch.Add(ToString(invoice.BuyerInformation));
textSearch.Add(ToString(invoice.ProductInformation));
textSearch.Add(ToString(invoice.BuyerInformation, null));
textSearch.Add(ToString(invoice.ProductInformation, null));
textSearch.Add(invoice.StoreId);
AddToTextSearch(invoice.Id, textSearch.ToArray());
@ -158,7 +162,18 @@ namespace BTCPayServer.Services.Invoices
return invoice;
}
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network)
private static string GetDestination(PaymentMethod paymentMethod)
{
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
{
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).DepositAddress.ScriptPubKey.Hash.ToString();
}
///////////////
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
}
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network)
{
using (var context = _ContextFactory.CreateContext())
{
@ -166,52 +181,71 @@ namespace BTCPayServer.Services.Invoices
if (invoice == null)
return false;
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob);
var cryptoData = invoiceEntity.GetCryptoData();
var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault();
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType(), null);
if (currencyData == null)
return false;
if (currencyData.DepositAddress != null)
var existingPaymentMethod = currencyData.GetPaymentMethodDetails();
if (existingPaymentMethod.GetPaymentDestination() != null)
{
MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode);
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
}
currencyData.DepositAddress = bitcoinAddress.ToString();
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
currencyData.SetPaymentMethodDetails(existingPaymentMethod);
#pragma warning disable CS0618
if (network.IsBTC)
{
invoiceEntity.DepositAddress = currencyData.DepositAddress;
}
#pragma warning restore CS0618
invoiceEntity.SetCryptoData(cryptoData);
invoice.Blob = ToBytes(invoiceEntity);
invoiceEntity.SetPaymentMethod(currencyData);
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
context.AddressInvoices.Add(new AddressInvoiceData() {
InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }
.SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode));
context.AddressInvoices.Add(new AddressInvoiceData()
{
InvoiceDataId = invoiceId,
CreatedTime = DateTimeOffset.UtcNow
}
.Set(GetDestination(currencyData), currencyData.GetId()));
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
{
InvoiceDataId = invoiceId,
Assigned = DateTimeOffset.UtcNow
}.SetAddress(bitcoinAddress.ToString(), network.CryptoCode));
}.SetAddress(paymentMethod.GetPaymentDestination(), network.CryptoCode));
await context.SaveChangesAsync();
AddToTextSearch(invoice.Id, bitcoinAddress.ToString());
AddToTextSearch(invoice.Id, paymentMethod.GetPaymentDestination());
return true;
}
}
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
public async Task AddInvoiceEvent(string invoiceId, object evt)
{
foreach (var address in entity.GetCryptoData())
using (var context = _ContextFactory.CreateContext())
{
if (cryptoCode != null && cryptoCode != address.Value.CryptoCode)
context.InvoiceEvents.Add(new InvoiceEventData()
{
InvoiceDataId = invoiceId,
Message = evt.ToString(),
Timestamp = DateTimeOffset.UtcNow,
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
});
await context.SaveChangesAsync();
}
}
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, PaymentMethodId paymentMethodId)
{
foreach (var address in entity.GetPaymentMethods(null))
{
if (paymentMethodId != null && paymentMethodId != address.GetId())
continue;
var historical = new HistoricalAddressInvoiceData();
historical.InvoiceDataId = invoiceId;
historical.SetAddress(address.Value.DepositAddress, cryptoCode);
historical.SetAddress(address.GetPaymentMethodDetails().GetPaymentDestination(), address.GetId().ToString());
historical.UnAssigned = DateTimeOffset.UtcNow;
context.Attach(historical);
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
@ -225,7 +259,7 @@ namespace BTCPayServer.Services.Invoices
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
if (invoiceData == null)
return;
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
MarkUnassigned(invoiceId, invoiceEntity, context, null);
try
{
@ -308,13 +342,15 @@ namespace BTCPayServer.Services.Invoices
private InvoiceEntity ToEntity(InvoiceData invoice)
{
var entity = ToObject<InvoiceEntity>(invoice.Blob);
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
#pragma warning disable CS0618
entity.Payments = invoice.Payments.Select(p =>
{
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
paymentEntity.Accounted = p.Accounted;
return paymentEntity;
}).ToList();
#pragma warning restore CS0618
entity.ExceptionStatus = invoice.ExceptionStatus;
entity.Status = invoice.Status;
entity.RefundMail = invoice.CustomerEmail;
@ -325,7 +361,11 @@ namespace BTCPayServer.Services.Invoices
}
if (invoice.AddressInvoices != null)
{
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetHash() + a.GetCryptoCode()).ToHashSet();
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetpaymentMethodId().ToString()).ToHashSet();
}
if(invoice.Events != null)
{
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
}
return entity;
}
@ -339,8 +379,10 @@ namespace BTCPayServer.Services.Invoices
.Invoices
.Include(o => o.Payments)
.Include(o => o.RefundAddresses);
if(queryObject.IncludeAddresses)
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);
@ -360,7 +402,7 @@ namespace BTCPayServer.Services.Invoices
{
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
if (ids.Count == 0)
return new InvoiceEntity[0];
return Array.Empty<InvoiceEntity>();
query = query.Where(i => ids.Contains(i.Id));
}
@ -394,7 +436,7 @@ namespace BTCPayServer.Services.Invoices
}
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs)
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
{
if (outputs.Length == 0)
return;
@ -408,47 +450,49 @@ namespace BTCPayServer.Services.Invoices
{
Id = invoiceId + "-" + i,
InvoiceDataId = invoiceId,
Blob = ToBytes(output)
Blob = ToBytes(output, network)
});
i++;
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray();
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(network)).Where(a => a != null).ToArray();
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
}
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin, string cryptoCode)
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode)
{
using (var context = _ContextFactory.CreateContext())
{
PaymentEntity entity = new PaymentEntity
{
Outpoint = receivedCoin.Outpoint,
#pragma warning disable CS0618
Output = receivedCoin.TxOut,
CryptoCode = cryptoCode,
#pragma warning restore CS0618
ReceivedTime = DateTime.UtcNow
ReceivedTime = date.UtcDateTime,
Accounted = false
};
entity.SetCryptoPaymentData(paymentData);
PaymentData data = new PaymentData
{
Id = receivedCoin.Outpoint.ToString(),
Blob = ToBytes(entity),
InvoiceDataId = invoiceId
Id = paymentData.GetPaymentId(),
Blob = ToBytes(entity, null),
InvoiceDataId = invoiceId,
Accounted = false
};
context.Payments.Add(data);
await context.SaveChangesAsync().ConfigureAwait(false);
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString());
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
return entity;
}
}
public async Task UpdatePayments(List<AccountedPaymentEntity> payments)
public async Task UpdatePayments(List<PaymentEntity> payments)
{
if (payments.Count == 0)
return;
@ -456,34 +500,37 @@ namespace BTCPayServer.Services.Invoices
{
foreach (var payment in payments)
{
var paymentData = payment.GetCryptoPaymentData();
var data = new PaymentData();
data.Id = payment.Payment.Outpoint.ToString();
data.Accounted = payment.Payment.Accounted;
data.Id = paymentData.GetPaymentId();
data.Accounted = payment.Accounted;
data.Blob = ToBytes(payment, null);
context.Attach(data);
context.Entry(data).Property(o => o.Accounted).IsModified = true;
context.Entry(data).Property(o => o.Blob).IsModified = true;
}
await context.SaveChangesAsync().ConfigureAwait(false);
}
}
private T ToObject<T>(byte[] value)
private T ToObject<T>(byte[] value, Network network)
{
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), network);
}
private byte[] ToBytes<T>(T obj)
private byte[] ToBytes<T>(T obj, Network network)
{
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj));
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj, network));
}
private T Clone<T>(T invoice)
private T Clone<T>(T invoice, Network network)
{
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network);
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice, network), network);
}
private string ToString<T>(T data)
private string ToString<T>(T data, Network network)
{
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
return NBitcoin.JsonConverters.Serializer.ToString(data, network);
}
public void Dispose()
@ -549,5 +596,7 @@ namespace BTCPayServer.Services.Invoices
set;
}
public bool IncludeAddresses { get; set; }
public bool IncludeEvents { get; set; }
}
}

@ -1,4 +1,6 @@
using Hangfire;
using BTCPayServer.Logging;
using Microsoft.Extensions.Logging;
using Hangfire;
using System;
using System.Collections.Generic;
using System.Linq;
@ -20,10 +22,16 @@ namespace BTCPayServer.Services.Mails
_JobClient = jobClient;
_Repository = repository;
}
public Task SendEmailAsync(string email, string subject, string message)
public async Task SendEmailAsync(string email, string subject, string message)
{
var settings = await _Repository.GetSettingAsync<EmailSettings>();
if (settings == null)
{
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
return;
}
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
return Task.CompletedTask;
return;
}
public async Task SendMailCore(string email, string subject, string message)

@ -11,6 +11,16 @@ namespace BTCPayServer.Services.Rates
{
IMemoryCache _Cache;
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
ConcurrentDictionary<string, IRateProvider> _LongCacheProviders = new ConcurrentDictionary<string, IRateProvider>();
public IMemoryCache Cache
{
get
{
return _Cache;
}
}
public CachedDefaultRateProviderFactory(IMemoryCache cache)
{
if (cache == null)
@ -18,10 +28,13 @@ namespace BTCPayServer.Services.Rates
_Cache = cache;
}
public IRateProvider RateProvider { get; set; }
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
public IRateProvider GetRateProvider(BTCPayNetwork network)
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
}
}
}

@ -19,38 +19,43 @@ namespace BTCPayServer.Services.Rates
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this._MemoryCache = memoryCache;
this.MemoryCache = memoryCache;
this._CryptoCode = cryptoCode;
}
public IRateProvider Inner
{
get
{
return _Inner;
}
}
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
public Task<decimal> GetRateAsync(string currency)
{
return _MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode, (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRateAsync(currency);
});
}
private bool TryGetFromCache(string key, out object obj)
{
obj = _MemoryCache.Get(key);
return obj != null;
}
public Task<ICollection<Rate>> GetRatesAsync()
{
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync();
});
}
public string AdditionalScope { get; set; }
}
}

@ -2,6 +2,7 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
@ -24,6 +25,8 @@ namespace BTCPayServer.Services.Rates
CryptoCode = cryptoCode ?? "BTC";
}
public string Exchange { get; set; }
public string CryptoCode { get; set; }
public string Market
@ -45,7 +48,15 @@ namespace BTCPayServer.Services.Rates
private async Task<Dictionary<string, decimal>> GetRatesCore()
{
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
HttpResponseMessage resp = null;
if (Exchange == null)
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
}
else
{
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
}
using (resp)
{
@ -57,15 +68,24 @@ namespace BTCPayServer.Services.Rates
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
if(Exchange != null)
{
rates = (JObject)rates["symbols"];
}
return rates.Properties()
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => ToDecimal(p.Value["last"]));
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
{
TryToDecimal(p, out decimal v);
return v;
});
}
}
private decimal ToDecimal(JToken token)
private bool TryToDecimal(JProperty p, out decimal v)
{
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
JToken token = p.Value[Exchange == null ? "last" : "bid"];
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
}
public async Task<ICollection<Rate>> GetRatesAsync()

@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Rates
{
public interface IRateProviderFactory
{
IRateProvider GetRateProvider(BTCPayNetwork network);
IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache);
}
}

@ -6,17 +6,38 @@ using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class MockRateProviderFactory : IRateProviderFactory
{
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
public MockRateProviderFactory()
{
}
public void AddMock(MockRateProvider mock)
{
_Mocks.Add(mock);
}
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
{
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
}
}
public class MockRateProvider : IRateProvider
{
List<Rate> _Rates;
public MockRateProvider(params Rate[] rates)
public string CryptoCode { get; }
public MockRateProvider(string cryptoCode, params Rate[] rates)
{
_Rates = new List<Rate>(rates);
CryptoCode = cryptoCode;
}
public MockRateProvider(List<Rate> rates)
public MockRateProvider(string cryptoCode, List<Rate> rates)
{
_Rates = rates;
CryptoCode = cryptoCode;
}
public Task<decimal> GetRateAsync(string currency)
{

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
namespace BTCPayServer.Services.Rates
{
public class TweakRateProvider : IRateProvider
{
private BTCPayNetwork network;
private IRateProvider rateProvider;
private List<RateRule> rateRules;
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
if (rateProvider == null)
throw new ArgumentNullException(nameof(rateProvider));
if (rateRules == null)
throw new ArgumentNullException(nameof(rateRules));
this.network = network;
this.rateProvider = rateProvider;
this.rateRules = rateRules;
}
public async Task<decimal> GetRateAsync(string currency)
{
var rate = await rateProvider.GetRateAsync(currency);
foreach(var rule in rateRules)
{
rate = rule.Apply(network, rate);
}
return rate;
}
public async Task<ICollection<Rate>> GetRatesAsync()
{
List<Rate> rates = new List<Rate>();
foreach (var rate in await rateProvider.GetRatesAsync())
{
var localRate = rate.Value;
foreach (var rule in rateRules)
{
localRate = rule.Apply(network, localRate);
}
rates.Add(new Rate(rate.Currency, localRate));
}
return rates;
}
}
}

@ -68,7 +68,8 @@ namespace BTCPayServer.Services.Stores
StoreData store = new StoreData
{
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
StoreName = name
StoreName = name,
SpeedPolicy = Invoices.SpeedPolicy.MediumSpeed
};
var userStore = new UserStore
{

@ -1,4 +1,5 @@
using NBitcoin;
using Microsoft.Extensions.Logging;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using System;
@ -9,82 +10,172 @@ using System.Threading.Tasks;
using BTCPayServer.Data;
using System.Threading;
using NBXplorer.Models;
using Microsoft.Extensions.Caching.Memory;
using BTCPayServer.Logging;
using System.Collections.Concurrent;
namespace BTCPayServer.Services.Wallets
{
public class KnownState
public class ReceivedCoin
{
public uint256 UnconfirmedHash { get; set; }
public uint256 ConfirmedHash { get; set; }
public Coin Coin { get; set; }
public DateTimeOffset Timestamp { get; set; }
public KeyPath KeyPath { get; set; }
}
public class GetCoinsResult
public class NetworkCoins
{
public Coin[] Coins { get; set; }
public KnownState State { get; set; }
public DerivationStrategy Strategy { get; set; }
public class TimestampedCoin
{
public DateTimeOffset DateTime { get; set; }
public Coin Coin { get; set; }
}
public TimestampedCoin[] TimestampedCoins { get; set; }
public DerivationStrategyBase Strategy { get; set; }
public BTCPayWallet Wallet { get; set; }
}
public class BTCPayWallet
{
private ExplorerClientProvider _Client;
ApplicationDbContextFactory _DBFactory;
public BTCPayWallet(ExplorerClientProvider client, ApplicationDbContextFactory factory)
private ExplorerClient _Client;
private IMemoryCache _MemoryCache;
public BTCPayWallet(ExplorerClient client, IMemoryCache memoryCache, BTCPayNetwork network)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
if (factory == null)
throw new ArgumentNullException(nameof(factory));
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
_Client = client;
_DBFactory = factory;
_Network = network;
_MemoryCache = memoryCache;
}
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategy derivationStrategy)
private readonly BTCPayNetwork _Network;
public BTCPayNetwork Network
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);
var pathInfo = await client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(client.Network);
}
public async Task TrackAsync(DerivationStrategy derivationStrategy)
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);
await client.TrackAsync(derivationStrategy.DerivationStrategyBase);
}
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
{
var client = _Client.GetExplorerClient(network);
return client.GetTransactionAsync(txId, cancellation);
}
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var client = _Client.GetExplorerClient(strategy.Network);
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
return new GetCoinsResult()
get
{
Coins = utxos,
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
Strategy = strategy,
};
return _Network;
}
}
public Task BroadcastTransactionsAsync(BTCPayNetwork network, List<Transaction> transactions)
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
var client = _Client.GetExplorerClient(network);
var tasks = transactions.Select(t => client.BroadcastAsync(t)).ToArray();
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
// Might happen on some broken install
if (pathInfo == null)
{
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
}
return pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork);
}
public async Task<(BitcoinAddress, KeyPath)> GetChangeAddressAsync(DerivationStrategyBase derivationStrategy)
{
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
// Might happen on some broken install
if (pathInfo == null)
{
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
}
return (pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork), pathInfo.KeyPath);
}
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
{
await _Client.TrackAsync(derivationStrategy);
}
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken))
{
if (txId == null)
throw new ArgumentNullException(nameof(txId));
var tx = await _Client.GetTransactionAsync(txId, cancellation);
return tx;
}
public void InvalidateCache(DerivationStrategyBase strategy)
{
_MemoryCache.Remove("CACHEDCOINS_" + strategy.ToString());
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
}
ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
private async Task<UTXOChanges> GetUTXOChanges(DerivationStrategyBase strategy, CancellationToken cancellation)
{
var thisCompletionSource = new TaskCompletionSource<UTXOChanges>();
var completionSource = _FetchingUTXOs.GetOrAdd(strategy.ToString(), (s) => thisCompletionSource);
if (thisCompletionSource != completionSource)
return await completionSource.Task;
try
{
var utxos = await _MemoryCache.GetOrCreateAsync("CACHEDCOINS_" + strategy.ToString(), async entry =>
{
var now = DateTimeOffset.UtcNow;
UTXOChanges result = null;
try
{
result = await _Client.GetUTXOsAsync(strategy, null, false, cancellation).ConfigureAwait(false);
}
catch
{
Logs.PayServer.LogError("Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers");
throw;
}
var spentTime = DateTimeOffset.UtcNow - now;
if (spentTime.TotalSeconds > 30)
{
Logs.PayServer.LogWarning($"NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers");
}
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return result;
});
completionSource.TrySetResult(utxos);
}
catch (Exception ex)
{
completionSource.TrySetException(ex);
}
finally
{
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
}
return await completionSource.Task;
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
return Task.WhenAll(tasks);
}
public async Task<Money> GetBalance(DerivationStrategy derivationStrategy)
public async Task<ReceivedCoin[]> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);
var result = await client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true);
return result.Confirmed.UTXOs.Select(u => u.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Value))
.Sum();
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
return (await GetUTXOChanges(derivationStrategy, cancellation))
.GetUnspentUTXOs()
.Select(c => new ReceivedCoin()
{
Coin = c.AsCoin(derivationStrategy),
KeyPath = c.KeyPath,
Timestamp = c.Timestamp
}).ToArray();
}
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
UTXOChanges changes = await GetUTXOChanges(derivationStrategy, cancellation);
return changes.GetUnspentUTXOs().Select(c => c.Value).Sum();
}
}
}

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace BTCPayServer.Services.Wallets
{
public class BTCPayWalletProvider
{
private ExplorerClientProvider _Client;
BTCPayNetworkProvider _NetworkProvider;
IOptions<MemoryCacheOptions> _Options;
public BTCPayWalletProvider(ExplorerClientProvider client,
IOptions<MemoryCacheOptions> memoryCacheOption,
BTCPayNetworkProvider networkProvider)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
_NetworkProvider = networkProvider;
_Options = memoryCacheOption;
foreach(var network in networkProvider.GetAll())
{
var explorerClient = _Client.GetExplorerClient(network.CryptoCode);
if (explorerClient == null)
continue;
_Wallets.Add(network.CryptoCode, new BTCPayWallet(explorerClient, new MemoryCache(_Options), network));
}
}
Dictionary<string, BTCPayWallet> _Wallets = new Dictionary<string, BTCPayWallet>();
public BTCPayWallet GetWallet(BTCPayNetwork network)
{
if (network == null)
throw new ArgumentNullException(nameof(network));
return GetWallet(network.CryptoCode);
}
public BTCPayWallet GetWallet(string cryptoCode)
{
if (cryptoCode == null)
throw new ArgumentNullException(nameof(cryptoCode));
_Wallets.TryGetValue(cryptoCode, out var result);
return result;
}
public bool IsAvailable(BTCPayNetwork network)
{
return _Client.IsAvailable(network);
}
public IEnumerable<BTCPayWallet> GetWallets()
{
foreach (var w in _Wallets)
yield return w.Value;
}
}
}

@ -57,10 +57,10 @@
<h2>Video tutorials</h2>
<div class="row">
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
<iframe width="400" height="225" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
</div>
<div class="col-md-6 text-center">
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
<iframe width="400" height="225" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
</div>
</div>
</div>
@ -122,10 +122,10 @@
</div>
<div class="row">
<div class="col-lg-4 ml-auto text-center">
<a href="http://52.191.212.129:3000/">
<a href="http://slack.forkbitpay.ninja/">
<img src="~/img/slack.png" height="100" />
</a>
<p><a href="http://52.191.212.129:3000/">On Slack</a></p>
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
</div>
<div class="col-lg-4 mr-auto text-center">
<a href="https://twitter.com/BtcpayServer">

@ -21,7 +21,7 @@
@Model.ToJSVariableModel("srvModel")
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vue.min.js" type="text/javascript" defer="defer"></script>
<script src="~/js/vue-qrcode.js" type="text/javascript" defer="defer"></script>
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
<!-- <script src="img/Intl.js" type="text/javascript" defer="defer"></script>
@ -123,14 +123,14 @@
<div class="line-items">
<!---->
<div class="line-items__item">
<div class="line-items__item__label" i18n="">Payment Amount</div>
<div class="line-items__item__value">{{srvModel.btcAmount}} {{ srvModel.cryptoCode }}</div>
<div class="line-items__item__label" i18n="">Order Amount</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Network Cost</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.txCount }} transaction x {{ srvModel.txFees}} {{ srvModel.cryptoCode }}</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
@ -158,6 +158,7 @@
<div adjust-height="" class="payment-box">
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode :val="srvModel.invoiceBitcoinUrl" :size="256" bg-color="#f5f5f7" fg-color="#000" />
</div>
<div class="payment__details__instruction__open-wallet">
@ -360,7 +361,7 @@
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="@Model.CryptoImage">
<img :src="srvModel.cryptoImage" />
</div>
<div class="manual-box__address__wrapper__value">{{srvModel.btcAddress}}</div>
</div>
@ -491,7 +492,7 @@
<div>
<div class="expired__body">
<div class="expired__header" i18n="">What happened?</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for 15 minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
<div class="expired__text" i18n="">
If the transaction
@ -503,7 +504,7 @@
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
<!---->
<span>
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.OrderId}}
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
</span>
</div>
</div>
@ -605,8 +606,17 @@
</div>
</div>
<div class="footer">
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px; display: none;">
<div></div>
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
@if(Model.AvailableCryptos.Count > 1)
{
<div style="text-align:center">Accepted here</div>
<div style="text-align:center">
@foreach(var crypto in Model.AvailableCryptos)
{
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.paymentMethodId='@crypto.paymentMethodId'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.paymentMethodId" src="@crypto.CryptoImage" /></a>
}
</div>
}
</div>
</div>
</div>

@ -53,10 +53,18 @@
<th>Monitoring date</th>
<td>@Model.MonitoringDate</td>
</tr>
<tr>
<th>Transaction speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Status Exception</th>
<td>@Model.StatusException</td>
</tr>
<tr>
<th>Refund email</th>
<td>@Model.RefundEmail</td>
@ -73,6 +81,10 @@
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td>@Model.RedirectUrl</td>
</tr>
</table>
</div>
@ -174,6 +186,7 @@
<th>Deposit address</th>
<th>Transaction Id</th>
<th>Confirmations</th>
<th>Replaced</th>
</tr>
</thead>
<tbody>
@ -185,6 +198,7 @@
<td>@payment.DepositAddress</td>
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
<td>@payment.Confirmations</td>
<td>@payment.Replaced</td>
</tr>
}
</tbody>
@ -215,5 +229,28 @@
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h3>Events</h3>
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Date</th>
<th>Message</th>
</tr>
</thead>
<tbody>
@foreach(var evt in Model.Events)
{
<tr>
<td>@evt.Timestamp</td>
<td>@evt.Message</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</section>

@ -0,0 +1,94 @@
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
<!-- Modal -->
<div id="modalDialog" class="modal-dialog animated bounceInRight">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Your nodes are synching...</h4>
<button type="button" class="close" onclick="dismissSyncModal()">&times;</button>
</div>
<div class="modal-body">
<p>
Some of your nodes are still synching...<br />
BTCPay Server will not work correctly until it is over.
</p>
@foreach (var line in dashboard.GetAll())
{
<h4>@line.Network.CryptoCode</h4>
@if (line.Status == null)
{
<ul>
<li>The node is offline</li>
@if (line.Error != null)
{
<li>Last error: @line.Error</li>
}
</ul>
}
else
{
<ul>
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
@if (line.Status.BitcoinStatus == null)
{
if (line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
{
<li>The node is starting...</li>
}
else
{
<li>The node is offline</li>
@if (line.Error != null)
{
<li>Last error: @line.Error</li>
}
}
}
else if (line.Status.BitcoinStatus.IsSynched)
{
<li>The node is synched (Height: @line.Status.BitcoinStatus.Headers)</li>
@if (line.Status.BitcoinStatus.IsSynched &&
line.Status.SyncHeight.HasValue &&
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
{
<li>NBXplorer is synching... (Height: @line.Status.SyncHeight.Value)</li>
}
}
else
{
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
}
</ul>
@if (!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
{
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
</div>
</div>
}
}
}
</div>
</div>
</div>
<link href="~/vendor/animatecss/animate.css" rel="stylesheet" />
<script type="text/javascript">
function dismissSyncModal() {
$("#modalDialog").addClass('animated bounceOutRight')
}
</script>
<style type="text/css">
#modalDialog {
position: fixed;
bottom: 20px;
right: 20px;
margin: 0px;
}
</style>

@ -38,7 +38,7 @@
<body id="page-top">
@{
if(ViewBag.AlwaysShrinkNavBar == null)
if (ViewBag.AlwaysShrinkNavBar == null)
{
ViewBag.AlwaysShrinkNavBar = true;
}
@ -48,30 +48,36 @@
<!-- Navigation -->
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
<div class="container">
<a class="navbar-brand js-scroll-trigger" href="~/"><img src="~/img/logo.png" height="45"></a>
<a class="navbar-brand js-scroll-trigger" href="~/">
<img src="~/img/logo.png" height="45">
@if(env.ChainType != NBXplorer.ChainType.Main)
{
<span class="badge badge-warning" style="font-size:10px;">@env.ChainType.ToString()</span>
}
</a>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
@if(SignInManager.IsSignedIn(User))
{
@if(User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
{
@if(User.IsInRole(Roles.ServerAdmin))
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>
<li class="nav-item">
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
</li>}
else
{
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
</ul>
</div>
@ -80,77 +86,6 @@
@RenderBody()
@if(!dashboard.IsFullySynched())
{
<!-- Modal -->
<div id="synching-modal" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Your nodes are synching...</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<p>
Some of your nodes are still synching...<br />
BTCPay Server will not work correctly until it is over.
</p>
@foreach(var line in dashboard.GetAll())
{
<h4>@line.Network.CryptoCode</h4>
@if(line.Status == null)
{
<p>NBXplorer is offline</p>
}
else
{
<ul>
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
@if(line.Status.BitcoinStatus == null)
{
if(line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
{
<li>The node is starting...</li>
}
else
{
<li>The node is offline</li>
}
}
else if(line.Status.BitcoinStatus.IsSynched)
{
<li>The node is synched</li>
}
else
{
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
}
</ul>
@if(!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
{
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)line.Status.BitcoinStatus.VerificationProgress * 100)"
aria-valuemin="0" aria-valuemax="100" style="width:@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%">
@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%
</div>
</div>
}
}
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
}
<footer class="bg-dark">
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
</footer>
@ -168,13 +103,10 @@
<!-- Custom scripts for this template -->
<script src="~/js/creative.js"></script>
@if(!dashboard.IsFullySynched())
@if (!dashboard.IsFullySynched())
{
<script type="text/javascript">
$(function () {
$("#synching-modal").modal();
});
</script>
@Html.Partial("SyncModal")
}
@RenderSection("Scripts", required: false)

@ -13,31 +13,35 @@
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="col-md-8">
<form method="post">
<div class="form-group">
<h5>Derivation Scheme</h5>
@if(Model.AddressSamples.Count == 0)
{
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
}
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
</div>
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
@if(Model.AddressSamples.Count == 0)
{
<div class="form-group">
<label asp-for="DerivationScheme"></label>
<input asp-for="DerivationScheme" class="form-control" />
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
</p>
<p id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
</p>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
<thead class="thead-inverse">
@ -73,34 +77,49 @@
</tr>
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-info">Continue</button>
}
else
{
<table class="table">
<thead class="thead-inverse">
<div class="form-group">
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
</div>
<input type="hidden" asp-for="CryptoCurrency" />
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Key path</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<th>Key path</th>
<th>Address</th>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
</thead>
<tbody>
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
<td>@sample.Address</td>
</tr>
}
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
</tbody>
</table>
}
</div>
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>
</form>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
<script type="text/javascript">
@Model.ServerUrl.ToJSVariableModel("srvModel");
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
}

@ -14,8 +14,10 @@ namespace BTCPayServer.Views.Stores
public static string Tokens => "Tokens";
public static string Wallet => "Wallet";
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);

@ -25,11 +25,6 @@
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreName"></label>
<input asp-for="StoreName" class="form-control" />
<span asp-validation-for="StoreName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreWebsite"></label>
<input asp-for="StoreWebsite" class="form-control" />
@ -43,6 +38,24 @@
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="PreferredExchange"></label>
<input asp-for="PreferredExchange" class="form-control" />
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
</p>
</div>
<div class="form-group">
<label asp-for="RateMultiplier"></label>
<input asp-for="RateMultiplier" class="form-control" />
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="InvoiceExpiration"></label>
<input asp-for="InvoiceExpiration" class="form-control" />
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="MonitoringExpiration"></label>
<input asp-for="MonitoringExpiration" class="form-control" />

@ -0,0 +1,74 @@
@model WalletModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.AddActivePage(StoreNavPages.Wallet);
}
<h4>@ViewData["Title"]</h4>
<div class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<span id="alertMessage"></span>
</div>
<div class="row">
<div class="col-md-10">
<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 />
If your Ledger wallet is not detected:
</p>
<ul>
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
</ul>
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
<p id="hw-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
<p id="hw-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
<p id="check-loading" style="display:none;"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
<p id="check-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="check-label">An error happened</span></p>
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="col-md-6">
<form id="sendform" style="display:none;">
<div class="form-group">
<label asp-for="CryptoCurrency"></label>
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Substract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
</div>
@section Scripts
{
<script type="text/javascript">
@Model.ToJSVariableModel("srvModel")
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

@ -4,5 +4,6 @@
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
</ul>

@ -43,7 +43,7 @@ h6 {
}
p {
font-size: 16px;
/*font-size: 16px;*/
line-height: 1.5;
margin-bottom: 20px;
}

@ -0,0 +1,77 @@
$(function () {
var ledgerDetected = false;
var recommendedPubKey = "";
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
function WriteAlert(type, message) {
}
function Write(prefix, type, message) {
if (type === "error") {
$("#no-ledger-info").css("display", "block");
$("#ledger-in fo").css("display", "none");
}
}
$("#ledger-info-recommended").on("click", function (elem) {
elem.preventDefault();
$("#DerivationScheme").val(recommendedPubKey);
$("#DerivationSchemeFormat").val("BTCPay");
return false;
});
$("#CryptoCurrency").on("change", function (elem) {
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "none");
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
var cryptoCode = $("#CryptoCurrency").val();
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
recommendedPubKey = result.extPubKey;
$("#no-ledger-info").css("display", "none");
$("#ledger-info").css("display", "block");
}
});
};
bridge.isSupported()
.then(function (supported) {
if (!supported) {
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason) {
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('hw', 'error', result.error);
} else {
Write('hw', 'success', 'Ledger detected');
ledgerDetected = true;
updateInfo();
}
});
}
});
});

@ -0,0 +1,145 @@
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var recommendedFees = "";
var recommendedBalance = "";
function WriteAlert(type, message) {
$(".alert").removeClass("alert-danger");
$(".alert").removeClass("alert-warning");
$(".alert").removeClass("alert-success");
$(".alert").addClass("alert-" + type);
$(".alert").css("display", "block");
$("#alertMessage").text(message);
}
function Write(prefix, type, message) {
$("#" + prefix + "-loading").css("display", "none");
$("#" + prefix + "-error").css("display", "none");
$("#" + prefix + "-success").css("display", "none");
$("#" + prefix+"-" + type).css("display", "block");
$("." + prefix +"-label").text(message);
}
$("#sendform").on("submit", function (elem) {
elem.preventDefault();
if ($("#amount-textbox").val() === "") {
$("#amount-textbox").val(recommendedBalance);
$("#substract-checkbox").prop("checked", true);
}
if ($("#fee-textbox").val() === "") {
$("#fee-textbox").val(recommendedFees);
}
var args = "";
args += "cryptoCode=" + $("#cryptoCurrencies").val();
args += "&destination=" + $("#destination-textbox").val();
args += "&amount=" + $("#amount-textbox").val();
args += "&feeRate=" + $("#fee-textbox").val();
args += "&substractFees=" + $("#substract-checkbox").prop("checked");
WriteAlert("warning", 'Please validate the transaction on your ledger');
var confirmButton = $("#confirm-button");
confirmButton.prop("disabled", true);
confirmButton.addClass("disabled");
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
.catch(function (reason) {
WriteAlert("danger", reason);
confirmButton.prop("disabled", false);
confirmButton.removeClass("disabled");
})
.then(function (result) {
if (!result)
return;
confirmButton.prop("disabled", false);
confirmButton.removeClass("disabled");
if (result.error) {
WriteAlert("danger", result.error);
} else {
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
updateInfo();
}
});
return false;
});
$("#crypto-balance-link").on("click", function (elem) {
elem.preventDefault();
var val = $("#crypto-balance-link").text();
$("#amount-textbox").val(val);
$("#substract-checkbox").prop('checked', true);
return false;
});
$("#crypto-fee-link").on("click", function (elem) {
elem.preventDefault();
var val = $("#crypto-fee-link").text();
$("#fee-textbox").val(val);
return false;
});
$("#cryptoCurrencies").on("change", function (elem) {
updateInfo();
});
var updateInfo = function () {
if (!ledgerDetected)
return false;
$(".crypto-info").css("display", "none");
var cryptoCode = $("#cryptoCurrencies").val();
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
.catch(function (reason) { Write('check', 'error', reason); })
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('check', 'error', result.error);
return;
}
else {
Write('check', 'success', 'This store is configured to use your ledger');
$(".crypto-info").css("display", "block");
recommendedFees = result.recommendedSatoshiPerByte;
recommendedBalance = result.balance;
$("#crypto-fee").text(result.recommendedSatoshiPerByte);
$("#crypto-balance").text(result.balance);
$("#crypto-code").text(cryptoCode);
}
});
};
bridge.isSupported()
.then(function (supported) {
if (!supported) {
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
}
else {
bridge.sendCommand('test', null, 5)
.catch(function (reason)
{
if (reason.message === "Sign failed")
reason = "Have you forgot to activate browser support in your ledger app?";
Write('hw', 'error', reason);
})
.then(function (result) {
if (!result)
return;
if (result.error) {
Write('hw', 'error', result.error);
} else {
Write('hw', 'success', 'Ledger detected');
$("#sendform").css("display", "block");
ledgerDetected = true;
updateInfo();
}
});
}
});
});

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