Compare commits

..

50 Commits

Author SHA1 Message Date
1e582625f3 fix migration bug 2018-08-01 18:50:33 +09:00
39b018fdf3 fix test 2018-08-01 18:42:28 +09:00
83304de1c6 Remove the concept of "Rate multiplier" and replace it with the concept of "Spread" 2018-08-01 18:38:46 +09:00
5c8e03dcbf More user friendly Update Store screen 2018-08-01 15:59:29 +09:00
4dddc539f6 bump 2018-07-31 00:27:17 +09:00
9950b781b4 Slight UI adjustment to disable or enable method of payment 2018-07-31 00:26:49 +09:00
7a32f692d1 Add test for Disabling PaymentMethod 2018-07-31 00:18:58 +09:00
d480be925b Can disable method of payments 2018-07-30 23:54:31 +09:00
3775317047 Fix CanGetRates test, and fix #178 2018-07-30 23:22:26 +09:00
500bdd9bf1 Fix GetRates 2018-07-30 23:07:29 +09:00
57bda24664 Fix other DDOS related to GetRate 2018-07-30 22:51:39 +09:00
6401af00fe Fix potential DDOS on get rate 2018-07-30 22:45:28 +09:00
3b3a18bbbc Update README.md 2018-07-30 13:43:46 +09:00
b4e9dfeeaf Merge pull request #234 from DeltaEngine/master
Added Dash support
2018-07-28 23:21:20 +09:00
16f5def245 Reverted Dash_btc rule parsing check on request 2018-07-28 16:11:02 +02:00
26e7de534b Added Dash to readme 2018-07-27 21:07:43 +02:00
a3ae694048 Added Dash support for BTCPayServer 2018-07-27 21:06:19 +02:00
b04d70f141 bump 2018-07-27 18:18:27 +09:00
101d6131c7 Merge pull request #197 from Kukks/feature/bitpayrates
[WIP] Bitpay rates api
2018-07-27 18:18:05 +09:00
faabd68f6f Merge remote-tracking branch 'origin/master' into feature/bitpayrates 2018-07-27 11:16:52 +02:00
aa72b814da bump 2018-07-27 18:04:57 +09:00
0dcda0f289 Fix: Inverse rule was not found in BTCPay with X_X 2018-07-27 18:04:41 +09:00
a2b039f983 Report errors of LND 2018-07-27 15:49:57 +09:00
1a54f2d01a remove redundant cryptocode field in payment method interface 2018-07-27 08:41:36 +02:00
4276994265 fix bitpayconstraint for rates 2018-07-27 07:55:42 +02:00
2c6133b4f7 let X_X rates show in rates api as bitpay does 2018-07-27 07:55:18 +02:00
64181d1a93 use default crypto for /rates route 2018-07-27 07:54:55 +02:00
25e9a27a78 add in bitpay api constraints to actions 2018-07-27 06:38:54 +02:00
f3edaf5160 Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-07-27 05:57:25 +02:00
7bfdf2d11d Order transactions in transaction list view 2018-07-27 12:03:56 +09:00
d2808cf662 bump 2018-07-27 01:35:07 +09:00
b68fcec692 Fix rate in the WalletSend 2018-07-27 01:17:43 +09:00
86644d38d7 Show rate error to the model in WalletSend 2018-07-27 00:32:09 +09:00
52f60b0457 Can show the transaction list in wallet menu 2018-07-27 00:08:07 +09:00
638b58ab48 remove debug u2f 2018-07-26 23:26:06 +09:00
1606f43609 Show the fiat price when sending coins 2018-07-26 23:23:28 +09:00
ad1307746c Add a "Wallet" menu 2018-07-26 22:32:50 +09:00
22307b8a2b Merge pull request #233 from danielalexiuc/master
Fix small typo on server settings page
2018-07-25 10:59:43 +09:00
f1c244bf6a Fix small typo 2018-07-25 11:44:26 +10:00
8cafa8a483 Merge remote-tracking branch 'origin/master' into feature/bitpayrates 2018-06-12 15:34:09 +02:00
724af44e41 Merge branch 'master' into feature/bitpayrates 2018-06-04 15:09:14 +02:00
bac9ef4f93 add some UT and fix error message + bump Nbitpayclient 2018-05-29 17:12:07 +02:00
ada6f3b844 Finish rate api 2018-05-28 15:30:43 +02:00
c8a26ce952 api fixes 2018-05-28 14:55:49 +02:00
6cf80b7533 small rename 2018-05-28 14:29:23 +02:00
79df523bb2 reorder methods 2018-05-28 10:20:18 +02:00
e921f9757a Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-28 09:05:11 +02:00
bed9737d64 Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-26 14:42:17 +02:00
2583eb15ec Merge remote-tracking branch 'btcpayserver/master' into feature/bitpayrates 2018-05-21 16:49:43 +02:00
1879ea55e8 init bitpay rates api 2018-05-21 16:49:37 +02:00
56 changed files with 1466 additions and 522 deletions

View File

