Compare commits

...

127 Commits

Author SHA1 Message Date
ee524e36c5 bump 2020-02-24 21:25:52 +09:00
f097ecdc80 fix: remove ipn via email #1241 (#1337)
* fix: remove ipn via email #1241

* fix: remove ipn via email #1241
2020-02-24 21:21:03 +09:00
a354f7d9dd add GET endpoint for pay button (#1349)
closes #889
2020-02-24 21:18:04 +09:00
29d51ad2a2 Adding 1 second leeway for expiration 2020-02-21 17:09:03 -06:00
1be6408246 Adding logging to try and catch situations where invoice is not expired 2020-02-21 17:09:03 -06:00
34702d2633 Revoke Legacy Api Keys (#1344)
closes #1333
2020-02-21 13:40:00 +09:00
b79b310bd5 Revert "Sort invoice list (Fix #1329)"
This reverts commit dc4f8a1fbe841c0824d545582cc46ae533e955c4.
2020-02-21 11:29:09 +09:00
dc4f8a1fbe Sort invoice list (Fix #1329) 2020-02-19 22:04:46 +09:00
6d0896084f Add JS Modal test (#1342) 2020-02-19 17:39:14 +09:00
d31bff7070 BPU Prep Work Part2 (#1340)
* BPU Prep Work Part2

* Adjust tests to use the hot wallet when registering deriv scheme
* Add amount to payment data view for onchain payments
* Make zone limits higher when in dev mode (for tests in next PR)
* Make IPaymentMethodDetails serialize/deserialize through payment type using the network
* Allow named settings through settings repo
* Refactor some extensions for next PR

* pr changes

* use json convert for now
2020-02-19 17:35:23 +09:00
f2aab4cf03 Add warning if fail to load rates from cache 2020-02-16 23:04:48 +09:00
c03dc48fe9 Do not crash if can't load rate cache 2020-02-16 22:07:56 +09:00
143c909812 bump 2020-02-16 19:58:53 +09:00
821b904163 Added SendGrid, Mailgun to Quick-fill email settings (#1335) 2020-02-15 14:37:29 +09:00
6015eb337a Fix broken link 2020-02-15 14:36:36 +09:00
5d817a0483 Revert fix mysql 2020-02-14 00:23:00 +09:00
ee9905e85a Fix mysql 2020-02-14 00:07:19 +09:00
ff4c7c364e Fix mysql 2020-02-14 00:02:47 +09:00
a2d657f5cb Fix mysql migration 2020-02-13 23:58:48 +09:00
db6a4687d2 Wallet prep work for BPU (#1331)
* Wallet prep work for BPU

This PR prepares the wallet for #1321. It makes transfers from the vault and ledger to go to their own post actions for processing (not particularly useful in this PR but is needed in BPU to propose a new tx)  It also makes the Sign with seed consistent with redirect to /psbt/ready after signing which it did not do (it stayed on the seed route)

* fix test

* add assert
2020-02-13 22:06:00 +09:00
07f0d95f56 BIP21 Support for Wallet spending (#1322)
* BIP21 Support for Wallet spending

* extract bip21 loading to method

* add bip21 parsing test
2020-02-13 17:18:43 +09:00
1a409a441d Update POLIS related entries (#1313)
* Update Polis related info and services

* Fix Polis Rate Fetcher

* Fix Polis ratefetcher - Cryptopia is obsolete

* POLIS rate provider changes to comply with internal testing

* URL / pair alignment

* Add small doc to re-trigger testing
2020-02-13 14:44:31 +09:00
445e184154 Merge pull request #1328 from pavlenex/readme
Minor readme cleanup + license clarification
2020-02-13 14:42:18 +09:00
9a10f55a85 Merge remote-tracking branch 'upstream/master' into readme 2020-02-12 19:11:53 +01:00
ae33b1d0a8 Fix PSBT Redirect No-access issues 2020-02-12 16:35:24 +09:00
4ed2db83a5 fix a broken link 2020-02-09 19:01:52 +01:00
500aa85142 Fix broken links 2020-02-09 17:35:00 +01:00
3b6cc84a93 Minor readme cleanup + license clarification 2020-02-09 17:33:44 +01:00
5ce29d2bb8 Merge pull request #1325 from Kukks/fix-revoke-access
Fixes #1324 store token revoke redirect error
2020-02-08 21:54:43 +00:00
3184d2b2df Merge pull request #1327 from Kukks/coldcard-fix
Fix #1326 Coldcard import dialog
2020-02-08 21:54:19 +00:00
f5e65ec2a6 Fix #1326 Coldcard import dialog 2020-02-08 10:54:34 +01:00
66488d813b Fixes #1324 store token revoke redirect error 2020-02-07 08:23:00 +01:00
4853cfe41a bump 2020-02-03 19:13:51 +09:00
dc7733abcd Merge pull request #1041 from Kukks/satscurrency
Add sats as a native currency
2020-02-03 08:42:35 +00:00
771c8e2758 Merge pull request #1314 from btcpayserver/feature/errorpages
Adding error pages to handle HTTP errors
2020-02-03 08:39:58 +00:00
24664b60af Adding test ensuring that api errors are properly returned 2020-02-03 02:21:03 -06:00
82393eb8bb Fixing api exception handling in the pipeline 2020-02-03 02:18:36 -06:00
b432d8903f Grammar fix by Kukks 2020-02-01 11:16:40 -06:00
ea9169f607 Updating 404 page not found assert 2020-02-01 02:29:08 -06:00
496a6f0f55 Special page to handle 429 errors 2020-02-01 01:59:56 -06:00
fb2a0fb7fb Special page to handle 500 errors 2020-02-01 01:58:17 -06:00
ef503fa907 Special page to handle 404 errors 2020-02-01 01:41:27 -06:00
fe2eca4fda Adding prettier error handling page in the pipeline 2020-02-01 01:40:50 -06:00
88835b5b55 Moving _LayoutWelcome to shared folder 2020-02-01 00:32:31 -06:00
876c940032 Reverting delegate reference to previous state until Nicolas confirms change 2020-02-01 00:26:01 -06:00
a08d5be35c Expanding tests to check implicit conversion of Sats to BTC 2020-01-29 22:31:43 -06:00
0074790684 Remove "#nullable enable" directive and unnecessary operators 2020-01-29 01:53:47 -06:00
23aaf794ef Add nullable enable directive to HttpClientRequestMaker.MakeRequestAsync 2020-01-29 01:53:47 -06:00
bb12d37416 Displaying sats in a more user-friendly way (space as group separator) (#1306)
Fix: #1146
2020-01-27 19:57:46 +09:00
e058903450 Do not show assets in sync modal (#1309) 2020-01-26 19:45:52 +09:00
06f1c17a5f Make unused assets in store settings collapsed (#1310) 2020-01-26 19:45:24 +09:00
e00136de93 Fix spurious DefaultAntiforgery errors 2020-01-26 15:02:40 +09:00
56d8c033d7 Update display text on the view model. 2020-01-24 15:45:35 -06:00
666682677c Merge pull request #1303 from btcpayserver/feat/viewnewwindow
Providing open in new window split buttons
2020-01-24 15:34:25 -06:00
652b958d4f Removing viewapp command now that we directly redirect in cshtml 2020-01-24 15:11:34 -06:00
c7c0db612a Restoring IDs Selenium depends on for tests 2020-01-23 20:40:20 -06:00
a83edce4dc Updating idents, code formatting 2020-01-23 20:19:24 -06:00
f99058a9fa Adding code comment for review 2020-01-23 20:18:33 -06:00
a907143d81 Providing open in new window split button when updating crowdfund
Unifying styles on POS and Crowdfund settings

co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 20:17:29 -06:00
4ae173bb69 Providing open in new window split button when updating POS app
co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 20:04:34 -06:00
1436420a93 Providing link to view app in new window
co-authored-by: radWorx <dramirez@soulrivers.com>
2020-01-23 19:51:57 -06:00
086cbaa231 Add clightning rest services page (#1297)
* Add clightning rest services page

* fix rebase
2020-01-23 22:20:37 +09:00
5dd3112e0d Ensure "import from....a new/existing seed" modal text is readable in Casa theme (#1300)
fix #1299
2020-01-23 22:20:00 +09:00
b42e4f240a Fix (#1301)
* Fix seed signing validation

* fix ident
2020-01-23 22:02:37 +09:00
7076692069 fix configurator password loader (#1298) 2020-01-22 15:16:32 +09:00
dcb3601791 Fix ETB asset id 2020-01-21 18:22:42 +01:00
54c7c0d696 Add currency precision based on network (#1294) 2020-01-21 22:28:13 +09:00
f324185d82 bump nbx 2020-01-21 21:47:51 +09:00
a63502873c Add implicit hidden rate rule for sats in parser 2020-01-21 13:34:00 +01:00
f5cbf6672a remove default rate rule for sats 2020-01-21 13:34:00 +01:00
a78dff5931 remove padding 2020-01-21 13:34:00 +01:00
f8139a9156 cleanup (remove sats rate provider and just use rate scripting) 2020-01-21 13:34:00 +01:00
27a61b7afd fix test 2020-01-21 13:34:00 +01:00
71671b9e16 Add sats as a native currency
This will allow you to create an invoice where its primary currency is denominated in sats
2020-01-21 13:33:59 +01:00
c68bf5220e bump 2020-01-21 21:09:49 +09:00
80ee03d897 Remove dead link 2020-01-21 21:06:35 +09:00
d0bfa67495 Fix build 2020-01-21 21:04:35 +09:00
bdb2edba12 Fix U2F signing 2020-01-21 21:00:34 +09:00
78d8f4e011 Fix rescan wallet link 2020-01-21 20:54:45 +09:00
1bfe9dda97 Integrate Configurator External Service (#1190) 2020-01-21 18:27:10 +09:00
8e6f43cd3a Sign with NBX Seed (#1218) 2020-01-21 17:33:12 +09:00
6848482999 Remove the next address to pay to from Invoice details page (Fix #1056) (#1283) 2020-01-21 16:53:24 +09:00
43967ee86e bump 2020-01-21 13:20:52 +09:00
61b99f6630 Add support for ETB liquid asset (#1295) 2020-01-21 13:19:55 +09:00
7e073fb7e1 Add test CanCreateSqlitedb 2020-01-19 22:10:05 +09:00
1ceb5cb576 Fix sqlite madness (#1293)
* Need to reference Microsoft.EntityFrameworkCore.Design else you can't generate migrations
* fix design time migrations issues
* update snapshot
```
Your startup project 'BTCPayServer.Data' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is c
orrect, install the package, and try again.
```
*
2020-01-19 21:57:50 +09:00
53a60d1660 Add Direct Provider with bitfinex, okex and coinbasepro 2020-01-18 21:48:04 +09:00
25b733ca7f Remove knowledge of ExchangeName from BackgroundRateFetcher 2020-01-18 19:42:46 +09:00
1bf680fdb9 Improve tests to not create new HttpClient every times 2020-01-18 19:23:40 +09:00
76008c9f5c bump to sdk 3.1.101 and version 3.1.1 (security patch) 2020-01-18 16:59:25 +09:00
1bf04ac4ac Generate less keys with nbxplorer 2020-01-18 16:20:03 +09:00
4b088defd3 Increase timeout for AssertHappyMessage test 2020-01-18 15:36:20 +09:00
a2be7ee471 Fix statusMessage handling for the Receive wallet page 2020-01-18 15:32:01 +09:00
025da0261d new feature: Wallet Receive Page (#1065)
* new feature: Wallet Receive Page

closes #965

* Conserve addresses by waiting till address is spent before generating each run

* fix tests

* Filter by cryptocode before matching outpoint

* fix build

* fix edge case issue

* use address in keypathinfo directly

* rebase fixes

* rebase fixes

* remove duplicate code

* fix messy condition

* fixes

* fix e2e

* fix
2020-01-18 14:12:27 +09:00
4ac79a7ea3 Fixing SQLite in Invoices page (Fix #1287) 2020-01-17 21:45:20 +09:00
ef20a03b95 Fix send from ledger and send from vault 2020-01-17 21:38:27 +09:00
d9681398e5 Bump 2020-01-17 18:21:21 +09:00
25f19e5d9f Merge pull request #1286 from NicolasDorier/rate/refactor
Refactor rate handling to prevent error of exchange name and re-reroute coinaverage to coingecko
2020-01-17 18:20:43 +09:00
aab6fcd508 use coingecko if coinaverage is set 2020-01-17 18:15:08 +09:00
a8ac01cd8b Refactor rate handling to prevent error of exchange name 2020-01-17 18:11:05 +09:00
605a0fd3c9 bump 2020-01-17 15:16:57 +09:00
90ec416125 Add exponential backoff for CoinGecko, pass the cancellationtoken around 2020-01-17 15:08:28 +09:00
827b6085af Do not hammer CoinGecko with tests 2020-01-17 14:56:05 +09:00
ff11e6e032 Remove CoinAverage RateSource enum 2020-01-17 14:51:07 +09:00
9b55648e41 Fix build 2020-01-17 14:45:26 +09:00
6dffbbd93d Remove CachedRateProvider 2020-01-17 14:42:02 +09:00
7a0991d6b1 Remove CoinAverage integration (2) 2020-01-17 14:30:51 +09:00
9b165de5e6 Remove CoinAverage integration 2020-01-17 14:29:22 +09:00
1b9a4e7775 Coingecko should use BackgroundFetcherRateProvider instead of CachedRateProvider 2020-01-17 14:23:04 +09:00
48799562f8 Fix comment 2020-01-17 14:18:18 +09:00
7d545ca682 Remove ability to set custom cache, fix coinaverage not really using coinaverage 2020-01-17 14:16:12 +09:00
9739f3fb25 LN store config: fix a typo (#1285) 2020-01-17 12:12:12 +09:00
bb12de8945 Fix Sqlite migration (#1284) 2020-01-16 22:05:33 +09:00
feabeafed9 Fix Selenium tests ran in Debug mode 2020-01-16 18:03:41 +09:00
6b427e99ca use directly clightning integration instead of charge during debug 2020-01-16 17:15:11 +09:00
31db34ec8d Revert "Revert RazorCompileOnBuild=false temporarily"
This reverts commit 92e5f2852a866bd11bbb4a53b5ebcb6967152c89.
2020-01-16 16:52:46 +09:00
bf614cd322 Make sure the payment button does not error 500 if node not ready (Fix #1180) 2020-01-16 16:25:37 +09:00
9410933e1c Fix: Adding comment on wallet transactions causes 500 error (Close #1280) 2020-01-16 15:19:45 +09:00
c269dee980 Liquid changes (#1281)
Add assetid to bip21 for liquid
change liquid icons
change liquid asset name
change currency code displayed in checkout to one set in network
2020-01-16 15:01:01 +09:00
5aefb585e9 Fix Serilog logging too much 2020-01-16 14:00:31 +09:00
780cf67a1b bump bitcoin core 2020-01-15 13:25:29 +09:00
12e7c5e5e1 Updating referenced lnd to 0.8.2-beta (#1279) 2020-01-15 13:24:10 +09:00
92e5f2852a Revert RazorCompileOnBuild=false temporarily 2020-01-15 00:37:42 +09:00
0fbda9441a Fix AddressInUseException in tests 2020-01-15 00:22:31 +09:00
32a82bbb7c bump 2020-01-15 00:01:36 +09:00
628d0bb690 fix tests 2020-01-15 00:00:36 +09:00
05223c1a5f Bump NBX (#1277) 2020-01-14 23:10:58 +09:00
164 changed files with 3149 additions and 1760 deletions

1
.gitignore vendored
View File

@ -293,3 +293,4 @@ BTCPayServer/wwwroot/bundles/*
!BTCPayServer/wwwroot/bundles/.gitignore
.vscode
BTCPayServer/testpwd

View File

@ -16,13 +16,13 @@ namespace BTCPayServer
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Polis",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://insight.polispay.org/tx/{0}" : "https://insight.polispay.org/tx/{0}",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockbook.polispay.org/tx/{0}" : "https://blockbook.polispay.org/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "polis",
DefaultRateRules = new[]
{
"POLIS_X = POLIS_BTC * BTC_X",
"POLIS_BTC = cryptopia(POLIS_BTC)"
"POLIS_BTC = polispay(POLIS_BTC)"
},
CryptoImagePath = "imlegacy/polis.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),

View File

@ -59,7 +59,8 @@ namespace BTCPayServer
InitGroestlcoin();
InitViacoin();
InitMonero();
InitPolis();
// Assume that electrum mappings are same as BTC if not specified
foreach (var network in _Networks.Values.OfType<BTCPayNetwork>())
{
@ -77,7 +78,6 @@ namespace BTCPayServer
}
// Disabled because of https://twitter.com/Cryptopia_NZ/status/1085084168852291586
//InitPolis();
//InitBitcoinplus();
//InitUfo();
}

View File

@ -28,24 +28,10 @@ namespace BTCPayServer
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid.svg",
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
},
SupportRBF = true
});
}
}

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NBitcoin;
using NBXplorer;
using NBitcoin;
namespace BTCPayServer
{
@ -16,6 +11,7 @@ namespace BTCPayServer
{
CryptoCode = "USDt",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"USDT_UST = 1",
@ -23,28 +19,37 @@ namespace BTCPayServer
"USDT_BTC = bitfinex(UST_BTC)",
},
AssetId = new uint256("ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"),
DisplayName = "Tether USD",
DisplayName = "Liquid Tether",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/liquid-tether.svg",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
//https://github.com/spesmilo/electrum/blob/11733d6bc271646a00b69ff07657119598874da4/electrum/constants.py
ElectrumMapping = NetworkType == NetworkType.Mainnet
? new Dictionary<uint, DerivationType>()
{
{0x0488b21eU, DerivationType.Legacy }, // xpub
{0x049d7cb2U, DerivationType.SegwitP2SH }, // ypub
{0x4b24746U, DerivationType.Segwit }, //zpub
}
: new Dictionary<uint, DerivationType>()
{
{0x043587cfU, DerivationType.Legacy},
{0x044a5262U, DerivationType.SegwitP2SH},
{0x045f1cf6U, DerivationType.Segwit}
}
SupportRBF = true
});
Add(new ElementsBTCPayNetwork()
{
CryptoCode = "ETB",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"ETB_X = ETB_BTC * BTC_X",
"ETB_BTC = bitpay(ETB_BTC)"
},
Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
BlockExplorerLink = NetworkType == NetworkType.Mainnet ? "https://blockstream.info/liquid/tx/{0}" : "https://blockstream.info/testnet/liquid/tx/{0}",
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "liquidnetwork",
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
});
}
}

View File

@ -6,21 +6,26 @@ using NBXplorer.Models;
namespace BTCPayServer
{
public class ElementsBTCPayNetwork:BTCPayNetwork
public class ElementsBTCPayNetwork : BTCPayNetwork
{
public string NetworkCryptoCode { get; set; }
public uint256 AssetId { get; set; }
public override bool ReadonlyWallet { get; set; } = true;
public int Divisibility { get; set; } = 8;
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(NewTransactionEvent evtOutputs)
public override IEnumerable<(MatchedOutput matchedOutput, OutPoint outPoint)> GetValidOutputs(
NewTransactionEvent evtOutputs)
{
return evtOutputs.Outputs.Where(output =>
return evtOutputs.Outputs.Where(output =>
output.Value is AssetMoney assetMoney && assetMoney.AssetId == AssetId).Select(output =>
{
var outpoint = new OutPoint(evtOutputs.TransactionData.TransactionHash, output.Index);
return (output, outpoint);
});
}
public override string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{base.GenerateBIP21(cryptoInfoAddress, cryptoInfoDue)}&assetid={AssetId}";
}
}
}

View File

@ -10,6 +10,7 @@ namespace BTCPayServer
{
CryptoCode = "XMR",
DisplayName = "Monero",
Divisibility = 12,
BlockExplorerLink =
NetworkType == NetworkType.Mainnet
? "https://www.exploremonero.com/transaction/{0}"

View File

@ -112,14 +112,20 @@ namespace BTCPayServer
return (output, outpoint);
});
}
public virtual string GenerateBIP21(string cryptoInfoAddress, Money cryptoInfoDue)
{
return $"{UriScheme}:{cryptoInfoAddress}?amount={cryptoInfoDue.ToString(false, true)}";
}
}
public abstract class BTCPayNetworkBase
{
public bool ShowSyncSummary { get; set; } = true;
public string CryptoCode { get; internal set; }
public string BlockExplorerLink { get; internal set; }
public string DisplayName { get; set; }
public int Divisibility { get; set; } = 8;
[Obsolete("Should not be needed")]
public bool IsBTC
{

View File

@ -4,6 +4,6 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="3.0.1" />
<PackageReference Include="NBXplorer.Client" Version="3.0.2" />
</ItemGroup>
</Project>

View File

@ -3,10 +3,11 @@
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.1" />
</ItemGroup>
</Project>

View File

@ -2,33 +2,33 @@
using System.Linq;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using OpenIddict.EntityFrameworkCore.Models;
namespace BTCPayServer.Data
{
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlite("Data Source=temp.db");
return new ApplicationDbContext(builder.Options, true);
}
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
//public ApplicationDbContext(): base(CreateMySql())
//{
private readonly bool _designTime;
//}
//private static DbContextOptions CreateMySql()
//{
// return new DbContextOptionsBuilder<ApplicationDbContext>()
// .UseMySql("Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;")
// .Options;
//}
public ApplicationDbContext()
{
}
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, bool designTime = false)
: base(options)
{
_designTime = designTime;
}
public DbSet<InvoiceData> Invoices
@ -257,6 +257,26 @@ namespace BTCPayServer.Data
builder.UseOpenIddict<BTCPayOpenIdClient, BTCPayOpenIdAuthorization, OpenIddictScope<string>, BTCPayOpenIdToken, string>();
if (Database.IsSqlite() && !_designTime)
{
// SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
// here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
// To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
// use the DateTimeOffsetToBinaryConverter
// Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
// This only supports millisecond precision, but should be sufficient for most use cases.
foreach (var entityType in builder.Model.GetEntityTypes())
{
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
foreach (var property in properties)
{
builder
.Entity(entityType.Name)
.Property(property.Name)
.HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter());
}
}
}
}
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Data;
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@ -10,21 +11,91 @@ namespace BTCPayServer.Migrations
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
if (!migrationBuilder.IsSqlite())
{
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: true,
oldClrType: typeof(string),
oldMaxLength: 450);
}
else
{
ReplaceOldTable(migrationBuilder, s =>
{
migrationBuilder.CreateTable(
name: s,
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
AuthorizationId = table.Column<string>(nullable: true, maxLength: null),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
CreationDate = table.Column<DateTimeOffset>(nullable: true),
ExpirationDate = table.Column<DateTimeOffset>(nullable: true),
Id = table.Column<string>(nullable: false, maxLength: null),
Payload = table.Column<string>(nullable: true),
Properties = table.Column<string>(nullable: true),
ReferenceId = table.Column<string>(maxLength: 100, nullable: true),
Status = table.Column<string>(maxLength: 25, nullable: false),
Subject = table.Column<string>(maxLength: 450, nullable: true),
Type = table.Column<string>(maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "OpenIddictAuthorizations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
}, "OpenIddictTokens");
ReplaceOldTable(migrationBuilder, s =>
{
migrationBuilder.CreateTable(
name: s,
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
Id = table.Column<string>(nullable: false, maxLength: null),
Properties = table.Column<string>(nullable: true),
Scopes = table.Column<string>(nullable: true),
Status = table.Column<string>(maxLength: 25, nullable: false),
Subject = table.Column<string>(maxLength: 450, nullable: true),
Type = table.Column<string>(maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
}, "OpenIddictAuthorizations");
}
migrationBuilder.AddColumn<string>(
name: "Requirements",
@ -34,27 +105,140 @@ namespace BTCPayServer.Migrations
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Requirements",
table: "OpenIddictApplications");
if (!migrationBuilder.IsSqlite())
{
migrationBuilder.DropColumn(
name: "Requirements",
table: "OpenIddictApplications");
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictTokens",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Subject",
table: "OpenIddictAuthorizations",
maxLength: 450,
nullable: false,
oldClrType: typeof(string),
oldMaxLength: 450,
oldNullable: true);
}
else
{
ReplaceOldTable(migrationBuilder, s =>
{
migrationBuilder.CreateTable(
name: s,
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
AuthorizationId = table.Column<string>(nullable: true, maxLength: null),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
CreationDate = table.Column<DateTimeOffset>(nullable: true),
ExpirationDate = table.Column<DateTimeOffset>(nullable: true),
Id = table.Column<string>(nullable: false, maxLength: null),
Payload = table.Column<string>(nullable: true),
Properties = table.Column<string>(nullable: true),
ReferenceId = table.Column<string>(maxLength: 100, nullable: true),
Status = table.Column<string>(maxLength: 25, nullable: false),
Subject = table.Column<string>(maxLength: 450, nullable: false),
Type = table.Column<string>(maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictTokens", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId",
column: x => x.AuthorizationId,
principalTable: "OpenIddictAuthorizations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
}, "OpenIddictTokens", "WHERE Subject IS NOT NULL");
ReplaceOldTable(migrationBuilder, s =>
{
migrationBuilder.CreateTable(
name: s,
columns: table => new
{
ApplicationId = table.Column<string>(nullable: true, maxLength: null),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
Id = table.Column<string>(nullable: false, maxLength: null),
Properties = table.Column<string>(nullable: true),
Scopes = table.Column<string>(nullable: true),
Status = table.Column<string>(maxLength: 25, nullable: false),
Subject = table.Column<string>(maxLength: 450, nullable: false),
Type = table.Column<string>(maxLength: 25, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId",
column: x => x.ApplicationId,
principalTable: "OpenIddictApplications",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
}, "OpenIddictAuthorizations", "WHERE Subject IS NOT NULL");
ReplaceOldTable(migrationBuilder, s =>
{
migrationBuilder.CreateTable(
name: s,
columns: table => new
{
ClientId = table.Column<string>(maxLength: 100, nullable: false),
ClientSecret = table.Column<string>(nullable: true),
ConcurrencyToken = table.Column<string>(maxLength: 50, nullable: true),
ConsentType = table.Column<string>(nullable: true),
DisplayName = table.Column<string>(nullable: true),
Id = table.Column<string>(nullable: false, maxLength: null),
Permissions = table.Column<string>(nullable: true),
PostLogoutRedirectUris = table.Column<string>(nullable: true),
Properties = table.Column<string>(nullable: true),
RedirectUris = table.Column<string>(nullable: true),
Type = table.Column<string>(maxLength: 25, nullable: false),
ApplicationUserId = table.Column<string>(nullable: true, maxLength: null)
},
constraints: table =>
{
table.PrimaryKey("PK_OpenIddictApplications", x => x.Id);
table.ForeignKey(
name: "FK_OpenIddictApplications_AspNetUsers_ApplicationUserId",
column: x => x.ApplicationUserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
}, "OpenIddictApplications", "",
"ClientId, ClientSecret, ConcurrencyToken, ConsentType, DisplayName, Id, Permissions, PostLogoutRedirectUris, Properties, RedirectUris, Type, ApplicationUserId");
}
}
private void ReplaceOldTable(MigrationBuilder migrationBuilder, Action<string> createTable, string tableName,
string whereClause = "", string columns = "*")
{
createTable.Invoke($"New_{tableName}");
migrationBuilder.Sql(
$"INSERT INTO New_{tableName} {(columns == "*" ? string.Empty : $"({columns})")}SELECT {columns} FROM {tableName} {whereClause};");
migrationBuilder.Sql("PRAGMA foreign_keys=\"0\"", true);
migrationBuilder.Sql($"DROP TABLE {tableName}", true);
migrationBuilder.Sql($"ALTER TABLE New_{tableName} RENAME TO {tableName}", true);
migrationBuilder.Sql("PRAGMA foreign_keys=\"1\"", true);
}
}
}

View File

@ -14,31 +14,16 @@ namespace BTCPayServer.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.14-servicing-32113");
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");
});
.HasAnnotation("ProductVersion", "3.1.1");
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasColumnType("TEXT")
.HasMaxLength(50);
b.HasKey("Id");
@ -48,22 +33,46 @@ namespace BTCPayServer.Migrations
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("CreatedTime")
.HasColumnType("TEXT");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("AppType");
b.Property<string>("AppType")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Created");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Settings");
b.Property<string>("Settings")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<bool>("TagAllInvoices");
b.Property<bool>("TagAllInvoices")
.HasColumnType("INTEGER");
b.HasKey("Id");
@ -75,41 +84,56 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<bool>("RequiresEmailConfirmation")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("UserName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
@ -127,27 +151,35 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdAuthorization", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("Properties");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Scopes");
b.Property<string>("Scopes")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<string>("Subject")
.HasColumnType("TEXT")
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
@ -160,36 +192,49 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdClient", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(100);
b.Property<string>("ClientSecret");
b.Property<string>("ClientSecret")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("ConsentType");
b.Property<string>("ConsentType")
.HasColumnType("TEXT");
b.Property<string>("DisplayName");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("Permissions");
b.Property<string>("Permissions")
.HasColumnType("TEXT");
b.Property<string>("PostLogoutRedirectUris");
b.Property<string>("PostLogoutRedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Properties");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("RedirectUris");
b.Property<string>("RedirectUris")
.HasColumnType("TEXT");
b.Property<string>("Requirements");
b.Property<string>("Requirements")
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
@ -205,36 +250,48 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.BTCPayOpenIdToken", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationId");
b.Property<string>("ApplicationId")
.HasColumnType("TEXT");
b.Property<string>("AuthorizationId");
b.Property<string>("AuthorizationId")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<DateTimeOffset?>("CreationDate");
b.Property<DateTimeOffset?>("CreationDate")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("ExpirationDate");
b.Property<DateTimeOffset?>("ExpirationDate")
.HasColumnType("TEXT");
b.Property<string>("Payload");
b.Property<string>("Payload")
.HasColumnType("TEXT");
b.Property<string>("Properties");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("ReferenceId")
.HasColumnType("TEXT")
.HasMaxLength(100);
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.Property<string>("Subject")
.HasColumnType("TEXT")
.HasMaxLength(450);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(25);
b.HasKey("Id");
@ -251,15 +308,20 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("Address");
b.Property<string>("Address")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Assigned");
b.Property<DateTimeOffset>("Assigned")
.HasColumnType("TEXT");
b.Property<string>("CryptoCode");
b.Property<string>("CryptoCode")
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("UnAssigned");
b.Property<DateTimeOffset?>("UnAssigned")
.HasColumnType("TEXT");
b.HasKey("InvoiceDataId", "Address");
@ -269,23 +331,31 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<DateTimeOffset>("Created");
b.Property<DateTimeOffset>("Created")
.HasColumnType("TEXT");
b.Property<string>("CustomerEmail");
b.Property<string>("CustomerEmail")
.HasColumnType("TEXT");
b.Property<string>("ExceptionStatus");
b.Property<string>("ExceptionStatus")
.HasColumnType("TEXT");
b.Property<string>("ItemCode");
b.Property<string>("ItemCode")
.HasColumnType("TEXT");
b.Property<string>("OrderId");
b.Property<string>("OrderId")
.HasColumnType("TEXT");
b.Property<string>("Status");
b.Property<string>("Status")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -296,13 +366,17 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.Property<string>("UniqueId");
b.Property<string>("UniqueId")
.HasColumnType("TEXT");
b.Property<string>("Message");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Timestamp");
b.Property<DateTimeOffset>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("InvoiceDataId", "UniqueId");
@ -312,15 +386,19 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("Label");
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("PairingTime");
b.Property<DateTimeOffset>("PairingTime")
.HasColumnType("TEXT");
b.Property<string>("SIN");
b.Property<string>("SIN")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -334,21 +412,28 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Expiration");
b.Property<DateTimeOffset>("Expiration")
.HasColumnType("TEXT");
b.Property<string>("Facade");
b.Property<string>("Facade")
.HasColumnType("TEXT");
b.Property<string>("Label");
b.Property<string>("Label")
.HasColumnType("TEXT");
b.Property<string>("SIN");
b.Property<string>("SIN")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<string>("TokenValue");
b.Property<string>("TokenValue")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -358,13 +443,16 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<bool>("Accounted");
b.Property<bool>("Accounted")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("InvoiceDataId");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -376,17 +464,21 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<DateTimeOffset>("Created")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValue(new DateTimeOffset(new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
b.Property<int>("Status");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -399,7 +491,8 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.Property<string>("Id")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -409,11 +502,13 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("InvoiceDataId");
b.Property<string>("InvoiceDataId")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -425,9 +520,10 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("Value");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -437,23 +533,31 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("DefaultCrypto");
b.Property<string>("DefaultCrypto")
.HasColumnType("TEXT");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategies")
.HasColumnType("TEXT");
b.Property<string>("DerivationStrategy");
b.Property<string>("DerivationStrategy")
.HasColumnType("TEXT");
b.Property<int>("SpeedPolicy");
b.Property<int>("SpeedPolicy")
.HasColumnType("INTEGER");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreBlob")
.HasColumnType("BLOB");
b.Property<byte[]>("StoreCertificate");
b.Property<byte[]>("StoreCertificate")
.HasColumnType("BLOB");
b.Property<string>("StoreName");
b.Property<string>("StoreName")
.HasColumnType("TEXT");
b.Property<string>("StoreWebsite");
b.Property<string>("StoreWebsite")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -463,15 +567,20 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.StoredFile", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("FileName");
b.Property<string>("FileName")
.HasColumnType("TEXT");
b.Property<string>("StorageFileName");
b.Property<string>("StorageFileName")
.HasColumnType("TEXT");
b.Property<DateTime>("Timestamp");
b.Property<DateTime>("Timestamp")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -483,22 +592,28 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.U2FDevice", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("ApplicationUserId");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<byte[]>("AttestationCert")
.IsRequired();
.IsRequired()
.HasColumnType("BLOB");
b.Property<int>("Counter");
b.Property<int>("Counter")
.HasColumnType("INTEGER");
b.Property<byte[]>("KeyHandle")
.IsRequired();
.IsRequired()
.HasColumnType("BLOB");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<byte[]>("PublicKey")
.IsRequired();
.IsRequired()
.HasColumnType("BLOB");
b.HasKey("Id");
@ -509,11 +624,14 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("ApplicationUserId")
.HasColumnType("TEXT");
b.Property<string>("StoreDataId");
b.Property<string>("StoreDataId")
.HasColumnType("TEXT");
b.Property<string>("Role");
b.Property<string>("Role")
.HasColumnType("TEXT");
b.HasKey("ApplicationUserId", "StoreDataId");
@ -525,9 +643,10 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.HasKey("Id");
@ -536,13 +655,17 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
{
b.Property<string>("WalletDataId");
b.Property<string>("WalletDataId")
.HasColumnType("TEXT");
b.Property<string>("TransactionId");
b.Property<string>("TransactionId")
.HasColumnType("TEXT");
b.Property<byte[]>("Blob");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("Labels");
b.Property<string>("Labels")
.HasColumnType("TEXT");
b.HasKey("WalletDataId", "TransactionId");
@ -552,15 +675,18 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.HasKey("Id");
@ -575,14 +701,18 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@ -594,14 +724,18 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
@ -612,14 +746,18 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired();
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
@ -630,9 +768,11 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
@ -643,13 +783,17 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
@ -659,23 +803,30 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictScope<string>", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyToken")
.IsConcurrencyToken()
.HasColumnType("TEXT")
.HasMaxLength(50);
b.Property<string>("Description");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<string>("DisplayName");
b.Property<string>("DisplayName")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(200);
b.Property<string>("Properties");
b.Property<string>("Properties")
.HasColumnType("TEXT");
b.Property<string>("Resources");
b.Property<string>("Resources")
.HasColumnType("TEXT");
b.HasKey("Id");
@ -685,14 +836,6 @@ namespace BTCPayServer.Migrations
b.ToTable("OpenIddictScopes");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -701,6 +844,14 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
@ -739,7 +890,8 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
@ -755,7 +907,8 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
@ -787,7 +940,8 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
@ -817,12 +971,14 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("BTCPayServer.Data.WalletTransactionData", b =>
@ -830,52 +986,59 @@ namespace BTCPayServer.Migrations
b.HasOne("BTCPayServer.Data.WalletData", "WalletData")
.WithMany("WalletTransactions")
.HasForeignKey("WalletDataId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser")
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser")
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("BTCPayServer.Data.ApplicationUser")
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Data.ApplicationUser")
b.HasOne("BTCPayServer.Data.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}

View File

@ -3,7 +3,6 @@ namespace BTCPayServer.Rating
public enum RateSource
{
Coingecko,
CoinAverage,
Direct
}
public class AvailableRateProvider
@ -11,11 +10,17 @@ namespace BTCPayServer.Rating
public string Name { get; }
public string Url { get; }
public string Id { get; }
public string SourceId { get; }
public RateSource Source { get; }
public AvailableRateProvider(string id, string name, string url, RateSource source)
public AvailableRateProvider(string id, string name, string url) : this(id, id, name, url, RateSource.Direct)
{
}
public AvailableRateProvider(string id, string sourceId, string name, string url, RateSource source)
{
Id = id;
SourceId = sourceId;
Name = name;
Url = url;
Source = source;

View File

@ -2,10 +2,6 @@
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />
<ItemGroup>
<Folder Include="Providers\" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />

View File

@ -11,7 +11,15 @@ namespace BTCPayServer.Rating
Dictionary<string, ExchangeRate> _AllRates = new Dictionary<string, ExchangeRate>();
public ExchangeRates()
{
}
public ExchangeRates(string exchangeName, IEnumerable<PairRate> rates)
{
foreach (var rate in rates)
{
Add(new ExchangeRate(exchangeName, rate.CurrencyPair, rate.BidAsk));
}
}
public ExchangeRates(IEnumerable<ExchangeRate> rates)
{
@ -218,6 +226,26 @@ namespace BTCPayServer.Rating
return $"({Bid.ToString(CultureInfo.InvariantCulture)} , {Ask.ToString(CultureInfo.InvariantCulture)})";
}
}
public class PairRate
{
public PairRate(CurrencyPair currencyPair, BidAsk bidAsk)
{
if (currencyPair == null)
throw new ArgumentNullException(nameof(currencyPair));
if (bidAsk == null)
throw new ArgumentNullException(nameof(bidAsk));
this.CurrencyPair = currencyPair;
this.BidAsk = bidAsk;
}
public CurrencyPair CurrencyPair { get; }
public BidAsk BidAsk { get; }
public override string ToString()
{
return $"{CurrencyPair} == {BidAsk}";
}
}
public class ExchangeRate
{
public ExchangeRate()

View File

@ -54,20 +54,19 @@ namespace BTCPayServer.Services.Rates
}
/// <summary>
/// This class is a decorator which handle caching and pre-emptive query to the underlying exchange
/// This class is a decorator which handle caching and pre-emptive query to the underlying rate provider
/// </summary>
public class BackgroundFetcherRateProvider : IRateProvider
{
public class LatestFetch
{
public ExchangeRates Latest;
public PairRate[] Latest;
public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Updated;
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
internal ExchangeRates GetResult()
internal PairRate[] GetResult()
{
if (Expiration <= DateTimeOffset.UtcNow)
{
@ -77,7 +76,7 @@ namespace BTCPayServer.Services.Rates
}
else
{
throw new InvalidOperationException($"The rate has expired ({ExchangeName})");
throw new InvalidOperationException($"The rate has expired");
}
}
return Latest;
@ -87,28 +86,23 @@ namespace BTCPayServer.Services.Rates
IRateProvider _Inner;
public IRateProvider Inner => _Inner;
public BackgroundFetcherRateProvider(string exchangeName, IRateProvider inner)
public BackgroundFetcherRateProvider(IRateProvider inner)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
if (exchangeName == null)
throw new ArgumentNullException(nameof(exchangeName));
_Inner = inner;
ExchangeName = exchangeName;
}
public BackgroundFetcherState GetState()
{
var state = new BackgroundFetcherState()
{
ExchangeName = ExchangeName,
LastRequested = LastRequested
};
if (_Latest is LatestFetch fetch)
{
state.LastUpdated = fetch.Updated;
state.Rates = fetch.Latest
.Where(e => e.Exchange == ExchangeName)
.Select(r => new BackgroundFetcherRate()
{
Pair = r.CurrencyPair,
@ -120,16 +114,13 @@ namespace BTCPayServer.Services.Rates
public void LoadState(BackgroundFetcherState state)
{
if (ExchangeName != state.ExchangeName)
throw new InvalidOperationException("The state does not belong to this fetcher");
if (state.LastRequested is DateTimeOffset lastRequested)
this.LastRequested = state.LastRequested;
if (state.LastUpdated is DateTimeOffset updated && state.Rates is List<BackgroundFetcherRate> rates)
{
var fetch = new LatestFetch()
{
ExchangeName = state.ExchangeName,
Latest = new ExchangeRates(rates.Select(r => new ExchangeRate(state.ExchangeName, r.Pair, r.BidAsk))),
Latest = rates.Select(r => new PairRate(r.Pair, r.BidAsk)).ToArray(),
Updated = updated,
NextRefresh = updated + RefreshRate,
Expiration = updated + ValidatyTime
@ -139,6 +130,9 @@ namespace BTCPayServer.Services.Rates
}
TimeSpan _RefreshRate = TimeSpan.FromSeconds(30);
/// <summary>
/// The timespan after which <see cref="UpdateIfNecessary(CancellationToken)"/> will get the rates from the underlying rate provider
/// </summary>
public TimeSpan RefreshRate
{
get
@ -156,6 +150,9 @@ namespace BTCPayServer.Services.Rates
}
TimeSpan _ValidatyTime = TimeSpan.FromMinutes(10);
/// <summary>
/// The timespan after which calls to <see cref="GetRatesAsync(CancellationToken)"/> will query underlying provider if the rate has not been updated
/// </summary>
public TimeSpan ValidatyTime
{
get
@ -201,7 +198,7 @@ namespace BTCPayServer.Services.Rates
}
LatestFetch _Latest;
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
LastRequested = DateTimeOffset.UtcNow;
var latest = _Latest;
@ -217,7 +214,6 @@ namespace BTCPayServer.Services.Rates
/// </summary>
public DateTimeOffset? LastRequested { get; set; }
public string ExchangeName { get; }
public DateTimeOffset? Expiration
{
get
@ -235,7 +231,6 @@ namespace BTCPayServer.Services.Rates
cancellationToken.ThrowIfCancellationRequested();
var previous = _Latest;
var fetch = new LatestFetch();
fetch.ExchangeName = ExchangeName;
try
{
var rates = await _Inner.GetRatesAsync(cancellationToken);

View File

@ -9,24 +9,22 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitbankRateProvider : IRateProvider, IHasExchangeName
public class BitbankRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public BitbankRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bitbank";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://public.bitbank.cc/prices", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
return new ExchangeRates(((jobj["data"] as JObject) ?? new JObject())
return ((jobj["data"] as JObject) ?? new JObject())
.Properties()
.Select(p => new ExchangeRate(ExchangeName, CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
.ToArray());
.Select(p => new PairRate(CurrencyPair.Parse(p.Name), CreateBidAsk(p)))
.ToArray();
}
private static BidAsk CreateBidAsk(JProperty p)

View File

@ -10,26 +10,23 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class BitpayRateProvider : IRateProvider, IHasExchangeName
public class BitpayRateProvider : IRateProvider
{
public const string BitpayName = "bitpay";
private readonly HttpClient _httpClient;
public BitpayRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => BitpayName;
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bitpay.com/rates", cancellationToken);
var jarray = (JArray)(await response.Content.ReadAsAsync<JObject>(cancellationToken))["data"];
return new ExchangeRates(jarray
return jarray
.Children<JObject>()
.Select(jobj => new ExchangeRate(ExchangeName, new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>())))
.Select(jobj => new PairRate(new CurrencyPair("BTC", jobj["code"].Value<string>()), new BidAsk(jobj["rate"].Value<decimal>())))
.Where(o => o.CurrencyPair.Right != "BTC")
.ToArray());
.ToArray();
}
}
}

View File

@ -7,21 +7,20 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class ByllsRateProvider : IRateProvider, IHasExchangeName
public class ByllsRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public ByllsRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public string ExchangeName => "bylls";
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", cancellationToken);
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["public_price"]["to_price"].Value<decimal>();
return new ExchangeRates(new[] { new ExchangeRate(ExchangeName, new CurrencyPair("BTC", "CAD"), new BidAsk(value)) });
return new[] { new PairRate(new CurrencyPair("BTC", "CAD"), new BidAsk(value)) };
}
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using BTCPayServer.Services.Rates;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Services.Rates
{
public class CachedRateProvider : IRateProvider, IHasExchangeName
{
private IRateProvider _Inner;
private IMemoryCache _MemoryCache;
public CachedRateProvider(string exchangeName, IRateProvider inner, IMemoryCache memoryCache)
{
if (inner == null)
throw new ArgumentNullException(nameof(inner));
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
this._Inner = inner;
this.MemoryCache = memoryCache;
this.ExchangeName = exchangeName;
}
public IRateProvider Inner
{
get
{
return _Inner;
}
}
public string ExchangeName { get; }
public TimeSpan CacheSpan
{
get;
set;
} = TimeSpan.FromMinutes(1.0);
public IMemoryCache MemoryCache { get => _MemoryCache; set => _MemoryCache = value; }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
return MemoryCache.GetOrCreateAsync("EXCHANGE_RATES_" + ExchangeName, (ICacheEntry entry) =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return _Inner.GetRatesAsync(cancellationToken);
});
}
}
}

View File

@ -1,195 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.ComponentModel;
using BTCPayServer.Rating;
using System.Threading;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageException : Exception
{
public CoinAverageException(string message) : base(message)
{
}
}
public class RatesSetting
{
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[DefaultValue(15)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int CacheInMinutes { get; set; } = 15;
}
public interface ICoinAverageAuthenticator
{
Task AddHeader(HttpRequestMessage message);
}
public class CoinAverageRateProvider : IRateProvider, IHasExchangeName
{
public const string CoinAverageName = "coinaverage";
public CoinAverageRateProvider()
{
}
public HttpClient HttpClient
{
get
{
return _LocalClient ?? _Client;
}
set
{
_LocalClient = value;
}
}
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
public string Exchange { get; set; } = CoinAverageName;
public string CryptoCode { get; set; }
public string Market
{
get; set;
} = "global";
public ICoinAverageAuthenticator Authenticator { get; set; }
public string ExchangeName => Exchange ?? CoinAverageName;
private bool TryToBidAsk(JProperty p, out BidAsk bidAsk)
{
bidAsk = null;
if (Exchange == CoinAverageName)
{
JToken last = p.Value["last"];
if (!decimal.TryParse(last.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v) ||
v <= 0)
return false;
bidAsk = new BidAsk(v);
return true;
}
else
{
JToken bid = p.Value["bid"];
JToken ask = p.Value["ask"];
if (bid == null || ask == null ||
!decimal.TryParse(bid.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v1) ||
!decimal.TryParse(ask.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var v2) ||
v1 > v2 ||
v1 <= 0 || v2 <= 0)
return false;
bidAsk = new BidAsk(v1, v2);
return true;
}
}
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
{
string url = Exchange == CoinAverageName ? $"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short"
: $"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}";
var request = new HttpRequestMessage(HttpMethod.Get, url);
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request, cancellationToken);
using (resp)
{
if ((int)resp.StatusCode == 401)
throw new CoinAverageException("Unauthorized access to the API");
if ((int)resp.StatusCode == 429)
throw new CoinAverageException("Exceed API limits");
if ((int)resp.StatusCode == 403)
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
resp.EnsureSuccessStatusCode();
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
if (Exchange != CoinAverageName)
{
rates = (JObject)rates["symbols"];
}
var exchangeRates = new ExchangeRates();
foreach (var prop in rates.Properties())
{
ExchangeRate exchangeRate = new ExchangeRate();
exchangeRate.Exchange = Exchange;
if (!TryToBidAsk(prop, out var value))
continue;
exchangeRate.BidAsk = value;
if (CurrencyPair.TryParse(prop.Name, out var pair))
{
exchangeRate.CurrencyPair = pair;
exchangeRates.Add(exchangeRate);
}
}
return exchangeRates;
}
}
public async Task TestAuthAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/blockchain/tx_price/BTCUSD/8a3b4394ba811a9e2b0bbf3cc56888d053ea21909299b2703cdc35e156c860ff");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
}
public async Task<GetRateLimitsResponse> GetRateLimitsAsync()
{
var request = new HttpRequestMessage(HttpMethod.Get, "https://apiv2.bitcoinaverage.com/info/ratelimits");
var auth = Authenticator;
if (auth != null)
{
await auth.AddHeader(request);
}
var resp = await HttpClient.SendAsync(request);
resp.EnsureSuccessStatusCode();
var jobj = JObject.Parse(await resp.Content.ReadAsStringAsync());
var response = new GetRateLimitsResponse();
response.CounterReset = TimeSpan.FromSeconds(jobj["counter_reset"].Value<int>());
var totalPeriod = jobj["total_period"].Value<string>();
if (totalPeriod == "24h")
{
response.TotalPeriod = TimeSpan.FromHours(24);
}
else if (totalPeriod == "30d")
{
response.TotalPeriod = TimeSpan.FromDays(30);
}
else
{
response.TotalPeriod = TimeSpan.FromSeconds(jobj["total_period"].Value<int>());
}
response.RequestsLeft = jobj["requests_left"].Value<int>();
response.RequestsPerPeriod = jobj["requests_per_period"].Value<int>();
return response;
}
}
public class GetRateLimitsResponse
{
public TimeSpan CounterReset { get; set; }
public int RequestsLeft { get; set; }
public int RequestsPerPeriod { get; set; }
public TimeSpan TotalPeriod { get; set; }
}
}

View File

@ -1,51 +0,0 @@
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public class CoinAverageSettingsAuthenticator : ICoinAverageAuthenticator
{
CoinAverageSettings _Settings;
public CoinAverageSettingsAuthenticator(CoinAverageSettings settings)
{
_Settings = settings;
}
public Task AddHeader(HttpRequestMessage message)
{
return _Settings.AddHeader(message);
}
}
public class CoinAverageSettings : ICoinAverageAuthenticator
{
private static readonly DateTime _epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public (String PublicKey, String PrivateKey)? KeyPair { get; set; }
public Task AddHeader(HttpRequestMessage message)
{
var signature = GetCoinAverageSignature();
if (signature != null)
{
message.Headers.Add("X-signature", signature);
}
return Task.CompletedTask;
}
public string GetCoinAverageSignature()
{
var keyPair = KeyPair;
if (!keyPair.HasValue)
return null;
if (string.IsNullOrEmpty(keyPair.Value.PublicKey) || string.IsNullOrEmpty(keyPair.Value.PrivateKey))
return null;
var timestamp = (int)((DateTime.UtcNow - _epochUtc).TotalSeconds);
var payload = timestamp + "." + keyPair.Value.PublicKey;
var digestValueBytes = new HMACSHA256(Encoding.ASCII.GetBytes(keyPair.Value.PrivateKey)).ComputeHash(Encoding.ASCII.GetBytes(payload));
var digestValueHex = NBitcoin.DataEncoders.Encoders.Hex.EncodeData(digestValueBytes);
return payload + "." + digestValueHex;
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
@ -11,18 +12,15 @@ using ExchangeSharp;
namespace BTCPayServer.Services.Rates
{
public class ExchangeSharpRateProvider : IRateProvider, IHasExchangeName
public class ExchangeSharpRateProvider<T> : IRateProvider where T : ExchangeAPI, new()
{
readonly ExchangeAPI _ExchangeAPI;
readonly string _ExchangeName;
public ExchangeSharpRateProvider(string exchangeName, ExchangeAPI exchangeAPI, bool reverseCurrencyPair = false)
HttpClient _httpClient;
public ExchangeSharpRateProvider(HttpClient httpClient, bool reverseCurrencyPair = false)
{
if (exchangeAPI == null)
throw new ArgumentNullException(nameof(exchangeAPI));
exchangeAPI.RequestTimeout = TimeSpan.FromSeconds(5.0);
_ExchangeAPI = exchangeAPI;
_ExchangeName = exchangeName;
if (httpClient == null)
throw new ArgumentNullException(nameof(httpClient));
ReverseCurrencyPair = reverseCurrencyPair;
_httpClient = httpClient;
}
public bool ReverseCurrencyPair
@ -30,45 +28,42 @@ namespace BTCPayServer.Services.Rates
get; set;
}
public string ExchangeName => _ExchangeName;
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
await new SynchronizationContextRemover();
var rates = await _ExchangeAPI.GetTickersAsync();
var exchangeRateTasks = rates
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
.Select(t => CreateExchangeRate(t));
var exchangeAPI = new T();
exchangeAPI.RequestMaker = new HttpClientRequestMaker(exchangeAPI, _httpClient, cancellationToken);
var rates = await exchangeAPI.GetTickersAsync();
var exchangeRates = await Task.WhenAll(exchangeRateTasks);
return new ExchangeRates(exchangeRates
var exchangeRateTasks = rates
.Where(t => t.Value.Ask != 0m && t.Value.Bid != 0m)
.Select(t => CreateExchangeRate(exchangeAPI, t));
var exchangeRates = await Task.WhenAll(exchangeRateTasks);
return exchangeRates
.Where(t => t != null)
.ToArray());
.ToArray();
}
// ExchangeSymbolToGlobalSymbol throws exception which would kill perf
ConcurrentDictionary<string, string> notFoundSymbols = new ConcurrentDictionary<string, string>();
private async Task<ExchangeRate> CreateExchangeRate(KeyValuePair<string, ExchangeTicker> ticker)
private async Task<PairRate> CreateExchangeRate(T exchangeAPI, KeyValuePair<string, ExchangeTicker> ticker)
{
if (notFoundSymbols.TryGetValue(ticker.Key, out _))
return null;
try
{
var tickerName = await _ExchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key);
var tickerName = await exchangeAPI.ExchangeMarketSymbolToGlobalMarketSymbolAsync(ticker.Key);
if (!CurrencyPair.TryParse(tickerName, out var pair))
{
notFoundSymbols.TryAdd(ticker.Key, ticker.Key);
return null;
}
if(ReverseCurrencyPair)
if (ReverseCurrencyPair)
pair = new CurrencyPair(pair.Right, pair.Left);
var rate = new ExchangeRate();
rate.CurrencyPair = pair;
rate.Exchange = _ExchangeName;
rate.BidAsk = new BidAsk(ticker.Value.Bid, ticker.Value.Ask);
return rate;
return new PairRate(pair, new BidAsk(ticker.Value.Bid, ticker.Value.Ask));
}
catch (ArgumentException)
{

View File

@ -17,7 +17,7 @@ namespace BTCPayServer.Services.Rates
_Providers = providers;
}
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
foreach (var p in _Providers)
{
@ -31,7 +31,7 @@ namespace BTCPayServer.Services.Rates
}
catch(Exception ex) { Exceptions.Add(ex); }
}
return new ExchangeRates();
return Array.Empty<PairRate>();
}
public List<Exception> Exceptions { get; set; } = new List<Exception>();

View File

@ -0,0 +1,216 @@
using System;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using ExchangeSharp;
using System.Threading;
namespace BTCPayServer.Services.Rates
{
internal class HttpClientRequestMaker : IAPIRequestMaker
{
class InternalHttpWebRequest : IHttpWebRequest
{
internal readonly HttpWebRequest Request;
public Uri RequestUri => Request.RequestUri;
public string Method
{
get
{
return Request.Method;
}
set
{
Request.Method = value;
}
}
public int Timeout
{
get
{
return Request.Timeout;
}
set
{
Request.Timeout = value;
}
}
public int ReadWriteTimeout
{
get
{
return Request.ReadWriteTimeout;
}
set
{
Request.ReadWriteTimeout = value;
}
}
public InternalHttpWebRequest(Uri fullUri)
{
Request = ((WebRequest.Create(fullUri) as HttpWebRequest) ?? throw new NullReferenceException("Failed to create HttpWebRequest"));
Request.KeepAlive = false;
}
public void AddHeader(string header, string value)
{
switch (header.ToStringLowerInvariant())
{
case "content-type":
Request.ContentType = value;
break;
case "content-length":
Request.ContentLength = value.ConvertInvariant<long>(0L);
break;
case "user-agent":
Request.UserAgent = value;
break;
case "accept":
Request.Accept = value;
break;
case "connection":
Request.Connection = value;
break;
default:
Request.Headers[header] = value;
break;
}
}
public Task WriteAllAsync(byte[] data, int index, int length)
{
throw new NotImplementedException();
}
public HttpRequestMessage ToHttpRequestMessage()
{
var httpRequest = new HttpRequestMessage(HttpMethod.Get, Request.RequestUri);
CopyHeadersFrom(httpRequest, Request);
return httpRequest;
}
internal void CopyHeadersFrom(HttpRequestMessage message, HttpWebRequest request)
{
foreach (string headerName in request.Headers)
{
string[] headerValues = request.Headers.GetValues(headerName);
if (!message.Headers.TryAddWithoutValidation(headerName, headerValues))
{
if (message.Content != null)
message.Content.Headers.TryAddWithoutValidation(headerName, headerValues);
}
}
}
}
class InternalHttpWebResponse : IHttpWebResponse
{
public InternalHttpWebResponse(HttpResponseMessage httpResponseMessage)
{
var headers = new Dictionary<string, List<string>>();
foreach (var h in httpResponseMessage.Headers)
{
if (!headers.TryGetValue(h.Key, out var list))
{
list = new List<string>();
headers.Add(h.Key, list);
}
list.AddRange(h.Value);
}
Headers = new Dictionary<string, IReadOnlyList<string>>(headers.Count);
foreach (var item in headers)
{
Headers.Add(item.Key, item.Value.AsReadOnly());
}
}
public Dictionary<string, IReadOnlyList<string>> Headers { get; }
static IReadOnlyList<string> Empty = new List<string>().AsReadOnly();
public IReadOnlyList<string> GetHeader(string name)
{
Headers.TryGetValue(name, out var list);
return list ?? Empty;
}
}
private readonly IAPIRequestHandler api;
private readonly HttpClient _httpClient;
private readonly CancellationToken _cancellationToken;
public HttpClientRequestMaker(IAPIRequestHandler api, HttpClient httpClient, CancellationToken cancellationToken)
{
if (api == null)
throw new ArgumentNullException(nameof(api));
if (httpClient == null)
throw new ArgumentNullException(nameof(httpClient));
this.api = api;
_httpClient = httpClient;
_cancellationToken = cancellationToken;
}
public Action<IAPIRequestMaker, RequestMakerState, object> RequestStateChanged
{
get;
set;
}
public async Task<string> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
{
await default(SynchronizationContextRemover);
await api.RateLimit.WaitToProceedAsync();
if (url[0] != '/')
{
url = "/" + url;
}
string uri2 = (baseUrl ?? api.BaseUrl) + url;
if (method == null)
{
method = api.RequestMethod;
}
Uri uri = api.ProcessRequestUrl(new UriBuilder(uri2), payload, method);
InternalHttpWebRequest request = new InternalHttpWebRequest(uri)
{
Method = method
};
request.AddHeader("content-type", api.RequestContentType);
request.AddHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36");
int num3 = request.Timeout = (request.ReadWriteTimeout = (int)api.RequestTimeout.TotalMilliseconds);
await api.ProcessRequestAsync(request, payload);
try
{
RequestStateChanged?.Invoke(this, RequestMakerState.Begin, uri.AbsoluteUri);
using var webHttpRequest = request.ToHttpRequestMessage();
using var webHttpResponse = await _httpClient.SendAsync(webHttpRequest, _cancellationToken);
string text = await webHttpResponse.Content.ReadAsStringAsync();
if (!webHttpResponse.IsSuccessStatusCode)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new APIException($"{webHttpResponse.StatusCode.ConvertInvariant<int>(0)} - {webHttpResponse.StatusCode}");
}
throw new APIException(text);
}
api.ProcessResponse(new InternalHttpWebResponse(webHttpResponse));
// local reference to handle delegate becoming null, extended discussion here:
// https://github.com/btcpayserver/btcpayserver/commit/00747906849f093712c3907c99404c55b3defa66#r37022103
var requestStateChanged = RequestStateChanged;
if (requestStateChanged != null)
{
requestStateChanged(this, RequestMakerState.Finished, text);
return text;
}
return text;
}
catch (Exception arg)
{
RequestStateChanged?.Invoke(this, RequestMakerState.Error, arg);
throw;
}
}
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Rates
{
public interface IHasExchangeName
{
string ExchangeName { get; }
}
}

View File

@ -9,6 +9,6 @@ namespace BTCPayServer.Services.Rates
{
public interface IRateProvider
{
Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken);
Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken);
}
}

View File

@ -14,7 +14,7 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
// Make sure that only one request is sent to kraken in general
public class KrakenExchangeRateProvider : IRateProvider, IHasExchangeName
public class KrakenExchangeRateProvider : IRateProvider
{
public KrakenExchangeRateProvider()
{
@ -33,8 +33,6 @@ namespace BTCPayServer.Services.Rates
}
}
public string ExchangeName => "kraken";
HttpClient _LocalClient;
static HttpClient _Client = new HttpClient();
@ -87,9 +85,9 @@ namespace BTCPayServer.Services.Rates
{ "ZGBP", "GBP" }
};
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var result = new ExchangeRates();
var result = new List<PairRate>();
var symbols = await GetSymbolsAsync(cancellationToken);
var normalizedPairsList = symbols.Where(s => !notFoundSymbols.ContainsKey(s)).Select(s => _Helper.NormalizeMarketSymbol(s)).ToList();
var csvPairsList = string.Join(",", normalizedPairsList);
@ -117,7 +115,7 @@ namespace BTCPayServer.Services.Rates
global = await _Helper.ExchangeMarketSymbolToGlobalMarketSymbolAsync(symbol);
}
if (CurrencyPair.TryParse(global, out var pair))
result.Add(new ExchangeRate("kraken", pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
result.Add(new PairRate(pair.Inverse(), new BidAsk(ticker.Bid, ticker.Ask)));
else
notFoundSymbols.TryAdd(symbol, symbol);
}
@ -127,7 +125,7 @@ namespace BTCPayServer.Services.Rates
}
}
}
return result;
return result.ToArray();
}
private static ExchangeTicker ConvertToExchangeTicker(string symbol, JToken ticker)

View File

@ -21,9 +21,9 @@ namespace BTCPayServer.Services.Rates
return _Instance;
}
}
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(new ExchangeRates());
return Task.FromResult(Array.Empty<PairRate>());
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Rating;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.Rates
{
public class PolisRateProvider : IRateProvider
{
private readonly HttpClient _httpClient;
public PolisRateProvider(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
}
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
var response = await _httpClient.GetAsync("https://obol.polispay.com/complex/btc/polis", cancellationToken); //Returns complex rate from BTC to POLIS
var jobj = await response.Content.ReadAsAsync<JObject>(cancellationToken);
var value = jobj["data"].Value<decimal>();
return new[] { new PairRate(new CurrencyPair("BTC", "POLIS"), new BidAsk(value)) };
}
}
}

View File

@ -99,6 +99,8 @@ namespace BTCPayServer.Rating
RuleList ruleList;
decimal _Spread;
private const string ImplicitSatsRule = "SATS_X = SATS_BTC * BTC_X;\nSATS_BTC = 0.00000001;\n";
public decimal Spread
{
get
@ -126,6 +128,7 @@ namespace BTCPayServer.Rating
}
public static bool TryParse(string str, out RateRules rules, out List<RateRulesErrors> errors)
{
str = ImplicitSatsRule + str;
rules = null;
errors = null;
var expression = CSharpSyntaxTree.ParseText(str, new CSharpParseOptions(LanguageVersion.Default).WithKind(SourceCodeKind.Script));
@ -195,6 +198,7 @@ namespace BTCPayServer.Rating
{
return root.NormalizeWhitespace("", "\n")
.ToFullString()
.Replace(ImplicitSatsRule, string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("{\n", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace("\n}", string.Empty, StringComparison.OrdinalIgnoreCase);
}

View File

@ -79,9 +79,9 @@ namespace BTCPayServer.Services.Rates
result.Latency = query.Latency;
if (query.Exception != null)
result.ExchangeExceptions.Add(query.Exception);
foreach (var rule in query.ExchangeRates)
foreach (var rule in query.PairRates)
{
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
rateRule.ExchangeRates.SetRate(query.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();

View File

@ -25,7 +25,7 @@ namespace BTCPayServer.Services.Rates
{
_inner = inner;
}
public async Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public async Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
DateTimeOffset now = DateTimeOffset.UtcNow;
try
@ -35,7 +35,7 @@ namespace BTCPayServer.Services.Rates
catch (Exception ex)
{
Exception = ex;
return new ExchangeRates();
return Array.Empty<PairRate>();
}
finally
{
@ -46,49 +46,15 @@ namespace BTCPayServer.Services.Rates
public class QueryRateResult
{
public TimeSpan Latency { get; set; }
public ExchangeRates ExchangeRates { get; set; }
public PairRate[] PairRates { get; set; }
public ExchangeException Exception { get; internal set; }
public string Exchange { get; internal set; }
}
public RateProviderFactory(IOptions<MemoryCacheOptions> cacheOptions,
IHttpClientFactory httpClientFactory,
CoinAverageSettings coinAverageSettings)
public RateProviderFactory(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
_CoinAverageSettings = coinAverageSettings;
_CacheOptions = cacheOptions;
// We use 15 min because of limits with free version of bitcoinaverage
CacheSpan = TimeSpan.FromMinutes(15.0);
InitExchanges();
}
private IOptions<MemoryCacheOptions> _CacheOptions;
TimeSpan _CacheSpan;
public TimeSpan CacheSpan
{
get
{
return _CacheSpan;
}
set
{
_CacheSpan = value;
InvalidateCache();
}
}
public void InvalidateCache()
{
var cache = new MemoryCache(_CacheOptions);
foreach (var provider in Providers.Select(p => p.Value as CachedRateProvider).Where(p => p != null))
{
provider.CacheSpan = CacheSpan;
provider.MemoryCache = cache;
}
if (Providers.TryGetValue(CoinGeckoRateProvider.CoinGeckoName, out var coinAverage) && coinAverage is BackgroundFetcherRateProvider c)
{
c.RefreshRate = CacheSpan;
c.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
}
}
CoinAverageSettings _CoinAverageSettings;
private readonly IHttpClientFactory _httpClientFactory;
private readonly Dictionary<string, IRateProvider> _DirectProviders = new Dictionary<string, IRateProvider>();
public Dictionary<string, IRateProvider> Providers
@ -98,95 +64,88 @@ namespace BTCPayServer.Services.Rates
return _DirectProviders;
}
}
internal IEnumerable<AvailableRateProvider> GetDirectlySupportedExchanges()
{
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr", RateSource.Direct);
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries", RateSource.Direct);
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker", RateSource.Direct);
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker", RateSource.Direct);
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker", RateSource.Direct);
yield return new AvailableRateProvider("binance", "Binance", "https://api.binance.com/api/v1/ticker/24hr");
yield return new AvailableRateProvider("bittrex", "Bittrex", "https://bittrex.com/api/v1.1/public/getmarketsummaries");
yield return new AvailableRateProvider("poloniex", "Poloniex", "https://poloniex.com/public?command=returnTicker");
yield return new AvailableRateProvider("hitbtc", "HitBTC", "https://api.hitbtc.com/api/2/public/ticker");
yield return new AvailableRateProvider("ndax", "NDAX", "https://ndax.io/api/returnTicker");
yield return new AvailableRateProvider(CoinGeckoRateProvider.CoinGeckoName, "Coin Gecko", "https://api.coingecko.com/api/v3/exchange_rates", RateSource.Direct);
yield return new AvailableRateProvider(CoinAverageRateProvider.CoinAverageName, "Coin Average", "https://apiv2.bitcoinaverage.com/indices/global/ticker/short", RateSource.Direct);
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD", RateSource.Direct);
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD", RateSource.Direct);
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices", RateSource.Direct);
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates", RateSource.Direct);
yield return new AvailableRateProvider("coingecko", "CoinGecko", "https://api.coingecko.com/api/v3/exchange_rates");
yield return new AvailableRateProvider("kraken", "Kraken", "https://api.kraken.com/0/public/Ticker?pair=ATOMETH,ATOMEUR,ATOMUSD,ATOMXBT,BATETH,BATEUR,BATUSD,BATXBT,BCHEUR,BCHUSD,BCHXBT,DAIEUR,DAIUSD,DAIUSDT,DASHEUR,DASHUSD,DASHXBT,EOSETH,EOSXBT,ETHCHF,ETHDAI,ETHUSDC,ETHUSDT,GNOETH,GNOXBT,ICXETH,ICXEUR,ICXUSD,ICXXBT,LINKETH,LINKEUR,LINKUSD,LINKXBT,LSKETH,LSKEUR,LSKUSD,LSKXBT,NANOETH,NANOEUR,NANOUSD,NANOXBT,OMGETH,OMGEUR,OMGUSD,OMGXBT,PAXGETH,PAXGEUR,PAXGUSD,PAXGXBT,SCETH,SCEUR,SCUSD,SCXBT,USDCEUR,USDCUSD,USDCUSDT,USDTCAD,USDTEUR,USDTGBP,USDTZUSD,WAVESETH,WAVESEUR,WAVESUSD,WAVESXBT,XBTCHF,XBTDAI,XBTUSDC,XBTUSDT,XDGEUR,XDGUSD,XETCXETH,XETCXXBT,XETCZEUR,XETCZUSD,XETHXXBT,XETHZCAD,XETHZEUR,XETHZGBP,XETHZJPY,XETHZUSD,XLTCXXBT,XLTCZEUR,XLTCZUSD,XMLNXETH,XMLNXXBT,XMLNZEUR,XMLNZUSD,XREPXETH,XREPXXBT,XREPZEUR,XXBTZCAD,XXBTZEUR,XXBTZGBP,XXBTZJPY,XXBTZUSD,XXDGXXBT,XXLMXXBT,XXMRXXBT,XXMRZEUR,XXMRZUSD,XXRPXXBT,XXRPZEUR,XXRPZUSD,XZECXXBT,XZECZEUR,XZECZUSD");
yield return new AvailableRateProvider("bylls", "Bylls", "https://bylls.com/api/price?from_currency=BTC&to_currency=CAD");
yield return new AvailableRateProvider("bitbank", "Bitbank", "https://public.bitbank.cc/prices");
yield return new AvailableRateProvider("bitpay", "Bitpay", "https://bitpay.com/rates");
yield return new AvailableRateProvider("polispay", "PolisPay", "https://obol.polispay.com/complex/btc/polis");
yield return new AvailableRateProvider("bitfinex", "Bitfinex", "https://api.bitfinex.com/v2/tickers?symbols=tBTCUSD,tLTCUSD,tLTCBTC,tETHUSD,tETHBTC,tETCBTC,tETCUSD,tRRTUSD,tRRTBTC,tZECUSD,tZECBTC,tXMRUSD,tXMRBTC,tDSHUSD,tDSHBTC,tBTCEUR,tBTCJPY,tXRPUSD,tXRPBTC,tIOTUSD,tIOTBTC,tIOTETH,tEOSUSD,tEOSBTC,tEOSETH,tSANUSD,tSANBTC,tSANETH,tOMGUSD,tOMGBTC,tOMGETH,tNEOUSD,tNEOBTC,tNEOETH,tETPUSD,tETPBTC,tETPETH,tQTMUSD,tQTMBTC,tQTMETH,tAVTUSD,tAVTBTC,tAVTETH,tEDOUSD,tEDOBTC,tEDOETH,tBTGUSD,tBTGBTC,tDATUSD,tDATBTC,tDATETH,tQSHUSD,tQSHBTC,tQSHETH,tYYWUSD,tYYWBTC,tYYWETH,tGNTUSD,tGNTBTC,tGNTETH,tSNTUSD,tSNTBTC,tSNTETH,tIOTEUR,tBATUSD,tBATBTC,tBATETH,tMNAUSD,tMNABTC,tMNAETH,tFUNUSD,tFUNBTC,tFUNETH,tZRXUSD,tZRXBTC,tZRXETH,tTNBUSD,tTNBBTC,tTNBETH,tSPKUSD,tSPKBTC,tSPKETH,tTRXUSD,tTRXBTC,tTRXETH,tRCNUSD,tRCNBTC,tRCNETH,tRLCUSD,tRLCBTC,tRLCETH,tAIDUSD,tAIDBTC,tAIDETH,tSNGUSD,tSNGBTC,tSNGETH,tREPUSD,tREPBTC,tREPETH,tELFUSD,tELFBTC,tELFETH,tNECUSD,tNECBTC,tNECETH,tBTCGBP,tETHEUR,tETHJPY,tETHGBP,tNEOEUR,tNEOJPY,tNEOGBP,tEOSEUR,tEOSJPY,tEOSGBP,tIOTJPY,tIOTGBP,tIOSUSD,tIOSBTC,tIOSETH,tAIOUSD,tAIOBTC,tAIOETH,tREQUSD,tREQBTC,tREQETH,tRDNUSD,tRDNBTC,tRDNETH,tLRCUSD,tLRCBTC,tLRCETH,tWAXUSD,tWAXBTC,tWAXETH,tDAIUSD,tDAIBTC,tDAIETH,tAGIUSD,tAGIBTC,tAGIETH,tBFTUSD,tBFTBTC,tBFTETH,tMTNUSD,tMTNBTC,tMTNETH,tODEUSD,tODEBTC,tODEETH,tANTUSD,tANTBTC,tANTETH,tDTHUSD,tDTHBTC,tDTHETH,tMITUSD,tMITBTC,tMITETH,tSTJUSD,tSTJBTC,tSTJETH,tXLMUSD,tXLMEUR,tXLMJPY,tXLMGBP,tXLMBTC,tXLMETH,tXVGUSD,tXVGEUR,tXVGJPY,tXVGGBP,tXVGBTC,tXVGETH,tBCIUSD,tBCIBTC,tMKRUSD,tMKRBTC,tMKRETH,tKNCUSD,tKNCBTC,tKNCETH,tPOAUSD,tPOABTC,tPOAETH,tEVTUSD,tLYMUSD,tLYMBTC,tLYMETH,tUTKUSD,tUTKBTC,tUTKETH,tVEEUSD,tVEEBTC,tVEEETH,tDADUSD,tDADBTC,tDADETH,tORSUSD,tORSBTC,tORSETH,tAUCUSD,tAUCBTC,tAUCETH,tPOYUSD,tPOYBTC,tPOYETH,tFSNUSD,tFSNBTC,tFSNETH,tCBTUSD,tCBTBTC,tCBTETH,tZCNUSD,tZCNBTC,tZCNETH,tSENUSD,tSENBTC,tSENETH,tNCAUSD,tNCABTC,tNCAETH,tCNDUSD,tCNDBTC,tCNDETH,tCTXUSD,tCTXBTC,tCTXETH,tPAIUSD,tPAIBTC,tSEEUSD,tSEEBTC,tSEEETH,tESSUSD,tESSBTC,tESSETH,tATMUSD,tATMBTC,tATMETH,tHOTUSD,tHOTBTC,tHOTETH,tDTAUSD,tDTABTC,tDTAETH,tIQXUSD,tIQXBTC,tIQXEOS,tWPRUSD,tWPRBTC,tWPRETH,tZILUSD,tZILBTC,tZILETH,tBNTUSD,tBNTBTC,tBNTETH,tABSUSD,tABSETH,tXRAUSD,tXRAETH,tMANUSD,tMANETH,tBBNUSD,tBBNETH,tNIOUSD,tNIOETH,tDGXUSD,tDGXETH,tVETUSD,tVETBTC,tVETETH,tUTNUSD,tUTNETH,tTKNUSD,tTKNETH,tGOTUSD,tGOTEUR,tGOTETH,tXTZUSD,tXTZBTC,tCNNUSD,tCNNETH,tBOXUSD,tBOXETH,tTRXEUR,tTRXGBP,tTRXJPY,tMGOUSD,tMGOETH,tRTEUSD,tRTEETH,tYGGUSD,tYGGETH,tMLNUSD,tMLNETH,tWTCUSD,tWTCETH,tCSXUSD,tCSXETH,tOMNUSD,tOMNBTC,tINTUSD,tINTETH,tDRNUSD,tDRNETH,tPNKUSD,tPNKETH,tDGBUSD,tDGBBTC,tBSVUSD,tBSVBTC,tBABUSD,tBABBTC,tWLOUSD,tWLOXLM,tVLDUSD,tVLDETH,tENJUSD,tENJETH,tONLUSD,tONLETH,tRBTUSD,tRBTBTC,tUSTUSD,tEUTEUR,tEUTUSD,tGSDUSD,tUDCUSD,tTSDUSD,tPAXUSD,tRIFUSD,tRIFBTC,tPASUSD,tPASETH,tVSYUSD,tVSYBTC,tZRXDAI,tMKRDAI,tOMGDAI,tBTTUSD,tBTTBTC,tBTCUST,tETHUST,tCLOUSD,tCLOBTC,tIMPUSD,tIMPETH,tLTCUST,tEOSUST,tBABUST,tSCRUSD,tSCRETH,tGNOUSD,tGNOETH,tGENUSD,tGENETH,tATOUSD,tATOBTC,tATOETH,tWBTUSD,tXCHUSD,tEUSUSD,tWBTETH,tXCHETH,tEUSETH,tLEOUSD,tLEOBTC,tLEOUST,tLEOEOS,tLEOETH,tASTUSD,tASTETH,tFOAUSD,tFOAETH,tUFRUSD,tUFRETH,tZBTUSD,tZBTUST,tOKBUSD,tUSKUSD,tGTXUSD,tKANUSD,tOKBUST,tOKBETH,tOKBBTC,tUSKUST,tUSKETH,tUSKBTC,tUSKEOS,tGTXUST,tKANUST,tAMPUSD,tALGUSD,tALGBTC,tALGUST,tBTCXCH,tSWMUSD,tSWMETH,tTRIUSD,tTRIETH,tLOOUSD,tLOOETH,tAMPUST,tDUSK:USD,tDUSK:BTC,tUOSUSD,tUOSBTC,tRRBUSD,tRRBUST,tDTXUSD,tDTXUST,tAMPBTC,tFTTUSD,tFTTUST,tPAXUST,tUDCUST,tTSDUST,tBTC:CNHT,tUST:CNHT,tCNH:CNHT,tCHZUSD,tCHZUST,tBTCF0:USTF0,tETHF0:USTF0");
yield return new AvailableRateProvider("okex", "OKEx", "https://www.okex.com/api/futures/v3/instruments/ticker");
yield return new AvailableRateProvider("coinbasepro", "Coinbase Pro", "https://api.pro.coinbase.com/products");
}
void InitExchanges()
{
// We need to be careful to only add exchanges which OnGetTickers implementation make only 1 request
Providers.Add("binance", new ExchangeSharpRateProvider("binance", new ExchangeBinanceAPI(), true));
Providers.Add("bittrex", new ExchangeSharpRateProvider("bittrex", new ExchangeBittrexAPI(), true));
Providers.Add("poloniex", new ExchangeSharpRateProvider("poloniex", new ExchangePoloniexAPI(), true));
Providers.Add("hitbtc", new ExchangeSharpRateProvider("hitbtc", new ExchangeHitBTCAPI(), true));
Providers.Add("ndax", new ExchangeSharpRateProvider("ndax", new ExchangeNDAXAPI(), true));
AddExchangeSharpProviders<ExchangeBinanceAPI>("binance");
AddExchangeSharpProviders<ExchangeBittrexAPI>("bittrex");
AddExchangeSharpProviders<ExchangePoloniexAPI>("poloniex");
AddExchangeSharpProviders<ExchangeHitBTCAPI>("hitbtc");
AddExchangeSharpProviders<ExchangeNDAXAPI>("ndax");
// Handmade providers
Providers.Add(CoinGeckoRateProvider.CoinGeckoName, new CoinGeckoRateProvider(_httpClientFactory));
Providers.Add(CoinAverageRateProvider.CoinAverageName, new CoinAverageRateProvider() { Exchange = CoinAverageRateProvider.CoinAverageName, HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_COINAVERAGE"), Authenticator = _CoinAverageSettings });
Providers.Add("coingecko", new CoinGeckoRateProvider(_httpClientFactory));
Providers.Add("kraken", new KrakenExchangeRateProvider() { HttpClient = _httpClientFactory?.CreateClient("EXCHANGE_KRAKEN") });
Providers.Add("bylls", new ByllsRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BYLLS")));
Providers.Add("bitbank", new BitbankRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITBANK")));
Providers.Add("bitpay", new BitpayRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_BITPAY")));
Providers.Add("polispay", new PolisRateProvider(_httpClientFactory?.CreateClient("EXCHANGE_POLIS")));
// Those exchanges make multiple requests when calling GetTickers so we remove them
//DirectProviders.Add("gemini", new ExchangeSharpRateProvider("gemini", new ExchangeGeminiAPI()));
//DirectProviders.Add("bitfinex", new ExchangeSharpRateProvider("bitfinex", new ExchangeBitfinexAPI()));
//DirectProviders.Add("okex", new ExchangeSharpRateProvider("okex", new ExchangeOkexAPI()));
//DirectProviders.Add("bitstamp", new ExchangeSharpRateProvider("bitstamp", new ExchangeBitstampAPI()));
// Backward compatibility: coinaverage should be using coingecko to prevent stores from breaking
Providers.Add("coinaverage", new CoinGeckoRateProvider(_httpClientFactory));
AddExchangeSharpProviders<ExchangeBitfinexAPI>("bitfinex");
AddExchangeSharpProviders<ExchangeOKExAPI>("okex");
AddExchangeSharpProviders<ExchangeCoinbaseAPI>("coinbasepro");
// Those exchanges make too many requests, exchange sharp do not parallelize so it is too slow...
//AddExchangeSharpProviders<ExchangeGeminiAPI>("gemini");
//AddExchangeSharpProviders<ExchangeBitstampAPI>("bitstamp");
//AddExchangeSharpProviders<ExchangeBitMEXAPI>("bitmex");
foreach (var provider in Providers.ToArray())
{
if (provider.Key == "cryptopia") // Shitty exchange, rate often unavailable, it spams the logs
continue;
var prov = new BackgroundFetcherRateProvider(provider.Key, Providers[provider.Key]);
if (provider.Key == CoinGeckoRateProvider.CoinGeckoName)
{
prov.RefreshRate = CacheSpan;
prov.ValidatyTime = CacheSpan + TimeSpan.FromMinutes(1.0);
}
else
{
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
}
var prov = new BackgroundFetcherRateProvider(Providers[provider.Key]);
prov.RefreshRate = TimeSpan.FromMinutes(1.0);
prov.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers[provider.Key] = prov;
}
Providers["gdax"] = Providers["coinbasepro"];
var cache = new MemoryCache(_CacheOptions);
foreach (var supportedExchange in GetCoinGeckoSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Id))
if (!Providers.ContainsKey(supportedExchange.Id) && supportedExchange.Id != CoinGeckoRateProvider.CoinGeckoName)
{
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
var coingecko = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Id
UnderlyingExchange = supportedExchange.SourceId
};
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Id, cached);
}
}
foreach (var supportedExchange in GetCoinAverageSupportedExchanges())
{
if (!Providers.ContainsKey(supportedExchange.Id))
{
var coinAverage = new CoinGeckoRateProvider(_httpClientFactory)
{
Exchange = supportedExchange.Id
};
var cached = new CachedRateProvider(supportedExchange.Id, coinAverage, cache)
{
CacheSpan = CacheSpan
};
Providers.Add(supportedExchange.Id, cached);
var bgFetcher = new BackgroundFetcherRateProvider(coingecko);
bgFetcher.RefreshRate = TimeSpan.FromMinutes(1.0);
bgFetcher.ValidatyTime = TimeSpan.FromMinutes(5.0);
Providers.Add(supportedExchange.Id, bgFetcher);
}
}
}
private IRateProvider AddExchangeSharpProviders<T>(string providerName) where T: ExchangeAPI, new()
{
var provider = new ExchangeSharpRateProvider<T>(_httpClientFactory.CreateClient($"EXCHANGE_{providerName}".ToUpperInvariant()), true);
Providers.Add(providerName, provider);
return provider;
}
IEnumerable<AvailableRateProvider> _AvailableRateProviders = null;
public IEnumerable<AvailableRateProvider> GetSupportedExchanges()
{
@ -201,91 +160,16 @@ namespace BTCPayServer.Services.Rates
{
availableProviders.TryAdd(exchange.Id, exchange);
}
foreach (var exchange in GetCoinAverageSupportedExchanges())
{
availableProviders.TryAdd(exchange.Id, exchange);
}
_AvailableRateProviders = availableProviders.Values.OrderBy(o => o.Name).ToArray();
}
return _AvailableRateProviders;
}
internal IEnumerable<AvailableRateProvider> GetCoinAverageSupportedExchanges()
{
foreach (var item in
new[] {
(DisplayName: "Idex", Name: "idex"),
(DisplayName: "Coinfloor", Name: "coinfloor"),
(DisplayName: "Okex", Name: "okex"),
(DisplayName: "Bitfinex", Name: "bitfinex"),
(DisplayName: "Bittylicious", Name: "bittylicious"),
(DisplayName: "BTC Markets", Name: "btcmarkets"),
(DisplayName: "Kucoin", Name: "kucoin"),
(DisplayName: "IDAX", Name: "idax"),
(DisplayName: "Kraken", Name: "kraken"),
(DisplayName: "Bit2C", Name: "bit2c"),
(DisplayName: "Mercado Bitcoin", Name: "mercado"),
(DisplayName: "CEX.IO", Name: "cex"),
(DisplayName: "Bitex.la", Name: "bitex"),
(DisplayName: "Quoine", Name: "quoine"),
(DisplayName: "Stex", Name: "stex"),
(DisplayName: "CoinTiger", Name: "cointiger"),
(DisplayName: "Poloniex", Name: "poloniex"),
(DisplayName: "Zaif", Name: "zaif"),
(DisplayName: "Huobi", Name: "huobi"),
(DisplayName: "QuickBitcoin", Name: "quickbitcoin"),
(DisplayName: "Tidex", Name: "tidex"),
(DisplayName: "Tokenomy", Name: "tokenomy"),
(DisplayName: "Bitcoin.co.id", Name: "bitcoin_co_id"),
(DisplayName: "Kryptono", Name: "kryptono"),
(DisplayName: "Bitso", Name: "bitso"),
(DisplayName: "Korbit", Name: "korbit"),
(DisplayName: "Yobit", Name: "yobit"),
(DisplayName: "BitBargain", Name: "bitbargain"),
(DisplayName: "Livecoin", Name: "livecoin"),
(DisplayName: "Hotbit", Name: "hotbit"),
(DisplayName: "Coincheck", Name: "coincheck"),
(DisplayName: "Binance", Name: "binance"),
(DisplayName: "Bit-Z", Name: "bitz"),
(DisplayName: "Coinbase Pro", Name: "coinbasepro"),
(DisplayName: "Rock Trading", Name: "rocktrading"),
(DisplayName: "Bittrex", Name: "bittrex"),
(DisplayName: "BitBay", Name: "bitbay"),
(DisplayName: "Tokenize", Name: "tokenize"),
(DisplayName: "Hitbtc", Name: "hitbtc"),
(DisplayName: "Upbit", Name: "upbit"),
(DisplayName: "Bitstamp", Name: "bitstamp"),
(DisplayName: "Luno", Name: "luno"),
(DisplayName: "Trade.io", Name: "tradeio"),
(DisplayName: "LocalBitcoins", Name: "localbitcoins"),
(DisplayName: "Independent Reserve", Name: "independentreserve"),
(DisplayName: "Coinsquare", Name: "coinsquare"),
(DisplayName: "Exmoney", Name: "exmoney"),
(DisplayName: "Coinegg", Name: "coinegg"),
(DisplayName: "FYB-SG", Name: "fybsg"),
(DisplayName: "Cryptonit", Name: "cryptonit"),
(DisplayName: "BTCTurk", Name: "btcturk"),
(DisplayName: "bitFlyer", Name: "bitflyer"),
(DisplayName: "Negocie Coins", Name: "negociecoins"),
(DisplayName: "OasisDEX", Name: "oasisdex"),
(DisplayName: "CoinMate", Name: "coinmate"),
(DisplayName: "BitForex", Name: "bitforex"),
(DisplayName: "Bitsquare", Name: "bitsquare"),
(DisplayName: "FYB-SE", Name: "fybse"),
(DisplayName: "itBit", Name: "itbit"),
})
{
yield return new AvailableRateProvider(item.Name, item.DisplayName, $"https://apiv2.bitcoinaverage.com/exchanges/{item.Name}", RateSource.CoinAverage);
}
yield return new AvailableRateProvider("gdax", string.Empty, $"https://apiv2.bitcoinaverage.com/exchanges/gdax", RateSource.CoinAverage);
}
internal IEnumerable<AvailableRateProvider> GetCoinGeckoSupportedExchanges()
{
return JArray.Parse(CoinGeckoRateProvider.SupportedExchanges).Select(token =>
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["name"].ToString(),
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko))
.Concat(new[] { new AvailableRateProvider("gdax", string.Empty, $"https://api.coingecko.com/api/v3/exchanges/gdax", RateSource.Coingecko) });
new AvailableRateProvider(Normalize(token["id"].ToString().ToLowerInvariant()), token["id"].ToString().ToLowerInvariant(), token["name"].ToString(),
$"https://api.coingecko.com/api/v3/exchanges/{token["id"]}/tickers", RateSource.Coingecko));
}
private string Normalize(string name)
@ -306,8 +190,9 @@ namespace BTCPayServer.Services.Rates
var value = await wrapper.GetRatesAsync(cancellationToken);
return new QueryRateResult()
{
Exchange = exchangeName,
Latency = wrapper.Latency,
ExchangeRates = value,
PairRates = value,
Exception = wrapper.Exception != null ? new ExchangeException() { Exception = wrapper.Exception, ExchangeName = exchangeName } : null
};
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
@ -7,7 +7,16 @@
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
<LangVersion>8.0</LangVersion>
<UserSecretsId>AB0AC1DD-9D26-485B-9416-56A33F268117</UserSecretsId>
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
<PreserveCompilationContext>true</PreserveCompilationContext>
</PropertyGroup>
<!--https://devblogs.microsoft.com/aspnet/testing-asp-net-core-mvc-web-apps-in-memory/-->
<Target Name="CopyAditionalFiles" AfterTargets="Build" Condition="'$(TargetFramework)'!=''">
<ItemGroup>
<DepsFilePaths Include="$([System.IO.Path]::ChangeExtension('%(_ResolvedProjectReferencePaths.FullPath)', '.deps.json'))" />
</ItemGroup>
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
</Target>
<PropertyGroup Condition="'$(CI_TESTS)' == 'true'">
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
@ -16,7 +25,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="78.0.3904.7000" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="79.0.3945.3600" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>

View File

@ -200,67 +200,27 @@ namespace BTCPayServer.Tests
rateProvider.Providers.Clear();
var coinAverageMock = new MockRateProvider();
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_USD"),
BidAsk = new BidAsk(5000m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("BTC_LTC"),
BidAsk = new BidAsk(162m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coingecko",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),
BidAsk = new BidAsk(500m)
});
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(4500m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_LTC"), new BidAsk(162m)));
coinAverageMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("LTC_USD"), new BidAsk(500m)));
rateProvider.Providers.Add("coingecko", coinAverageMock);
var bitflyerMock = new MockRateProvider();
bitflyerMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitflyer",
CurrencyPair = CurrencyPair.Parse("BTC_JPY"),
BidAsk = new BidAsk(700000m)
});
bitflyerMock.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_JPY"), new BidAsk(700000m)));
rateProvider.Providers.Add("bitflyer", bitflyerMock);
var quadrigacx = new MockRateProvider();
quadrigacx.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "quadrigacx",
CurrencyPair = CurrencyPair.Parse("BTC_CAD"),
BidAsk = new BidAsk(6000m)
});
quadrigacx.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("BTC_CAD"), new BidAsk(6000m)));
rateProvider.Providers.Add("quadrigacx", quadrigacx);
var bittrex = new MockRateProvider();
bittrex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bittrex",
CurrencyPair = CurrencyPair.Parse("DOGE_BTC"),
BidAsk = new BidAsk(0.004m)
});
bittrex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("DOGE_BTC"), new BidAsk(0.004m)));
rateProvider.Providers.Add("bittrex", bittrex);
var bitfinex = new MockRateProvider();
bitfinex.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "bitfinex",
CurrencyPair = CurrencyPair.Parse("UST_BTC"),
BidAsk = new BidAsk(0.000136m)
});
bitfinex.ExchangeRates.Add(new PairRate(CurrencyPair.Parse("UST_BTC"), new BidAsk(0.000136m)));
rateProvider.Providers.Add("bitfinex", bitfinex);
}

View File

@ -182,7 +182,7 @@ namespace BTCPayServer.Tests
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var httpClientFactory = TestUtils.CreateHttpFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
@ -213,7 +213,7 @@ namespace BTCPayServer.Tests
var factory = UnitTest1.CreateBTCPayRateFactory();
var fetcher = new RateFetcher(factory);
var httpClientFactory = new MockHttpClientFactory();
var httpClientFactory = TestUtils.CreateHttpFactory();
var changellyController = new ChangellyController(
new ChangellyClientProvider(tester.PayTester.StoreRepository, httpClientFactory),
tester.NetworkProvider, fetcher);
@ -243,12 +243,4 @@ namespace BTCPayServer.Tests
Assert.Equal(20, ChangellyCalculationHelper.ComputeCorrectAmount(10, 1, 2));
}
}
public class MockHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name)
{
return new HttpClient();
}
}
}

View File

@ -4,8 +4,10 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using NBitpayClient;
using OpenQA.Selenium;
using OpenQA.Selenium.Interactions;
@ -177,5 +179,43 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseJSModal()
{
using (var s = SeleniumTester.Create())
{
await s.StartAsync();
s.GoToRegister();
s.RegisterNewUser();
var store = s.CreateNewStore();
s.GoToStore(store.storeId);
s.AddDerivationScheme();
var invoiceId = s.CreateInvoice(store.storeId, 0.001m, "BTC", "a@x.com");
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
s.Driver.Navigate()
.GoToUrl(new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}"));
TestUtils.Eventually(() =>
{
Assert.True(s.Driver.FindElement(By.Name("btcpay")).Displayed);
});
await s.Server.ExplorerNode.SendToAddressAsync(BitcoinAddress.Create(invoice
.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike))
.GetPaymentMethodDetails().GetPaymentDestination(), Network.RegTest),
new Money(0.001m, MoneyUnit.BTC));
IWebElement closebutton = null;
TestUtils.Eventually(() =>
{
var iframe = s.Driver.SwitchTo().Frame("btcpay");
closebutton = iframe.FindElement(By.ClassName("close-action"));
Assert.True(closebutton.Displayed);
});
closebutton.Click();
s.Driver.AssertElementNotFound(By.Name("btcpay"));
Assert.Equal(s.Driver.Url,
new Uri(s.Server.PayTester.ServerUri, $"tests/index.html?invoice={invoiceId}").ToString());
}
}
}
}

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.100 AS builder
FROM mcr.microsoft.com/dotnet/core/sdk:3.1.101 AS builder
RUN apt-get update && apt-get install -y --no-install-recommends chromium-driver \
&& rm -rf /var/lib/apt/lists/*

View File

@ -10,15 +10,15 @@ namespace BTCPayServer.Tests.Mocks
{
public class MockRateProvider : IRateProvider
{
public ExchangeRates ExchangeRates { get; set; } = new ExchangeRates();
public List<PairRate> ExchangeRates { get; set; } = new List<PairRate>();
public MockRateProvider()
{
}
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
return Task.FromResult(ExchangeRates);
return Task.FromResult(ExchangeRates.ToArray());
}
}
}

View File

@ -71,7 +71,7 @@ namespace BTCPayServer.Tests
BitcoinAddress.Create(vmLedger.HintChange, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmLedger.WebsocketPath);
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"));
string redirectedPSBT = AssertRedirectedPSBT(await walletController.WalletSend(walletId, sendModel, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
var vmPSBT = await walletController.WalletPSBT(walletId, new WalletPSBTViewModel() { PSBT = redirectedPSBT }).AssertViewModelAsync<WalletPSBTViewModel>();
var unsignedPSBT = PSBT.Parse(vmPSBT.PSBT, user.SupportedNetwork.NBitcoinNetwork);
Assert.NotNull(vmPSBT.Decoded);
@ -80,14 +80,20 @@ namespace BTCPayServer.Tests
PSBT.Load(filePSBT.FileContents, user.SupportedNetwork.NBitcoinNetwork);
await walletController.WalletPSBT(walletId, vmPSBT, "ledger").AssertViewModelAsync<WalletSendLedgerModel>();
var vmPSBT2 = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
var vmPSBT2 = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.NotEmpty(vmPSBT2.Inputs.Where(i => i.Error != null));
Assert.Equal(vmPSBT.PSBT, vmPSBT2.PSBT);
var signedPSBT = unsignedPSBT.Clone();
signedPSBT.SignAll(user.DerivationScheme, user.ExtKey);
signedPSBT.SignAll(user.DerivationScheme, user.GenerateWalletResponseV.AccountHDKey, user.GenerateWalletResponseV.AccountKeyPath);
vmPSBT.PSBT = signedPSBT.ToBase64();
var psbtReady = await walletController.WalletPSBT(walletId, vmPSBT, "broadcast").AssertViewModelAsync<WalletPSBTReadyViewModel>();
var psbtReady = await walletController.WalletPSBTReady(walletId, new WalletPSBTReadyViewModel()
{
PSBT = AssertRedirectedPSBT( await walletController.WalletPSBT(walletId, vmPSBT, "broadcast"), nameof(walletController.WalletPSBTReady))
} ).AssertViewModelAsync<WalletPSBTReadyViewModel>();
Assert.Equal(2 + 1, psbtReady.Destinations.Count); // The fee is a destination
Assert.Contains(psbtReady.Destinations, d => d.Destination == sendDestination && !d.Positive);
Assert.Contains(psbtReady.Destinations, d => d.Positive);
@ -98,7 +104,7 @@ namespace BTCPayServer.Tests
var combineVM = await walletController.WalletPSBT(walletId, vmPSBT, "combine").AssertViewModelAsync<WalletPSBTCombineViewModel>();
Assert.Equal(vmPSBT.PSBT, combineVM.OtherPSBT);
combineVM.PSBT = signedPSBT.ToBase64();
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
var psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
var signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
@ -108,7 +114,7 @@ namespace BTCPayServer.Tests
// Can use uploaded file?
combineVM.PSBT = null;
combineVM.UploadedPSBTFile = TestUtils.GetFormFile("signedPSBT", signedPSBT.ToBytes());
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM));
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTCombine(walletId, combineVM), nameof(walletController.WalletPSBT));
signedPSBT2 = PSBT.Parse(psbt, user.SupportedNetwork.NBitcoinNetwork);
Assert.True(signedPSBT.TryFinalize(out _));
Assert.True(signedPSBT2.TryFinalize(out _));
@ -116,17 +122,18 @@ namespace BTCPayServer.Tests
var ready = (await walletController.WalletPSBTReady(walletId, signedPSBT.ToBase64())).AssertViewModel<WalletPSBTReadyViewModel>();
Assert.Equal(signedPSBT.ToBase64(), ready.PSBT);
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"));
psbt = AssertRedirectedPSBT(await walletController.WalletPSBTReady(walletId, ready, command: "analyze-psbt"), nameof(walletController.WalletPSBT));
Assert.Equal(signedPSBT.ToBase64(), psbt);
redirect = Assert.IsType<RedirectToActionResult>(await walletController.WalletPSBTReady(walletId, ready, command: "broadcast"));
Assert.Equal(nameof(walletController.WalletTransactions), redirect.ActionName);
}
}
private static string AssertRedirectedPSBT(IActionResult view)
private static string AssertRedirectedPSBT(IActionResult view, string actionName)
{
var postRedirectView = Assert.IsType<ViewResult>(view);
var postRedirectViewModel = Assert.IsType<PostRedirectViewModel>(postRedirectView.Model);
Assert.Equal(actionName, postRedirectViewModel.AspAction);
var redirectedPSBT = postRedirectViewModel.Parameters.Single(p => p.Key == "psbt").Value;
return redirectedPSBT;
}

View File

@ -5,6 +5,7 @@ using System.Text;
using BTCPayServer.Rating;
using Xunit;
using System.Globalization;
using Newtonsoft.Json;
namespace BTCPayServer.Tests
{
@ -24,6 +25,35 @@ namespace BTCPayServer.Tests
Assert.Equal(1.1m, rule.BidAsk.Ask);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanSerializeExchangeRatesCache()
{
HostedServices.RatesHostedService.ExchangeRatesCache cache = new HostedServices.RatesHostedService.ExchangeRatesCache();
cache.Created = DateTimeOffset.UtcNow;
cache.States = new List<Services.Rates.BackgroundFetcherState>();
cache.States.Add(new Services.Rates.BackgroundFetcherState()
{
ExchangeName = "Kraken",
LastRequested = DateTimeOffset.UtcNow,
LastUpdated = DateTimeOffset.UtcNow,
Rates = new List<Services.Rates.BackgroundFetcherRate>()
{
new Services.Rates.BackgroundFetcherRate()
{
Pair = new CurrencyPair("USD", "BTC"),
BidAsk = new BidAsk(1.0m, 2.0m)
}
}
});
var str = JsonConvert.SerializeObject(cache, Formatting.Indented);
var cache2 = JsonConvert.DeserializeObject<HostedServices.RatesHostedService.ExchangeRatesCache>(str);
Assert.Equal(cache.Created.ToUnixTimeSeconds(), cache2.Created.ToUnixTimeSeconds());
Assert.Equal(cache.States[0].Rates[0].BidAsk, cache2.States[0].Rates[0].BidAsk);
Assert.Equal(cache.States[0].Rates[0].Pair, cache2.States[0].Rates[0].Pair);
}
[Fact]
[Trait("Fast", "Fast")]
public void CanParseRateRules()
@ -56,6 +86,8 @@ namespace BTCPayServer.Tests
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02"),
(Pair: "SATS_CAD", Expected: "0.00000001 * coinbase(BTC_CAD)"),
(Pair: "Sats_USD", Expected: "0.00000001 * kraken(BTC_USD)")
};
foreach (var test in tests)
{
@ -102,6 +134,8 @@ namespace BTCPayServer.Tests
(Pair: "BTC_CAD", Expected: "coinbase(BTC_CAD)", ExpectedExchangeRates: "coinbase(BTC_CAD)"),
(Pair: "DOGE_CAD", Expected: "bittrex(DOGE_BTC) * coinbase(BTC_CAD) * 1.1", ExpectedExchangeRates: "bittrex(DOGE_BTC),coinbase(BTC_CAD)"),
(Pair: "LTC_CAD", Expected: "coinaverage(LTC_CAD) * 1.02", ExpectedExchangeRates: "coinaverage(LTC_CAD)"),
(Pair: "SATS_USD", Expected: "0.00000001 * kraken(BTC_USD)", ExpectedExchangeRates: "kraken(BTC_USD)"),
(Pair: "SATS_EUR", Expected: "0.00000001 * coinbase(BTC_EUR)", ExpectedExchangeRates: "coinbase(BTC_EUR)")
};
foreach (var test in tests2)
{
@ -189,6 +223,37 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal($"({(1m / 6100m).ToString(CultureInfo.InvariantCulture)}, {(1m / 6000m).ToString(CultureInfo.InvariantCulture)})", rule2.ToString(true));
// Make sure defining value in sats works
builder = new StringBuilder();
builder.AppendLine("BTC_USD = kraken(BTC_USD)");
builder.AppendLine("BTC_X = coinbase(BTC_X)");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rule2 = rules.GetRuleFor(CurrencyPair.Parse("SATS_USD"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("0.00000001 * (6000, 6100)", rule2.ToString(true));
Assert.Equal(0.00006m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_SATS"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (0.00000001 * (6000, 6100))", rule2.ToString(true));
Assert.Equal(1m / 0.000061m, rule2.BidAsk.Bid);
// testing rounding
rule2 = rules.GetRuleFor(CurrencyPair.Parse("Sats_EUR"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("0.00000001 * (1.23, 2.34)", rule2.ToString(true));
Assert.Equal(0.0000000234m, rule2.BidAsk.Ask);
Assert.Equal(0.0000000123m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("EUR_Sats"));
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_EUR"), new BidAsk(1.23m, 2.34m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (0.00000001 * (1.23, 2.34))", rule2.ToString(true));
Assert.Equal(1m / 0.0000000123m, rule2.BidAsk.Ask);
Assert.Equal(1m / 0.0000000234m, rule2.BidAsk.Bid);
}
}
}

View File

@ -18,6 +18,7 @@ using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Lightning;
using BTCPayServer.Lightning.CLightning;
using BTCPayServer.Models;
using BTCPayServer.Views.Stores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -70,18 +71,18 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
internal void AssertHappyMessage()
internal void AssertHappyMessage(StatusMessageModel.StatusSeverity severity = StatusMessageModel.StatusSeverity.Success)
{
using var cts = new CancellationTokenSource(10_000);
using var cts = new CancellationTokenSource(20_000);
while (!cts.IsCancellationRequested)
{
var success = Driver.FindElements(By.ClassName("alert-success")).Where(el => el.Displayed).Any();
var success = Driver.FindElements(By.ClassName($"alert-{StatusMessageModel.ToString(severity)}")).Any(el => el.Displayed);
if (success)
return;
Thread.Sleep(100);
}
Logs.Tester.LogInformation(this.Driver.PageSource);
Assert.True(false, "Should have shown happy message");
Assert.True(false, $"Should have shown {severity} message");
}
public static readonly TimeSpan ImplicitWait = TimeSpan.FromSeconds(10);
@ -202,7 +203,7 @@ namespace BTCPayServer.Tests
internal void AssertNotFound()
{
Assert.Contains("Status Code: 404; Not Found", Driver.PageSource);
Assert.Contains("404 - Page not found</h1>", Driver.PageSource);
}
public void GoToHome()

View File

@ -9,6 +9,8 @@ using System.Linq;
using NBitcoin;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
using BTCPayServer.Models;
using NBitcoin.Payment;
namespace BTCPayServer.Tests
{
@ -415,7 +417,7 @@ namespace BTCPayServer.Tests
s.Driver.Quit();
}
}
[Fact(Timeout = TestTimeout)]
public async Task CanManageWallet()
{
@ -427,22 +429,71 @@ namespace BTCPayServer.Tests
// In this test, we try to spend from a manual seed. We import the xpub 49'/0'/0', then try to use the seed
// to sign the transaction
var mnemonic = s.GenerateWallet("BTC", "", true, false);
s.GenerateWallet("BTC", "", true, false);
//let's test quickly the receive wallet page
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
//you cant use the Sign with NBX option without saving private keys when generating the wallet.
Assert.DoesNotContain("nbx-seed", s.Driver.PageSource);
s.Driver.FindElement(By.Id("WalletReceive")).Click();
//generate a receiving address
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
var receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
//unreserve
s.Driver.FindElement(By.CssSelector("button[value=unreserve-current-address]")).Click();
//generate it again, should be the same one as before as nothign got used in the meantime
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.True(s.Driver.FindElement(By.ClassName("qr-container")).Displayed);
Assert.Equal( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
//send money to addr and ensure it changed
var sess = await s.Server.ExplorerClient.CreateWebsocketNotificationSessionAsync();
sess.ListenAllTrackedSource();
var nextEvent = sess.NextEventAsync();
s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(receiveAddr, Network.RegTest),
Money.Parse("0.1"));
await nextEvent;
await Task.Delay(200);
s.Driver.Navigate().Refresh();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
receiveAddr = s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value");
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
s.GoToStore(storeId.storeId);
s.GenerateWallet("BTC", "", true, false);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletReceive")).Click();
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
Assert.NotEqual( receiveAddr, s.Driver.FindElement(By.Id("vue-address")).GetAttribute("value"));
var invoiceId = s.CreateInvoice(storeId.storeId);
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
var address = invoice.EntityToDTO().Addresses["BTC"];
//wallet should have been imported to bitcoin core wallet in watch only mode.
var result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
Assert.True(result.IsWatchOnly);
s.GoToStore(storeId.storeId);
mnemonic = s.GenerateWallet("BTC", "", true, true);
var mnemonic = s.GenerateWallet("BTC", "", true, true);
//lets import and save private keys
var root = new Mnemonic(mnemonic).DeriveExtKey();
invoiceId = s.CreateInvoice(storeId.storeId);
invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice( invoiceId);
address = invoice.EntityToDTO().Addresses["BTC"];
result = await s.Server.ExplorerNode.GetAddressInfoAsync(BitcoinAddress.Create(address, Network.RegTest));
//spendable from bitcoin core wallet!
Assert.False(result.IsWatchOnly);
var tx = s.Server.ExplorerNode.SendToAddress(BitcoinAddress.Create(address, Network.RegTest), Money.Coins(3.0m));
s.Server.ExplorerNode.Generate(1);
@ -499,7 +550,43 @@ namespace BTCPayServer.Tests
checkboxElement.Click();
}
}
SignWith(mnemonic);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
var jack = new Key().PubKey.Hash.GetAddress(Network.RegTest);
SetTransactionOutput(0, jack, 0.01m);
s.Driver.ScrollTo(By.Id("SendMenu"));
s.Driver.FindElement(By.Id("SendMenu")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=nbx-seed]")).Click();
Assert.Contains(jack.ToString(), s.Driver.PageSource);
Assert.Contains("0.01000000", s.Driver.PageSource);
s.Driver.FindElement(By.CssSelector("button[value=analyze-psbt]")).ForceClick();
Assert.EndsWith("psbt", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("#OtherActions")).ForceClick();
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Assert.EndsWith("psbt/ready", s.Driver.Url);
s.Driver.FindElement(By.CssSelector("button[value=broadcast]")).ForceClick();
Assert.Equal(walletTransactionLink, s.Driver.Url);
var bip21 = invoice.EntityToDTO().CryptoInfo.First().PaymentUrls.BIP21;
//let's make bip21 more interesting
bip21 += "&label=Solid Snake&message=Snake? Snake? SNAAAAKE!";
var parsedBip21 = new BitcoinUrlBuilder(bip21, Network.RegTest);
s.Driver.FindElement(By.Id("Wallets")).Click();
s.Driver.FindElement(By.LinkText("Manage")).Click();
s.Driver.FindElement(By.Id("WalletSend")).Click();
s.Driver.FindElement(By.Id("bip21parse")).Click();
s.Driver.SwitchTo().Alert().SendKeys(bip21);
s.Driver.SwitchTo().Alert().Accept();
s.AssertHappyMessage(StatusMessageModel.StatusSeverity.Info);
Assert.Equal(parsedBip21.Amount.ToString(false), s.Driver.FindElement(By.Id($"Outputs_0__Amount")).GetAttribute("value"));
Assert.Equal(parsedBip21.Address.ToString(), s.Driver.FindElement(By.Id($"Outputs_0__DestinationAddress")).GetAttribute("value"));
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Data;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using Microsoft.AspNetCore.Identity;
using NBXplorer.Models;
namespace BTCPayServer.Tests
{
@ -43,18 +44,13 @@ namespace BTCPayServer.Tests
var userManager = parent.PayTester.GetService<UserManager<ApplicationUser>>();
var u = await userManager.FindByIdAsync(UserId);
await userManager.AddToRoleAsync(u, Roles.ServerAdmin);
IsAdmin = true;
}
public void Register()
{
RegisterAsync().GetAwaiter().GetResult();
}
public BitcoinExtKey ExtKey
{
get; set;
}
public async Task GrantAccessAsync()
{
await RegisterAsync();
@ -100,26 +96,46 @@ namespace BTCPayServer.Tests
public BTCPayNetwork SupportedNetwork { get; set; }
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false)
public WalletId RegisterDerivationScheme(string crytoCode, bool segwit = false, bool importKeysToNBX = false)
{
return RegisterDerivationSchemeAsync(crytoCode, segwit).GetAwaiter().GetResult();
return RegisterDerivationSchemeAsync(crytoCode, segwit, importKeysToNBX).GetAwaiter().GetResult();
}
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false)
public async Task<WalletId> RegisterDerivationSchemeAsync(string cryptoCode, bool segwit = false, bool importKeysToNBX = false)
{
SupportedNetwork = parent.NetworkProvider.GetNetwork<BTCPayNetwork>(cryptoCode);
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
DerivationScheme = SupportedNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
GenerateWalletResponseV = await parent.ExplorerClient.GenerateWalletAsync(new GenerateWalletRequest()
{
ScriptPubKeyType = segwit ? ScriptPubKeyType.Segwit : ScriptPubKeyType.Legacy,
SavePrivateKeys = importKeysToNBX
});
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
Enabled = true,
CryptoCode = cryptoCode,
Network = SupportedNetwork,
RootFingerprint = GenerateWalletResponseV.AccountKeyPath.MasterFingerprint.ToString(),
RootKeyPath = SupportedNetwork.GetRootKeyPath(),
Source = "NBXplorer",
AccountKey = GenerateWalletResponseV.AccountHDKey.Neuter().ToWif(),
DerivationSchemeFormat = "BTCPay",
KeyPath = GenerateWalletResponseV.AccountKeyPath.KeyPath.ToString(),
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);
return new WalletId(StoreId, cryptoCode);
}
public DerivationStrategyBase DerivationScheme { get; set; }
public GenerateWalletResponse GenerateWalletResponseV { get; set; }
public DerivationStrategyBase DerivationScheme
{
get
{
return GenerateWalletResponseV.DerivationScheme;
}
}
private async Task RegisterAsync()
{

View File

@ -7,6 +7,8 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Xunit.Sdk;
using System.Linq;
using System.Net.Http;
using Microsoft.Extensions.DependencyInjection;
namespace BTCPayServer.Tests
{
@ -104,5 +106,12 @@ namespace BTCPayServer.Tests
}
}
}
internal static IHttpClientFactory CreateHttpFactory()
{
var services = new ServiceCollection();
services.AddHttpClient();
return services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>();
}
}
}

View File

@ -57,7 +57,7 @@ namespace BTCPayServer.Tests
.IsType<ViewResult>(manageController.AddU2FDevice("testdevice")).Model);
Assert.NotEmpty(addDeviceVM.Challenge);
Assert.Equal(addDeviceVM.Name, "testdevice");
Assert.Equal("testdevice", addDeviceVM.Name);
Assert.NotEmpty(addDeviceVM.Version);
Assert.Null(addDeviceVM.DeviceResponse);

View File

@ -150,17 +150,19 @@ namespace BTCPayServer.Tests
new BitcoinLikePaymentHandler(null, networkProvider, null, null),
new LightningLikePaymentHandler(null, null, networkProvider, null),
});
var networkBTC = networkProvider.GetNetwork("BTC");
var networkLTC = networkProvider.GetNetwork("LTC");
InvoiceEntity invoiceEntity = new InvoiceEntity();
invoiceEntity.Networks = networkProvider;
invoiceEntity.Payments = new System.Collections.Generic.List<PaymentEntity>();
invoiceEntity.ProductInformation = new ProductInformation() {Price = 100};
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
paymentMethods.Add(new PaymentMethod() {CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails(
paymentMethods.Add(new PaymentMethod() {Network = networkBTC, CryptoCode = "BTC", Rate = 10513.44m,}.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00000100m), DepositAddress = dummy
}));
paymentMethods.Add(new PaymentMethod() {CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails(
paymentMethods.Add(new PaymentMethod() {Network = networkLTC, CryptoCode = "LTC", Rate = 216.79m}.SetPaymentMethodDetails(
new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
{
NextNetworkFee = Money.Coins(0.00010000m), DepositAddress = dummy
@ -173,7 +175,7 @@ namespace BTCPayServer.Tests
invoiceEntity.Payments.Add(
new PaymentEntity()
{
Accounted = true,
CryptoCode = "BTC",
NetworkFee = 0.00000100m,
@ -315,7 +317,7 @@ namespace BTCPayServer.Tests
entity.Payments.Add(
new PaymentEntity()
{
Output = new TxOut(Money.Coins(0.2m), new Key()),
Output = new TxOut(Money.Coins(0.2m), new Key()),
Accounted = true
});
@ -330,15 +332,15 @@ namespace BTCPayServer.Tests
paymentMethods.Add(
new PaymentMethod()
{
CryptoCode = "BTC",
Rate = 1000,
CryptoCode = "BTC",
Rate = 1000,
NextNetworkFee = Money.Coins(0.1m)
});
paymentMethods.Add(
new PaymentMethod()
{
CryptoCode = "LTC",
Rate = 500,
CryptoCode = "LTC",
Rate = 500,
NextNetworkFee = Money.Coins(0.01m)
});
entity.SetPaymentMethods(paymentMethods);
@ -521,6 +523,39 @@ namespace BTCPayServer.Tests
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanThrowBitpay404Error()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Buyer = new Buyer() { email = "test@fwf.com" },
Price = 5000.0m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
try
{
var throwsBitpay404Error = user.BitPay.GetInvoice(invoice.Id + "123");
}
catch (BitPayException ex)
{
Assert.Equal("Object not found", ex.Errors.First());
}
}
}
[Fact]
[Trait("Fast", "Fast")]
public void RoundupCurrenciesCorrectly()
@ -819,7 +854,7 @@ namespace BTCPayServer.Tests
var walletId = new WalletId(acc.StoreId, "BTC");
acc.IsAdmin = true;
walletController = acc.GetController<WalletsController>();
var rescan = Assert.IsType<RescanWalletModel>(Assert.IsType<ViewResult>(walletController.WalletRescan(walletId).Result).Model);
Assert.True(rescan.Ok);
Assert.True(rescan.IsFullySync);
@ -862,6 +897,7 @@ namespace BTCPayServer.Tests
Assert.Equal(tx.Id, txId.ToString());
// Hijack the test to see if we can add label and comments
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello-pouet"));
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabel: "test"));
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addlabelclick: "test2"));
Assert.IsType<RedirectToActionResult>(await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
@ -1787,7 +1823,7 @@ namespace BTCPayServer.Tests
string content = "{\"keystore\": {\"ckcc_xpub\": \"xpub661MyMwAqRbcGVBsTGeNZN6QGVHmMHLdSA4FteGsRrEriu4pnVZMZWnruFFFXkMnyoBjyHndD3Qwcfz4MPzBUxjSevweNFQx7SAYZATtcDw\", \"xpub\": \"ypub6WWc2gWwHbdnAAyJDnR4SPL1phRh7REqrPBfZeizaQ1EmTshieRXJC3Z5YoU4wkcdKHEjQGkh6AYEzCQC1Kz3DNaWSwdc1pc8416hAjzqyD\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
derivationVM.ColdcardPublicFile = TestUtils.GetFormFile("wallet.json", content);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult()).Model;
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
Assert.False(derivationVM.Confirmation); // Should fail, we are giving a mainnet file to a testnet network
// And with a good file? (upub)
content = "{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"upub5DBYp1qGgsTrkzCptMGZc2x18pquLwGrBw6nS59T4NViZ4cni1mGowQzziy85K8vzkp1jVtWrSkLhqk9KDfvrGeB369wGNYf39kX8rQfiLn\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/49'/0'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}";
@ -1969,15 +2005,15 @@ donation:
Assert.Equal(10.00m, orangeInvoice.Price);
Assert.Equal("CAD", orangeInvoice.Currency);
Assert.Equal("orange", orangeInvoice.ItemDesc);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "apple").Result);
invoices = user.BitPay.GetInvoices();
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
Assert.NotNull(appleInvoice);
Assert.Equal("good apple", appleInvoice.ItemDesc);
// testing custom amount
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 6.6m, null, null, null, null, "donation").Result);
@ -2025,8 +2061,8 @@ donation:
Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility);
Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace);
}
//test inventory related features
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
vmpos.Title = "hello";
@ -2039,13 +2075,13 @@ inventoryitem:
noninventoryitem:
price: 10.0";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
//inventoryitem has 1 item available
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
//we already bought all available stock so this should fail
await Task.Delay(100);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "inventoryitem").Result);
//inventoryitem has unlimited items available
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 1, null, null, null, null, "noninventoryitem").Result);
@ -2055,7 +2091,7 @@ noninventoryitem:
Assert.Equal(2, invoices.Count(invoice => invoice.ItemCode.Equals("noninventoryitem")));
var inventoryItemInvoice = Assert.Single(invoices.Where(invoice => invoice.ItemCode.Equals("inventoryitem")));
Assert.NotNull(inventoryItemInvoice);
//let's mark the inventoryitem invoice as invalid, thsi should return the item to back in stock
var controller = tester.PayTester.GetController<InvoiceController>(user.UserId, user.StoreId);
var appService = tester.PayTester.GetService<AppService>();
@ -2067,7 +2103,7 @@ noninventoryitem:
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
Assert.Equal(1, appService.Parse(vmpos.Template, "BTC").Single(item => item.Id == "inventoryitem").Inventory);
}, 10000);
}
}
@ -2250,7 +2286,7 @@ noninventoryitem:
var cashCow = tester.ExplorerNode;
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
//
//
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
cashCow.SendToAddress(invoiceAddress, firstPayment);
Thread.Sleep(1000); // prevent race conditions, ordering payments
@ -2680,21 +2716,18 @@ noninventoryitem:
public void CanQueryDirectProviders()
{
var factory = CreateBTCPayRateFactory();
var directlySupported = factory.GetSupportedExchanges().Where(s => s.Source == RateSource.Direct).Select(s => s.Id).ToHashSet();
var all = string.Join("\r\n", factory.GetSupportedExchanges().Select(e => e.Id).ToArray());
foreach (var result in factory
.Providers
.Where(p => p.Value is BackgroundFetcherRateProvider)
.Where(p => p.Value is BackgroundFetcherRateProvider bf && !(bf.Inner is CoinGeckoRateProvider cg && cg.UnderlyingExchange != null))
.Select(p => (ExpectedName: p.Key, ResultAsync: p.Value.GetRatesAsync(default), Fetcher: (BackgroundFetcherRateProvider)p.Value))
.ToList())
{
Logs.Tester.LogInformation($"Testing {result.ExpectedName}");
if (result.ExpectedName == "quadrigacx")
continue; // 29 january, the exchange is down
if (result.ExpectedName == "coinaverage")
continue; // no more free plan
result.Fetcher.InvalidateCache();
var exchangeRates = result.ResultAsync.Result;
var exchangeRates = new ExchangeRates(result.ExpectedName, result.ResultAsync.Result);
result.Fetcher.InvalidateCache();
Assert.NotNull(exchangeRates);
Assert.NotEmpty(exchangeRates);
@ -2704,6 +2737,11 @@ noninventoryitem:
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "JPY") && e.BidAsk.Bid > 100m); // 1BTC will always be more than 100JPY
}
else if (result.ExpectedName == "polispay")
{
Assert.Contains(exchangeRates.ByExchange[result.ExpectedName],
e => e.CurrencyPair == new CurrencyPair("BTC", "POLIS") && e.BidAsk.Bid > 1.0m); // 1BTC will always be more than 1 POLIS
}
else
{
// This check if the currency pair is using right currency pair
@ -2715,6 +2753,12 @@ noninventoryitem:
&& e.BidAsk.Bid > 1.0m // 1BTC will always be more than 1USD
);
}
// We are not showing a directly implemented exchange as directly implemented in the UI
// we need to modify the AvailableRateProvider
// There are some exception we stopped supporting but don't want to break backward compat
if (result.ExpectedName != "coinaverage" && result.ExpectedName != "gdax")
Assert.Contains(result.ExpectedName, directlySupported);
}
// Kraken emit one request only after first GetRates
factory.Providers["kraken"].GetRatesAsync(default).GetAwaiter().GetResult();
@ -2730,7 +2774,7 @@ noninventoryitem:
await provider.GetRatesAsync(default);
var state = provider.GetState();
Assert.Single(state.Rates, r => r.Pair == new CurrencyPair("BTC", "EUR"));
var provider2 = new BackgroundFetcherRateProvider("kraken", provider.Inner)
var provider2 = new BackgroundFetcherRateProvider(provider.Inner)
{
RefreshRate = provider.RefreshRate,
ValidatyTime = provider.ValidatyTime
@ -2749,7 +2793,6 @@ noninventoryitem:
// Should not throw, as things should be cached
await provider2.GetRatesAsync(cts.Token);
}
Assert.Equal(provider.ExchangeName, provider2.ExchangeName);
Assert.Equal(provider.NextUpdate, provider2.NextUpdate);
Assert.NotEqual(provider.LastRequested, provider2.LastRequested);
Assert.Equal(provider.Expiration, provider2.Expiration);
@ -2785,23 +2828,18 @@ noninventoryitem:
public static RateProviderFactory CreateBTCPayRateFactory()
{
return new RateProviderFactory(CreateMemoryCache(), new MockHttpClientFactory(), new CoinAverageSettings());
}
private static MemoryCacheOptions CreateMemoryCache()
{
return new MemoryCacheOptions() { ExpirationScanFrequency = TimeSpan.FromSeconds(1.0) };
return new RateProviderFactory(TestUtils.CreateHttpFactory());
}
class SpyRateProvider : IRateProvider
{
public bool Hit { get; set; }
public Task<ExchangeRates> GetRatesAsync(CancellationToken cancellationToken)
public Task<PairRate[]> GetRatesAsync(CancellationToken cancellationToken)
{
Hit = true;
var rates = new ExchangeRates();
rates.Add(new ExchangeRate("coinaverage", CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates);
var rates = new List<PairRate>();
rates.Add(new PairRate(CurrencyPair.Parse("BTC_USD"), new BidAsk(5000)));
return Task.FromResult(rates.ToArray());
}
public void AssertHit()
@ -2889,47 +2927,33 @@ noninventoryitem:
return name;
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public async Task CanCreateSqlitedb()
{
if (File.Exists("temp.db"))
File.Delete("temp.db");
// This test sqlite can migrate
var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
builder.UseSqlite("Data Source=temp.db");
await new ApplicationDbContext(builder.Options).Database.MigrateAsync();
}
[Fact(Timeout = TestTimeout)]
[Trait("Fast", "Fast")]
public void CheckRatesProvider()
{
var spy = new SpyRateProvider();
RateRules.TryParse("X_X = coinaverage(X_X);", out var rateRules);
RateRules.TryParse("X_X = bittrex(X_X);", out var rateRules);
var factory = CreateBTCPayRateFactory();
factory.Providers.Clear();
factory.Providers.Add("coinaverage", new CachedRateProvider("coinaverage", spy, new MemoryCache(CreateMemoryCache())));
factory.Providers.Add("bittrex", new CachedRateProvider("bittrex", spy, new MemoryCache(CreateMemoryCache())));
factory.CacheSpan = TimeSpan.FromSeconds(1);
var fetcher = new RateFetcher(factory);
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
Thread.Sleep(3000);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = fetcher.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
Assert.Null(fetchedRate2.BidAsk);
Assert.Equal(RateRulesErrors.RateUnavailable, fetchedRate2.Errors.First());
// Should cache at exchange level this should not hit the cache as it is different exchange
RateRules.TryParse("X_X = bittrex(X_X);", out rateRules);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
factory.Providers.Clear();
var fetch = new BackgroundFetcherRateProvider("spy", spy);
var fetch = new BackgroundFetcherRateProvider(spy);
fetch.DoNotAutoFetchIfExpired = true;
factory.Providers.Add("bittrex", fetch);
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
var fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertHit();
fetchedRate = fetcher.FetchRate(CurrencyPair.Parse("BTC_USD"), rateRules, default).GetAwaiter().GetResult();
spy.AssertNotHit();
@ -2975,7 +2999,7 @@ noninventoryitem:
Assert.True(DerivationSchemeSettings.TryParseFromColdcard("{\"keystore\": {\"ckcc_xpub\": \"tpubD6NzVbkrYhZ4YHNiuTdTmHRmbcPRLfqgyneZFCL1mkzkUBjXriQShxTh9HL34FK2mhieasJVk9EzJrUfkFqRNQBjiXgx3n5BhPkxKBoFmaS\", \"xpub\": \"vpub5YjYxTemJ39tFRnuAhwduyxG2tKGjoEpmvqVQRPqdYrqa6YGoeSzBtHXaJUYB19zDbXs3JjbEcVWERjQBPf9bEfUUMZNMv1QnMyHV8JPqyf\", \"label\": \"Coldcard Import 0x60d1af8b\", \"ckcc_xfp\": 1624354699, \"type\": \"hardware\", \"hw_type\": \"coldcard\", \"derivation\": \"m/84'/1'/0'\"}, \"wallet_type\": \"standard\", \"use_encryption\": false, \"seed_version\": 17}", testnet, out settings));
Assert.True(settings.AccountDerivation is DirectDerivationStrategy s3 && s3.Segwit);
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
[Trait("Altcoins", "Altcoins")]
@ -2996,8 +3020,8 @@ noninventoryitem:
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC"));
Assert.Equal(2, invoice.SupportedTransactionCurrencies.Count);
invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(100, "BTC")
{
SupportedTransactionCurrencies = new Dictionary<string, InvoiceSupportedTransactionCurrency>()
@ -3008,13 +3032,13 @@ noninventoryitem:
}}
}
});
Assert.Single(invoice.SupportedTransactionCurrencies);
}
}
[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanLoginWithNoSecondaryAuthSystemsOrRequestItWhenAdded()
@ -3035,7 +3059,7 @@ noninventoryitem:
})).ActionName);
var manageController = user.GetController<ManageController>();
//by default no u2f devices available
Assert.Empty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
var addRequest = Assert.IsType<AddU2FDeviceViewModel>(Assert.IsType<ViewResult>(manageController.AddU2FDevice("label")).Model);
@ -3064,10 +3088,10 @@ noninventoryitem:
};
await context.U2FDevices.AddAsync(newDevice);
await context.SaveChangesAsync();
Assert.NotNull(newDevice.Id);
Assert.NotEmpty(Assert.IsType<U2FAuthenticationViewModel>(Assert.IsType<ViewResult>(await manageController.U2FAuthentication()).Model).Devices);
}
//check if we are showing the u2f login screen now
@ -3081,10 +3105,10 @@ noninventoryitem:
var vm = Assert.IsType<SecondaryLoginViewModel>(secondLoginResult.Model);
//2fa was never enabled for user so this should be empty
Assert.Null(vm.LoginWith2FaViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
Assert.NotNull(vm.LoginWithU2FViewModel);
}
}
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
{
var h = BitcoinAddress.Create(invoice.BitcoinAddress, Network.RegTest).ScriptPubKey.Hash.ToString();

View File

@ -10,13 +10,34 @@ namespace BTCPayServer.Tests
{
public class Utils
{
public static int _nextPort = 8001;
public static object _portLock = new object();
public static int FreeTcpPort()
{
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
l.Start();
int port = ((IPEndPoint)l.LocalEndpoint).Port;
l.Stop();
return port;
lock (_portLock)
{
using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
while (true)
{
try
{
var port = _nextPort++;
socket.Bind(new IPEndPoint(IPAddress.Loopback, port));
return port;
}
catch (SocketException)
{
// Retry unless exhausted
if (_nextPort == 65536)
{
throw;
}
}
}
}
}
}
// http://stackoverflow.com/a/14933880/2061103

View File

@ -64,7 +64,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:0.18.0
image: btcpayserver/bitcoin:0.19.0.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |
@ -76,7 +76,7 @@ services:
- customer_lnd
- merchant_lnd
nbxplorer:
image: nicolasdorier/nbxplorer:2.1.5
image: nicolasdorier/nbxplorer:2.1.8
restart: unless-stopped
ports:
- "32838:32838"
@ -98,6 +98,8 @@ services:
NBXPLORER_LBTCRPCUSER: "liquid"
NBXPLORER_LBTCRPCPASSWORD: "liquid"
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_MINGAPSIZE: 5
NBXPLORER_MAXGAPSIZE: 10
NBXPLORER_VERBOSE: 1
NBXPLORER_NOAUTH: 1
links:
@ -108,7 +110,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:0.18.0
image: btcpayserver/bitcoin:0.19.0.1
environment:
BITCOIN_NETWORK: regtest
BITCOIN_EXTRA_ARGS: |-
@ -257,7 +259,7 @@ services:
- "5432"
merchant_lnd:
image: btcpayserver/lnd:v0.7.1-beta-withseed
image: btcpayserver/lnd:v0.8.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"
@ -287,7 +289,7 @@ services:
- bitcoind
customer_lnd:
image: btcpayserver/lnd:v0.7.1-beta-withseed
image: btcpayserver/lnd:v0.8.2-beta
restart: unless-stopped
environment:
LND_CHAIN: "btc"

View File

@ -69,8 +69,8 @@
<PackageReference Include="OpenIddict" Version="3.0.0-alpha1.20058.15" />
<PackageReference Include="OpenIddict.Server.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="OpenIddict.Validation.AspNetCore" Version="3.0.0-alpha1.20058.15"></PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.0" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.1" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
</ItemGroup>
<ItemGroup>

View File

@ -141,6 +141,7 @@ namespace BTCPayServer.Configuration
ExternalServices.Load(net.CryptoCode, conf);
}
ExternalServices.LoadNonCryptoServices(conf);
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
var services = conf.GetOrDefault<string>("externalservices", null);

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
@ -46,7 +47,7 @@ namespace BTCPayServer.Configuration
}
connectionString.Server = serviceUri;
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest)
if (serviceType == ExternalServiceTypes.LNDGRPC || serviceType == ExternalServiceTypes.LNDRest || serviceType == ExternalServiceTypes.CLightningRest)
{
// Read the MacaroonDirectory
if (connectionString.MacaroonDirectoryPath != null)
@ -77,7 +78,7 @@ namespace BTCPayServer.Configuration
}
}
if (serviceType == ExternalServiceTypes.Charge || serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Spark)
if (new []{ExternalServiceTypes.Charge, ExternalServiceTypes.RTL, ExternalServiceTypes.Spark, ExternalServiceTypes.Configurator}.Contains(serviceType))
{
// Read access key from cookie file
if (connectionString.CookieFilePath != null)
@ -94,7 +95,7 @@ namespace BTCPayServer.Configuration
{
throw new System.IO.FileNotFoundException("Cookie file path not found", ex);
}
if (serviceType == ExternalServiceTypes.RTL)
if (serviceType == ExternalServiceTypes.RTL || serviceType == ExternalServiceTypes.Configurator)
{
connectionString.AccessKey = cookieFileContent;
}
@ -144,9 +145,7 @@ namespace BTCPayServer.Configuration
}
public bool? IsOnion()
{
if (this.Server == null || !this.Server.IsAbsoluteUri)
return null;
return this.Server.DnsSafeHost.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
return Server?.IsOnion();
}
public static bool TryParse(string str, out ExternalConnectionString result, out string error)
{

View File

@ -35,17 +35,30 @@ namespace BTCPayServer.Configuration
Load(configuration, cryptoCode, "rtl", ExternalServiceTypes.RTL, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"LND (Ride the Lightning server)");
"Ride the Lightning server");
Load(configuration, cryptoCode, "clightningrest", ExternalServiceTypes.CLightningRest, "Invalid setting {0}, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/clightning-rest/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning REST");
Load(configuration, cryptoCode, "charge", ExternalServiceTypes.Charge, "Invalid setting {0}, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
"Error: {1}",
"C-Lightning (Charge server)");
}
public void LoadNonCryptoServices(IConfiguration configuration)
{
Load(configuration, null, "configurator", ExternalServiceTypes.Configurator, "Invalid setting {0}, " + Environment.NewLine +
$"configurator: 'cookiefilepathfile=/etc/configurator/cookie'" + Environment.NewLine +
"Error: {1}",
"Configurator");
}
void Load(IConfiguration configuration, string cryptoCode, string serviceName, ExternalServiceTypes type, string errorMessage, string displayName)
{
var setting = $"{cryptoCode}.external.{serviceName}";
var setting = $"{(!string.IsNullOrEmpty(cryptoCode)? $"{cryptoCode}.": string.Empty)}external.{serviceName}";
var connStr = configuration.GetOrDefault<string>(setting, string.Empty);
if (connStr.Length != 0)
{
@ -65,8 +78,11 @@ namespace BTCPayServer.Configuration
public ExternalService GetService(string serviceName, string cryptoCode)
{
return this.FirstOrDefault(o => o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) &&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
return this.FirstOrDefault(o =>
(cryptoCode == null && o.CryptoCode == null) ||
(o.CryptoCode != null && o.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase))
&&
o.ServiceName.Equals(serviceName, StringComparison.OrdinalIgnoreCase));
}
}
@ -88,6 +104,8 @@ namespace BTCPayServer.Configuration
RTL,
Charge,
P2P,
RPC
RPC,
Configurator,
CLightningRest
}
}

View File

@ -22,8 +22,8 @@ namespace BTCPayServer.Controllers
return String.Empty;
}
}
[HttpGet]
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId)
@ -34,7 +34,6 @@ namespace BTCPayServer.Controllers
var settings = app.GetSettings<CrowdfundSettings>();
var vm = new UpdateCrowdfundViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Title = settings.Title,
StoreId = app.StoreDataId,
Enabled = settings.Enabled,
@ -61,8 +60,8 @@ namespace BTCPayServer.Controllers
SearchTerm = app.TagAllInvoices ? $"storeid:{app.StoreDataId}" : $"orderid:{AppService.GetCrowdfundOrderId(appId)}",
DisplayPerksRanking = settings.DisplayPerksRanking,
SortPerksByPopularity = settings.SortPerksByPopularity,
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
Sounds = string.Join(Environment.NewLine, settings.Sounds),
AnimationColors = string.Join(Environment.NewLine, settings.AnimationColors)
};
return View(vm);
}
@ -70,9 +69,9 @@ namespace BTCPayServer.Controllers
[Route("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
if (!string.IsNullOrEmpty(vm.TargetCurrency) && _currencies.GetCurrencyData(vm.TargetCurrency, false) == null)
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
try
{
_AppService.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
@ -98,14 +97,14 @@ namespace BTCPayServer.Controllers
}
var parsedSounds = vm.Sounds.Split(
new[] {"\r\n", "\r", "\n"},
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
).Select(s => s.Trim()).ToArray();
if (vm.SoundsEnabled && !parsedSounds.Any())
{
ModelState.AddModelError(nameof(vm.Sounds), "You must have at least one sound if you enable sounds");
}
var parsedAnimationColors = vm.AnimationColors.Split(
new[] { "\r\n", "\r", "\n" },
StringSplitOptions.None
@ -114,13 +113,13 @@ namespace BTCPayServer.Controllers
{
ModelState.AddModelError(nameof(vm.AnimationColors), "You must have at least one animation color if you enable animations");
}
if (!ModelState.IsValid)
{
return View(vm);
}
var app = await GetOwnedApp(appId, AppType.Crowdfund);
if (app == null)
return NotFound();
@ -139,7 +138,6 @@ namespace BTCPayServer.Controllers
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
DisqusEnabled = vm.DisqusEnabled,
@ -157,24 +155,16 @@ namespace BTCPayServer.Controllers
app.TagAllInvoices = vm.UseAllStoreInvoices;
app.SetSettings(newSettings);
if (command == "save")
{
await _AppService.UpdateOrCreateApp(app);
await _AppService.UpdateOrCreateApp(app);
_EventAggregator.Publish(new AppUpdated()
{
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
else if (command == "viewapp")
_EventAggregator.Publish(new AppUpdated()
{
return RedirectToAction(nameof(AppsPublicController.ViewCrowdfund), "AppsPublic", new { appId });
}
return NotFound();
AppId = appId,
StoreId = app.StoreDataId,
Settings = newSettings
});
TempData[WellKnownTempData.SuccessMessage] = "App updated";
return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
}
}
}

View File

@ -79,11 +79,10 @@ namespace BTCPayServer.Controllers
public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; }
public string Description { get; set; }
public string NotificationEmail { get; set; }
public string NotificationUrl { get; set; }
public bool? RedirectAutomatically { get; set; }
}
@ -96,10 +95,9 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var vm = new UpdatePointOfSaleViewModel()
{
NotificationEmailWarning = !await IsEmailConfigured(app.StoreDataId),
Id = appId,
StoreId = app.StoreDataId,
Title = settings.Title,
@ -116,10 +114,9 @@ namespace BTCPayServer.Controllers
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS,
Description = settings.Description,
NotificationEmail = settings.NotificationEmail,
NotificationUrl = settings.NotificationUrl,
SearchTerm = $"storeid:{app.StoreDataId}",
RedirectAutomatically = settings.RedirectAutomatically.HasValue? settings.RedirectAutomatically.Value? "true": "false" : ""
RedirectAutomatically = settings.RedirectAutomatically.HasValue ? settings.RedirectAutomatically.Value ? "true" : "false" : ""
};
if (HttpContext?.Request != null)
{
@ -194,11 +191,10 @@ namespace BTCPayServer.Controllers
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
NotificationEmail = vm.NotificationEmail,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically)? (bool?) null: bool.Parse(vm.RedirectAutomatically)
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? (bool?)null : bool.Parse(vm.RedirectAutomatically)
});
await _AppService.UpdateOrCreateApp(app);
TempData[WellKnownTempData.SuccessMessage] = "App updated";
@ -211,8 +207,8 @@ namespace BTCPayServer.Controllers
if (string.IsNullOrEmpty(list))
{
return Array.Empty<int>();
}
else
}
else
{
// Remove all characters except numeric and comma
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");

View File

@ -177,7 +177,6 @@ namespace BTCPayServer.Controllers
OrderId = orderId,
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
NotificationEmail = settings.NotificationEmail,
RedirectURL = redirectUrl ?? Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
@ -306,7 +305,6 @@ namespace BTCPayServer.Controllers
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
NotificationEmail = settings.NotificationEmail,
FullNotifications = true,
ExtendedNotifications = true,
RedirectURL = request.RedirectUrl ??

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers
{
[Route("[controller]/[action]")]
public class ErrorController : Controller
{
public IActionResult Handle(int? statusCode = null)
{
if (statusCode.HasValue)
{
var specialPages = new[] { 404, 429, 500 };
if (specialPages.Any(a => a == statusCode.Value))
{
var viewName = statusCode.ToString();
return View(viewName);
}
}
return View(statusCode);
}
}
}

View File

@ -65,7 +65,6 @@ namespace BTCPayServer.Controllers
BuyerInformation = invoice.BuyerInformation,
Fiat = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.Price, prodInfo.Currency),
TaxIncluded = _CurrencyNameTable.DisplayFormatCurrency(prodInfo.TaxIncluded, prodInfo.Currency),
NotificationEmail = invoice.NotificationEmail,
NotificationUrl = invoice.NotificationURL?.AbsoluteUri,
RedirectUrl = invoice.RedirectURL?.AbsoluteUri,
ProductInformation = invoice.ProductInformation,
@ -104,7 +103,6 @@ namespace BTCPayServer.Controllers
cryptoPayment.Paid = _CurrencyNameTable.DisplayFormatCurrency(accounting.CryptoPaid.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
cryptoPayment.Overpaid = _CurrencyNameTable.DisplayFormatCurrency(accounting.OverpaidHelper.ToDecimal(MoneyUnit.BTC), paymentMethodId.CryptoCode);
var paymentMethodDetails = data.GetPaymentMethodDetails();
cryptoPayment.Address = paymentMethodDetails.GetPaymentDestination();
cryptoPayment.Rate = ExchangeRate(data);
model.CryptoPayments.Add(cryptoPayment);
}
@ -276,7 +274,7 @@ namespace BTCPayServer.Controllers
return new PaymentModel.AvailableCrypto()
{
PaymentMethodId = kv.GetId().ToString(),
CryptoCode = kv.GetId().CryptoCode,
CryptoCode = kv.Network?.CryptoCode ?? kv.GetId().CryptoCode,
PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId),
IsLightning =
kv.GetId().PaymentType == PaymentTypes.LightningLike,
@ -539,7 +537,6 @@ namespace BTCPayServer.Controllers
PosData = model.PosData,
OrderId = model.OrderId,
//RedirectURL = redirect + "redirect",
NotificationEmail = model.NotificationEmail,
NotificationURL = model.NotificationUrl,
ItemDesc = model.ItemDesc,
FullNotifications = true,

View File

@ -26,6 +26,15 @@ namespace BTCPayServer.Controllers
private InvoiceController _InvoiceController;
private StoreRepository _StoreRepository;
[HttpGet]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[Route("api/v1/invoices")]
public async Task<IActionResult> PayButtonHandle(PayButtonViewModel model)
{
return await PayButtonHandle(model, CancellationToken.None);
}
[HttpPost]
[Route("api/v1/invoices")]
[MediaTypeAcceptConstraintAttribute("text/html")]
@ -49,17 +58,27 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
return View();
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
DataWrapper<InvoiceResponse> invoice = null;
try
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
Price = model.Price,
Currency = model.Currency,
ItemDesc = model.CheckoutDesc,
OrderId = model.OrderId,
NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
}
catch (BitpayHttpException e)
{
ModelState.AddModelError("Store", e.Message);
return View();
}
if (string.IsNullOrEmpty(model.CheckoutQueryString))
{
return Redirect(invoice.Data.Url);

View File

@ -84,76 +84,6 @@ namespace BTCPayServer.Controllers
_sshState = sshState;
}
[Route("server/rates")]
public async Task<IActionResult> Rates()
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
var vm = new RatesViewModel()
{
CacheMinutes = rates.CacheInMinutes,
PrivateKey = rates.PrivateKey,
PublicKey = rates.PublicKey
};
await FetchRateLimits(vm);
return View(vm);
}
private static async Task FetchRateLimits(RatesViewModel vm)
{
var coinAverage = GetCoinaverageService(vm, false);
if (coinAverage != null)
{
try
{
vm.RateLimits = await coinAverage.GetRateLimitsAsync();
}
catch { }
}
}
[Route("server/rates")]
[HttpPost]
public async Task<IActionResult> Rates(RatesViewModel vm)
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
rates.PrivateKey = vm.PrivateKey;
rates.PublicKey = vm.PublicKey;
rates.CacheInMinutes = vm.CacheMinutes;
try
{
var service = GetCoinaverageService(vm, true);
if (service != null)
await service.TestAuthAsync();
}
catch
{
ModelState.AddModelError(nameof(vm.PrivateKey), "Invalid API key pair");
}
if (!ModelState.IsValid)
{
await FetchRateLimits(vm);
return View(vm);
}
await _SettingsRepository.UpdateSetting(rates);
TempData[WellKnownTempData.SuccessMessage] = "Rate settings successfully updated";
return RedirectToAction(nameof(Rates));
}
private static CoinAverageRateProvider GetCoinaverageService(RatesViewModel vm, bool withAuth)
{
var settings = new CoinAverageSettings()
{
KeyPair = (vm.PublicKey, vm.PrivateKey)
};
if (!withAuth || settings.GetCoinAverageSignature() != null)
{
return new CoinAverageRateProvider()
{ Authenticator = settings };
}
return null;
}
[Route("server/users")]
public IActionResult ListUsers(int skip = 0, int count = 50)
{
@ -598,10 +528,12 @@ namespace BTCPayServer.Controllers
return null;
}
[Route("server/services/{serviceName}/{cryptoCode}")]
[Route("server/services/{serviceName}/{cryptoCode?}")]
public async Task<IActionResult> Service(string serviceName, string cryptoCode, bool showQR = false, uint? nonce = null)
{
if (!_dashBoard.IsFullySynched(cryptoCode, out _))
if (!string.IsNullOrEmpty(cryptoCode) && !_dashBoard.IsFullySynched(cryptoCode, out _))
{
TempData[WellKnownTempData.ErrorMessage] = $"{cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
@ -612,6 +544,7 @@ namespace BTCPayServer.Controllers
try
{
if (service.Type == ExternalServiceTypes.P2P)
{
return View("P2PService", new LightningWalletServices()
@ -662,9 +595,19 @@ namespace BTCPayServer.Controllers
vm.WalletName = service.DisplayName;
vm.ServiceLink = $"{connectionString.Server}?access-key={connectionString.AccessKey}";
return View("LightningWalletServices", vm);
case ExternalServiceTypes.CLightningRest:
return LndServices(service, connectionString, nonce, "CLightningRestServices");
case ExternalServiceTypes.LNDGRPC:
case ExternalServiceTypes.LNDRest:
return LndServices(service, connectionString, nonce);
case ExternalServiceTypes.Configurator:
return View("ConfiguratorService",
new LightningWalletServices()
{
ShowQR = showQR,
WalletName = service.ServiceName,
ServiceLink = $"{connectionString.Server}?password={connectionString.AccessKey}"
});
default:
throw new NotSupportedException(service.Type.ToString());
}
@ -733,7 +676,7 @@ namespace BTCPayServer.Controllers
return View(nameof(LightningChargeServices), vm);
}
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce)
private IActionResult LndServices(ExternalService service, ExternalConnectionString connectionString, uint? nonce, string view = nameof(LndServices))
{
var model = new LndServicesViewModel();
if (service.Type == ExternalServiceTypes.LNDGRPC)
@ -743,7 +686,7 @@ namespace BTCPayServer.Controllers
model.ConnectionType = "GRPC";
model.GRPCSSLCipherSuites = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256";
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
{
model.Uri = connectionString.Server.AbsoluteUri;
model.ConnectionType = "REST";
@ -772,7 +715,7 @@ namespace BTCPayServer.Controllers
}
}
return View(nameof(LndServices), model);
return View(view, model);
}
private static uint GetConfigKey(string type, string serviceName, string cryptoCode, uint nonce)
@ -824,10 +767,10 @@ namespace BTCPayServer.Controllers
grpcConf.SSL = connectionString.Server.Scheme == "https";
confs.Configurations.Add(grpcConf);
}
else if (service.Type == ExternalServiceTypes.LNDRest)
else if (service.Type == ExternalServiceTypes.LNDRest || service.Type == ExternalServiceTypes.CLightningRest)
{
var restconf = new LNDRestConfiguration();
restconf.Type = "lnd-rest";
restconf.Type = service.Type == ExternalServiceTypes.LNDRest? "lnd-rest": "clightning-rest";
restconf.Uri = connectionString.Server.AbsoluteUri;
confs.Configurations.Add(restconf);
}
@ -848,7 +791,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Service), new { cryptoCode = cryptoCode, serviceName = serviceName, nonce = nonce });
}
[Route("server/services/dynamic-dns")]
public async Task<IActionResult> DynamicDnsServices()
{

View File

@ -9,6 +9,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Payments;
@ -267,6 +268,11 @@ namespace BTCPayServer.Controllers
}
await _Repo.UpdateStore(store);
_EventAggregator.Publish(new WalletChangedEvent()
{
WalletId = new WalletId(storeId, cryptoCode)
});
if (willBeExcluded != wasExcluded)
{
var label = willBeExcluded ? "disabled" : "enabled";

View File

@ -58,6 +58,7 @@ namespace BTCPayServer.Controllers
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
SettingsRepository settingsRepository,
IAuthorizationService authorizationService,
EventAggregator eventAggregator,
CssThemeManager cssThemeManager)
{
_RateFactory = rateFactory;
@ -74,6 +75,7 @@ namespace BTCPayServer.Controllers
_settingsRepository = settingsRepository;
_authorizationService = authorizationService;
_CssThemeManager = cssThemeManager;
_EventAggregator = eventAggregator;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_FeeRateProvider = feeRateProvider;
@ -100,6 +102,7 @@ namespace BTCPayServer.Controllers
private readonly SettingsRepository _settingsRepository;
private readonly IAuthorizationService _authorizationService;
private readonly CssThemeManager _CssThemeManager;
private readonly EventAggregator _EventAggregator;
[TempData]
public bool StoreNotConfigured
@ -496,13 +499,16 @@ namespace BTCPayServer.Controllers
case BitcoinPaymentType _:
var strategy = derivationByCryptoCode.TryGet(paymentMethodId.CryptoCode);
var network = _NetworkProvider.GetNetwork<BTCPayNetwork>(paymentMethodId.CryptoCode);
var value = strategy?.ToPrettyString() ?? string.Empty;
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = paymentMethodId.CryptoCode,
WalletSupported = network.WalletSupported,
Value = strategy?.ToPrettyString() ?? string.Empty,
Value = value,
WalletId = new WalletId(store.Id, paymentMethodId.CryptoCode),
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null
Enabled = !excludeFilters.Match(paymentMethodId) && strategy != null,
Collapsed = network is ElementsBTCPayNetwork elementsBTCPayNetwork && elementsBTCPayNetwork.NetworkCryptoCode != elementsBTCPayNetwork.CryptoCode && string.IsNullOrEmpty(value)
});
break;
case LightningPaymentType _:
@ -641,6 +647,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
@ -667,7 +674,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token";
else
TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
return RedirectToAction(nameof(ListTokens));
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId});
}
[HttpGet]
@ -774,13 +781,22 @@ namespace BTCPayServer.Controllers
[HttpPost]
[Route("{storeId}/tokens/apikey")]
public async Task<IActionResult> GenerateAPIKey(string storeId)
public async Task<IActionResult> GenerateAPIKey(string storeId, string command="")
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
if (command == "revoke")
{
await _TokenRepository.RevokeLegacyAPIKeys(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "API Key revoked";
}
else
{
await _TokenRepository.GenerateLegacyAPIKey(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "API Key re-generated";
}
return RedirectToAction(nameof(ListTokens), new
{
storeId

View File

@ -7,6 +7,7 @@ using BTCPayServer.ModelBinders;
using BTCPayServer.Models.WalletViewModels;
using Microsoft.AspNetCore.Mvc;
using NBitcoin;
using NBXplorer;
using NBXplorer.Models;
namespace BTCPayServer.Controllers
@ -19,7 +20,6 @@ namespace BTCPayServer.Controllers
{
var nbx = ExplorerClientProvider.GetExplorerClient(network);
CreatePSBTRequest psbtRequest = new CreatePSBTRequest();
foreach (var transactionOutput in sendModel.Outputs)
{
var psbtDestination = new CreatePSBTDestination();
@ -56,12 +56,15 @@ namespace BTCPayServer.Controllers
{
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
vm.CryptoCode = network.CryptoCode;
vm.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.Mnemonic));
if (await vm.GetPSBT(network.NBitcoinNetwork) is PSBT psbt)
{
vm.Decoded = psbt.ToString();
vm.PSBT = psbt.ToBase64();
}
return View(vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
return View(nameof(WalletPSBT), vm ?? new WalletPSBTViewModel() { CryptoCode = walletId.CryptoCode });
}
[HttpPost]
[Route("{walletId}/psbt")]
@ -93,7 +96,7 @@ namespace BTCPayServer.Controllers
case "vault":
return ViewVault(walletId, psbt);
case "ledger":
return ViewWalletSendLedger(psbt);
return ViewWalletSendLedger(walletId, psbt);
case "update":
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
psbt = await UpdatePSBT(derivationSchemeSettings, psbt, network);
@ -103,12 +106,25 @@ namespace BTCPayServer.Controllers
return View(vm);
}
TempData[WellKnownTempData.SuccessMessage] = "PSBT updated!";
return RedirectToWalletPSBT(walletId, psbt, vm.FileName);
return RedirectToWalletPSBT(psbt, vm.FileName);
case "seed":
return SignWithSeed(walletId, psbt.ToBase64());
case "nbx-seed":
if (await CanUseHotWallet())
{
var derivationScheme = GetDerivationSchemeSettings(walletId);
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation,
WellknownMetadataKeys.MasterHDKey);
return await SignWithSeed(walletId,
new SignWithSeedViewModel() {SeedOrKey = extKey, PSBT = psbt.ToBase64()});
}
return View(vm);
case "broadcast":
{
return await WalletPSBTReady(walletId, psbt.ToBase64());
return RedirectToWalletPSBTReady(psbt.ToBase64());
}
case "combine":
ModelState.Remove(nameof(vm.PSBT));
@ -145,7 +161,6 @@ namespace BTCPayServer.Controllers
var vm = new WalletPSBTReadyViewModel() { PSBT = psbt };
vm.SigningKey = signingKey;
vm.SigningKeyPath = signingKeyPath;
var derivationSchemeSettings = GetDerivationSchemeSettings(walletId);
if (derivationSchemeSettings == null)
return NotFound();
@ -207,7 +222,7 @@ namespace BTCPayServer.Controllers
vm.CanCalculateBalance = true;
vm.Positive = balanceChange >= Money.Zero;
}
vm.Inputs = new List<WalletPSBTReadyViewModel.InputViewModel>();
foreach (var input in psbtObject.Inputs)
{
var inputVm = new WalletPSBTReadyViewModel.InputViewModel();
@ -220,7 +235,7 @@ namespace BTCPayServer.Controllers
inputVm.Positive = balanceChange2 >= Money.Zero;
inputVm.Index = (int)input.Index;
}
vm.Destinations = new List<WalletPSBTReadyViewModel.DestinationViewModel>();
foreach (var output in psbtObject.Outputs)
{
var dest = new WalletPSBTReadyViewModel.DestinationViewModel();
@ -280,14 +295,14 @@ namespace BTCPayServer.Controllers
catch
{
vm.GlobalError = "Invalid PSBT";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
if (command == "broadcast")
{
if (!psbt.IsAllFinalized() && !psbt.TryFinalize(out var errors))
{
vm.SetErrors(errors);
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
var transaction = psbt.ExtractTransaction();
try
@ -296,24 +311,24 @@ namespace BTCPayServer.Controllers
if (!broadcastResult.Success)
{
vm.GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
}
catch (Exception ex)
{
vm.GlobalError = "Error while broadcasting: " + ex.Message;
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
return RedirectToWalletTransaction(walletId, transaction);
}
else if (command == "analyze-psbt")
{
return RedirectToWalletPSBT(walletId, psbt);
return RedirectToWalletPSBT(psbt);
}
else
{
vm.GlobalError = "Unknown command";
return View(vm);
return View(nameof(WalletPSBTReady),vm);
}
}
@ -342,7 +357,7 @@ namespace BTCPayServer.Controllers
}
sourcePSBT = sourcePSBT.Combine(psbt);
TempData[WellKnownTempData.SuccessMessage] = "PSBT Successfully combined!";
return RedirectToWalletPSBT(walletId, sourcePSBT);
return RedirectToWalletPSBT(sourcePSBT);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
@ -12,24 +11,21 @@ using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using LedgerWallet;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.Extensions.Options;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
namespace BTCPayServer.Controllers
{
@ -49,6 +45,9 @@ namespace BTCPayServer.Controllers
private readonly IAuthorizationService _authorizationService;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly EventAggregator _EventAggregator;
private readonly SettingsRepository _settingsRepository;
public RateFetcher RateFetcher { get; }
CurrencyNameTable _currencyTable;
@ -63,7 +62,10 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
BTCPayWalletProvider walletProvider,
WalletReceiveStateService walletReceiveStateService,
EventAggregator eventAggregator,
SettingsRepository settingsRepository)
{
_currencyTable = currencyTable;
Repository = repo;
@ -77,6 +79,9 @@ namespace BTCPayServer.Controllers
ExplorerClientProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
_WalletReceiveStateService = walletReceiveStateService;
_EventAggregator = eventAggregator;
_settingsRepository = settingsRepository;
}
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
@ -231,6 +236,7 @@ namespace BTCPayServer.Controllers
[HttpGet]
[Route("{walletId}")]
[Route("{walletId}/transactions")]
public async Task<IActionResult> WalletTransactions(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, string labelFilter = null)
@ -294,6 +300,79 @@ namespace BTCPayServer.Controllers
return $"{walletId}:{txId}";
}
[HttpGet]
[Route("{walletId}/receive")]
public IActionResult WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var address = _WalletReceiveStateService.Get(walletId)?.Address;
return View(new WalletReceiveViewModel()
{
CryptoCode = walletId.CryptoCode,
Address = address?.ToString(),
CryptoImage = GetImage(paymentMethod.PaymentId, network)
});
}
[HttpPost]
[Route("{walletId}/receive")]
public async Task<IActionResult> WalletReceive([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletReceiveViewModel viewModel, string command)
{
if (walletId?.StoreId == null)
return NotFound();
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);
if (paymentMethod == null)
return NotFound();
var network = this.NetworkProvider.GetNetwork<BTCPayNetwork>(walletId?.CryptoCode);
if (network == null)
return NotFound();
var wallet = _walletProvider.GetWallet(network);
switch (command)
{
case "unreserve-current-address":
KeyPathInformation cachedAddress = _WalletReceiveStateService.Get(walletId);
if (cachedAddress == null)
{
break;
}
var address = cachedAddress.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork);
ExplorerClientProvider.GetExplorerClient(network)
.CancelReservation(cachedAddress.DerivationStrategy, new[] {cachedAddress.KeyPath});
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
AllowDismiss = true,
Message = $"Address {address} was unreserved.",
Severity = StatusMessageModel.StatusSeverity.Success,
});
_WalletReceiveStateService.Remove(walletId);
break;
case "generate-new-address":
var reserve = (await wallet.ReserveAddressAsync(paymentMethod.AccountDerivation));
_WalletReceiveStateService.Set(walletId, reserve);
break;
}
return RedirectToAction(nameof(WalletReceive), new {walletId});
}
private async Task<bool> CanUseHotWallet()
{
var isAdmin = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
if (isAdmin)
return true;
var policies = await _settingsRepository.GetSettingAsync<PoliciesSettings>();
return policies?.AllowHotWalletForAll is true;
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
@ -331,6 +410,9 @@ namespace BTCPayServer.Controllers
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(paymentMethod.AccountDerivation);
model.NBXSeedAvailable = await CanUseHotWallet() && !string.IsNullOrEmpty(await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(GetDerivationSchemeSettings(walletId).AccountDerivation,
WellknownMetadataKeys.MasterHDKey));
model.CurrentBalance = await balance;
model.RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi;
model.FeeSatoshiPerByte = model.RecommendedSatoshiPerByte;
@ -356,12 +438,13 @@ namespace BTCPayServer.Controllers
}
return View(model);
}
[HttpPost]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default)
WalletId walletId, WalletSendModel vm, string command = "", CancellationToken cancellation = default, string bip21 = "")
{
if (walletId?.StoreId == null)
return NotFound();
@ -372,6 +455,13 @@ namespace BTCPayServer.Controllers
if (network == null || network.ReadonlyWallet)
return NotFound();
vm.SupportRBF = network.SupportRBF;
if (!string.IsNullOrEmpty(bip21))
{
LoadFromBIP21(vm, bip21, network);
return View(vm);
}
decimal transactionAmountSum = 0;
if (command == "add-output")
@ -387,7 +477,6 @@ namespace BTCPayServer.Controllers
vm.Outputs.RemoveAt(index);
return View(vm);
}
if (!vm.Outputs.Any())
{
@ -480,20 +569,70 @@ namespace BTCPayServer.Controllers
{
case "vault":
return ViewVault(walletId, psbt.PSBT);
case "nbx-seed":
var extKey = await ExplorerClientProvider.GetExplorerClient(network)
.GetMetadataAsync<string>(derivationScheme.AccountDerivation, WellknownMetadataKeys.MasterHDKey, cancellation);
return await SignWithSeed(walletId, new SignWithSeedViewModel()
{
SeedOrKey = extKey,
PSBT = psbt.PSBT.ToBase64()
});
case "ledger":
return ViewWalletSendLedger(psbt.PSBT, psbt.ChangeAddress);
return ViewWalletSendLedger(walletId, psbt.PSBT, psbt.ChangeAddress);
case "seed":
return SignWithSeed(walletId, psbt.PSBT.ToBase64());
case "analyze-psbt":
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $"{output.Amount}->{output.DestinationAddress}{(output.SubtractFeesFromOutput ? "-Fees" : string.Empty)}"))}.psbt";
return RedirectToWalletPSBT(walletId, psbt.PSBT, name);
return RedirectToWalletPSBT(psbt.PSBT, name);
default:
return View(vm);
}
}
private void LoadFromBIP21(WalletSendModel vm, string bip21, BTCPayNetwork network)
{
try
{
if (bip21.StartsWith(network.UriScheme, StringComparison.InvariantCultureIgnoreCase))
{
bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}";
}
var uriBuilder = new NBitcoin.Payment.BitcoinUrlBuilder(bip21, network.NBitcoinNetwork);
vm.Outputs = new List<WalletSendModel.TransactionOutput>()
{
new WalletSendModel.TransactionOutput()
{
Amount = uriBuilder.Amount.ToDecimal(MoneyUnit.BTC),
DestinationAddress = uriBuilder.Address.ToString(),
SubtractFeesFromOutput = false
}
};
if (!string.IsNullOrEmpty(uriBuilder.Label) || !string.IsNullOrEmpty(uriBuilder.Message))
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Info,
Html =
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to {uriBuilder.Label}")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for {uriBuilder.Message}")}"
});
}
}
catch (Exception)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Error,
Message = "The provided BIP21 payment URI was malformed"
});
}
ModelState.Clear();
}
private IActionResult ViewVault(WalletId walletId, PSBT psbt)
{
return View("WalletSendVault", new WalletSendVaultModel()
@ -504,7 +643,31 @@ namespace BTCPayServer.Controllers
});
}
private IActionResult RedirectToWalletPSBT(WalletId walletId, PSBT psbt, string fileName = null)
[HttpPost]
[Route("{walletId}/vault")]
public async Task<IActionResult> SubmitVault([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendVaultModel model)
{
return RedirectToWalletPSBTReady(model.PSBT);
}
private IActionResult RedirectToWalletPSBTReady(string psbt, string signingKey= null, string signingKeyPath = null)
{
var vm = new PostRedirectViewModel()
{
AspController = "Wallets",
AspAction = nameof(WalletPSBTReady),
Parameters =
{
new KeyValuePair<string, string>("psbt", psbt),
new KeyValuePair<string, string>("SigningKey", signingKey),
new KeyValuePair<string, string>("SigningKeyPath", signingKeyPath)
}
};
return View("PostRedirect", vm);
}
private IActionResult RedirectToWalletPSBT(PSBT psbt, string fileName = null)
{
var vm = new PostRedirectViewModel()
{
@ -542,17 +705,25 @@ namespace BTCPayServer.Controllers
return null;
}
private ViewResult ViewWalletSendLedger(PSBT psbt, BitcoinAddress hintChange = null)
private ViewResult ViewWalletSendLedger(WalletId walletId, PSBT psbt, BitcoinAddress hintChange = null)
{
SetAmbientPSBT(psbt);
return View("WalletSendLedger", new WalletSendLedgerModel()
{
PSBT = psbt.ToBase64(),
HintChange = hintChange?.ToString(),
WebsocketPath = this.Url.Action(nameof(LedgerConnection))
WebsocketPath = this.Url.Action(nameof(LedgerConnection), new { walletId = walletId.ToString() })
});
}
[HttpPost]
[Route("{walletId}/ledger")]
public async Task<IActionResult> SubmitLedger([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId, WalletSendLedgerModel model)
{
return RedirectToWalletPSBTReady(model.PSBT);
}
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed([ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId,string psbt)
@ -569,7 +740,7 @@ namespace BTCPayServer.Controllers
{
if (!ModelState.IsValid)
{
return View(viewModel);
return View("SignWithSeed", viewModel);
}
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
if (network == null)
@ -592,7 +763,7 @@ namespace BTCPayServer.Controllers
if (!ModelState.IsValid)
{
return View(viewModel);
return View("SignWithSeed", viewModel);
}
ExtKey signingKey = null;
@ -605,7 +776,7 @@ namespace BTCPayServer.Controllers
if (rootedKeyPath == null)
{
ModelState.AddModelError(nameof(viewModel.SeedOrKey), "The master fingerprint and/or account key path of your seed are not set in the wallet settings.");
return View(viewModel);
return View("SignWithSeed", viewModel);
}
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
if (rootedKeyPath.MasterFingerprint == extKey.GetPublicKey().GetHDFingerPrint())
@ -626,7 +797,7 @@ namespace BTCPayServer.Controllers
return View(viewModel);
}
ModelState.Remove(nameof(viewModel.PSBT));
return await WalletPSBTReady(walletId, psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
return RedirectToWalletPSBTReady(psbt.ToBase64(), signingKey.GetWif(network.NBitcoinNetwork).ToString(), rootedKeyPath?.ToString());
}
private bool PSBTChanged(PSBT psbt, Action act)
@ -957,6 +1128,23 @@ namespace BTCPayServer.Controllers
return NotFound();
}
}
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
{
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
? Url.Content(network.CryptoImagePath)
: Url.Content(network.LightningImagePath);
return "/" + res;
}
}
public class WalletReceiveViewModel
{
public string CryptoImage { get; set; }
public string CryptoCode { get; set; }
public string Address { get; set; }
}

View File

@ -48,9 +48,9 @@ namespace BTCPayServer
throw new FormatException();
}
if (type == DerivationType.Segwit)
return new DirectDerivationStrategy(extPubKey) { Segwit = true };
return new DirectDerivationStrategy(extPubKey, true);
if (type == DerivationType.Legacy)
return new DirectDerivationStrategy(extPubKey) { Segwit = false };
return new DirectDerivationStrategy(extPubKey, false);
if (type == DerivationType.SegwitP2SH)
return BtcPayNetwork.NBXplorerNetwork.DerivationStrategyFactory.Parse(extPubKey.ToString() + "-[p2sh]");
throw new FormatException();
@ -79,7 +79,10 @@ namespace BTCPayServer
}
if (!Network.Consensus.SupportSegwit)
{
hintedLabels.Add("legacy");
str = str.Replace("-[p2sh]", string.Empty, StringComparison.OrdinalIgnoreCase);
}
try
{

View File

@ -0,0 +1,10 @@
using NBXplorer.Models;
namespace BTCPayServer.Events
{
public class NewOnChainTransactionEvent
{
public NewTransactionEvent NewTransactionEvent { get; set; }
public string CryptoCode { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace BTCPayServer.Events
{
public class WalletChangedEvent
{
public WalletId WalletId { get; set; }
}
}

View File

@ -186,11 +186,19 @@ namespace BTCPayServer
public static bool IsSegwit(this DerivationStrategyBase derivationStrategyBase)
{
if (IsSegwitCore(derivationStrategyBase))
return true;
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner));
return ScriptPubKeyType(derivationStrategyBase) != NBitcoin.ScriptPubKeyType.Legacy;
}
public static ScriptPubKeyType ScriptPubKeyType(this DerivationStrategyBase derivationStrategyBase)
{
if (IsSegwitCore(derivationStrategyBase))
{
return NBitcoin.ScriptPubKeyType.Segwit;
}
return (derivationStrategyBase is P2SHDerivationStrategy p2shStrat && IsSegwitCore(p2shStrat.Inner))
? NBitcoin.ScriptPubKeyType.SegwitP2SH
: NBitcoin.ScriptPubKeyType.Legacy;
}
private static bool IsSegwitCore(DerivationStrategyBase derivationStrategyBase)
{
return (derivationStrategyBase is P2WSHDerivationStrategy) ||
@ -257,6 +265,12 @@ namespace BTCPayServer
return false;
return request.Host.Host.EndsWith(".onion", StringComparison.OrdinalIgnoreCase);
}
public static bool IsOnion(this Uri uri)
{
return uri?.DnsSafeHost?.EndsWith(".onion", StringComparison.OrdinalIgnoreCase) is true;
}
public static string GetAbsoluteRoot(this HttpRequest request)
{

View File

@ -128,6 +128,7 @@ namespace BTCPayServer.HostedServices
emailBody);
}
if (invoice.NotificationURL != null)
{
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Notification = notification });

View File

@ -54,7 +54,7 @@ namespace BTCPayServer.HostedServices
private async Task UpdateInvoice(UpdateInvoiceContext context)
{
var invoice = context.Invoice;
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime < DateTimeOffset.UtcNow)
if (invoice.Status == InvoiceStatus.New && invoice.ExpirationTime <= DateTimeOffset.UtcNow)
{
context.MarkDirty();
await _InvoiceRepository.UnaffectAddress(invoice.Id);
@ -180,7 +180,11 @@ namespace BTCPayServer.HostedServices
{
if (invoiceId == null)
throw new ArgumentNullException(nameof(invoiceId));
_WatchRequests.Writer.TryWrite(invoiceId);
if (!_WatchRequests.Writer.TryWrite(invoiceId))
{
Logs.PayServer.LogWarning($"Failed to write invoice {invoiceId} into WatchRequests channel");
}
}
private async Task Wait(string invoiceId)
@ -188,13 +192,16 @@ namespace BTCPayServer.HostedServices
var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
try
{
var delay = invoice.ExpirationTime - DateTimeOffset.UtcNow;
// add 1 second to ensure watch won't trigger moments before invoice expires
var delay = invoice.ExpirationTime.AddSeconds(1) - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, _Cts.Token);
}
Watch(invoiceId);
delay = invoice.MonitoringExpiration - DateTimeOffset.UtcNow;
// add 1 second to ensure watch won't trigger moments before monitoring expires
delay = invoice.MonitoringExpiration.AddSeconds(1) - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, _Cts.Token);

View File

@ -29,14 +29,11 @@ namespace BTCPayServer.HostedServices
}
}
private SettingsRepository _SettingsRepository;
private CoinAverageSettings _coinAverageSettings;
RateProviderFactory _RateProviderFactory;
public RatesHostedService(SettingsRepository repo,
RateProviderFactory rateProviderFactory,
CoinAverageSettings coinAverageSettings)
RateProviderFactory rateProviderFactory)
{
this._SettingsRepository = repo;
_coinAverageSettings = coinAverageSettings;
_RateProviderFactory = rateProviderFactory;
}
@ -44,7 +41,6 @@ namespace BTCPayServer.HostedServices
{
return new Task[]
{
CreateLoopTask(RefreshCoinAverageSettings),
CreateLoopTask(RefreshRates)
};
}
@ -54,13 +50,20 @@ namespace BTCPayServer.HostedServices
return fetcher.LastRequested is DateTimeOffset v &&
DateTimeOffset.UtcNow - v < TimeSpan.FromDays(1.0);
}
IEnumerable<(string ExchangeName, BackgroundFetcherRateProvider Fetcher)> GetStillUsedProviders()
{
foreach (var kv in _RateProviderFactory.Providers)
{
if (kv.Value is BackgroundFetcherRateProvider fetcher && IsStillUsed(fetcher))
{
yield return (kv.Key, fetcher);
}
}
}
async Task RefreshRates()
{
var usedProviders = _RateProviderFactory.Providers
.Select(p => p.Value)
.OfType<BackgroundFetcherRateProvider>()
.Where(IsStillUsed)
.ToArray();
var usedProviders = GetStillUsedProviders().ToArray();
if (usedProviders.Length == 0)
{
await Task.Delay(TimeSpan.FromSeconds(30), Cancellation);
@ -72,11 +75,11 @@ namespace BTCPayServer.HostedServices
try
{
await Task.WhenAll(usedProviders
.Select(p => p.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
.Select(p => p.Fetcher.UpdateIfNecessary(timeout.Token).ContinueWith(t =>
{
if (t.Result.Exception != null)
{
Logs.PayServer.LogWarning($"Error while contacting {p.ExchangeName}: {t.Result.Exception.Message}");
Logs.PayServer.LogWarning($"Error while contacting exchange {p.ExchangeName}: {t.Result.Exception.Message}");
}
}, TaskScheduler.Default))
.ToArray()).WithCancellation(timeout.Token);
@ -112,20 +115,27 @@ namespace BTCPayServer.HostedServices
private async Task TryLoadRateCache()
{
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
if (cache != null)
try
{
_LastCacheDate = cache.Created;
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
foreach (var obj in _RateProviderFactory.Providers
.Select(p => p.Value)
.OfType<BackgroundFetcherRateProvider>()
.Select(v => (Fetcher: v, State: stateByExchange.TryGet(v.ExchangeName)))
.Where(v => v.State != null))
var cache = await _SettingsRepository.GetSettingAsync<ExchangeRatesCache>();
if (cache != null)
{
obj.Fetcher.LoadState(obj.State);
_LastCacheDate = cache.Created;
var stateByExchange = cache.States.ToDictionary(o => o.ExchangeName);
foreach (var provider in _RateProviderFactory.Providers)
{
if (stateByExchange.TryGetValue(provider.Key, out var state) &&
provider.Value is BackgroundFetcherRateProvider fetcher)
{
fetcher.LoadState(state);
}
}
}
}
catch (Exception ex)
{
Logs.PayServer.LogWarning(ex, "Warning: Error while trying to load rates from cache");
}
}
DateTimeOffset? _LastCacheDate;
@ -134,28 +144,16 @@ namespace BTCPayServer.HostedServices
var cache = new ExchangeRatesCache();
cache.Created = DateTimeOffset.UtcNow;
_LastCacheDate = cache.Created;
cache.States = _RateProviderFactory.Providers
.Select(p => p.Value)
.OfType<BackgroundFetcherRateProvider>()
.Where(IsStillUsed)
.Select(p => p.GetState())
.ToList();
await _SettingsRepository.UpdateSetting(cache);
}
async Task RefreshCoinAverageSettings()
{
var rates = (await _SettingsRepository.GetSettingAsync<RatesSetting>()) ?? new RatesSetting();
_RateProviderFactory.CacheSpan = TimeSpan.FromMinutes(rates.CacheInMinutes);
if (!string.IsNullOrWhiteSpace(rates.PrivateKey) && !string.IsNullOrWhiteSpace(rates.PublicKey))
var usedProviders = GetStillUsedProviders().ToArray();
cache.States = new List<BackgroundFetcherState>(usedProviders.Length);
foreach (var provider in usedProviders)
{
_coinAverageSettings.KeyPair = (rates.PublicKey, rates.PrivateKey);
var state = provider.Fetcher.GetState();
state.ExchangeName = provider.ExchangeName;
cache.States.Add(state);
}
else
{
_coinAverageSettings.KeyPair = null;
}
await _SettingsRepository.WaitSettingsChanged<RatesSetting>(Cancellation);
await _SettingsRepository.UpdateSetting(cache);
}
}
}

View File

@ -0,0 +1,51 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Events;
using BTCPayServer.Services.Wallets;
using Microsoft.Extensions.Hosting;
using NBXplorer;
namespace BTCPayServer.HostedServices
{
public class WalletReceiveCacheUpdater : IHostedService
{
private readonly EventAggregator _EventAggregator;
private readonly WalletReceiveStateService _WalletReceiveStateService;
private readonly CompositeDisposable _Leases = new CompositeDisposable();
public WalletReceiveCacheUpdater(EventAggregator eventAggregator,
WalletReceiveStateService walletReceiveStateService)
{
_EventAggregator = eventAggregator;
_WalletReceiveStateService = walletReceiveStateService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_Leases.Add(_EventAggregator.Subscribe<WalletChangedEvent>(evt =>
_WalletReceiveStateService.Remove(evt.WalletId)));
_Leases.Add(_EventAggregator.Subscribe<NewOnChainTransactionEvent>(evt =>
{
var matching = _WalletReceiveStateService
.GetByDerivation(evt.CryptoCode, evt.NewTransactionEvent.DerivationStrategy).Where(pair =>
evt.NewTransactionEvent.Outputs.Any(output => output.ScriptPubKey == pair.Value.ScriptPubKey));
foreach (var keyValuePair in matching)
{
_WalletReceiveStateService.Remove(keyValuePair.Key);
}
}));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_Leases.Dispose();
return Task.CompletedTask;
}
}
}

View File

@ -97,7 +97,6 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<U2FService>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
var opts = o.GetRequiredService<BTCPayServerOptions>();
@ -178,6 +177,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<PaymentRequestRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<WalletReceiveStateService>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{
@ -217,6 +217,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, DynamicDnsHostedService>();
services.AddSingleton<IHostedService, TorServicesHostedService>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IHostedService, WalletReceiveCacheUpdater>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddScoped<IAuthorizationHandler, CookieAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OpenIdAuthorizationHandler>();
@ -259,10 +260,20 @@ namespace BTCPayServer.Hosting
{
options.AddPolicy(CorsPolicies.All, p => p.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
});
var rateLimits = new RateLimitService();
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
services.AddSingleton(rateLimits);
services.AddSingleton(provider =>
{
var btcPayEnv = provider.GetService<BTCPayServerEnvironment>();
var rateLimits = new RateLimitService();
if (btcPayEnv.IsDevelopping)
{
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=1000r/min burst=100 nodelay");
}
else
{
rateLimits.SetZone($"zone={ZoneLimits.Login} rate=5r/min burst=3 nodelay");
}
return rateLimits;
});
services.AddLogging(logBuilder =>
@ -275,7 +286,7 @@ namespace BTCPayServer.Hosting
.MinimumLevel.Is(BTCPayServerOptions.GetDebugLogLevel(configuration))
.WriteTo.File(debugLogFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: MAX_DEBUG_LOG_FILE_SIZE, rollOnFileSizeLimit: true, retainedFileCountLimit: 1)
.CreateLogger();
logBuilder.AddSerilog(Serilog.Log.Logger);
logBuilder.AddProvider(new Serilog.Extensions.Logging.SerilogLoggerProvider(Log.Logger));
}
});
return services;

View File

@ -62,10 +62,12 @@ namespace BTCPayServer.Hosting
catch (UnauthorizedAccessException ex)
{
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
return;
}
catch (BitpayHttpException ex)
{
await HandleBitpayHttpException(httpContext, ex);
return;
}
catch (Exception ex)
{
@ -133,13 +135,9 @@ namespace BTCPayServer.Hosting
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;
using (var writer = new StreamWriter(httpContext.Response.Body, new UTF8Encoding(false), 1024, true))
{
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
writer.Write(result);
await writer.FlushAsync();
}
httpContext.Response.ContentType = "application/json";
var result = JsonConvert.SerializeObject(new BitpayErrorsModel(ex));
await httpContext.Response.WriteAsync(result);
}
}
}

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Hosting;
using OpenIddict.Validation.AspNetCore;
using OpenIddict.Abstractions;
@ -49,6 +50,9 @@ namespace BTCPayServer.Hosting
Logs.Configure(LoggerFactory);
services.ConfigureBTCPayServer(Configuration);
services.AddMemoryCache();
services.AddDataProtection()
.SetApplicationName("BTCPay Server")
.PersistKeysToFileSystem(GetDataDir());
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
@ -140,6 +144,11 @@ namespace BTCPayServer.Hosting
}
}
private DirectoryInfo GetDataDir()
{
return new DirectoryInfo(Configuration.GetDataDir(DefaultConfiguration.GetNetworkType(Configuration)));
}
private void ConfigureOpenIddict(IServiceCollection services)
{
// Register the OpenIddict services.
@ -232,6 +241,10 @@ namespace BTCPayServer.Hosting
forwardingOptions.KnownProxies.Clear();
forwardingOptions.ForwardedHeaders = ForwardedHeaders.All;
app.UseForwardedHeaders(forwardingOptions);
app.UseStatusCodePagesWithReExecute("/Error/Handle", "?statusCode={0}");
app.UsePayServer();
app.UseRouting();
app.UseCors();
@ -243,7 +256,6 @@ namespace BTCPayServer.Hosting
app.UseSession();
app.UseWebSockets();
app.UseStatusCodePages();
app.UseEndpoints(endpoints =>
{

View File

@ -10,21 +10,22 @@ namespace BTCPayServer.Models.AppViewModels
public class UpdateCrowdfundViewModel
{
public string StoreId { get; set; }
[Required] [MaxLength(30)] public string Title { get; set; }
[Required]
[MaxLength(30)]
public string Title { get; set; }
[MaxLength(50)] public string Tagline { get; set; }
[MaxLength(50)]
public string Tagline { get; set; }
[Required]
public string Description { get; set; }
[Required] public string Description { get; set; }
[Display(Name = "Featured Image")]
public string MainImageUrl { get; set; }
[Display(Name = "Callback Notification Url")]
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice IPN Notification")]
[EmailAddress]
public string NotificationEmail { get; set; }
[Required]
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
@ -59,7 +60,8 @@ namespace BTCPayServer.Models.AppViewModels
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
[Display(Name = "Reset goal every")]
public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
public int ResetEveryAmount { get; set; } = 1;
@ -78,7 +80,7 @@ namespace BTCPayServer.Models.AppViewModels
public string EmbeddedCSS { get; set; }
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
public bool UseAllStoreInvoices { get; set; }
public bool UseAllStoreInvoices { get; set; }
public string AppId { get; set; }
public string SearchTerm { get; set; }
@ -90,10 +92,16 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Sounds to play when a payment is made. One sound per line")]
public string Sounds{ get; set; }
public string Sounds { get; set; }
[Display(Name = "Colors to rotate between with animation when a payment is made. First color is the default background. One color per line. Can be any valid css color value.")]
public string AnimationColors{ get; set; }
public string AnimationColors { get; set; }
// NOTE: Improve validation if needed
public bool ModelWithMinimumData
{
get { return Description != null && Title != null && TargetCurrency != null; }
}
public bool NotificationEmailWarning { get; set; }
}
}

View File

@ -32,9 +32,6 @@ namespace BTCPayServer.Models.AppViewModels
[Display(Name = "Callback Notification Url")]
[Uri]
public string NotificationUrl { get; set; }
[Display(Name = "Invoice IPN Notification")]
[EmailAddress]
public string NotificationEmail { get; set; }
[Required]
[MaxLength(30)]
@ -84,7 +81,6 @@ namespace BTCPayServer.Models.AppViewModels
}
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
public bool NotificationEmailWarning { get; set; }
public string EmbeddedCSS { get; set; }
public string Description { get; set; }
}

View File

@ -15,6 +15,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
Currency = "USD";
}
[Required]
public decimal? Amount
{
@ -28,11 +29,13 @@ namespace BTCPayServer.Models.InvoicingModels
}
[Required]
[DisplayName("Store Id")]
public string StoreId
{
get; set;
}
[DisplayName("Order Id")]
public string OrderId
{
get; set;
@ -51,18 +54,12 @@ namespace BTCPayServer.Models.InvoicingModels
}
[EmailAddress]
[DisplayName("Buyer Email")]
public string BuyerEmail
{
get; set;
}
[EmailAddress]
[DisplayName("Notification Email")]
public string NotificationEmail
{
get; set;
}
[Uri]
[DisplayName("Notification Url")]
public string NotificationUrl
@ -72,22 +69,19 @@ namespace BTCPayServer.Models.InvoicingModels
public SelectList Stores
{
get;
set;
get; set;
}
[DisplayName("Supported Transaction Currencies")]
public List<string> SupportedTransactionCurrencies
{
get;
set;
get; set;
}
[DisplayName("Available Payment Methods")]
public SelectList AvailablePaymentMethods
{
get;
set;
get; set;
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
using NBitcoin;
using Newtonsoft.Json;
@ -21,6 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels
public string TransactionLink { get; set; }
public bool Replaced { get; set; }
public BitcoinLikePaymentData CryptoPaymentData { get; set; }
}
public class OffChainPaymentViewModel
@ -36,7 +38,6 @@ namespace BTCPayServer.Models.InvoicingModels
public string PaymentMethod { get; set; }
public string Due { get; set; }
public string Paid { get; set; }
public string Address { get; internal set; }
public string Rate { get; internal set; }
public string PaymentUrl { get; internal set; }
public string Overpaid { get; set; }

View File

@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer.Models.ServerViewModels
{
public class RatesViewModel
{
[Display(Name = "Bitcoin average api keys")]
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
[Display(Name = "Cache the rates for ... minutes")]
[Range(0, 60)]
public int CacheMinutes { get; set; }
public GetRateLimitsResponse RateLimits { get; internal set; }
}
}

View File

@ -15,26 +15,8 @@ namespace BTCPayServer.Models
public StatusSeverity Severity { get; set; }
public bool AllowDismiss { get; set; } = true;
public string SeverityCSS
{
get
{
switch (Severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
case StatusSeverity.Warning:
return "warning";
default:
throw new ArgumentOutOfRangeException();
}
}
}
public string SeverityCSS => ToString(Severity);
private void ParseNonJsonStatus(string s)
{
Message = s;
@ -43,6 +25,23 @@ namespace BTCPayServer.Models
: StatusSeverity.Success;
}
public static string ToString(StatusSeverity severity)
{
switch (severity)
{
case StatusSeverity.Info:
return "info";
case StatusSeverity.Error:
return "danger";
case StatusSeverity.Success:
return "success";
case StatusSeverity.Warning:
return "warning";
default:
throw new ArgumentOutOfRangeException();
}
}
public enum StatusSeverity
{
Info,

View File

@ -19,7 +19,7 @@ namespace BTCPayServer.Models.StoreViewModels
public void SetExchangeRates(IEnumerable<AvailableRateProvider> supportedList, string preferredExchange)
{
var defaultStore = preferredExchange ?? CoinGeckoRateProvider.CoinGeckoName;
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, GetName(a), a.Url, a.Source)).ToArray();
supportedList = supportedList.Select(a => new AvailableRateProvider(a.Id, a.SourceId, GetName(a), a.Url, a.Source)).ToArray();
var chosen = supportedList.FirstOrDefault(f => f.Id == defaultStore) ?? supportedList.FirstOrDefault();
Exchanges = new SelectList(supportedList, nameof(chosen.Id), nameof(chosen.Name), chosen);
PreferredExchange = chosen.Id;
@ -33,9 +33,7 @@ namespace BTCPayServer.Models.StoreViewModels
case Rating.RateSource.Direct:
return a.Name;
case Rating.RateSource.Coingecko:
return $"{a.Name} (via CoinGecko, free)";
case Rating.RateSource.CoinAverage:
return $"{a.Name} (via BitcoinAverage, commercial)";
return $"{a.Name} (via CoinGecko)";
default:
throw new NotSupportedException(a.Source.ToString());
}

View File

@ -14,6 +14,7 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}
public class AdditionalPaymentMethod

View File

@ -13,6 +13,8 @@ namespace BTCPayServer.Models.WalletViewModels
public string CryptoCode { get; set; }
public string Decoded { get; set; }
string _FileName;
public bool NBXSeedAvailable { get; set; }
public string FileName
{
get

View File

@ -45,5 +45,7 @@ namespace BTCPayServer.Models.WalletViewModels
public bool SupportRBF { get; set; }
[Display(Name = "Disable RBF")]
public bool DisableRBF { get; set; }
public bool NBXSeedAvailable { get; set; }
}
}

View File

@ -10,6 +10,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using NBitcoin;
using NBXplorer.Models;
namespace BTCPayServer.Payments.Bitcoin
{
@ -35,7 +36,7 @@ namespace BTCPayServer.Payments.Bitcoin
{
public Task<FeeRate> GetFeeRate;
public Task<FeeRate> GetNetworkFeeRate;
public Task<BitcoinAddress> ReserveAddress;
public Task<KeyPathInformation> ReserveAddress;
}
public override void PreparePaymentModel(PaymentModel model, InvoiceResponse invoiceResponse,
@ -141,7 +142,7 @@ namespace BTCPayServer.Payments.Bitcoin
onchainMethod.NextNetworkFee = Money.Zero;
break;
}
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
onchainMethod.DepositAddress = (await prepare.ReserveAddress).Address.ToString();
return onchainMethod;
}
}

View File

@ -145,6 +145,11 @@ namespace BTCPayServer.Payments.Bitcoin
break;
case NBXplorer.Models.NewTransactionEvent evt:
wallet.InvalidateCache(evt.DerivationStrategy);
_Aggregator.Publish(new NewOnChainTransactionEvent()
{
CryptoCode = wallet.Network.CryptoCode,
NewTransactionEvent = evt
});
foreach (var output in network.GetValidOutputs(evt))
{
var key = output.Item1.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant();
@ -373,7 +378,7 @@ namespace BTCPayServer.Payments.Bitcoin
paymentMethod.Calculate().Due > Money.Zero)
{
var address = await wallet.ReserveAddressAsync(strategy);
btc.DepositAddress = address.ToString();
btc.DepositAddress = address.Address.ToString();
await _InvoiceRepository.NewAddress(invoice.Id, btc, wallet.Network);
_Aggregator.Publish(new InvoiceNewAddressEvent(invoice.Id, address.ToString(), wallet.Network));
paymentMethod.SetPaymentMethodDetails(btc);

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Payments.Lightning
var storeBlob = store.GetStoreBlob();
var test = GetNodeInfo(paymentMethod.PreferOnion, supportedPaymentMethod, (BTCPayNetwork)network);
var invoice = paymentMethod.ParentEntity;
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, network.Divisibility);
var client = _lightningClientFactory.Create(supportedPaymentMethod.GetLightningUrl(), (BTCPayNetwork)network);
var expiry = invoice.ExpirationTime - DateTimeOffset.UtcNow;
if (expiry < TimeSpan.Zero)
@ -180,11 +180,15 @@ namespace BTCPayServer.Payments.Lightning
model.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
if (storeBlob.LightningAmountInSatoshi && model.CryptoCode == "BTC" )
{
var satoshiCulture = new CultureInfo(CultureInfo.InvariantCulture.Name);
satoshiCulture.NumberFormat.NumberGroupSeparator = " ";
model.CryptoCode = "Sats";
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
model.BtcDue = Money.Parse(model.BtcDue).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.BtcPaid = Money.Parse(model.BtcPaid).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString("N0", satoshiCulture);
model.NetworkFee = new Money(model.NetworkFee, MoneyUnit.BTC).ToUnit(MoneyUnit.Satoshi);
model.OrderAmount = Money.Parse(model.OrderAmount).ToUnit(MoneyUnit.Satoshi).ToString(CultureInfo.InvariantCulture);
}
}
public override string GetCryptoImage(PaymentMethodId paymentMethodId)

View File

@ -31,9 +31,14 @@ namespace BTCPayServer.Payments
return ((BTCPayNetwork) network).ToString(paymentData);
}
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(str);
return JsonConvert.DeserializeObject<BitcoinLikeOnChainPaymentMethod>(str);
}
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
{
return JsonConvert.SerializeObject(details);
}
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)

View File

@ -29,11 +29,16 @@ namespace BTCPayServer.Payments
return ((BTCPayNetwork) network).ToString(paymentData);
}
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<Payments.Lightning.LightningLikePaymentMethodDetails>(str);
}
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
{
return JsonConvert.SerializeObject(details);
}
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
{
return JsonConvert.DeserializeObject<LightningSupportedPaymentMethod>(value.ToString());

View File

@ -62,9 +62,9 @@ namespace BTCPayServer.Payments
public abstract string GetId();
public abstract CryptoPaymentData DeserializePaymentData(BTCPayNetworkBase network, string str);
public abstract string SerializePaymentData(BTCPayNetworkBase network, CryptoPaymentData paymentData);
public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(string str);
public abstract IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str);
public abstract string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details);
public abstract ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value);
public abstract string GetTransactionLink(BTCPayNetworkBase network, string txId);
public abstract string InvoiceViewPaymentPartialName { get; }
}

View File

@ -8,7 +8,7 @@
"BTCPAY_LAUNCHSETTINGS": "true",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
@ -34,12 +34,13 @@
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_LBTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify",
"BTCPAY_BTCLIGHTNING": "type=clightning;server=tcp://127.0.0.1:30993",
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true",
"BTCPAY_BTCEXTERNALLNDSEEDBACKUP": "../BTCPayServer.Tests/TestData/LndSeedBackup/walletunlock.json",
"BTCPAY_BTCEXTERNALSPARK": "server=/spark/btc/;cookiefile=fake",
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/mycharge/btc/;cookiefilepath=fake",
"BTCPAY_EXTERNALCONFIGURATOR": "passwordfile=testpwd;server=/configurator",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_ALLOW-ADMIN-REGISTRATION": "true",
"BTCPAY_DISABLE-REGISTRATION": "false",

View File

@ -76,6 +76,21 @@ namespace BTCPayServer.Security.Bitpay
}
}
public async Task RevokeLegacyAPIKeys(string storeId)
{
var keys = await GetLegacyAPIKeys(storeId);
if (!keys.Any())
{
return;
}
using (var ctx = _Factory.CreateContext())
{
ctx.ApiKeys.RemoveRange(keys.Select(s => new APIKeyData() {Id = s}));
await ctx.SaveChangesAsync();
}
}
public async Task<string[]> GetLegacyAPIKeys(string storeId)
{
using (var ctx = _Factory.CreateContext())

View File

@ -24,11 +24,16 @@ namespace BTCPayServer.Services.Altcoins.Monero.Payments
return JsonConvert.SerializeObject(paymentData);
}
public override IPaymentMethodDetails DeserializePaymentMethodDetails(string str)
public override IPaymentMethodDetails DeserializePaymentMethodDetails(BTCPayNetworkBase network, string str)
{
return JsonConvert.DeserializeObject<MoneroLikeOnChainPaymentMethodDetails>(str);
}
public override string SerializePaymentMethodDetails(BTCPayNetworkBase network, IPaymentMethodDetails details)
{
return JsonConvert.SerializeObject(details);
}
public override ISupportedPaymentMethod DeserializeSupportedPaymentMethod(BTCPayNetworkBase network, JToken value)
{
return JsonConvert.DeserializeObject<MoneroSupportedPaymentMethod>(value.ToString());

View File

@ -82,8 +82,6 @@ namespace BTCPayServer.Services.Apps
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/unstoppable.wav",
"//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/whickedsick.wav"
};
public string NotificationEmail { get; set; }
}
public enum CrowdfundResetEvery
{

View File

@ -461,8 +461,6 @@ namespace BTCPayServer.Services.Invoices
}
else if (paymentId.PaymentType == PaymentTypes.BTCLike)
{
var scheme = ((BTCPayNetwork)info.Network).UriScheme;
var minerInfo = new MinerFeeInfo();
minerInfo.TotalFee = accounting.NetworkFee.Satoshi;
minerInfo.SatoshiPerBytes = ((BitcoinLikeOnChainPaymentMethod)info.GetPaymentMethodDetails()).FeeRate
@ -470,7 +468,7 @@ namespace BTCPayServer.Services.Invoices
dto.MinerFees.TryAdd(cryptoInfo.CryptoCode, minerInfo);
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
BIP21 = ((BTCPayNetwork)info.Network).GenerateBIP21(cryptoInfo.Address, cryptoInfo.Due),
};
#pragma warning disable 618
@ -794,7 +792,7 @@ namespace BTCPayServer.Services.Invoices
}
else
{
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(PaymentMethodDetails.ToString());
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString());
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
{
btcLike.NextNetworkFee = NextNetworkFee;
@ -823,8 +821,7 @@ namespace BTCPayServer.Services.Invoices
FeeRate = bitcoinPaymentMethod.FeeRate;
DepositAddress = bitcoinPaymentMethod.DepositAddress;
}
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
PaymentMethodDetails = jobj;
PaymentMethodDetails = JObject.Parse(paymentMethod.GetPaymentType().SerializePaymentMethodDetails(Network, paymentMethod));
#pragma warning restore CS0618 // Type or member is obsolete
return this;
@ -849,7 +846,7 @@ namespace BTCPayServer.Services.Invoices
var paid = 0m;
var cryptoPaid = 0.0m;
int precision = 8;
int precision = Network?.Divisibility ?? 8;
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
int txRequired = 0;
@ -859,8 +856,8 @@ namespace BTCPayServer.Services.Invoices
.OrderBy(p => p.ReceivedTime)
.Select(_ =>
{
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
paid += _.GetValue(paymentMethods, GetId());
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee, precision);
paid += _.GetValue(paymentMethods, GetId(), null, precision);
if (!paidEnough)
{
totalDue += txFee;
@ -993,18 +990,18 @@ namespace BTCPayServer.Services.Invoices
#pragma warning restore CS0618
return this;
}
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value = null)
internal decimal GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, decimal? value, int precision)
{
value = value ?? this.GetCryptoPaymentData().GetValue();
var to = paymentMethodId;
var from = this.GetPaymentMethodId();
if (to == from)
return decimal.Round(value.Value, 8);
return decimal.Round(value.Value, precision);
var fromRate = paymentMethods[from].Rate;
var toRate = paymentMethods[to].Rate;
var fiatValue = fromRate * decimal.Round(value.Value, 8);
var fiatValue = fromRate * decimal.Round(value.Value, precision);
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
return otherCurrencyValue;
}

View File

@ -96,8 +96,14 @@ namespace BTCPayServer.Services.Rates
foreach (var network in new BTCPayNetworkProvider(NetworkType.Mainnet).GetAll())
{
AddCurrency(_CurrencyProviders, network.CryptoCode, 8, network.CryptoCode);
AddCurrency(_CurrencyProviders, network.CryptoCode, network.Divisibility, network.CryptoCode);
}
_CurrencyProviders.TryAdd("SATS",
new NumberFormatInfo()
{
CurrencySymbol = "sats", CurrencyDecimalDigits = 0, CurrencyPositivePattern = 3
});
}
return _CurrencyProviders.TryGet(currency.ToUpperInvariant());
}
@ -180,7 +186,7 @@ namespace BTCPayServer.Services.Rates
if (!dico.TryAdd(network.CryptoCode, new CurrencyData()
{
Code = network.CryptoCode,
Divisibility = 8,
Divisibility = network.Divisibility,
Name = network.CryptoCode,
Crypto = true
}))
@ -189,6 +195,15 @@ namespace BTCPayServer.Services.Rates
}
}
dico.TryAdd("SATS", new CurrencyData()
{
Code = "SATS",
Crypto = true,
Divisibility = 0,
Name = "Satoshis",
Symbol = "Sats",
});
return dico.Values.ToArray();
}

View File

@ -19,9 +19,9 @@ namespace BTCPayServer.Services
_EventAggregator = eventAggregator;
}
public async Task<T> GetSettingAsync<T>()
public async Task<T> GetSettingAsync<T>(string name = null)
{
var name = typeof(T).FullName;
name ??= typeof(T).FullName;
using (var ctx = _ContextFactory.CreateContext())
{
var data = await ctx.Settings.Where(s => s.Id == name).FirstOrDefaultAsync();
@ -31,9 +31,9 @@ namespace BTCPayServer.Services
}
}
public async Task UpdateSetting<T>(T obj)
public async Task UpdateSetting<T>(T obj, string name = null)
{
var name = obj.GetType().FullName;
name ??= obj.GetType().FullName;
using (var ctx = _ContextFactory.CreateContext())
{
var settings = new SettingData();

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