@ -138,6 +138,12 @@ namespace BTCPayServer.Tests
BidAsk = new BidAsk(4500m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_BTC"),
BidAsk = new BidAsk(0.001m)
});
coinAverageMock.ExchangeRates.Add(new Rating.ExchangeRate()
{
Exchange = "coinaverage",
CurrencyPair = CurrencyPair.Parse("LTC_USD"),

View File

@ -46,8 +46,8 @@ namespace BTCPayServer.Tests
{
Assert.Equal(test.Expected, rules.GetRuleFor(CurrencyPair.Parse(test.Pair)).ToString());
}
rules.GlobalMultiplier = 2.32m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * 2.32", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
rules.Spread = 0.2m;
Assert.Equal("(bittrex(DOGE_BTC) * gdax(BTC_USD) * 1.1) * (0.8, 1.2)", rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD")).ToString());
////////////////
// Check errors conditions
@ -104,7 +104,7 @@ namespace BTCPayServer.Tests
rule2.Reevaluate();
Assert.False(rule2.HasError);
Assert.Equal("5000 * 2000.4 * 1.1", rule2.ToString(true));
Assert.Equal(rule2.Value, 5000m * 2000.4m * 1.1m);
Assert.Equal(rule2.BidAsk.Bid, 5000m * 2000.4m * 1.1m);
////////
// Make sure parenthesis are correctly calculated
@ -113,22 +113,22 @@ namespace BTCPayServer.Tests
builder.AppendLine("BTC_USD = -3 + coinbase(BTC_CAD) + 50 - 5");
builder.AppendLine("DOGE_BTC = 2000");
Assert.True(RateRules.TryParse(builder.ToString(), out rules));
rules.GlobalMultiplier = 1.1m;
rules.Spread = 0.1m;
rule2 = rules.GetRuleFor(CurrencyPair.Parse("DOGE_USD"));
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * 1.1", rule2.ToString());
Assert.Equal("(2000 * (-3 + coinbase(BTC_CAD) + 50 - 5)) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * 1.1", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 1.1m, rule2.Value.Value);
Assert.Equal("(2000 * (-3 + 1000 + 50 - 5)) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((2000m * (-3m + 1000m + 50m - 5m)) * 0.9m, rule2.BidAsk.Bid);
// Test inverse
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_DOGE"));
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * 1.1", rule2.ToString());
Assert.Equal("(1 / (2000 * (-3 + coinbase(BTC_CAD) + 50 - 5))) * (0.9, 1.1)", rule2.ToString());
rule2.ExchangeRates.SetRate("coinbase", CurrencyPair.Parse("BTC_CAD"), new BidAsk(1000m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * 1.1", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 1.1m, rule2.Value.Value);
Assert.Equal("(1 / (2000 * (-3 + 1000 + 50 - 5))) * (0.9, 1.1)", rule2.ToString(true));
Assert.Equal((1.0m / (2000m * (-3m + 1000m + 50m - 5m))) * 0.9m, rule2.BidAsk.Bid);
////////
// Make sure kraken is not converted to CurrencyPair
@ -147,12 +147,12 @@ namespace BTCPayServer.Tests
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("(6000, 6100)", rule2.ToString(true));
Assert.Equal(6000m, rule2.Value.Value);
Assert.Equal(6000m, rule2.BidAsk.Bid);
rule2 = rules.GetRuleFor(CurrencyPair.Parse("USD_BTC"));
rule2.ExchangeRates.SetRate("kraken", CurrencyPair.Parse("BTC_USD"), new BidAsk(6000m, 6100m));
Assert.True(rule2.Reevaluate());
Assert.Equal("1 / (6000, 6100)", rule2.ToString(true));
Assert.Equal(1m / 6100m, rule2.Value.Value);
Assert.Equal(1m / 6100m, rule2.BidAsk.Bid);
// Make sure the inverse has more priority than X_X or CDNT_X
builder = new StringBuilder();

View File

@ -35,6 +35,7 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Stores;
using System.Net.Http;
using System.Text;
using BTCPayServer.Models;
using BTCPayServer.Rating;
using BTCPayServer.Validation;
using ExchangeSharp;
@ -521,14 +522,14 @@ namespace BTCPayServer.Tests
// We get this format from "openssl x509 -noout -fingerprint -sha256 -inform pem -in <certificate>"
var certthumbprint2 = "C5:1B:B1:D4:02:30:6D:0D:A0:0E:85:58:1B:32:AA:56:16:6B:CB:AB:7E:B8:88:FF:92:5D:71:67:EB:43:6D:06";
var lndUri = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint}";
var lndUri2 = $"type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;macaroon={macaroon};certthumbprint={certthumbprint2}";
var certificateHash = new X509Certificate2(Encoders.Hex.DecodeData("2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494942396a4343415a7967417749424167495156397a62474252724e54716b4e4b55676d72524d377a414b42676771686b6a4f50515144416a41784d5238770a485159445651514b45785a73626d5167595856306232646c626d56795958526c5a43426a5a584a304d51347744415944565151444577564754304e56557a41650a467730784f4441304d6a55794d7a517a4d6a4261467730784f5441324d6a41794d7a517a4d6a42614d444578487a416442674e5642416f54466d78755a4342680a645852765a3256755a584a686447566b49474e6c636e5178446a414d42674e5642414d5442555a50513156544d466b77457759484b6f5a497a6a3043415159490a4b6f5a497a6a304441516344516741454b7557424568564f75707965434157476130766e713262712f59396b41755a78616865646d454553482b753936436d450a397577486b4b2b4a7667547a66385141783550513741357254637155374b57595170303175364f426c5443426b6a414f42674e56485138424166384542414d430a4171517744775944565230544151482f42415577417745422f7a427642674e56485245456144426d6767564754304e565534494a6247396a5957786f62334e300a6877522f4141414268784141414141414141414141414141414141414141414268775373474f69786877514b41457342687753702f717473687754417141724c0a687753702f6d4a72687753702f754f77687753702f714e59687753702f6874436877514b70514157687753702f6c42514d416f4743437147534d343942414d430a413067414d45554349464866716d595a5043647a4a5178386b47586859473834394c31766541364c784d6f7a4f5774356d726835416945413662756e51556c710a6558553070474168776c3041654d726a4d4974394c7652736179756162565a593278343d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"))
.GetCertHash(System.Security.Cryptography.HashAlgorithmName.SHA256);
Assert.True(LightningConnectionString.TryParse(lndUri, false, out conn));
Assert.True(LightningConnectionString.TryParse(lndUri2, false, out var conn2));
Assert.Equal(conn2.ToString(), conn.ToString());
@ -574,7 +575,7 @@ namespace BTCPayServer.Tests
user.GrantAccess();
user.RegisterLightningNode("BTC", type);
user.RegisterDerivationScheme("BTC");
tester.PrepareLightning(type);
Task.WaitAll(CanSendLightningPaymentCore(tester, user));
@ -731,6 +732,40 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanGetRates()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var acc = tester.NewAccount();
acc.GrantAccess();
acc.RegisterDerivationScheme("BTC");
acc.RegisterDerivationScheme("LTC");
var rateController = acc.GetController<RateController>();
var GetBaseCurrencyRatesResult = JObject.Parse(((JsonResult)rateController.GetBaseCurrencyRates("BTC", acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetBaseCurrencyRatesResult);
Assert.NotNull(GetBaseCurrencyRatesResult.Data);
Assert.Equal(2, GetBaseCurrencyRatesResult.Data.Length);
Assert.Single(GetBaseCurrencyRatesResult.Data.Where(o => o.Code == "LTC"));
var GetRatesResult = JObject.Parse(((JsonResult)rateController.GetRates(null, acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate[]>>();
Assert.NotNull(GetRatesResult);
Assert.NotNull(GetRatesResult.Data);
Assert.Equal(2, GetRatesResult.Data.Length);
var GetCurrencyPairRateResult = JObject.Parse(((JsonResult)rateController.GetCurrencyPairRate("BTC", "LTC", acc.StoreId)
.GetAwaiter().GetResult()).Value.ToJson()).ToObject<DataWrapper<Rate>>();
Assert.NotNull(GetCurrencyPairRateResult);
Assert.NotNull(GetCurrencyPairRateResult.Data);
Assert.Equal("LTC", GetCurrencyPairRateResult.Data.Code);
}
}
private void AssertSearchInvoice(TestAccount acc, bool expected, string invoiceId, string filter)
{
var result = (Models.InvoicingModels.InvoicesModel)((ViewResult)acc.GetController<InvoiceController>().ListInvoices(filter).Result).Model;
@ -947,8 +982,8 @@ namespace BTCPayServer.Tests
var storeController = user.GetController<StoresController>();
var vm = (RatesViewModel)((ViewResult)storeController.Rates()).Model;
Assert.Equal(1.0, vm.RateMultiplier);
vm.RateMultiplier = 0.5;
Assert.Equal(0.0, vm.Spread);
vm.Spread = 40;
storeController.Rates(vm).Wait();
@ -962,7 +997,11 @@ namespace BTCPayServer.Tests
FullNotifications = true
}, Facade.Merchant);
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
// The rate was 5000 USD per BTC
// Now it should be 3000 USD per BTC
// So the expected price should be
var expected = Money.Coins(5000m / 3000m);
Assert.True(invoice2.BtcPrice.Almost(expected, 0.00001m));
}
}
@ -1044,7 +1083,7 @@ namespace BTCPayServer.Tests
var rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.False(rateVm.ShowScripting);
Assert.Equal("coinaverage", rateVm.PreferredExchange);
Assert.Equal(1.0, rateVm.RateMultiplier);
Assert.Equal(0.0, rateVm.Spread);
Assert.Null(rateVm.TestRateRules);
rateVm.PreferredExchange = "bitflyer";
@ -1053,13 +1092,13 @@ namespace BTCPayServer.Tests
Assert.Equal("bitflyer", rateVm.PreferredExchange);
rateVm.ScriptTest = "BTC_JPY,BTC_CAD";
rateVm.RateMultiplier = 1.1;
rateVm.Spread = 10;
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.NotNull(rateVm.TestRateRules);
Assert.Equal(2, rateVm.TestRateRules.Count);
Assert.False(rateVm.TestRateRules[0].Error);
Assert.StartsWith("(bitflyer(BTC_JPY)) * 1.10 =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.StartsWith("(bitflyer(BTC_JPY)) * (0.9, 1.1) =", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.True(rateVm.TestRateRules[1].Error);
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
@ -1072,19 +1111,19 @@ namespace BTCPayServer.Tests
rateVm.ScriptTest = "BTC_JPY";
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.ShowScripting);
Assert.Contains("(bitflyer(BTC_JPY)) * 1.10 = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
Assert.Contains("(bitflyer(BTC_JPY)) * (0.9, 1.1) = ", rateVm.TestRateRules[0].Rule, StringComparison.OrdinalIgnoreCase);
rateVm.ScriptTest = "BTC_USD,BTC_CAD,DOGE_USD,DOGE_CAD";
rateVm.Script = "DOGE_X = bittrex(DOGE_BTC) * BTC_X;\n" +
"X_CAD = quadrigacx(X_CAD);\n" +
"X_X = gdax(X_X);";
rateVm.RateMultiplier = 0.5;
rateVm.Spread = 50;
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates(rateVm, "Test").Result).Model);
Assert.True(rateVm.TestRateRules.All(t => !t.Error));
Assert.IsType<RedirectToActionResult>(store.Rates(rateVm, "Save").Result);
store = user.GetController<StoresController>();
rateVm = Assert.IsType<RatesViewModel>(Assert.IsType<ViewResult>(store.Rates()).Model);
Assert.Equal(0.5, rateVm.RateMultiplier);
Assert.Equal(50, rateVm.Spread);
Assert.True(rateVm.ShowScripting);
Assert.Contains("DOGE_X", rateVm.Script, StringComparison.OrdinalIgnoreCase);
}
@ -1261,6 +1300,64 @@ namespace BTCPayServer.Tests
Assert.Equal($"{tpub}-[p2sh]", result.ToString());
}
[Fact]
public void CanDisablePaymentMethods()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.RegisterDerivationScheme("LTC");
user.RegisterLightningNode("BTC", LightningConnectionType.CLightning);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(3, invoice.CryptoInfo.Length);
var controller = user.GetController<StoresController>();
var lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.True(lightningVM.Enabled);
lightningVM.Enabled = false;
controller.AddLightningNode(user.StoreId, lightningVM, "save", "BTC").GetAwaiter().GetResult();
lightningVM = (LightningNodeViewModel)Assert.IsType<ViewResult>(controller.AddLightningNode(user.StoreId, "BTC")).Model;
Assert.False(lightningVM.Enabled);
var derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.True(derivationVM.Enabled);
derivationVM.Enabled = false;
Assert.IsType<RedirectToActionResult>(controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult());
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
// Confirmation
controller.AddDerivationScheme(user.StoreId, derivationVM, "BTC").GetAwaiter().GetResult();
Assert.False(derivationVM.Enabled);
derivationVM = (DerivationSchemeViewModel)Assert.IsType<ViewResult>(controller.AddDerivationScheme(user.StoreId, "BTC")).Model;
Assert.False(derivationVM.Enabled);
invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.5m,
Currency = "USD",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Single(invoice.CryptoInfo);
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
}
}
[Fact]
public void CanSetPaymentMethodLimits()
{
@ -1592,7 +1689,7 @@ namespace BTCPayServer.Tests
foreach (var value in result)
{
var rateResult = value.Value.GetAwaiter().GetResult();
Assert.NotNull(rateResult.Value);
Assert.NotNull(rateResult.BidAsk);
}
}
@ -1629,7 +1726,7 @@ namespace BTCPayServer.Tests
// Should cache at exchange level so this should hit the cache
var fetchedRate2 = factory.FetchRate(CurrencyPair.Parse("LTC_USD"), rateRules).GetAwaiter().GetResult();
Assert.True(fetchedRate.Cached);
Assert.NotEqual(fetchedRate.Value.Value, fetchedRate2.Value.Value);
Assert.NotEqual(fetchedRate.BidAsk.Bid, fetchedRate2.BidAsk.Bid);
// 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);

View File

@ -0,0 +1,35 @@
using NBitcoin;
namespace BTCPayServer
{
public partial class BTCPayNetworkProvider
{
public void InitDash()
{
//not needed: NBitcoin.Altcoins.Dash.Instance.EnsureRegistered();
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("DASH");
Add(new BTCPayNetwork()
{
CryptoCode = nbxplorerNetwork.CryptoCode,
DisplayName = "Dash",
BlockExplorerLink = NetworkType == NetworkType.Mainnet
? "https://insight.dash.org/insight/tx/{0}"
: "https://testnet-insight.dashevo.org/insight/tx/{0}",
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
NBXplorerNetwork = nbxplorerNetwork,
UriScheme = "dash",
DefaultRateRules = new[]
{
"DASH_X = DASH_BTC * BTC_X",
"DASH_BTC = bittrex(DASH_BTC)"
},
CryptoImagePath = "imlegacy/dash.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NetworkType),
//https://github.com/satoshilabs/slips/blob/master/slip-0044.md
CoinType = NetworkType == NetworkType.Mainnet ? new KeyPath("5'")
: new KeyPath("1'"),
MinFee = Money.Satoshis(1m)
});
}
}
}

View File

@ -50,6 +50,7 @@ namespace BTCPayServer
InitDogecoin();
InitBitcoinGold();
InitMonacoin();
InitDash();
InitPolis();
InitFeathercoin();
InitGroestlcoin();

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.2.72</Version>
<Version>1.0.2.84</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
@ -125,6 +125,21 @@
<Content Update="Views\Server\Services.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\ListWallets.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\WalletTransactions.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_Nav.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewImports.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Wallets\_ViewStart.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
</ItemGroup>
<ItemGroup>

View File

@ -213,7 +213,7 @@ namespace BTCPayServer.Controllers
bool isDefaultCrypto = false;
if (paymentMethodIdStr == null)
{
paymentMethodIdStr = store.GetDefaultCrypto();
paymentMethodIdStr = store.GetDefaultCrypto(_NetworkProvider);
isDefaultCrypto = true;
}

View File

@ -123,9 +123,10 @@ namespace BTCPayServer.Controllers
HashSet<CurrencyPair> currencyPairsToFetch = new HashSet<CurrencyPair>();
var rules = storeBlob.GetRateRules(_NetworkProvider);
var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any()
foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider)
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c => _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode))
.Where(c => c != null))
{
currencyPairsToFetch.Add(new CurrencyPair(network.CryptoCode, invoice.Currency));
@ -140,6 +141,7 @@ namespace BTCPayServer.Controllers
var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair);
var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(s => !excludeFilter.Match(s.PaymentId))
.Select(c =>
(Handler: (IPaymentMethodHandler)_ServiceProvider.GetService(typeof(IPaymentMethodHandler<>).MakeGenericType(c.GetType())),
SupportedPaymentMethod: c,
@ -209,7 +211,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = store.GetStoreBlob();
var rate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, entity.ProductInformation.Currency)];
if (rate.Value == null)
if (rate.BidAsk == null)
{
return null;
}
@ -217,7 +219,7 @@ namespace BTCPayServer.Controllers
paymentMethod.ParentEntity = entity;
paymentMethod.Network = network;
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate.Value.Value;
paymentMethod.Rate = rate.BidAsk.Bid;
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network);
if (storeBlob.NetworkFeeDisabled)
paymentDetails.SetNoTxFee();
@ -244,9 +246,9 @@ namespace BTCPayServer.Controllers
if (compare != null)
{
var limitValueRate = await fetchingByCurrencyPair[new CurrencyPair(network.CryptoCode, limitValue.Currency)];
if (limitValueRate.Value.HasValue)
if (limitValueRate.BidAsk != null)
{
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.Value.Value);
var limitValueCrypto = Money.Coins(limitValue.Value / limitValueRate.BidAsk.Bid);
if (compare(paymentMethod.Calculate().Due, limitValueCrypto))
{
logs.Write($"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}");

View File

@ -31,6 +31,49 @@ namespace BTCPayServer.Controllers
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
}
[Route("rates/{baseCurrency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetBaseCurrencyRates(string baseCurrency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var store = this.HttpContext.GetStoreData();
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
var err = Json(new BitpayErrorsModel() { Error = "Store not found" });
err.StatusCode = 404;
return err;
}
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Where(method => !string.IsNullOrEmpty(method.PaymentId.CryptoCode))
.Select(method => method.PaymentId.CryptoCode).Distinct();
var currencypairs = BuildCurrencyPairs(currencyCodes, baseCurrency);
var result = await GetRates2(currencypairs, store.Id);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate[]>(rates));
}
[Route("rates/{baseCurrency}/{currency}")]
[HttpGet]
[BitpayAPIConstraint]
public async Task<IActionResult> GetCurrencyPairRate(string baseCurrency, string currency, string storeId)
{
storeId = storeId ?? this.HttpContext.GetStoreData()?.Id;
var result = await GetRates2($"{baseCurrency}_{currency}", storeId);
var rates = (result as JsonResult)?.Value as Rate[];
if (rates == null)
return result;
return Json(new DataWrapper<Rate>(rates.First()));
}
[Route("rates")]
[HttpGet]
[BitpayAPIConstraint]
@ -44,19 +87,19 @@ namespace BTCPayServer.Controllers
return Json(new DataWrapper<Rate[]>(rates));
}
[Route("api/rates")]
[HttpGet]
public async Task<IActionResult> GetRates2(string currencyPairs, string storeId)
{
if(storeId == null || currencyPairs == null)
if (storeId == null)
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings) and currencyPairs (eg. BTC_USD,LTC_CAD)" });
var result = Json(new BitpayErrorsModel() { Error = "You need to specify storeId (in your store settings)" });
result.StatusCode = 400;
return result;
}
var store = this.HttpContext.GetStoreData();
if(store == null || store.Id != storeId)
if (store == null || store.Id != storeId)
store = await _StoreRepo.FindStore(storeId);
if (store == null)
{
@ -64,12 +107,30 @@ namespace BTCPayServer.Controllers
result.StatusCode = 404;
return result;
}
if (currencyPairs == null)
{
var supportedMethods = store.GetSupportedPaymentMethods(_NetworkProvider);
var currencyCodes = supportedMethods.Select(method => method.PaymentId.CryptoCode).Distinct();
var defaultCrypto = store.GetDefaultCrypto(_NetworkProvider);
currencyPairs = BuildCurrencyPairs(currencyCodes, defaultCrypto);
if (string.IsNullOrEmpty(currencyPairs))
{
var result = Json(new BitpayErrorsModel() { Error = "You need to specify currencyPairs (eg. BTC_USD,LTC_CAD)" });
result.StatusCode = 400;
return result;
}
}
var rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
HashSet<CurrencyPair> pairs = new HashSet<CurrencyPair>();
foreach(var currency in currencyPairs.Split(','))
foreach (var currency in currencyPairs.Split(','))
{
if(!CurrencyPair.TryParse(currency, out var pair))
if (!CurrencyPair.TryParse(currency, out var pair))
{
var result = Json(new BitpayErrorsModel() { Error = $"Currency pair {currency} uncorrectly formatted" });
result.StatusCode = 400;
@ -81,7 +142,7 @@ namespace BTCPayServer.Controllers
var fetching = _RateProviderFactory.FetchRates(pairs, rules);
await Task.WhenAll(fetching.Select(f => f.Value).ToArray());
return Json(pairs
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().Value))
.Select(r => (Pair: r, Value: fetching[r].GetAwaiter().GetResult().BidAsk?.Bid))
.Where(r => r.Value.HasValue)
.Select(r =>
new Rate()
@ -94,6 +155,20 @@ namespace BTCPayServer.Controllers
}).Where(n => n.Name != null).ToArray());
}
private static string BuildCurrencyPairs(IEnumerable<string> currencyCodes, string baseCrypto)
{
StringBuilder currencyPairsBuilder = new StringBuilder();
bool first = true;
foreach (var currencyCode in currencyCodes)
{
if(!first)
currencyPairsBuilder.Append(",");
first = false;
currencyPairsBuilder.Append($"{baseCrypto}_{currencyCode}");
}
return currencyPairsBuilder.ToString();
}
public class Rate
{

View File

@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers
}
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
vm.ServerUrl = GetStoreUrl(storeId);
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
vm.RootKeyPath = network.GetRootKeyPath();
SetExistingValues(store, vm);
@ -44,6 +44,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, DerivationSchemeViewModel vm)
{
vm.DerivationScheme = GetExistingDerivationStrategy(vm.CryptoCode, store)?.DerivationStrategyBase.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.BTCLike));
}
private DerivationStrategy GetExistingDerivationStrategy(string cryptoCode, StoreData store)
@ -59,7 +60,7 @@ namespace BTCPayServer.Controllers
[Route("{storeId}/derivations/{cryptoCode}")]
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
{
vm.ServerUrl = GetStoreUrl(storeId);
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
vm.CryptoCode = cryptoCode;
var store = HttpContext.GetStoreData();
if (store == null)
@ -78,6 +79,11 @@ namespace BTCPayServer.Controllers
}
PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider)
.Where(c => c.PaymentId == paymentMethodId)
.OfType<DerivationStrategy>()
.Select(c => c.DerivationStrategyBase.ToString())
.FirstOrDefault();
DerivationStrategy strategy = null;
try
{
@ -94,10 +100,32 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
var showAddress = (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // Testing hint address
(!vm.Confirmation && strategy != null && exisingStrategy != strategy.DerivationStrategyBase.ToString()); // Checking addresses after setting xpub
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
if (!showAddress)
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethodId, !vm.Enabled);
store.SetStoreBlob(storeBlob);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else if (!string.IsNullOrEmpty(vm.HintAddress))
{
BitcoinAddress address = null;
try
@ -123,26 +151,8 @@ namespace BTCPayServer.Controllers
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
return ShowAddresses(vm, strategy);
}
else
{
try
{
if (strategy != null)
await wallet.TrackAsync(strategy.DerivationStrategyBase);
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
}
catch
{
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
return View(vm);
}
await _Repo.UpdateStore(store);
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
return ShowAddresses(vm, strategy);
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
@ -161,279 +171,5 @@ namespace BTCPayServer.Controllers
vm.Confirmation = true;
return View(vm);
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
[HttpGet]
[Route("{storeId}/ws/ledger")]
public async Task<IActionResult> LedgerConnection(
string storeId,
string command,
// getinfo
string cryptoCode = null,
// getxpub
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _ExplorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(strategyBase);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = GetDerivationStrategy(store, network);
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
{
var strategy = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
if (strategy == null)
{
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
}
return strategy.DerivationStrategyBase;
}
}
}

View File

@ -24,9 +24,11 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
LightningNodeViewModel vm = new LightningNodeViewModel();
vm.CryptoCode = cryptoCode;
vm.InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString();
LightningNodeViewModel vm = new LightningNodeViewModel
{
CryptoCode = cryptoCode,
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
};
SetExistingValues(store, vm);
return View(vm);
}
@ -34,6 +36,7 @@ namespace BTCPayServer.Controllers
private void SetExistingValues(StoreData store, LightningNodeViewModel vm)
{
vm.ConnectionString = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store)?.GetLightningUrl()?.ToString();
vm.Enabled = !store.GetStoreBlob().IsExcluded(new PaymentMethodId(vm.CryptoCode, PaymentTypes.LightningLike));
}
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
{
@ -132,41 +135,43 @@ namespace BTCPayServer.Controllers
CryptoCode = paymentMethodId.CryptoCode
};
paymentMethod.SetLightningUrl(connectionString);
var storeBlob = store.GetStoreBlob();
storeBlob.SetExcluded(paymentMethod.PaymentId , !vm.Enabled);
store.SetStoreBlob(storeBlob);
}
if (command == "save")
switch (command)
{
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else // if(command == "test")
{
if (paymentMethod == null)
{
case "save":
store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod);
await _Repo.UpdateStore(store);
StatusMessage = $"Lightning node modified ({network.CryptoCode})";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
case "test" when paymentMethod == null:
ModelState.AddModelError(nameof(vm.ConnectionString), "Missing url parameter");
return View(vm);
}
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
case "test":
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
try
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
var info = await handler.Test(paymentMethod, network);
if (!vm.SkipPortTest)
{
await handler.TestConnection(info, cts.Token);
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
await handler.TestConnection(info, cts.Token);
}
}
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
vm.StatusMessage = $"Connection to the lightning node succeeded ({info})";
}
catch (Exception ex)
{
vm.StatusMessage = $"Error: {ex.Message}";
return View(vm);
}
return View(vm);
default:
return View(vm);
}
}

View File

@ -38,11 +38,9 @@ namespace BTCPayServer.Controllers
BTCPayRateProviderFactory _RateFactory;
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
BTCPayServerOptions btcpayServerOptions,
BTCPayServerEnvironment btcpayEnv,
IOptions<MvcJsonOptions> mvcJsonOptions,
StoreRepository repo,
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
@ -56,7 +54,6 @@ namespace BTCPayServer.Controllers
IHostingEnvironment env)
{
_RateFactory = rateFactory;
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
@ -66,19 +63,16 @@ namespace BTCPayServer.Controllers
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
_MvcJsonOptions = mvcJsonOptions.Value;
_FeeRateProvider = feeRateProvider;
_ServiceProvider = serviceProvider;
_BtcpayServerOptions = btcpayServerOptions;
_BTCPayEnv = btcpayEnv;
}
NBXplorerDashboard _Dashboard;
BTCPayServerOptions _BtcpayServerOptions;
BTCPayServerEnvironment _BTCPayEnv;
IServiceProvider _ServiceProvider;
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
private MvcJsonOptions _MvcJsonOptions;
private IFeeProviderFactory _FeeRateProvider;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
@ -94,21 +88,6 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")]
public IActionResult Wallet(string cryptoCode)
{
WalletModel model = new WalletModel();
model.ServerUrl = GetStoreUrl(StoreData.Id);
model.CryptoCurrency = cryptoCode;
return View(model);
}
private string GetStoreUrl(string storeId)
{
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
}
[HttpGet]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers()
@ -200,7 +179,7 @@ namespace BTCPayServer.Controllers
var storeBlob = StoreData.GetStoreBlob();
var vm = new RatesViewModel();
vm.SetExchangeRates(GetSupportedExchanges(), storeBlob.PreferredExchange ?? CoinAverageRateProvider.CoinAverageName);
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
vm.Spread = (double)(storeBlob.Spread * 100m);
vm.Script = storeBlob.GetRateRules(_NetworkProvider).ToString();
vm.DefaultScript = storeBlob.GetDefaultRateRules(_NetworkProvider).ToString();
vm.AvailableExchanges = GetSupportedExchanges();
@ -225,7 +204,7 @@ namespace BTCPayServer.Controllers
model.AvailableExchanges = GetSupportedExchanges();
blob.PreferredExchange = model.PreferredExchange;
blob.SetRateMultiplier(model.RateMultiplier);
blob.Spread = (decimal)model.Spread / 100.0m;
if (!model.ShowScripting)
{
@ -283,7 +262,7 @@ namespace BTCPayServer.Controllers
{
CurrencyPair = fetch.Key.ToString(),
Error = testResult.Errors.Count != 0,
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.Value.Value.ToString(CultureInfo.InvariantCulture)
Rule = testResult.Errors.Count == 0 ? testResult.Rule + " = " + testResult.BidAsk.Bid.ToString(CultureInfo.InvariantCulture)
: testResult.EvaluatedRule
});
}
@ -338,7 +317,7 @@ namespace BTCPayServer.Controllers
{
var storeBlob = StoreData.GetStoreBlob();
var vm = new CheckoutExperienceViewModel();
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto());
vm.SetCryptoCurrencies(_ExplorerProvider, StoreData.GetDefaultCrypto(_NetworkProvider));
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.OnChainMinValue = storeBlob.OnChainMinValue?.ToString() ?? "";
@ -373,7 +352,7 @@ namespace BTCPayServer.Controllers
}
bool needUpdate = false;
var blob = StoreData.GetStoreBlob();
if (StoreData.GetDefaultCrypto() != model.DefaultCryptoCurrency)
if (StoreData.GetDefaultCrypto(_NetworkProvider) != model.DefaultCryptoCurrency)
{
needUpdate = true;
StoreData.SetDefaultCrypto(model.DefaultCryptoCurrency);
@ -425,7 +404,7 @@ namespace BTCPayServer.Controllers
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.SpeedPolicy = store.SpeedPolicy;
vm.CanDelete = _Repo.CanDeleteStores();
AddPaymentMethods(store, vm);
AddPaymentMethods(store, storeBlob, vm);
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
vm.LightningDescriptionTemplate = storeBlob.LightningDescriptionTemplate;
@ -434,8 +413,9 @@ namespace BTCPayServer.Controllers
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
private void AddPaymentMethods(StoreData store, StoreBlob storeBlob, StoreViewModel vm)
{
var excludeFilters = storeBlob.GetExcludedPaymentMethods();
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
@ -447,7 +427,9 @@ namespace BTCPayServer.Controllers
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
{
Crypto = network.CryptoCode,
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.BTCLike))
});
}
@ -459,10 +441,12 @@ namespace BTCPayServer.Controllers
foreach (var network in _NetworkProvider.GetAll())
{
var lightning = lightningByCryptoCode.TryGet(network.CryptoCode);
var paymentId = new Payments.PaymentMethodId(network.CryptoCode, Payments.PaymentTypes.LightningLike);
vm.LightningNodes.Add(new StoreViewModel.LightningNode()
{
CryptoCode = network.CryptoCode,
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty
Address = lightning?.GetLightningUrl()?.BaseUri.AbsoluteUri ?? string.Empty,
Enabled = !excludeFilters.Match(paymentId)
});
}
}

View File

@ -23,18 +23,15 @@ namespace BTCPayServer.Controllers
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private UserManager<ApplicationUser> _UserManager;
private BTCPayWalletProvider _WalletProvider;
public UserStoresController(
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository)
{
_Repo = storeRepository;
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
@ -85,17 +82,6 @@ namespace BTCPayServer.Controllers
{
StoresViewModel result = new StoresViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
@ -104,8 +90,7 @@ namespace BTCPayServer.Controllers
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
});
}
return View(result);
@ -128,22 +113,6 @@ namespace BTCPayServer.Controllers
});
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View File

@ -0,0 +1,487 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.HostedServices;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.WalletViewModels;
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.Extensions.Options;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using static BTCPayServer.Controllers.StoresController;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
public class WalletsController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
private readonly NBXplorerDashboard _dashboard;
private readonly ExplorerClientProvider _explorerProvider;
private readonly IFeeProviderFactory _feeRateProvider;
private readonly BTCPayWalletProvider _walletProvider;
BTCPayRateProviderFactory _RateProvider;
CurrencyNameTable _currencyTable;
public WalletsController(StoreRepository repo,
CurrencyNameTable currencyTable,
BTCPayNetworkProvider networkProvider,
UserManager<ApplicationUser> userManager,
IOptions<MvcJsonOptions> mvcJsonOptions,
NBXplorerDashboard dashboard,
BTCPayRateProviderFactory rateProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
BTCPayWalletProvider walletProvider)
{
_currencyTable = currencyTable;
_Repo = repo;
_RateProvider = rateProvider;
_NetworkProvider = networkProvider;
_userManager = userManager;
_mvcJsonOptions = mvcJsonOptions;
_dashboard = dashboard;
_explorerProvider = explorerProvider;
_feeRateProvider = feeRateProvider;
_walletProvider = walletProvider;
}
public async Task<IActionResult> ListWallets()
{
var wallets = new ListWalletsViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var onChainWallets = stores
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase,
Network: d.Network)))
.Where(_ => _.Wallet != null)
.Select(_ => (Wallet: _.Wallet,
Store: s,
Balance: GetBalanceString(_.Wallet, _.DerivationStrategy),
DerivationStrategy: _.DerivationStrategy,
Network: _.Network)))
.ToList();
foreach (var wallet in onChainWallets)
{
ListWalletsViewModel.WalletViewModel walletVm = new ListWalletsViewModel.WalletViewModel();
wallets.Wallets.Add(walletVm);
walletVm.Balance = await wallet.Balance + " " + wallet.Wallet.Network.CryptoCode;
if (!wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key))
{
walletVm.Balance = "";
}
walletVm.CryptoCode = wallet.Network.CryptoCode;
walletVm.StoreId = wallet.Store.Id;
walletVm.Id = new WalletId(wallet.Store.Id, wallet.Network.CryptoCode);
walletVm.StoreName = wallet.Store.StoreName;
walletVm.IsOwner = wallet.Store.HasClaim(Policies.CanModifyStoreSettings.Key);
}
return View(wallets);
}
[HttpGet]
[Route("{walletId}")]
public async Task<IActionResult> WalletTransactions(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
var transactions = await wallet.FetchTransactions(paymentMethod.DerivationStrategyBase);
var model = new ListTransactionsViewModel();
foreach(var tx in transactions.UnconfirmedTransactions.Transactions.Concat(transactions.ConfirmedTransactions.Transactions))
{
var vm = new ListTransactionsViewModel.TransactionViewModel();
model.Transactions.Add(vm);
vm.Id = tx.TransactionId.ToString();
vm.Link = string.Format(CultureInfo.InvariantCulture, paymentMethod.Network.BlockExplorerLink, vm.Id);
vm.Timestamp = tx.Timestamp;
vm.Positive = tx.BalanceChange >= Money.Zero;
vm.Balance = tx.BalanceChange.ToString();
}
model.Transactions = model.Transactions.OrderByDescending(t => t.Timestamp).ToList();
return View(model);
}
[HttpGet]
[Route("{walletId}/send")]
public async Task<IActionResult> WalletSend(
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId)
{
if (walletId?.StoreId == null)
return NotFound();
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);
if (paymentMethod == null)
return NotFound();
var storeData = store.GetStoreBlob();
var rateRules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
rateRules.Spread = 0.0m;
var currencyPair = new Rating.CurrencyPair(paymentMethod.PaymentId.CryptoCode, GetCurrencyCode(storeData.DefaultLang) ?? "USD");
WalletModel model = new WalletModel();
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
model.CryptoCurrency = walletId.CryptoCode;
using (CancellationTokenSource cts = new CancellationTokenSource())
{
try
{
cts.CancelAfter(TimeSpan.FromSeconds(5));
var result = await _RateProvider.FetchRate(currencyPair, rateRules).WithCancellation(cts.Token);
if (result.BidAsk != null)
{
model.Rate = result.BidAsk.Center;
model.Divisibility = _currencyTable.GetNumberFormatInfo(currencyPair.Right, true).CurrencyDecimalDigits;
model.Fiat = currencyPair.Right;
}
else
{
model.RateError = $"{result.EvaluatedRule} ({string.Join(", ", result.Errors.OfType<object>().ToArray())})";
}
}
catch(Exception ex) { model.RateError = ex.Message; }
}
return View(model);
}
private string GetCurrencyCode(string defaultLang)
{
if (defaultLang == null)
return null;
try
{
var ri = new RegionInfo(defaultLang);
return ri.ISOCurrencySymbol;
}
catch(ArgumentException) { }
return null;
}
private DerivationStrategy GetPaymentMethod(WalletId walletId, StoreData store)
{
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
return null;
var paymentMethod = store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
return paymentMethod;
}
private static async Task<string> GetBalanceString(BTCPayWallet wallet, DerivationStrategyBase derivationStrategy)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await wallet.GetBalance(derivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _userManager.GetUserId(User);
}
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
{
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
}
[HttpGet]
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
public async Task<IActionResult> LedgerConnection(
string command,
// getinfo
string cryptoCode = null,
// getxpub
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
DerivationStrategyBase derivationScheme = null,
int account = 0,
// sendtoaddress
string destination = null, string amount = null, string feeRate = null, string substractFees = null
)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
using (var normalOperationTimeout = new CancellationTokenSource())
using (var signTimeout = new CancellationTokenSource())
{
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
var hw = new HardwareWalletService(webSocket);
object result = null;
try
{
BTCPayNetwork network = null;
if (cryptoCode != null)
{
network = _NetworkProvider.GetNetwork(cryptoCode);
if (network == null)
throw new FormatException("Invalid value for crypto code");
}
BitcoinAddress destinationAddress = null;
if (destination != null)
{
try
{
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
}
catch { }
if (destinationAddress == null)
throw new FormatException("Invalid value for destination");
}
FeeRate feeRateValue = null;
if (feeRate != null)
{
try
{
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
}
catch { }
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
throw new FormatException("Invalid value for fee rate");
}
Money amountBTC = null;
if (amount != null)
{
try
{
amountBTC = Money.Parse(amount);
}
catch { }
if (amountBTC == null || amountBTC <= Money.Zero)
throw new FormatException("Invalid value for amount");
}
bool subsctractFeesValue = false;
if (substractFees != null)
{
try
{
subsctractFeesValue = bool.Parse(substractFees);
}
catch { throw new FormatException("Invalid value for subtract fees"); }
}
if (command == "test")
{
result = await hw.Test(normalOperationTimeout.Token);
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
result = getxpubResult;
}
if (command == "getinfo")
{
var strategy = GetDirectDerivationStrategy(derivationScheme);
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
{
throw new Exception($"This store is not configured to use this ledger");
}
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
var recommendedFees = feeProvider.GetFeeRateAsync();
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
}
if (command == "sendtoaddress")
{
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(derivationScheme);
var wallet = _walletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(derivationScheme);
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
var changeAddress = await change;
var send = new[] { (
destination: destinationAddress as IDestination,
amount: amountBTC,
substractFees: subsctractFeesValue) };
foreach (var element in send)
{
if (element.destination == null)
throw new ArgumentNullException(nameof(element.destination));
if (element.amount == null)
throw new ArgumentNullException(nameof(element.amount));
if (element.amount <= Money.Zero)
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
}
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
if (foundKeyPath == null)
{
throw new HardwareWalletException($"This store is not configured to use this ledger");
}
TransactionBuilder builder = new TransactionBuilder();
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
builder.SetConsensusFactory(network.NBitcoinNetwork);
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
foreach (var element in send)
{
builder.Send(element.destination, element.amount);
if (element.substractFees)
builder.SubtractFees();
}
builder.SetChange(changeAddress.Item1);
if (network.MinFee == null)
{
builder.SendEstimatedFees(feeRateValue);
}
else
{
var estimatedFee = builder.EstimateFees(feeRateValue);
if (network.MinFee > estimatedFee)
builder.SendFees(network.MinFee);
else
builder.SendEstimatedFees(feeRateValue);
}
builder.Shuffle();
var unsigned = builder.BuildTransaction(false);
var keypaths = new Dictionary<Script, KeyPath>();
foreach (var c in unspentCoins)
{
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
}
var hasChange = unsigned.Outputs.Count == 2;
var usedCoins = builder.FindSpentCoins(unsigned);
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
if (!strategy.Segwit)
{
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
var explorer = _explorerProvider.GetExplorerClient(network);
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
foreach (var getTransactionAsync in getTransactionAsyncs)
{
var tx = (await getTransactionAsync.Op);
if (tx == null)
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
}
}
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
{
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
InputCoin = c,
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
try
{
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
if (!broadcastResult[0].Success)
{
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
}
}
catch (Exception ex)
{
throw new Exception("Error while broadcasting: " + ex.Message);
}
wallet.InvalidateCache(derivationScheme);
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
}
}
catch (OperationCanceledException)
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
catch (Exception ex)
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
finally { hw.Dispose(); }
try
{
if (result != null)
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _mvcJsonOptions.Value.SerializerSettings));
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
}
}
catch { }
finally
{
await webSocket.CloseSocket();
}
}
return new EmptyResult();
}
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
{
if (strategy == null)
throw new Exception("The derivation scheme is not provided");
var directStrategy = strategy as DirectDerivationStrategy;
if (directStrategy == null)
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
return directStrategy;
}
}
public class GetInfoResult
{
public int RecommendedSatoshiPerByte { get; set; }
public double Balance { get; set; }
}
public class SendToAddressResult
{
public string TransactionId { get; set; }
}
}

View File

@ -197,9 +197,9 @@ namespace BTCPayServer.Data
public IEnumerable<APIKeyData> APIKeys { get; set; }
#pragma warning disable CS0618
public string GetDefaultCrypto()
public string GetDefaultCrypto(BTCPayNetworkProvider networkProvider = null)
{
return DefaultCrypto ?? "BTC";
return DefaultCrypto ?? (networkProvider == null ? "BTC" : GetSupportedPaymentMethods(networkProvider).First().PaymentId.CryptoCode);
}
public void SetDefaultCrypto(string defaultCryptoCurrency)
{
@ -281,23 +281,9 @@ namespace BTCPayServer.Data
set;
}
public void SetRateMultiplier(double rate)
{
RateRules = new List<RateRule_Obsolete>();
RateRules.Add(new RateRule_Obsolete() { Multiplier = rate });
}
public decimal GetRateMultiplier()
{
decimal rate = 1.0m;
if (RateRules == null || RateRules.Count == 0)
return rate;
foreach (var rule in RateRules)
{
rate = rule.Apply(null, rate);
}
return rate;
}
public decimal Spread { get; set; } = 0.0m;
[Obsolete]
public List<RateRule_Obsolete> RateRules { get; set; } = new List<RateRule_Obsolete>();
public string PreferredExchange { get; set; }
@ -336,15 +322,15 @@ namespace BTCPayServer.Data
public BTCPayServer.Rating.RateRules GetRateRules(BTCPayNetworkProvider networkProvider)
{
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
if (!RateScripting ||
string.IsNullOrEmpty(RateScript) ||
!BTCPayServer.Rating.RateRules.TryParse(RateScript, out var rules))
{
return GetDefaultRateRules(networkProvider);
}
else
{
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}
}
@ -370,8 +356,37 @@ namespace BTCPayServer.Data
builder.AppendLine($"X_X = {preferredExchange}(X_X);");
BTCPayServer.Rating.RateRules.TryParse(builder.ToString(), out var rules);
rules.GlobalMultiplier = GetRateMultiplier();
rules.Spread = Spread;
return rules;
}
[Obsolete("Use GetExcludedPaymentMethods instead")]
public string[] ExcludedPaymentMethods { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{
#pragma warning disable CS0618 // Type or member is obsolete
if (ExcludedPaymentMethods == null || ExcludedPaymentMethods.Length == 0)
return PaymentFilter.Never();
return PaymentFilter.Any(ExcludedPaymentMethods.Select(p => PaymentFilter.WhereIs(PaymentMethodId.Parse(p))).ToArray());
#pragma warning restore CS0618 // Type or member is obsolete
}
public bool IsExcluded(PaymentMethodId paymentMethodId)
{
return GetExcludedPaymentMethods().Match(paymentMethodId);
}
public void SetExcluded(PaymentMethodId paymentMethodId, bool value)
{
#pragma warning disable CS0618 // Type or member is obsolete
var methods = new HashSet<string>(ExcludedPaymentMethods ?? Array.Empty<string>());
if (value)
methods.Add(paymentMethodId.ToString());
else
methods.Remove(paymentMethodId.ToString());
ExcludedPaymentMethods = methods.ToArray();
#pragma warning restore CS0618 // Type or member is obsolete
}
}
}

View File

@ -32,7 +32,7 @@ namespace BTCPayServer
public BTCPayNetwork Network { get { return this._Network; } }
public DerivationStrategyBase DerivationStrategyBase { get { return this._DerivationStrategy; } }
public DerivationStrategyBase DerivationStrategyBase => this._DerivationStrategy;
public PaymentMethodId PaymentId => new PaymentMethodId(Network.CryptoCode, PaymentTypes.BTCLike);

View File

@ -202,7 +202,8 @@ namespace BTCPayServer.HostedServices
PaymentSubtotals = dto.PaymentSubtotals,
PaymentTotals = dto.PaymentTotals,
AmountPaid = dto.AmountPaid,
ExchangeRates = dto.ExchangeRates
ExchangeRates = dto.ExchangeRates,
};
// We keep backward compatibility with bitpay by passing BTC info to the notification

View File

@ -53,6 +53,12 @@ namespace BTCPayServer.HostedServices
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertMultiplierToSpread)
{
await ConvertMultiplierToSpread();
settings.ConvertMultiplierToSpread = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -61,6 +67,31 @@ namespace BTCPayServer.HostedServices
}
}
private async Task ConvertMultiplierToSpread()
{
using (var ctx = _DBContextFactory.CreateContext())
{
foreach (var store in await ctx.Stores.ToArrayAsync())
{
var blob = store.GetStoreBlob();
#pragma warning disable CS0612 // Type or member is obsolete
decimal multiplier = 1.0m;
if (blob.RateRules != null && blob.RateRules.Count != 0)
{
foreach (var rule in blob.RateRules)
{
multiplier = rule.Apply(null, multiplier);
}
}
blob.RateRules = null;
blob.Spread = Math.Min(1.0m, Math.Max(0m, -(multiplier - 1.0m)));
store.SetStoreBlob(blob);
#pragma warning restore CS0612 // Type or member is obsolete
}
await ctx.SaveChangesAsync();
}
}
private Task UnreachableStoreCheck()
{
return _StoreRepository.CleanUnreachableStores();

View File

@ -39,6 +39,8 @@ using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.Security;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Hosting
{
@ -105,7 +107,11 @@ namespace BTCPayServer.Hosting
});
services.AddSingleton<CssThemeManager>();
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
services.Configure<MvcOptions>((o) => {
o.Filters.Add(new ContentSecurityPolicyCssThemeManager());
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
});
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
services.AddSingleton<IHostedService, MigratorHostedService>();

View File

@ -100,7 +100,7 @@ namespace BTCPayServer.Hosting
(isJson || httpContext.Request.Query.ContainsKey("token")))
return true;
if (path.Equals("/rates", StringComparison.OrdinalIgnoreCase) &&
if (path.StartsWith("/rates", StringComparison.OrdinalIgnoreCase) &&
httpContext.Request.Method == "GET")
return true;

View File

@ -0,0 +1,58 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBitcoin;
using System.Reflection;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.ModelBinders
{
public class DerivationSchemeModelBinder : IModelBinder
{
public DerivationSchemeModelBinder()
{
}
#region IModelBinder Members
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(DerivationStrategyBase).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string key = val.FirstValue as string;
if (key == null)
{
return Task.CompletedTask;
}
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
var network = networkProvider.GetNetwork(cryptoCode ?? "BTC");
try
{
var data = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(key);
if (!bindingContext.ModelType.IsInstanceOfType(data))
{
throw new FormatException("Invalid destination type");
}
bindingContext.Result = ModelBindingResult.Success(data);
}
catch { throw new FormatException("Invalid derivation scheme"); }
return Task.CompletedTask;
}
#endregion
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.ModelBinders
{
public class WalletIdModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (!typeof(WalletId).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
{
return Task.CompletedTask;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return Task.CompletedTask;
}
string key = val.FirstValue as string;
if (key == null)
{
return Task.CompletedTask;
}
if(WalletId.TryParse(key, out var walletId))
{
bindingContext.Result = ModelBindingResult.Success(walletId);
}
return Task.CompletedTask;
}
}
}

View File

@ -27,6 +27,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }
public bool Enabled { get; set; } = true;
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }

View File

@ -24,5 +24,6 @@ namespace BTCPayServer.Models.StoreViewModels
public string StatusMessage { get; set; }
public string InternalLightningNode { get; internal set; }
public bool SkipPortTest { get; set; }
public bool Enabled { get; set; } = true;
}
}

View File

@ -44,9 +44,9 @@ namespace BTCPayServer.Models.StoreViewModels
public string ScriptTest { get; set; }
public CoinAverageExchange[] AvailableExchanges { get; set; }
[Display(Name = "Multiply the rate by... (Setting to 1.01 would apply a discount of 1% to the purchase)")]
[Range(0.01, 10.0)]
public double RateMultiplier
[Display(Name = "Add a spread on exchange rate of ... %")]
[Range(0.0, 100.0)]
public double Spread
{
get;
set;

View File

@ -18,6 +18,8 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string Crypto { get; set; }
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool Enabled { get; set; }
}
public StoreViewModel()
@ -82,6 +84,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
public string CryptoCode { get; set; }
public string Address { get; set; }
public bool Enabled { get; set; }
}
public List<LightningNode> LightningNodes
{

View File

@ -34,10 +34,6 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
public string[] Balances
{
get; set;
}
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class ListTransactionsViewModel
{
public class TransactionViewModel
{
public DateTimeOffset Timestamp { get; set; }
public string Id { get; set; }
public string Link { get; set; }
public bool Positive { get; set; }
public string Balance { get; set; }
}
public List<TransactionViewModel> Transactions { get; set; } = new List<TransactionViewModel>();
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.WalletViewModels
{
public class ListWalletsViewModel
{
public class WalletViewModel
{
public string StoreName { get; set; }
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public string Balance { get; set; }
public bool IsOwner { get; set; }
public WalletId Id { get; set; }
}
public List<WalletViewModel> Wallets { get; set; } = new List<WalletViewModel>();
}
}

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.StoreViewModels
namespace BTCPayServer.Models.WalletViewModels
{
public class WalletModel
{
@ -15,5 +15,9 @@ namespace BTCPayServer.Models.StoreViewModels
get;
set;
}
public decimal? Rate { get; set; }
public int Divisibility { get; set; }
public string Fiat { get; set; }
public string RateError { get; set; }
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Payments
{
public interface IPaymentFilter
{
bool Match(PaymentMethodId paymentMethodId);
}
public class PaymentFilter
{
class NeverPaymentFilter : IPaymentFilter
{
private static readonly NeverPaymentFilter _Instance = new NeverPaymentFilter();
public static NeverPaymentFilter Instance
{
get
{
return _Instance;
}
}
public bool Match(PaymentMethodId paymentMethodId)
{
return false;
}
}
class CompositePaymentFilter : IPaymentFilter
{
private readonly IPaymentFilter[] _filters;
public CompositePaymentFilter(IPaymentFilter[] filters)
{
_filters = filters;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return _filters.Any(f => f.Match(paymentMethodId));
}
}
class PaymentIdFilter : IPaymentFilter
{
private readonly PaymentMethodId _paymentMethodId;
public PaymentIdFilter(PaymentMethodId paymentMethodId)
{
_paymentMethodId = paymentMethodId;
}
public bool Match(PaymentMethodId paymentMethodId)
{
return paymentMethodId == _paymentMethodId;
}
}
public static IPaymentFilter Never()
{
return NeverPaymentFilter.Instance;
}
public static IPaymentFilter Any(IPaymentFilter[] filters)
{
if (filters == null)
throw new ArgumentNullException(nameof(filters));
return new CompositePaymentFilter(filters);
}
public static IPaymentFilter WhereIs(PaymentMethodId paymentMethodId)
{
if (paymentMethodId == null)
throw new ArgumentNullException(nameof(paymentMethodId));
return new PaymentIdFilter(paymentMethodId);
}
}
}

View File

@ -183,18 +183,24 @@ namespace BTCPayServer.Payments.Lightning.Lnd
NodeId = resp.Identity_pubkey
};
try
{
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var node = await _rpcClient.GetNodeInfoAsync(resp.Identity_pubkey, cancellation);
if (node.Node.Addresses == null || node.Node.Addresses.Count == 0)
throw new Exception("Lnd External IP not set, make sure you use --externalip=$EXTERNALIP parameter on lnd");
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
var firstNodeInfo = node.Node.Addresses.First();
var externalHostPort = firstNodeInfo.Addr.Split(':');
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
nodeInfo.Address = externalHostPort[0];
nodeInfo.P2PPort = ConvertInv.ToInt32(externalHostPort[1]);
return nodeInfo;
return nodeInfo;
}
catch (SwaggerException ex) when (!string.IsNullOrEmpty(ex.Response))
{
throw new Exception("LND threw an error: " + ex.Response);
}
}
public async Task<LightningInvoice> GetInvoice(string invoiceId, CancellationToken cancellation = default(CancellationToken))

View File

@ -138,6 +138,9 @@ namespace BTCPayServer.Rating
return _Ask;
}
}
public decimal Center => (Ask + Bid) / 2.0m;
public BidAsk Inverse()
{
return new BidAsk(1.0m / Ask, 1.0m / Bid);

View File

@ -99,7 +99,20 @@ namespace BTCPayServer.Rating
SyntaxNode root;
RuleList ruleList;
public decimal GlobalMultiplier { get; set; } = 1.0m;
decimal _Spread;
public decimal Spread
{
get
{
return _Spread;
}
set
{
if (value > 1.0m || value < 0.0m)
throw new ArgumentOutOfRangeException(paramName: nameof(value), message: "The spread should be between 0 and 1");
_Spread = value;
}
}
RateRules(SyntaxNode root)
{
@ -133,10 +146,12 @@ namespace BTCPayServer.Rating
{
if (currencyPair.Left == "X" || currencyPair.Right == "X")
throw new ArgumentException(paramName: nameof(currencyPair), message: "Invalid X currency");
if (currencyPair.Left == currencyPair.Right)
return new RateRule(this, currencyPair, CreateExpression("1.0"));
var candidate = FindBestCandidate(currencyPair);
if (GlobalMultiplier != decimal.One)
if (Spread != decimal.Zero)
{
candidate = CreateExpression($"({candidate}) * {GlobalMultiplier.ToString(CultureInfo.InvariantCulture)}");
candidate = CreateExpression($"({candidate}) * ({(1.0m - Spread).ToString(CultureInfo.InvariantCulture)}, {(1.0m + Spread).ToString(CultureInfo.InvariantCulture)})");
}
return new RateRule(this, currencyPair, candidate);
}
@ -255,7 +270,7 @@ namespace BTCPayServer.Rating
{
case SyntaxKind.UnaryMinusExpression:
var v = Values.Pop();
if(v.Bid == v.Ask)
if (v.Bid == v.Ask)
{
Values.Push(-v);
}
@ -336,7 +351,7 @@ namespace BTCPayServer.Rating
{
_TupleValues = new Stack<decimal>();
base.VisitTupleExpression(node);
if(_TupleValues.Count != 2)
if (_TupleValues.Count != 2)
{
Errors.Add(RateRulesErrors.MissingArgument);
}
@ -504,7 +519,7 @@ namespace BTCPayServer.Rating
public bool Reevaluate()
{
_Value = null;
_BidAsk = null;
_EvaluatedNode = null;
_Evaluated = null;
Errors.Clear();
@ -524,7 +539,7 @@ namespace BTCPayServer.Rating
Errors.AddRange(calculate.Errors);
return false;
}
_Value = calculate.Values.Pop().Bid;
_BidAsk = calculate.Values.Pop();
_EvaluatedNode = result;
return true;
}
@ -563,12 +578,12 @@ namespace BTCPayServer.Rating
return expression.NormalizeWhitespace("", "\n").ToString();
}
decimal? _Value;
public decimal? Value
BidAsk _BidAsk;
public BidAsk BidAsk
{
get
{
return _Value;
return _BidAsk;
}
}
}

View File

@ -9,5 +9,6 @@ namespace BTCPayServer.Services
{
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
}
}

View File

@ -22,7 +22,7 @@ namespace BTCPayServer.Services.Rates
public string Rule { get; set; }
public string EvaluatedRule { get; set; }
public HashSet<RateRulesErrors> Errors { get; set; }
public decimal? Value { get; set; }
public BidAsk BidAsk { get; set; }
public bool Cached { get; internal set; }
}
@ -172,11 +172,11 @@ namespace BTCPayServer.Services.Rates
result.ExchangeExceptions.AddRange(query.Exceptions);
foreach (var rule in query.ExchangeRates)
{
rateRule.ExchangeRates.Add(rule);
rateRule.ExchangeRates.SetRate(rule.Exchange, rule.CurrencyPair, rule.BidAsk);
}
}
rateRule.Reevaluate();
result.Value = rateRule.Value;
result.BidAsk = rateRule.BidAsk;
result.Errors = rateRule.Errors;
result.EvaluatedRule = rateRule.ToString(true);
result.Rule = rateRule.ToString(false);

View File

@ -151,6 +151,11 @@ namespace BTCPayServer.Services.Wallets
return await completionSource.Task;
}
public Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Client.GetTransactionsAsync(derivationStrategyBase, null, false);
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();

View File

@ -17,7 +17,7 @@
<div class="col-md-8">
<div class="form-group">
<p>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection informaiton here.<br /></span>
<span>BTCPay exposes gRPC services for outside consumption, you will find connection information here.<br /></span>
</p>
</div>

View File

@ -61,6 +61,7 @@
}
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>

View File

@ -1,7 +1,7 @@
@model DerivationSchemeViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Add derivation scheme";
ViewData["Title"] = $"{Model.CryptoCode} Derivation scheme";
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
@ -16,7 +16,7 @@
<div class="row">
<div class="col-md-8">
<form method="post">
@if (!Model.Confirmation)
@if(!Model.Confirmation)
{
<div class="form-group">
<h5>Derivation Scheme</h5>
@ -33,7 +33,7 @@
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for (int i = 0; i < 4; i++)
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
}
@ -77,7 +77,11 @@
</tbody>
</table>
</div>
<button name="command" type="submit" class="btn btn-primary">Continue</button>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" class="btn btn-primary" value="save">Continue</button>
}
else
{
@ -87,6 +91,7 @@
</div>
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="Enabled" />
<div class="form-group">
<table class="table table-sm table-responsive-md">
<thead>
@ -96,7 +101,7 @@
</tr>
</thead>
<tbody>
@foreach (var sample in Model.AddressSamples)
@foreach(var sample in Model.AddressSamples)
{
<tr>
<td>@sample.KeyPath</td>
@ -116,7 +121,7 @@
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-primary">Confirm</button>
<button name="command" type="submit" class="btn btn-primary" value="save">Confirm</button>
}
</form>
</div>

View File

@ -62,6 +62,10 @@
</p>
}
</div>
<div class="form-group">
<label asp-for="Enabled"></label>
<input asp-for="Enabled" type="checkbox" class="form-check" />
</div>
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
<button name="command" type="submit" value="test" class="btn btn-secondary">Test connection</button>
</form>

View File

@ -132,9 +132,9 @@
</div>
}
<div class="form-group">
<label asp-for="RateMultiplier"></label>
<input asp-for="RateMultiplier" class="form-control" />
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
<label asp-for="Spread"></label>
<input asp-for="Spread" class="form-control" />
<span asp-validation-for="Spread" class="text-danger"></span>
</div>
<div class="form-group">
<h5>Testing</h5>

View File

@ -5,6 +5,21 @@
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
<style type="text/css">
.smMaxWidth {
max-width: 150px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 300px;
}
}
</style>
<h4>@ViewData["Title"]</h4>
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
@ -70,7 +85,8 @@
<tr>
<th>Crypto</th>
<th>Derivation Scheme</th>
<th style="text-align:right">Actions</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right;">Actions</th>
</tr>
</thead>
<tbody>
@ -78,11 +94,21 @@
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td class="smMaxWidth text-truncate">@scheme.Value</td>
<td style="text-align:center;">
@if(scheme.Enabled)
{
<span class="fa fa-check"></span>
}
else
{
<span class="fa fa-times"></span>
}
</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
<a asp-action="WalletTransactions" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>
@ -106,6 +132,7 @@
<tr>
<th>Crypto</th>
<th>Address</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
@ -114,7 +141,17 @@
{
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td class="smMaxWidth text-truncate">@scheme.Address</td>
<td style="text-align:center;">
@if(scheme.Enabled)
{
<span class="fa fa-check"></span>
}
else
{
<span class="fa fa-times"></span>
}
</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}

View File

@ -27,7 +27,6 @@
<tr>
<th>Name</th>
<th>Website</th>
<th>On-Chain balances</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
@ -42,16 +41,6 @@
<a href="@store.WebSite">@store.WebSite</a>
}
</td>
<td>
@for (int i = 0; i < store.Balances.Length; i++)
{
<span>@store.Balances[i]</span>
if (i != store.Balances.Length - 1)
{
<br />
}
}
</td>
<td style="text-align:right">
@if (store.IsOwner)
{

View File

@ -0,0 +1,56 @@
@model BTCPayServer.Models.WalletViewModels.ListWalletsViewModel
@{
ViewData["Title"] = "Wallets";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@ViewData["Title"]</h2>
<hr class="primary">
<p>Create and manage wallets.</p>
</div>
</div>
<div class="row">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Store Name</th>
<th>Crypto Code</th>
<th>Balance</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var wallet in Model.Wallets)
{
<tr>
@if(wallet.IsOwner)
{
<td><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
}
else
{
<td>@wallet.StoreName</td>
}
<td>@wallet.CryptoCode</td>
<td>@wallet.Balance</td>
<td style="text-align:right">
<a asp-action="WalletTransactions" asp-route-walletId="@wallet.Id">Manage</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</section>

View File

@ -2,6 +2,7 @@
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
}
<h4>@ViewData["Title"]</h4>
@ -40,7 +41,14 @@
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<div class="input-group">
<input id="amount-textbox" name="Amount" class="form-control" type="text" onkeyup='updateFiatValue();' />
<div class="input-group-prepend">
<span class="input-group-text text-muted" style="display:none;" id="fiatValue"></span>
</div>
</div>
@*<span class="text-muted" id="fiatValue"></span>*@
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
@ -62,11 +70,11 @@
</form>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
@section Scripts
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

View File

@ -0,0 +1,54 @@
@model ListTransactionsViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage wallet";
ViewData.SetActivePageAndTitle(WalletsNavPages.Transactions);
}
<style type="text/css">
.smMaxWidth {
max-width: 200px;
}
@@media (min-width: 768px) {
.smMaxWidth {
max-width: 400px;
}
}
</style>
<h4>@ViewData["Title"]</h4>
<div class="row">
<div class="col-md-10">
<table class="table table-sm table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th>Timestamp</th>
<th>Transaction Id</th>
<th style="text-align:right">Balance</th>
</tr>
</thead>
<tbody>
@foreach(var transaction in Model.Transactions)
{
<tr>
<td>@transaction.Timestamp.ToBrowserDate()</td>
<td class="smMaxWidth text-truncate">
<a href="@transaction.Link" target="_blank">
@transaction.Id
</a>
</td>
@if(transaction.Positive)
{
<td style="text-align:right; color:green;">@transaction.Balance</td>
}
else
{
<td style="text-align:right; color:red;">@transaction.Balance</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Views.Wallets
{
public enum WalletsNavPages
{
Send,
Transactions
}
}

View File

@ -0,0 +1,7 @@
@inject SignInManager<ApplicationUser> SignInManager
<div class="nav flex-column nav-pills">
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Transactions)" asp-action="WalletTransactions">Transactions</a>
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
</div>

View File

@ -0,0 +1,2 @@
@using BTCPayServer.Views.Wallets
@using BTCPayServer.Models.WalletViewModels

View File

@ -0,0 +1,3 @@
@{
ViewBag.MainTitle = "Manage wallet";
}

40
BTCPayServer/WalletId.cs Normal file
View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class WalletId
{
static readonly Regex _WalletStoreRegex = new Regex("^S-([a-zA-Z0-9]{30,60})-([a-zA-Z]{2,5})$");
public static bool TryParse(string str, out WalletId walletId)
{
walletId = null;
WalletId w = new WalletId();
var match = _WalletStoreRegex.Match(str);
if (!match.Success)
return false;
w.StoreId = match.Groups[1].Value;
w.CryptoCode = match.Groups[2].Value.ToUpperInvariant();
walletId = w;
return true;
}
public WalletId()
{
}
public WalletId(string storeId, string cryptoCode)
{
StoreId = storeId;
CryptoCode = cryptoCode;
}
public string StoreId { get; set; }
public string CryptoCode { get; set; }
public override string ToString()
{
return $"S-{StoreId}-{CryptoCode.ToUpperInvariant()}";
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,6 +1,6 @@
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel);
var cryptoSelector = $("#CryptoCurrency");
function GetSelectedCryptoCode() {

View File

@ -1,6 +1,17 @@
$(function () {

function updateFiatValue() {
if (srvModel.rate !== null) {
var fiatValue = $("#fiatValue");
fiatValue.css("display", "inline");
var amountValue = parseFloat($("#amount-textbox").val());
if (!isNaN(amountValue)) {
fiatValue.text("= " + (srvModel.rate * amountValue).toFixed(srvModel.divisibility) + " " + srvModel.fiat);
}
}
}
$(function () {
var ledgerDetected = false;
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl);
var recommendedFees = "";
var recommendedBalance = "";
var cryptoCode = $("#cryptoCode").val();

View File

@ -29,6 +29,7 @@ In addition to Bitcoin, we support the following crypto currencies:
* Feathercoin
* Groestlcoin
* Litecoin
* Dash
* Monacoin
* Polis
* UFO
@ -72,4 +73,4 @@ On linux:
## Other dependencies
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/blob/master/Deployment.md).
For more information see the documentation [How to deploy a BTCPay server instance](https://github.com/btcpayserver/btcpayserver-doc/#deployment).