Compare commits
144 Commits
Author | SHA1 | Date | |
---|---|---|---|
65f5a38b4a | |||
271cbf682f | |||
a634593903 | |||
af94de93d1 | |||
35f669aa15 | |||
4795bd8108 | |||
29aed99fd1 | |||
aa4519ac30 | |||
752133b01c | |||
fe0c21ba08 | |||
90904a6b5e | |||
8e3f7ea68d | |||
a239104a28 | |||
3bc232e1da | |||
a1ee09cd85 | |||
b898cc030c | |||
0602353dd2 | |||
9d406923ae | |||
aa8565e3cc | |||
5de330b1f9 | |||
66597aed46 | |||
3071826f06 | |||
c3684eb064 | |||
335dd9e66d | |||
cd1611dbcd | |||
c17793aca9 | |||
01d898b618 | |||
17069c311b | |||
921d072942 | |||
6181e8b3e4 | |||
93fc12bb2e | |||
8e73c1a2f0 | |||
e97c15578d | |||
fd4f4e6aff | |||
cedf8f75e8 | |||
cd0a650df4 | |||
6d6b9e2ba6 | |||
fd915fdc5c | |||
59be813fe9 | |||
465fbdd47f | |||
f220abb716 | |||
db46ca87d7 | |||
d873a1a545 | |||
a464a8702b | |||
63722b932a | |||
698b3c46cd | |||
df81051d07 | |||
ac70a77361 | |||
59a2432af9 | |||
ea4fa8d5d4 | |||
ade3eff75c | |||
db2a2a2b6c | |||
579dcb5af8 | |||
69247dee8a | |||
7b9541b8e9 | |||
a12e4d7f64 | |||
897da9b07a | |||
293525d480 | |||
198e810355 | |||
fe25e00c94 | |||
8b129ab2e5 | |||
770bed54d1 | |||
774817d4ac | |||
b8068b2ae8 | |||
3007a6bbc8 | |||
1c0c8fece2 | |||
c52eee47f0 | |||
f88c98b9d9 | |||
b0e9e10f7e | |||
39d47e33f6 | |||
4b7b6c6327 | |||
a59edc5e8c | |||
5ba322ea6a | |||
b47b4b10cb | |||
c88f391935 | |||
26d3178e93 | |||
1ad27c7827 | |||
4200a8eed5 | |||
daceb7af8e | |||
f703b53bce | |||
2762224f0f | |||
86fc64d184 | |||
726cd6fd49 | |||
be1c4666e0 | |||
b75dfc4191 | |||
97815f8daf | |||
af16e1db69 | |||
5f6913b3a2 | |||
2b31af80cb | |||
3f9889d374 | |||
f8189c64a4 | |||
c9b5f89f17 | |||
ecb82f2cc9 | |||
f340c6eb7f | |||
ba0e080816 | |||
bb3d107309 | |||
8517b222bf | |||
aed32204b5 | |||
6b4eeff3f1 | |||
e3cc589ebb | |||
4a152e8ffc | |||
d54a9474d1 | |||
98472211fc | |||
099c9fa1f9 | |||
5226b77ffc | |||
290779ee39 | |||
4f39a8060c | |||
92caa98dfb | |||
df7bb9e2f8 | |||
02a039d695 | |||
b5e4c803aa | |||
2b7c70622f | |||
88779e7129 | |||
6beb7abfd2 | |||
a1ebedc0d1 | |||
d5ad0cdb39 | |||
39fb8dbb6a | |||
58194cb060 | |||
ef165e15bf | |||
bf871c46ec | |||
52331e057f | |||
b59021a0be | |||
8596e16feb | |||
223558c01d | |||
3a91965187 | |||
55d50af39d | |||
3ff293ab7f | |||
7bcf2b5472 | |||
983f34814f | |||
a33e20b46b | |||
bafdcb04ed | |||
cb4468d3b3 | |||
de6f0008a6 | |||
7618eacef1 | |||
842e083ebe | |||
1c510df3fc | |||
c80ffe396e | |||
401a31e5c2 | |||
2df60bd121 | |||
6d10c8a6c1 | |||
44898b5e23 | |||
c0f53db561 | |||
133fb96d28 | |||
98b7ad62af |
BTCPayServer.Tests
BTCPayServer.Tests.csprojBTCPayServerTester.csDockerfileEclairTester.csServerTester.csTestAccount.csUnitTest1.csUnitTestPeusa.csdocker-compose.yml
BTCPayServer
BTCPayNetwork.csBTCPayNetworkProvider.Bitcoin.csBTCPayNetworkProvider.Litecoin.csBTCPayNetworkProvider.csBTCPayServer.csprojBTCPayServer.ruleset
DockerfileLICENSEREADME.mdglobal.jsonConfiguration
Controllers
AccountController.csInvoiceController.PaymentProtocol.csInvoiceController.UI.csInvoiceController.csManageController.csRateController.csStoresController.cs
Data
AddressInvoiceData.csApplicationDbContext.csHistoricalAddressInvoiceData.csInvoiceData.csInvoiceEventData.csStoreData.cs
Eclair
EventAggregator.csEvents
InvoiceDataChangedEvent.csInvoiceEvent.csInvoiceIPNEvent.csInvoiceNeedUpdateEvent.csInvoiceNewAddressEvent.csInvoiceStatusChangedEvent.csInvoiceStopWatchedEvent.csNBXplorerStateChangedEvent.csNewBlockEvent.csTxOutReceivedEvent.cs
ExplorerClientProvider.csExtensions.csHostedServices
Hosting
Logging
Migrations
Models
Payments
Program.csProperties
SearchString.csServices
BTCPayServerEnvironment.csHardwareWalletService.cs
Invoices
Mails
Rates
CachedDefaultRateProviderFactory.csCachedRateProvider.csCoinAverageRateProvider.csIRateProviderFactory.csMockRateProvider.csTweakRateProvider.cs
Stores
Wallets
Views
Home
Invoice
Shared
Stores
wwwroot
css
js
vendor
@ -4,6 +4,7 @@
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -43,21 +43,15 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string CookieFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Uri LTCNBXplorerUri { get; set; }
|
||||
|
||||
public Uri ServerUri
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public ExtKey HDPrivateKey
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Postgres
|
||||
{
|
||||
get; set;
|
||||
@ -73,27 +67,42 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
if (!Directory.Exists(_Directory))
|
||||
Directory.CreateDirectory(_Directory);
|
||||
string chain = ChainType.Regtest.ToNetwork().Name;
|
||||
string chainDirectory = Path.Combine(_Directory, chain);
|
||||
if (!Directory.Exists(chainDirectory))
|
||||
Directory.CreateDirectory(chainDirectory);
|
||||
|
||||
|
||||
HDPrivateKey = new ExtKey();
|
||||
StringBuilder config = new StringBuilder();
|
||||
config.AppendLine($"regtest=1");
|
||||
config.AppendLine($"{chain.ToLowerInvariant()}=1");
|
||||
config.AppendLine($"port={Port}");
|
||||
config.AppendLine($"explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"explorer.cookiefile={CookieFile}");
|
||||
config.AppendLine($"hdpubkey={HDPrivateKey.Neuter().ToString(Network.RegTest)}");
|
||||
config.AppendLine($"chains=btc,ltc");
|
||||
|
||||
config.AppendLine($"btc.explorer.url={NBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"btc.explorer.cookiefile=0");
|
||||
|
||||
config.AppendLine($"ltc.explorer.url={LTCNBXplorerUri.AbsoluteUri}");
|
||||
config.AppendLine($"ltc.explorer.cookiefile=0");
|
||||
|
||||
if (Postgres != null)
|
||||
config.AppendLine($"postgres=" + Postgres);
|
||||
File.WriteAllText(Path.Combine(_Directory, "settings.config"), config.ToString());
|
||||
var confPath = Path.Combine(chainDirectory, "settings.config");
|
||||
File.WriteAllText(confPath, config.ToString());
|
||||
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory });
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
|
||||
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton<IRateProvider>(new MockRateProvider(new Rate("USD", 5000m)));
|
||||
var mockRates = new MockRateProviderFactory();
|
||||
var btc = new MockRateProvider("BTC", new Rate("USD", 5000m));
|
||||
var ltc = new MockRateProvider("LTC", new Rate("USD", 500m));
|
||||
mockRates.AddMock(btc);
|
||||
mockRates.AddMock(ltc);
|
||||
s.AddSingleton<IRateProviderFactory>(mockRates);
|
||||
s.AddLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM microsoft/dotnet:2.0.0-sdk
|
||||
FROM microsoft/dotnet:2.0.5-sdk-2.1.4
|
||||
WORKDIR /app
|
||||
# caches restore result by copying csproj file separately
|
||||
COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj
|
||||
|
@ -12,7 +12,7 @@ namespace BTCPayServer.Tests
|
||||
public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost)
|
||||
{
|
||||
this.parent = parent;
|
||||
RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
|
||||
//RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network);
|
||||
P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost);
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Eclair;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -47,14 +48,20 @@ namespace BTCPayServer.Tests
|
||||
Directory.CreateDirectory(_Directory);
|
||||
|
||||
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_RPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), Network);
|
||||
ExplorerClient = new ExplorerClient(Network, new Uri(GetEnvironment("TESTS_NBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
NetworkProvider = new BTCPayNetworkProvider(ChainType.Regtest);
|
||||
ExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_BTCRPCCONNECTION", "server=http://127.0.0.1:43782;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("BTC").NBitcoinNetwork);
|
||||
LTCExplorerNode = new RPCClient(RPCCredentialString.Parse(GetEnvironment("TESTS_LTCRPCCONNECTION", "server=http://127.0.0.1:43783;ceiwHEbqWI83:DwubwWsoo3")), NetworkProvider.GetNetwork("LTC").NBitcoinNetwork);
|
||||
|
||||
ExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("BTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_BTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
LTCExplorerClient = new ExplorerClient(NetworkProvider.GetNetwork("LTC").NBXplorerNetwork, new Uri(GetEnvironment("TESTS_LTCNBXPLORERURL", "http://127.0.0.1:32838/")));
|
||||
|
||||
PayTester = new BTCPayServerTester(Path.Combine(_Directory, "pay"))
|
||||
{
|
||||
NBXplorerUri = ExplorerClient.Address,
|
||||
LTCNBXplorerUri = LTCExplorerClient.Address,
|
||||
Postgres = GetEnvironment("TESTS_POSTGRES", "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver")
|
||||
};
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString()));
|
||||
PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString(CultureInfo.InvariantCulture)), CultureInfo.InvariantCulture);
|
||||
PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1");
|
||||
PayTester.Start();
|
||||
|
||||
@ -102,15 +109,23 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
return new TestAccount(this);
|
||||
}
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; private set; }
|
||||
public RPCClient ExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public RPCClient LTCExplorerNode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ExplorerClient ExplorerClient
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ExplorerClient LTCExplorerClient { get; set; }
|
||||
|
||||
HttpClient _Http = new HttpClient();
|
||||
|
||||
@ -212,12 +227,6 @@ namespace BTCPayServer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Network.RegTest;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (PayTester != null)
|
||||
|
@ -46,31 +46,55 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<ViewResult>(await store.RequestPairing(pairingCode.ToString()));
|
||||
await store.Pair(pairingCode.ToString(), StoreId);
|
||||
}
|
||||
public StoresController CreateStore()
|
||||
public StoresController CreateStore(string cryptoCode = null)
|
||||
{
|
||||
return CreateStoreAsync().GetAwaiter().GetResult();
|
||||
return CreateStoreAsync(cryptoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<StoresController> CreateStoreAsync()
|
||||
|
||||
public string CryptoCode { get; set; } = "BTC";
|
||||
public async Task<StoresController> CreateStoreAsync(string cryptoCode = null)
|
||||
{
|
||||
ExtKey = new ExtKey().GetWif(parent.Network);
|
||||
cryptoCode = cryptoCode ?? CryptoCode;
|
||||
SupportedNetwork = parent.NetworkProvider.GetNetwork(cryptoCode);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
|
||||
StoreId = store.CreatedStoreId;
|
||||
DerivationScheme = new DerivationStrategyFactory(parent.Network).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
await store.UpdateStore(StoreId, new StoreViewModel()
|
||||
{
|
||||
SpeedPolicy = SpeedPolicy.MediumSpeed
|
||||
});
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
var vm = (StoreViewModel)((ViewResult)await store.UpdateStore(StoreId)).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(StoreId, vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = "BTC",
|
||||
CryptoCurrency = cryptoCode,
|
||||
DerivationSchemeFormat = "BTCPay",
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
return store;
|
||||
}
|
||||
|
||||
public BTCPayNetwork SupportedNetwork { get; set; }
|
||||
|
||||
public void RegisterDerivationScheme(string crytoCode)
|
||||
{
|
||||
RegisterDerivationSchemeAsync(crytoCode).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task RegisterDerivationSchemeAsync(string crytoCode)
|
||||
{
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId);
|
||||
var networkProvider = parent.PayTester.GetService<BTCPayNetworkProvider>();
|
||||
var derivation = new DerivationStrategyFactory(networkProvider.GetNetwork(crytoCode).NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + "-[legacy]");
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
CryptoCurrency = crytoCode,
|
||||
DerivationSchemeFormat = crytoCode,
|
||||
DerivationScheme = derivation.ToString(),
|
||||
Confirmation = true
|
||||
}, "Save");
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
private async Task RegisterAsync()
|
||||
|
@ -24,6 +24,10 @@ using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Eclair;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -43,75 +47,85 @@ namespace BTCPayServer.Tests
|
||||
entity.TxFee = Money.Coins(0.1m);
|
||||
entity.Rate = 5000;
|
||||
|
||||
var cryptoData = entity.GetCryptoData("BTC");
|
||||
Assert.NotNull(cryptoData); // Should use legacy data to build itself
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
|
||||
var accounting = cryptoData.Calculate();
|
||||
// Some check that handling legacy stuff does not break things
|
||||
var paymentMethod = entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
paymentMethod.Calculate();
|
||||
Assert.NotNull(paymentMethod);
|
||||
Assert.Null(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
entity.SetPaymentMethod(new PaymentMethod() { ParentEntity = entity, Rate = entity.Rate, CryptoCode = "BTC", TxFee = entity.TxFee });
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, false).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
Assert.NotNull(entity.GetPaymentMethods(null, true).TryGet("BTC", PaymentTypes.BTCLike));
|
||||
////////////////////
|
||||
|
||||
var accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
|
||||
|
||||
accounting = cryptoData.Calculate();
|
||||
accounting = paymentMethod.Calculate();
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
accounting = cryptoData.Calculate();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
|
||||
|
||||
accounting = cryptoData.Calculate();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
|
||||
accounting = cryptoData.Calculate();
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity = new InvoiceEntity();
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.SetCryptoData(new System.Collections.Generic.Dictionary<string, CryptoData>(new KeyValuePair<string, CryptoData>[] {
|
||||
new KeyValuePair<string,CryptoData>("BTC", new CryptoData()
|
||||
{
|
||||
Rate = 1000,
|
||||
TxFee = Money.Coins(0.1m)
|
||||
}),
|
||||
new KeyValuePair<string,CryptoData>("LTC", new CryptoData()
|
||||
{
|
||||
Rate = 500,
|
||||
TxFee = Money.Coins(0.01m)
|
||||
})
|
||||
}));
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Rate = 1000,
|
||||
TxFee = Money.Coins(0.1m)
|
||||
});
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Rate = 500,
|
||||
TxFee = Money.Coins(0.01m)
|
||||
});
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
cryptoData = entity.GetCryptoData("BTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(5.1m), accounting.Due);
|
||||
|
||||
cryptoData = entity.GetCryptoData("LTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
|
||||
cryptoData = entity.GetCryptoData("BTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m), accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxCount);
|
||||
|
||||
cryptoData = entity.GetCryptoData("LTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2 - 2.0m /* 8.21m */), accounting.Due);
|
||||
Assert.Equal(Money.Coins(0.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
@ -120,16 +134,16 @@ namespace BTCPayServer.Tests
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
|
||||
|
||||
cryptoData = entity.GetCryptoData("BTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(4.2m - 0.5m + 0.01m / 2), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(5.2m + 0.01m / 2), accounting.TotalDue); // The fee for LTC added
|
||||
Assert.Equal(2, accounting.TxCount);
|
||||
|
||||
cryptoData = entity.GetCryptoData("LTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(8.21m - 1.0m + 0.01m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m), accounting.Paid);
|
||||
@ -139,8 +153,8 @@ namespace BTCPayServer.Tests
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
|
||||
|
||||
cryptoData = entity.GetCryptoData("BTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m) + remaining, accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(1.5m) + remaining, accounting.Paid);
|
||||
@ -148,8 +162,8 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(accounting.Paid, accounting.TotalDue);
|
||||
Assert.Equal(2, accounting.TxCount);
|
||||
|
||||
cryptoData = entity.GetCryptoData("LTC");
|
||||
accounting = cryptoData.Calculate();
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("LTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.0m), accounting.CryptoPaid);
|
||||
Assert.Equal(Money.Coins(3.0m) + remaining * 2, accounting.Paid);
|
||||
@ -270,7 +284,8 @@ namespace BTCPayServer.Tests
|
||||
OrderId = "orderId",
|
||||
NotificationURL = callbackServer.GetUri().AbsoluteUri,
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
@ -301,7 +316,7 @@ namespace BTCPayServer.Tests
|
||||
pairingCode = acc.BitPay.RequestClientAuthorization("test1", Facade.Merchant);
|
||||
var store2 = acc.CreateStore();
|
||||
store2.Pair(pairingCode.ToString(), store2.CreatedStoreId).GetAwaiter().GetResult();
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage);
|
||||
Assert.Contains(nameof(PairingResult.ReusedKey), store2.StatusMessage, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@ -319,24 +334,26 @@ namespace BTCPayServer.Tests
|
||||
Currency = "USD"
|
||||
}, Facade.Merchant);
|
||||
|
||||
var payment1 = Money.Coins(0.04m);
|
||||
var payment2 = Money.Coins(0.08m);
|
||||
var payment1 = invoice.BtcDue + Money.Coins(0.0001m);
|
||||
var payment2 = invoice.BtcDue;
|
||||
var tx1 = new uint256(tester.ExplorerNode.SendCommand("sendtoaddress", new object[]
|
||||
{
|
||||
invoice.BitcoinAddress.ToString(),
|
||||
invoice.BitcoinAddress,
|
||||
payment1.ToString(),
|
||||
null, //comment
|
||||
null, //comment_to
|
||||
false, //subtractfeefromamount
|
||||
true, //replaceable
|
||||
}).ResultString);
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment1, invoice.BtcPaid);
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, tester.Network);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
Assert.Equal("paidOver", invoice.ExceptionStatus.ToString());
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, user.SupportedNetwork.NBitcoinNetwork);
|
||||
});
|
||||
|
||||
var tx = tester.ExplorerNode.GetRawTransaction(new uint256(tx1));
|
||||
@ -344,18 +361,17 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
input.ScriptSig = Script.Empty; //Strip signatures
|
||||
}
|
||||
var change = tx.Outputs.First(o => o.Value != payment1);
|
||||
var output = tx.Outputs.First(o => o.Value == payment1);
|
||||
output.Value = payment2;
|
||||
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
|
||||
change.Value -= (payment2 - payment1) * 2; //Add more fees
|
||||
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
|
||||
tester.ExplorerNode.SendRawTransaction(replaced);
|
||||
var test = tester.ExplorerClient.Sync(user.DerivationScheme, null);
|
||||
var test = tester.ExplorerClient.GetUTXOs(user.DerivationScheme, null);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(payment2, invoice.BtcPaid);
|
||||
Assert.Equal("False", invoice.ExceptionStatus.ToString());
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -384,6 +400,198 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public void CanTweakRate()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
var storeController = tester.PayTester.GetController<StoresController>(user.UserId);
|
||||
var vm = (StoreViewModel)((ViewResult)storeController.UpdateStore(user.StoreId).Result).Model;
|
||||
Assert.Equal(1.0, vm.RateMultiplier);
|
||||
vm.RateMultiplier = 0.5;
|
||||
storeController.UpdateStore(user.StoreId, vm).Wait();
|
||||
|
||||
|
||||
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.True(invoice2.BtcPrice.Almost(invoice1.BtcPrice * 2, 0.00001m));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanHaveLTCOnlyStore()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.CryptoCode = "LTC";
|
||||
user.GrantAccess();
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
Assert.Single(invoice.CryptoInfo);
|
||||
Assert.Equal("LTC", invoice.CryptoInfo[0].CryptoCode);
|
||||
var cashCow = tester.LTCExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.1m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(firstPayment, invoice.CryptoInfo[0].Paid);
|
||||
});
|
||||
|
||||
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
|
||||
|
||||
var controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
|
||||
//////////////////////
|
||||
|
||||
// Despite it is called BitcoinAddress it should be LTC because BTC is not available
|
||||
Assert.Null(invoice.BitcoinAddress);
|
||||
Assert.NotEqual(1.0, invoice.Rate);
|
||||
Assert.NotEqual(invoice.BtcDue, invoice.CryptoInfo[0].Due); // Should be BTC rate
|
||||
cashCow.SendToAddress(invoiceAddress, invoice.CryptoInfo[0].Due);
|
||||
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal("paid", checkout.Status);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanPayWithTwoCurrencies()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
|
||||
// First we try payment with a merchant having only BTC
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
cashCow.Generate(2); // get some money in case
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
});
|
||||
|
||||
Assert.Single(invoice.CryptoInfo); // Only BTC should be presented
|
||||
|
||||
var controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
var checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, null).GetAwaiter().GetResult()).Value;
|
||||
Assert.Single(checkout.AvailableCryptos);
|
||||
Assert.Equal("BTC", checkout.CryptoCode);
|
||||
|
||||
//////////////////////
|
||||
|
||||
// Retry now with LTC enabled
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
cashCow = tester.ExplorerNode;
|
||||
invoiceAddress = BitcoinAddress.Create(invoice.BitcoinAddress, cashCow.Network);
|
||||
firstPayment = Money.Coins(0.04m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Logs.Tester.LogInformation("First payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.True(invoice.BtcPaid == firstPayment);
|
||||
});
|
||||
|
||||
cashCow = tester.LTCExplorerNode;
|
||||
var ltcCryptoInfo = invoice.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "LTC");
|
||||
Assert.NotNull(ltcCryptoInfo);
|
||||
invoiceAddress = BitcoinAddress.Create(ltcCryptoInfo.Address, cashCow.Network);
|
||||
var secondPayment = Money.Coins(decimal.Parse(ltcCryptoInfo.Due, CultureInfo.InvariantCulture));
|
||||
cashCow.Generate(2); // LTC is not worth a lot, so just to make sure we have money...
|
||||
cashCow.SendToAddress(invoiceAddress, secondPayment);
|
||||
Logs.Tester.LogInformation("Second payment sent to " + invoiceAddress);
|
||||
Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal(Money.Zero, invoice.BtcDue);
|
||||
var ltcPaid = invoice.CryptoInfo.First(c => c.CryptoCode == "LTC");
|
||||
Assert.Equal(Money.Zero, ltcPaid.Due);
|
||||
Assert.Equal(secondPayment, ltcPaid.CryptoPaid);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
});
|
||||
|
||||
controller = tester.PayTester.GetController<InvoiceController>(null);
|
||||
checkout = (Models.InvoicingModels.PaymentModel)((JsonResult)controller.GetStatus(invoice.Id, "LTC").GetAwaiter().GetResult()).Value;
|
||||
Assert.Equal(2, checkout.AvailableCryptos.Count);
|
||||
Assert.Equal("LTC", checkout.CryptoCode);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvoiceFlowThroughDifferentStatesCorrectly()
|
||||
{
|
||||
@ -398,8 +606,6 @@ namespace BTCPayServer.Tests
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
//RedirectURL = redirect + "redirect",
|
||||
//NotificationURL = CallbackUri + "/notification",
|
||||
ItemDesc = "Some description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
@ -428,11 +634,11 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("new", invoice.Status);
|
||||
Assert.False((bool)((JValue)invoice.ExceptionStatus).Value);
|
||||
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.DateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime + TimeSpan.FromDays(2)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5)));
|
||||
Assert.Single(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime + TimeSpan.FromDays(1.0)));
|
||||
Assert.Empty(user.BitPay.GetInvoices(invoice.InvoiceTime.UtcDateTime - TimeSpan.FromDays(5), invoice.InvoiceTime.DateTime - TimeSpan.FromDays(1)));
|
||||
|
||||
|
||||
var firstPayment = Money.Coins(0.04m);
|
||||
@ -466,9 +672,9 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(IsMapped(localInvoice, ctx));
|
||||
|
||||
invoiceEntity = repo.GetInvoice(null, invoice.Id, true).GetAwaiter().GetResult();
|
||||
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress.ToString());
|
||||
var historical1 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == invoice.BitcoinAddress);
|
||||
Assert.NotNull(historical1.UnAssigned);
|
||||
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress.ToString());
|
||||
var historical2 = invoiceEntity.HistoricalAddresses.FirstOrDefault(h => h.GetAddress() == localInvoice.BitcoinAddress);
|
||||
Assert.Null(historical2.UnAssigned);
|
||||
invoiceAddress = BitcoinAddress.Create(localInvoice.BitcoinAddress, cashCow.Network);
|
||||
secondPayment = localInvoice.BtcDue;
|
||||
@ -552,17 +758,21 @@ namespace BTCPayServer.Tests
|
||||
var b = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
//Manually check that cache get hit after 10 sec
|
||||
var c = cached.GetRateAsync("JPY").GetAwaiter().GetResult();
|
||||
|
||||
var bitstamp = new CoinAverageRateProvider("BTC") { Exchange = "bitstamp" };
|
||||
var bitstampRate = bitstamp.GetRateAsync("USD").GetAwaiter().GetResult();
|
||||
Assert.Throws<RateUnavailableException>(() => bitstamp.GetRateAsync("XXXXX").GetAwaiter().GetResult());
|
||||
}
|
||||
|
||||
private static bool IsMapped(Invoice invoice, ApplicationDbContext ctx)
|
||||
{
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash;
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetHash() == h) != null;
|
||||
var h = BitcoinAddress.Create(invoice.BitcoinAddress).ScriptPubKey.Hash.ToString();
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
|
@ -13,25 +13,27 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
// Unit test that generates temorary checkout Bitpay page
|
||||
// https://forkbitpay.slack.com/archives/C7M093Z55/p1508293682000217
|
||||
[Fact]
|
||||
public void BitpayCheckout()
|
||||
{
|
||||
var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
var url = new Uri("https://test.bitpay.com/");
|
||||
var btcpay = new Bitpay(key, url);
|
||||
var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
{
|
||||
|
||||
Price = 5.0,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
ItemDesc = "Hello from the otherside"
|
||||
}, Facade.Merchant);
|
||||
// Testnet of Bitpay down
|
||||
//[Fact]
|
||||
//public void BitpayCheckout()
|
||||
//{
|
||||
// var key = new Key(Encoders.Hex.DecodeData("7b70a06f35562873e3dcb46005ed0fe78e1991ad906e56adaaafa40ba861e056"));
|
||||
// var url = new Uri("https://test.bitpay.com/");
|
||||
// var btcpay = new Bitpay(key, url);
|
||||
// var invoice = btcpay.CreateInvoice(new Invoice()
|
||||
// {
|
||||
|
||||
// go to invoice.Url
|
||||
Console.WriteLine(invoice.Url);
|
||||
}
|
||||
// Price = 5.0,
|
||||
// Currency = "USD",
|
||||
// PosData = "posData",
|
||||
// OrderId = "cdfd8a5f-6928-4c3b-ba9b-ddf438029e73",
|
||||
// ItemDesc = "Hello from the otherside"
|
||||
// }, Facade.Merchant);
|
||||
|
||||
// // go to invoice.Url
|
||||
// Console.WriteLine(invoice.Url);
|
||||
//}
|
||||
|
||||
// Generating Extended public key to use on http://localhost:14142/stores/{storeId}
|
||||
[Fact]
|
||||
|
@ -1,4 +1,4 @@
|
||||
version: "3"
|
||||
version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
@ -10,17 +10,17 @@ services:
|
||||
context: ..
|
||||
dockerfile: BTCPayServer.Tests/Dockerfile
|
||||
environment:
|
||||
TESTS_RPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://bitcoin-nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://litecoin-nbxplorer:32839/
|
||||
TESTS_BTCRPCCONNECTION: server=http://bitcoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_LTCRPCCONNECTION: server=http://litecoind:43782;ceiwHEbqWI83:DwubwWsoo3
|
||||
TESTS_BTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_LTCNBXPLORERURL: http://nbxplorer:32838/
|
||||
TESTS_POSTGRES: User ID=postgres;Host=postgres;Port=5432;Database=btcpayserver
|
||||
TESTS_PORT: 80
|
||||
TESTS_HOSTNAME: tests
|
||||
expose:
|
||||
- "80"
|
||||
links:
|
||||
- bitcoin-nbxplorer
|
||||
- litecoin-nbxplorer
|
||||
- nbxplorer
|
||||
- postgres
|
||||
extra_hosts:
|
||||
- "tests:127.0.0.1"
|
||||
@ -33,45 +33,32 @@ services:
|
||||
regtest=1
|
||||
connect=bitcoind:39388
|
||||
links:
|
||||
- bitcoin-nbxplorer
|
||||
- litecoin-nbxplorer
|
||||
- nbxplorer
|
||||
- postgres
|
||||
|
||||
bitcoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.40
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.1.13
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
- "32838"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: regtest
|
||||
NBXPLORER_RPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_RPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_RPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_NODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_CHAINS: "btc,ltc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_BTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_BTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_LTCRPCURL: http://litecoind:43782/
|
||||
NBXPLORER_LTCNODEENDPOINT: litecoind:39388
|
||||
NBXPLORER_LTCRPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_LTCRPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
litecoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.40
|
||||
ports:
|
||||
- "32839:32839"
|
||||
expose:
|
||||
- "32839"
|
||||
environment:
|
||||
NBXPLORER_NETWORK: ltc-regtest
|
||||
NBXPLORER_RPCURL: http://litecoind:43782/
|
||||
NBXPLORER_RPCUSER: ceiwHEbqWI83
|
||||
NBXPLORER_RPCPASSWORD: DwubwWsoo3
|
||||
NBXPLORER_NODEENDPOINT: litecoind:39388
|
||||
NBXPLORER_BIND: 0.0.0.0:32839
|
||||
NBXPLORER_VERBOSE: 1
|
||||
NBXPLORER_NOAUTH: 1
|
||||
links:
|
||||
- litecoind
|
||||
- litecoind
|
||||
|
||||
bitcoind:
|
||||
container_name: btcpayserver_dev_bitcoind
|
||||
@ -85,15 +72,11 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:29000
|
||||
zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
txindex=1
|
||||
ports:
|
||||
- "43782:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "29000" # zmq
|
||||
|
||||
litecoind:
|
||||
container_name: btcpayserver_dev_litecoind
|
||||
@ -107,15 +90,11 @@ services:
|
||||
rpcport=43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
zmqpubrawblock=tcp://0.0.0.0:29000
|
||||
zmqpubrawtx=tcp://0.0.0.0:29000
|
||||
txindex=1
|
||||
ports:
|
||||
- "43783:43782"
|
||||
expose:
|
||||
- "43782" # RPC
|
||||
- "39388" # P2P
|
||||
- "29000" # zmq
|
||||
|
||||
postgres:
|
||||
image: postgres:9.6.5
|
||||
|
@ -1,12 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BTCPayDefaultSettings
|
||||
{
|
||||
static BTCPayDefaultSettings()
|
||||
{
|
||||
_Settings = new Dictionary<ChainType, BTCPayDefaultSettings>();
|
||||
foreach (var chainType in new[] { ChainType.Main, ChainType.Test, ChainType.Regtest })
|
||||
{
|
||||
var btcNetwork = (chainType == ChainType.Main ? Network.Main :
|
||||
chainType == ChainType.Regtest ? Network.RegTest :
|
||||
chainType == ChainType.Test ? Network.TestNet : throw new NotSupportedException(chainType.ToString()));
|
||||
|
||||
var settings = new BTCPayDefaultSettings();
|
||||
_Settings.Add(chainType, settings);
|
||||
settings.ChainType = chainType;
|
||||
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", btcNetwork.Name);
|
||||
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
|
||||
settings.DefaultPort = (chainType == ChainType.Main ? 23000 :
|
||||
chainType == ChainType.Regtest ? 23002 :
|
||||
chainType == ChainType.Test ? 23001 : throw new NotSupportedException(chainType.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<ChainType, BTCPayDefaultSettings> _Settings;
|
||||
|
||||
public static BTCPayDefaultSettings GetDefaultSettings(ChainType chainType)
|
||||
{
|
||||
return _Settings[chainType];
|
||||
}
|
||||
|
||||
public string DefaultDataDirectory { get; set; }
|
||||
public string DefaultConfigurationFile { get; set; }
|
||||
public ChainType ChainType { get; internal set; }
|
||||
public int DefaultPort { get; set; }
|
||||
}
|
||||
public class BTCPayNetwork
|
||||
{
|
||||
public Network NBitcoinNetwork { get; set; }
|
||||
@ -25,5 +62,15 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
public string CryptoImagePath { get; set; }
|
||||
public NBXplorer.NBXplorerNetwork NBXplorerNetwork { get; set; }
|
||||
|
||||
public BTCPayDefaultSettings DefaultSettings { get; set; }
|
||||
public KeyPath CoinType { get; internal set; }
|
||||
public int MaxTrackedConfirmation { get; internal set; } = 6;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return CryptoCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
34
BTCPayServer/BTCPayNetworkProvider.Bitcoin.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitBitcoin()
|
||||
{
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("BTC");
|
||||
var coinaverage = new CoinAverageRateProvider("BTC");
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main? "https://www.smartbit.com.au/tx/{0}" : "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("0'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
33
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
33
BTCPayServer/BTCPayNetworkProvider.Litecoin.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using NBitcoin;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
public void InitLitecoin()
|
||||
{
|
||||
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
|
||||
var ltcRate = new CoinAverageRateProvider("LTC");
|
||||
|
||||
var nbxplorerNetwork = NBXplorerNetworkProvider.GetFromCryptoCode("LTC");
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = nbxplorerNetwork.CryptoCode,
|
||||
BlockExplorerLink = NBXplorerNetworkProvider.ChainType == ChainType.Main ? "https://live.blockcypher.com/ltc/tx/{0}/" : "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = nbxplorerNetwork.NBitcoinNetwork,
|
||||
NBXplorerNetwork = nbxplorerNetwork,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg",
|
||||
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(NBXplorerNetworkProvider.ChainType),
|
||||
CoinType = NBXplorerNetworkProvider.ChainType == ChainType.Main ? new KeyPath("2'") : new KeyPath("3'")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,93 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public class BTCPayNetworkProvider
|
||||
public partial class BTCPayNetworkProvider
|
||||
{
|
||||
static BTCPayNetworkProvider()
|
||||
{
|
||||
NBXplorer.Altcoins.Litecoin.Networks.EnsureRegistered();
|
||||
}
|
||||
Dictionary<string, BTCPayNetwork> _Networks = new Dictionary<string, BTCPayNetwork>();
|
||||
public BTCPayNetworkProvider(Network network)
|
||||
|
||||
|
||||
private readonly NBXplorerNetworkProvider _NBXplorerNetworkProvider;
|
||||
public NBXplorerNetworkProvider NBXplorerNetworkProvider
|
||||
{
|
||||
var coinaverage = new CoinAverageRateProvider("BTC");
|
||||
var bitpay = new BitpayRateProvider(new Bitpay(new Key(), new Uri("https://bitpay.com/")));
|
||||
var btcRate = new FallbackRateProvider(new IRateProvider[] { coinaverage, bitpay });
|
||||
|
||||
var ltcRate = new CoinAverageRateProvider("LTC");
|
||||
if (network == Network.Main)
|
||||
get
|
||||
{
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
BlockExplorerLink = "https://www.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = Network.Main,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
|
||||
});
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
BlockExplorerLink = "https://live.blockcypher.com/ltc/tx/{0}/",
|
||||
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Mainnet,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
|
||||
});
|
||||
return _NBXplorerNetworkProvider;
|
||||
}
|
||||
}
|
||||
|
||||
if (network == Network.TestNet)
|
||||
{
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = Network.TestNet,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
|
||||
});
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Testnet,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
|
||||
});
|
||||
}
|
||||
|
||||
if (network == Network.RegTest)
|
||||
{
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
BlockExplorerLink = "https://testnet.smartbit.com.au/tx/{0}",
|
||||
NBitcoinNetwork = Network.RegTest,
|
||||
UriScheme = "bitcoin",
|
||||
DefaultRateProvider = btcRate,
|
||||
CryptoImagePath = "imlegacy/bitcoin-symbol.svg"
|
||||
});
|
||||
Add(new BTCPayNetwork()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
BlockExplorerLink = "http://explorer.litecointools.com/tx/{0}",
|
||||
NBitcoinNetwork = NBXplorer.Altcoins.Litecoin.Networks.Regtest,
|
||||
UriScheme = "litecoin",
|
||||
DefaultRateProvider = ltcRate,
|
||||
CryptoImagePath = "imlegacy/litecoin-symbol.svg"
|
||||
});
|
||||
}
|
||||
public BTCPayNetworkProvider(ChainType chainType)
|
||||
{
|
||||
_NBXplorerNetworkProvider = new NBXplorerNetworkProvider(chainType);
|
||||
InitBitcoin();
|
||||
InitLitecoin();
|
||||
}
|
||||
|
||||
[Obsolete("To use only for legacy stuff")]
|
||||
|
@ -2,7 +2,8 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.0.57</Version>
|
||||
<Version>1.0.1.36</Version>
|
||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Build\dockerfiles\**" />
|
||||
@ -20,23 +21,26 @@
|
||||
<PackageReference Include="Hangfire" Version="1.6.17" />
|
||||
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
|
||||
<PackageReference Include="Hangfire.PostgreSql" Version="1.4.8.1" />
|
||||
<PackageReference Include="LedgerWallet" Version="1.0.1.32" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.6.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.14" />
|
||||
<PackageReference Include="Microsoft.NetCore.Analyzers" Version="2.6.0" />
|
||||
<PackageReference Include="NBitcoin" Version="4.0.0.55" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.17" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.26" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.1.9" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="2.0.1" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
|
||||
<PackageReference Include="System.Xml.XmlSerializer" Version="4.0.11" />
|
||||
<PackageReference Include="Text.Analyzers" Version="2.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -99,4 +103,10 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="devtest.pfx">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
72
BTCPayServer/BTCPayServer.ruleset
Normal file
72
BTCPayServer/BTCPayServer.ruleset
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="Microsoft Managed Recommended Rules" Description="These rules focus on the most critical problems in your code, including potential security holes, application crashes, and other important logic and design errors. It is recommended to include this rule set in any custom rule set you create for your projects." ToolsVersion="10.0">
|
||||
<Localization ResourceAssembly="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.dll" ResourceBaseName="Microsoft.VisualStudio.CodeAnalysis.RuleSets.Strings.Localized">
|
||||
<Name Resource="MinimumRecommendedRules_Name" />
|
||||
<Description Resource="MinimumRecommendedRules_Description" />
|
||||
</Localization>
|
||||
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
|
||||
<Rule Id="CA1001" Action="Warning" />
|
||||
<Rule Id="CA1009" Action="Warning" />
|
||||
<Rule Id="CA1016" Action="Warning" />
|
||||
<Rule Id="CA1033" Action="Warning" />
|
||||
<Rule Id="CA1049" Action="Warning" />
|
||||
<Rule Id="CA1060" Action="Warning" />
|
||||
<Rule Id="CA1061" Action="Warning" />
|
||||
<Rule Id="CA1063" Action="Warning" />
|
||||
<Rule Id="CA1065" Action="Warning" />
|
||||
<Rule Id="CA1301" Action="Warning" />
|
||||
<Rule Id="CA1400" Action="Warning" />
|
||||
<Rule Id="CA1401" Action="Warning" />
|
||||
<Rule Id="CA1403" Action="Warning" />
|
||||
<Rule Id="CA1404" Action="Warning" />
|
||||
<Rule Id="CA1405" Action="Warning" />
|
||||
<Rule Id="CA1410" Action="Warning" />
|
||||
<Rule Id="CA1415" Action="Warning" />
|
||||
<Rule Id="CA1821" Action="Warning" />
|
||||
<Rule Id="CA1900" Action="Warning" />
|
||||
<Rule Id="CA1901" Action="Warning" />
|
||||
<Rule Id="CA2002" Action="Warning" />
|
||||
<Rule Id="CA2100" Action="Warning" />
|
||||
<Rule Id="CA2101" Action="Warning" />
|
||||
<Rule Id="CA2108" Action="Warning" />
|
||||
<Rule Id="CA2111" Action="Warning" />
|
||||
<Rule Id="CA2112" Action="Warning" />
|
||||
<Rule Id="CA2114" Action="Warning" />
|
||||
<Rule Id="CA2116" Action="Warning" />
|
||||
<Rule Id="CA2117" Action="Warning" />
|
||||
<Rule Id="CA2122" Action="Warning" />
|
||||
<Rule Id="CA2123" Action="Warning" />
|
||||
<Rule Id="CA2124" Action="Warning" />
|
||||
<Rule Id="CA2126" Action="Warning" />
|
||||
<Rule Id="CA2131" Action="Warning" />
|
||||
<Rule Id="CA2132" Action="Warning" />
|
||||
<Rule Id="CA2133" Action="Warning" />
|
||||
<Rule Id="CA2134" Action="Warning" />
|
||||
<Rule Id="CA2137" Action="Warning" />
|
||||
<Rule Id="CA2138" Action="Warning" />
|
||||
<Rule Id="CA2140" Action="Warning" />
|
||||
<Rule Id="CA2141" Action="Warning" />
|
||||
<Rule Id="CA2146" Action="Warning" />
|
||||
<Rule Id="CA2147" Action="Warning" />
|
||||
<Rule Id="CA2149" Action="Warning" />
|
||||
<Rule Id="CA2200" Action="Warning" />
|
||||
<Rule Id="CA2202" Action="Warning" />
|
||||
<Rule Id="CA2207" Action="Warning" />
|
||||
<Rule Id="CA2212" Action="Warning" />
|
||||
<Rule Id="CA2213" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="Warning" />
|
||||
<Rule Id="CA2216" Action="Warning" />
|
||||
<Rule Id="CA2220" Action="Warning" />
|
||||
<Rule Id="CA2229" Action="Warning" />
|
||||
<Rule Id="CA2231" Action="Warning" />
|
||||
<Rule Id="CA2232" Action="Warning" />
|
||||
<Rule Id="CA2235" Action="Warning" />
|
||||
<Rule Id="CA2236" Action="Warning" />
|
||||
<Rule Id="CA2237" Action="Warning" />
|
||||
<Rule Id="CA2238" Action="Warning" />
|
||||
<Rule Id="CA2240" Action="Warning" />
|
||||
<Rule Id="CA2241" Action="Warning" />
|
||||
<Rule Id="CA2242" Action="Warning" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Analyzers" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Analyzers" />
|
||||
</RuleSet>
|
@ -13,9 +13,16 @@ using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class NBXplorerConnectionSetting
|
||||
{
|
||||
public string CryptoCode { get; internal set; }
|
||||
public Uri ExplorerUri { get; internal set; }
|
||||
public string CookieFile { get; internal set; }
|
||||
}
|
||||
|
||||
public class BTCPayServerOptions
|
||||
{
|
||||
public Network Network
|
||||
public ChainType ChainType
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -35,52 +42,44 @@ namespace BTCPayServer.Configuration
|
||||
set;
|
||||
}
|
||||
|
||||
public List<NBXplorerConnectionSetting> NBXplorerConnectionSettings
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
public void LoadArgs(IConfiguration conf)
|
||||
{
|
||||
var networkInfo = DefaultConfiguration.GetNetwork(conf);
|
||||
Network = networkInfo?.Network;
|
||||
if (Network == null)
|
||||
throw new ConfigException("Invalid network");
|
||||
ChainType = DefaultConfiguration.GetChainType(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainType);
|
||||
DataDir = conf.GetOrDefault<string>("datadir", defaultSettings.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + ChainType.ToString());
|
||||
|
||||
DataDir = conf.GetOrDefault<string>("datadir", networkInfo.DefaultDataDirectory);
|
||||
Logs.Configuration.LogInformation("Network: " + Network);
|
||||
|
||||
|
||||
|
||||
foreach (var net in new BTCPayNetworkProvider(Network).GetAll())
|
||||
var supportedChains = conf.GetOrDefault<string>("chains", "btc")
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.ToUpperInvariant());
|
||||
var validChains = new List<string>();
|
||||
foreach (var net in new BTCPayNetworkProvider(ChainType).GetAll())
|
||||
{
|
||||
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(net.NBitcoinNetwork.Name);
|
||||
var explorer = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", null);
|
||||
var cookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
|
||||
if (explorer != null)
|
||||
if (supportedChains.Contains(net.CryptoCode))
|
||||
{
|
||||
ExplorerFactories.Add(net.CryptoCode, (n) => CreateExplorerClient(n, explorer, cookieFile));
|
||||
validChains.Add(net.CryptoCode);
|
||||
NBXplorerConnectionSetting setting = new NBXplorerConnectionSetting();
|
||||
setting.CryptoCode = net.CryptoCode;
|
||||
setting.ExplorerUri = conf.GetOrDefault<Uri>($"{net.CryptoCode}.explorer.url", net.NBXplorerNetwork.DefaultSettings.DefaultUrl);
|
||||
setting.CookieFile = conf.GetOrDefault<string>($"{net.CryptoCode}.explorer.cookiefile", net.NBXplorerNetwork.DefaultSettings.DefaultCookieFile);
|
||||
NBXplorerConnectionSettings.Add(setting);
|
||||
}
|
||||
}
|
||||
var invalidChains = String.Join(',', supportedChains.Where(s => !validChains.Contains(s)).ToArray());
|
||||
if(!string.IsNullOrEmpty(invalidChains))
|
||||
throw new ConfigException($"Invalid chains {invalidChains}");
|
||||
|
||||
// Handle legacy explorer.url and explorer.cookiefile
|
||||
if (ExplorerFactories.Count == 0)
|
||||
{
|
||||
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(Network.Name); // Will get BTC info
|
||||
var explorer = conf.GetOrDefault<Uri>($"explorer.url", new Uri(nbxplorer.GetDefaultExplorerUrl(), UriKind.Absolute));
|
||||
var cookieFile = conf.GetOrDefault<string>($"explorer.cookiefile", nbxplorer.GetDefaultCookieFile());
|
||||
ExplorerFactories.Add("BTC", (n) => CreateExplorerClient(n, explorer, cookieFile));
|
||||
}
|
||||
//////
|
||||
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
|
||||
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBitcoinNetwork, uri);
|
||||
if (!explorer.SetCookieAuth(cookieFile))
|
||||
explorer.SetNoAuth();
|
||||
return explorer;
|
||||
}
|
||||
|
||||
public Dictionary<string, Func<BTCPayNetwork, ExplorerClient>> ExplorerFactories = new Dictionary<string, Func<BTCPayNetwork, ExplorerClient>>();
|
||||
public string PostgresConnectionString
|
||||
{
|
||||
get;
|
||||
|
@ -13,7 +13,7 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty, StringComparison.InvariantCulture)];
|
||||
if (str == null)
|
||||
return defaultValue;
|
||||
if (typeof(T) == typeof(bool))
|
||||
@ -32,12 +32,12 @@ namespace BTCPayServer.Configuration
|
||||
return (T)(object)str;
|
||||
else if (typeof(T) == typeof(IPEndPoint))
|
||||
{
|
||||
var separator = str.LastIndexOf(":");
|
||||
var separator = str.LastIndexOf(":", StringComparison.InvariantCulture);
|
||||
if (separator == -1)
|
||||
throw new FormatException();
|
||||
var ip = str.Substring(0, separator);
|
||||
var port = str.Substring(separator + 1);
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port));
|
||||
return (T)(object)new IPEndPoint(IPAddress.Parse(ip), int.Parse(port, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (typeof(T) == typeof(int))
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using System.Text;
|
||||
using CommandLine;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
@ -17,20 +18,24 @@ namespace BTCPayServer.Configuration
|
||||
{
|
||||
protected override CommandLineApplication CreateCommandLineApplicationCore()
|
||||
{
|
||||
var provider = new BTCPayNetworkProvider(ChainType.Main);
|
||||
var chains = string.Join(",", provider.GetAll().Select(n => n.CryptoCode.ToLowerInvariant()).ToArray());
|
||||
CommandLineApplication app = new CommandLineApplication(true)
|
||||
{
|
||||
FullName = "BTCPay\r\nOpen source, self-hosted payment processor.",
|
||||
Name = "BTCPay"
|
||||
};
|
||||
app.HelpOption("-? | -h | --help");
|
||||
app.Option("-n | --network", $"Set the network among ({NetworkInformation.ToStringAll()}) (default: {Network.Main.ToString()})", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest", CommandOptionType.BoolValue);
|
||||
app.Option("-n | --network", $"Set the network among (mainnet,testnet,regtest) (default: mainnet)", CommandOptionType.SingleValue);
|
||||
app.Option("--testnet | -testnet", $"Use testnet (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--regtest | -regtest", $"Use regtest (Deprecated, use --network instead)", CommandOptionType.BoolValue);
|
||||
app.Option("--chains | -c", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Connection string to postgres database (default: sqlite is used)", CommandOptionType.SingleValue);
|
||||
foreach (var network in new BTCPayNetworkProvider(Network.Main).GetAll())
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
app.Option($"--{network.CryptoCode}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: If no explorer is specified, the default for Bitcoin will be selected)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{network.CryptoCode}explorercookiefile", $"Path to the cookie file (default: Default setting of NBXplorer for the network)", CommandOptionType.SingleValue);
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
app.Option($"--{crypto}explorerurl", $"Url of the NBxplorer for {network.CryptoCode} (default: {network.NBXplorerNetwork.DefaultSettings.DefaultUrl})", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue);
|
||||
}
|
||||
app.Option("--externalurl", $"The expected external url of this service, to use if BTCPay is behind a reverse proxy (default: empty, use the incoming HTTP request to figure out)", CommandOptionType.SingleValue);
|
||||
return app;
|
||||
@ -40,57 +45,64 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override string GetDefaultDataDir(IConfiguration conf)
|
||||
{
|
||||
return GetNetwork(conf).DefaultDataDirectory;
|
||||
return BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultDataDirectory;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFile(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
var network = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
var dataDir = conf["datadir"];
|
||||
if (dataDir == null)
|
||||
return network.DefaultConfigurationFile;
|
||||
var fileName = Path.GetFileName(network.DefaultConfigurationFile);
|
||||
return Path.Combine(dataDir, fileName);
|
||||
var chainDir = Path.GetFileName(Path.GetDirectoryName(network.DefaultConfigurationFile));
|
||||
chainDir = Path.Combine(dataDir, chainDir);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(chainDir))
|
||||
Directory.CreateDirectory(chainDir);
|
||||
}
|
||||
catch { }
|
||||
return Path.Combine(chainDir, fileName);
|
||||
}
|
||||
|
||||
public static NetworkInformation GetNetwork(IConfiguration conf)
|
||||
public static ChainType GetChainType(IConfiguration conf)
|
||||
{
|
||||
var network = conf.GetOrDefault<string>("network", null);
|
||||
if (network != null)
|
||||
{
|
||||
var info = NetworkInformation.GetNetworkByName(network);
|
||||
if (info == null)
|
||||
throw new ConfigException($"Invalid network name {network}");
|
||||
return info;
|
||||
var n = Network.GetNetwork(network);
|
||||
if (n == null)
|
||||
{
|
||||
throw new ConfigException($"Invalid network parameter '{network}'");
|
||||
}
|
||||
return n.ToChainType();
|
||||
}
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainType.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainType.Test : ChainType.Main;
|
||||
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? Network.RegTest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? Network.TestNet : Network.Main;
|
||||
|
||||
return NetworkInformation.GetNetworkByName(net.Name);
|
||||
return net;
|
||||
}
|
||||
|
||||
protected override string GetDefaultConfigurationFileTemplate(IConfiguration conf)
|
||||
{
|
||||
var network = GetNetwork(conf);
|
||||
var defaultSettings = BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.AppendLine("### Global settings ###");
|
||||
builder.AppendLine("#testnet=0");
|
||||
builder.AppendLine("#regtest=0");
|
||||
builder.AppendLine("#network=mainnet");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Server settings ###");
|
||||
builder.AppendLine("#port=" + network.DefaultPort);
|
||||
builder.AppendLine("#port=" + defaultSettings.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### Database ###");
|
||||
builder.AppendLine("#postgres=User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=myDataBase;");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("### NBXplorer settings ###");
|
||||
foreach (var n in new BTCPayNetworkProvider(network.Network).GetAll())
|
||||
foreach (var n in new BTCPayNetworkProvider(defaultSettings.ChainType).GetAll())
|
||||
{
|
||||
var nbxplorer = NBXplorer.Configuration.NetworkInformation.GetNetworkByName(n.NBitcoinNetwork.ToString());
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={nbxplorer.GetDefaultExplorerUrl()}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ nbxplorer.GetDefaultCookieFile()}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.url={n.NBXplorerNetwork.DefaultSettings.DefaultUrl}");
|
||||
builder.AppendLine($"#{n.CryptoCode}.explorer.cookiefile={ n.NBXplorerNetwork.DefaultSettings.DefaultCookieFile}");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
@ -99,7 +111,7 @@ namespace BTCPayServer.Configuration
|
||||
|
||||
protected override IPEndPoint GetDefaultEndpoint(IConfiguration conf)
|
||||
{
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), GetNetwork(conf).DefaultPort);
|
||||
return new IPEndPoint(IPAddress.Parse("127.0.0.1"), BTCPayDefaultSettings.GetDefaultSettings(GetChainType(conf)).DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,83 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Configuration
|
||||
{
|
||||
public class NetworkInformation
|
||||
{
|
||||
static NetworkInformation()
|
||||
{
|
||||
_Networks = new Dictionary<string, NetworkInformation>();
|
||||
foreach (var network in new[] { Network.Main, Network.TestNet, Network.RegTest })
|
||||
{
|
||||
NetworkInformation info = new NetworkInformation();
|
||||
info.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("BTCPayServer", network.Name);
|
||||
info.DefaultConfigurationFile = Path.Combine(info.DefaultDataDirectory, "settings.config");
|
||||
info.Network = network;
|
||||
info.DefaultPort = 23002;
|
||||
_Networks.Add(network.Name, info);
|
||||
if (network == Network.Main)
|
||||
{
|
||||
info.DefaultPort = 23000;
|
||||
}
|
||||
if (network == Network.TestNet)
|
||||
{
|
||||
info.DefaultPort = 23001;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, NetworkInformation> _Networks;
|
||||
public static NetworkInformation GetNetworkByName(string name)
|
||||
{
|
||||
var value = _Networks.TryGet(name);
|
||||
if (value != null)
|
||||
return value;
|
||||
|
||||
//Maybe alias ?
|
||||
var network = Network.GetNetwork(name);
|
||||
if (network != null)
|
||||
{
|
||||
value = _Networks.TryGet(network.Name);
|
||||
if (value != null)
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Network Network
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DefaultConfigurationFile
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string DefaultDataDirectory
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public int DefaultPort
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Network.ToString();
|
||||
}
|
||||
|
||||
public static string ToStringAll()
|
||||
{
|
||||
return string.Join(", ", _Networks.Select(n => n.Key).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ using BTCPayServer.Models.AccountViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Logging;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -25,10 +26,10 @@ namespace BTCPayServer.Controllers
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly SignInManager<ApplicationUser> _signInManager;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILogger _logger;
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
@ -36,16 +37,15 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository,
|
||||
ILogger<AccountController> logger)
|
||||
SettingsRepository settingsRepository)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
[TempData]
|
||||
@ -150,7 +150,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||
}
|
||||
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var authenticatorCode = model.TwoFactorCode.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, model.RememberMachine);
|
||||
|
||||
@ -204,7 +204,7 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException($"Unable to load two-factor authentication user.");
|
||||
}
|
||||
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty);
|
||||
var recoveryCode = model.RecoveryCode.Replace(" ", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode);
|
||||
|
||||
@ -259,12 +259,10 @@ namespace BTCPayServer.Controllers
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_logger.LogInformation("User created a new account with password.");
|
||||
|
||||
var admin = await _userManager.GetUsersInRoleAsync(Roles.ServerAdmin);
|
||||
Logs.PayServer.LogInformation($"A new user just registered {user.Email} {(admin.Count == 0 ? "(admin)" : "")}");
|
||||
if (admin.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Admin created.");
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
}
|
||||
@ -291,7 +289,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Test property
|
||||
/// </summary>
|
||||
public string RegisteredUserId
|
||||
|
@ -9,13 +9,14 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{cryptoCode?}")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest")]
|
||||
public async Task<IActionResult> GetInvoiceRequest(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
@ -23,11 +24,12 @@ namespace BTCPayServer.Controllers
|
||||
cryptoCode = "BTC";
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(network))
|
||||
var paymentMethodId = new PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
if (invoice == null || invoice.IsExpired() || network == null || !invoice.Support(paymentMethodId))
|
||||
return NotFound();
|
||||
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoData = dto.CryptoInfo.First(c => c.CryptoCode.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase));
|
||||
var paymentMethod = dto.CryptoInfo.First(c => c.GetpaymentMethodId() == paymentMethodId);
|
||||
PaymentRequest request = new PaymentRequest
|
||||
{
|
||||
DetailsVersion = 1
|
||||
@ -35,7 +37,7 @@ namespace BTCPayServer.Controllers
|
||||
request.Details.Expires = invoice.ExpirationTime;
|
||||
request.Details.Memo = invoice.ProductInformation.ItemDesc;
|
||||
request.Details.Network = network.NBitcoinNetwork;
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = cryptoData.Due, Script = BitcoinAddress.Create(cryptoData.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.Outputs.Add(new PaymentOutput() { Amount = paymentMethod.Due, Script = BitcoinAddress.Create(paymentMethod.Address, network.NBitcoinNetwork).ScriptPubKey });
|
||||
request.Details.MerchantData = Encoding.UTF8.GetBytes(invoice.Id);
|
||||
request.Details.Time = DateTimeOffset.UtcNow;
|
||||
request.Details.PaymentUrl = new Uri(invoice.ServerUrl.WithTrailingSlash() + ($"i/{invoice.Id}"), UriKind.Absolute);
|
||||
@ -66,14 +68,18 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> PostPayment(string invoiceId, string cryptoCode = null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
if (invoice == null || invoice.IsExpired())
|
||||
return NotFound();
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null || invoice == null || invoice.IsExpired() || !invoice.Support(new Services.Invoices.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike)))
|
||||
return NotFound();
|
||||
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
return NotFound();
|
||||
var payment = PaymentMessage.Load(Request.Body);
|
||||
var unused = _Wallet.BroadcastTransactionsAsync(network, payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
|
||||
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
|
||||
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray(), network.NBitcoinNetwork);
|
||||
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Events;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -33,7 +34,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
UserId = GetUserId(),
|
||||
InvoiceId = invoiceId,
|
||||
IncludeAddresses = true
|
||||
IncludeAddresses = true,
|
||||
IncludeEvents = true
|
||||
})).FirstOrDefault();
|
||||
if (invoice == null)
|
||||
return NotFound();
|
||||
@ -47,6 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreLink = Url.Action(nameof(StoresController.UpdateStore), "Stores", new { storeId = store.Id }),
|
||||
Id = invoice.Id,
|
||||
Status = invoice.Status,
|
||||
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" : invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" : "low",
|
||||
RefundEmail = invoice.RefundMail,
|
||||
CreatedDate = invoice.InvoiceTime,
|
||||
ExpirationDate = invoice.ExpirationTime,
|
||||
@ -55,36 +58,65 @@ namespace BTCPayServer.Controllers
|
||||
BuyerInformation = invoice.BuyerInformation,
|
||||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
RedirectUrl = invoice.RedirectURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events
|
||||
};
|
||||
|
||||
foreach (var data in invoice.GetCryptoData())
|
||||
foreach (var data in invoice.GetPaymentMethods(null))
|
||||
{
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode.Equals(data.Key, StringComparison.OrdinalIgnoreCase));
|
||||
var accounting = data.Value.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.Key);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == data.GetId());
|
||||
var accounting = data.Calculate();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(data.GetId().CryptoCode);
|
||||
var cryptoPayment = new InvoiceDetailsModel.CryptoPayment();
|
||||
cryptoPayment.CryptoCode = paymentNetwork.CryptoCode;
|
||||
cryptoPayment.Due = accounting.Due.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Paid = accounting.CryptoPaid.ToString() + $" {paymentNetwork.CryptoCode}";
|
||||
cryptoPayment.Address = data.Value.DepositAddress.ToString();
|
||||
cryptoPayment.Rate = FormatCurrency(data.Value);
|
||||
|
||||
var onchainMethod = data.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod;
|
||||
if(onchainMethod != null)
|
||||
{
|
||||
cryptoPayment.Address = onchainMethod.DepositAddress.ToString();
|
||||
}
|
||||
cryptoPayment.Rate = FormatCurrency(data);
|
||||
cryptoPayment.PaymentUrl = cryptoInfo.PaymentUrls.BIP21;
|
||||
model.CryptoPayments.Add(cryptoPayment);
|
||||
}
|
||||
|
||||
var payments = invoice
|
||||
.Payments
|
||||
.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(async payment =>
|
||||
{
|
||||
var paymentData = (Payments.Bitcoin.BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
var paymentNetwork = _NetworkProvider.GetNetwork(payment.GetCryptoCode());
|
||||
m.CryptoCode = payment.GetCryptoCode();
|
||||
m.DepositAddress = payment.GetScriptPubKey().GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
m.Confirmations = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(payment.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
m.TransactionId = payment.Outpoint.Hash.ToString();
|
||||
m.DepositAddress = paymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork);
|
||||
|
||||
int confirmationCount = 0;
|
||||
if(paymentData.Legacy) // The confirmation count in the paymentData is not up to date
|
||||
{
|
||||
confirmationCount = (await _ExplorerClients.GetExplorerClient(payment.GetCryptoCode())?.GetTransactionAsync(paymentData.Outpoint.Hash))?.Confirmations ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
confirmationCount = paymentData.ConfirmationCount;
|
||||
}
|
||||
if(confirmationCount >= paymentNetwork.MaxTrackedConfirmation)
|
||||
{
|
||||
m.Confirmations = "At least " + (paymentNetwork.MaxTrackedConfirmation);
|
||||
}
|
||||
else
|
||||
{
|
||||
m.Confirmations = confirmationCount.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
m.TransactionId = paymentData.Outpoint.Hash.ToString();
|
||||
m.ReceivedTime = payment.ReceivedTime;
|
||||
m.TransactionLink = string.Format(paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.TransactionLink = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink, m.TransactionId);
|
||||
m.Replaced = !payment.Accounted;
|
||||
return m;
|
||||
})
|
||||
.ToArray();
|
||||
@ -97,75 +129,105 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}")]
|
||||
[Route("i/{invoiceId}/{cryptoCode}")]
|
||||
[Route("i/{invoiceId}/{paymentMethodId}")]
|
||||
[Route("invoice")]
|
||||
[AcceptMediaTypeConstraint("application/bitcoin-paymentrequest", false)]
|
||||
[XFrameOptionsAttribute(null)]
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string cryptoCode = null)
|
||||
public async Task<IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null)
|
||||
{
|
||||
//Keep compatibility with Bitpay
|
||||
invoiceId = invoiceId ?? id;
|
||||
id = invoiceId;
|
||||
////
|
||||
|
||||
var model = await GetInvoiceModel(invoiceId, cryptoCode);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
|
||||
return View(nameof(Checkout), model);
|
||||
}
|
||||
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string cryptoCode)
|
||||
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, string paymentMethodIdStr)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
if (cryptoCode == null)
|
||||
cryptoCode = store.GetDefaultCrypto();
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (invoice == null || network == null || !invoice.Support(network))
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var cryptoData = invoice.GetCryptoData(network);
|
||||
var store = await _StoreRepository.FindStore(invoice.StoreId);
|
||||
bool isDefaultCrypto = false;
|
||||
if (paymentMethodIdStr == null)
|
||||
{
|
||||
paymentMethodIdStr = store.GetDefaultCrypto();
|
||||
isDefaultCrypto = true;
|
||||
}
|
||||
|
||||
var paymentMethodId = PaymentMethodId.Parse(paymentMethodIdStr);
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
|
||||
if (invoice == null || network == null)
|
||||
return null;
|
||||
if (!invoice.Support(paymentMethodId))
|
||||
{
|
||||
if(!isDefaultCrypto)
|
||||
return null;
|
||||
var paymentMethodTemp = invoice.GetPaymentMethods(_NetworkProvider).First();
|
||||
network = paymentMethodTemp.Network;
|
||||
paymentMethodId = paymentMethodTemp.GetId();
|
||||
}
|
||||
|
||||
var paymentMethod = invoice.GetPaymentMethod(paymentMethodId, _NetworkProvider);
|
||||
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
var dto = invoice.EntityToDTO(_NetworkProvider);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.CryptoCode == network.CryptoCode);
|
||||
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
|
||||
|
||||
var currency = invoice.ProductInformation.Currency;
|
||||
var accounting = cryptoData.Calculate();
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var model = new PaymentModel()
|
||||
{
|
||||
CryptoCode = network.CryptoCode,
|
||||
PaymentMethodId = paymentMethodId.ToString(),
|
||||
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
|
||||
OrderId = invoice.OrderId,
|
||||
InvoiceId = invoice.Id,
|
||||
BtcAddress = cryptoData.DepositAddress,
|
||||
BtcAmount = (accounting.TotalDue - cryptoData.TxFee).ToString(),
|
||||
BtcTotalDue = accounting.TotalDue.ToString(),
|
||||
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
|
||||
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
|
||||
BtcDue = accounting.Due.ToString(),
|
||||
CustomerEmail = invoice.RefundMail,
|
||||
ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds),
|
||||
MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds,
|
||||
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
|
||||
ItemDesc = invoice.ProductInformation.ItemDesc,
|
||||
Rate = FormatCurrency(cryptoData),
|
||||
Rate = FormatCurrency(paymentMethod),
|
||||
MerchantRefLink = invoice.RedirectURL ?? "/",
|
||||
StoreName = store.StoreName,
|
||||
TxFees = cryptoData.TxFee.ToString(),
|
||||
InvoiceBitcoinUrl = cryptoInfo.PaymentUrls.BIP21,
|
||||
TxCount = accounting.TxCount,
|
||||
BtcPaid = accounting.Paid.ToString(),
|
||||
Status = invoice.Status,
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath)
|
||||
CryptoImage = "/" + Url.Content(network.CryptoImagePath),
|
||||
NetworkFeeDescription = $"{accounting.TxCount} transaction{(accounting.TxCount > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
.Select(kv=> new PaymentModel.AvailableCrypto()
|
||||
{
|
||||
PaymentMethodId = kv.GetId().ToString(),
|
||||
CryptoImage = "/" + kv.Network.CryptoImagePath,
|
||||
Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() })
|
||||
}).Where(c => c.CryptoImage != "/")
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
|
||||
if (isMultiCurrency)
|
||||
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";
|
||||
|
||||
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
|
||||
model.TimeLeft = PrettyPrint(expiration);
|
||||
return model;
|
||||
}
|
||||
|
||||
private string FormatCurrency(CryptoData cryptoData)
|
||||
private string FormatCurrency(PaymentMethod paymentMethod)
|
||||
{
|
||||
string currency = cryptoData.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(cryptoData.Rate, currency);
|
||||
string currency = paymentMethod.ParentEntity.ProductInformation.Currency;
|
||||
return FormatCurrency(paymentMethod.Rate, currency);
|
||||
}
|
||||
public string FormatCurrency(decimal price, string currency)
|
||||
{
|
||||
@ -176,19 +238,19 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (expiration.Days >= 1)
|
||||
builder.Append(expiration.Days.ToString());
|
||||
builder.Append(expiration.Days.ToString(CultureInfo.InvariantCulture));
|
||||
if (expiration.Hours >= 1)
|
||||
builder.Append(expiration.Hours.ToString("00"));
|
||||
builder.Append($"{expiration.Minutes.ToString("00")}:{expiration.Seconds.ToString("00")}");
|
||||
builder.Append(expiration.Hours.ToString("00", CultureInfo.InvariantCulture));
|
||||
builder.Append($"{expiration.Minutes.ToString("00", CultureInfo.InvariantCulture)}:{expiration.Seconds.ToString("00", CultureInfo.InvariantCulture)}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("i/{invoiceId}/status")]
|
||||
[Route("i/{invoiceId}/{cryptoCode}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string cryptoCode)
|
||||
[Route("i/{invoiceId}/{paymentMethodId}/status")]
|
||||
public async Task<IActionResult> GetStatus(string invoiceId, string paymentMethodId = null)
|
||||
{
|
||||
var model = await GetInvoiceModel(invoiceId, cryptoCode);
|
||||
var model = await GetInvoiceModel(invoiceId, paymentMethodId);
|
||||
if (model == null)
|
||||
return NotFound();
|
||||
return Json(model);
|
||||
@ -208,8 +270,8 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNewAddressEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId)));
|
||||
while (true)
|
||||
{
|
||||
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
|
||||
@ -220,7 +282,7 @@ namespace BTCPayServer.Controllers
|
||||
finally
|
||||
{
|
||||
leases.Dispose();
|
||||
await CloseSocket(webSocket);
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
return new EmptyResult();
|
||||
}
|
||||
@ -236,22 +298,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
catch { await CloseSocket(webSocket); }
|
||||
}
|
||||
|
||||
private static async Task CloseSocket(WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { webSocket.Dispose(); }
|
||||
catch { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -385,6 +432,7 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> InvalidatePaidInvoice(string invoiceId)
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoiceId, 1008, "invoice_markedInvalid"));
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
}
|
||||
|
||||
|
@ -39,13 +39,14 @@ using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class InvoiceController : Controller
|
||||
{
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
BTCPayWallet _Wallet;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
IRateProviderFactory _RateProviders;
|
||||
StoreRepository _StoreRepository;
|
||||
UserManager<ApplicationUser> _UserManager;
|
||||
@ -57,7 +58,7 @@ namespace BTCPayServer.Controllers
|
||||
public InvoiceController(InvoiceRepository invoiceRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
BTCPayWallet wallet,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
IRateProviderFactory rateProviders,
|
||||
StoreRepository storeRepository,
|
||||
EventAggregator eventAggregator,
|
||||
@ -69,7 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
|
||||
_RateProviders = rateProviders ?? throw new ArgumentNullException(nameof(rateProviders));
|
||||
_UserManager = userManager;
|
||||
_FeeProviderFactory = feeProviderFactory ?? throw new ArgumentNullException(nameof(feeProviderFactory));
|
||||
@ -78,11 +79,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)
|
||||
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl)
|
||||
{
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).ToList();
|
||||
var derivationStrategies = store.GetDerivationStrategies(_NetworkProvider).Where(c => _ExplorerClients.IsAvailable(c.Network.CryptoCode)).ToList();
|
||||
if (derivationStrategies.Count == 0)
|
||||
throw new BitpayHttpException(400, "This store has not configured the derivation strategy");
|
||||
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
|
||||
var entity = new InvoiceEntity
|
||||
{
|
||||
InvoiceTime = DateTimeOffset.UtcNow
|
||||
@ -94,7 +95,7 @@ namespace BTCPayServer.Controllers
|
||||
if (notificationUri == null || (notificationUri.Scheme != "http" && notificationUri.Scheme != "https")) //TODO: Filer non routable addresses ?
|
||||
notificationUri = null;
|
||||
EmailAddressAttribute emailValidator = new EmailAddressAttribute();
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(expiryMinutes);
|
||||
entity.ExpirationTime = entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration);
|
||||
entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration);
|
||||
entity.OrderId = invoice.OrderId;
|
||||
entity.ServerUrl = serverUrl;
|
||||
@ -116,45 +117,79 @@ namespace BTCPayServer.Controllers
|
||||
entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy);
|
||||
|
||||
var queries = derivationStrategies
|
||||
.Select(derivationStrategy =>
|
||||
.Select(derivationStrategy => (Wallet: _WalletProvider.GetWallet(derivationStrategy.Network),
|
||||
DerivationStrategy: derivationStrategy.DerivationStrategyBase,
|
||||
Network: derivationStrategy.Network,
|
||||
RateProvider: _RateProviders.GetRateProvider(derivationStrategy.Network, false),
|
||||
FeeRateProvider: _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network)))
|
||||
.Where(_ => _.Wallet != null &&
|
||||
_.FeeRateProvider != null &&
|
||||
_.RateProvider != null)
|
||||
.Select(_ =>
|
||||
{
|
||||
return new
|
||||
{
|
||||
network = derivationStrategy.Network,
|
||||
getFeeRate = _FeeProviderFactory.CreateFeeProvider(derivationStrategy.Network).GetFeeRateAsync(),
|
||||
getRate = _RateProviders.GetRateProvider(derivationStrategy.Network).GetRateAsync(invoice.Currency),
|
||||
getAddress = _Wallet.ReserveAddressAsync(derivationStrategy)
|
||||
network = _.Network,
|
||||
getFeeRate = _.FeeRateProvider.GetFeeRateAsync(),
|
||||
getRate = storeBlob.ApplyRateRules(_.Network, _.RateProvider).GetRateAsync(invoice.Currency),
|
||||
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
|
||||
};
|
||||
});
|
||||
|
||||
var cryptoDatas = new Dictionary<string, CryptoData>();
|
||||
bool legacyBTCisSet = false;
|
||||
var paymentMethods = new PaymentMethodDictionary();
|
||||
foreach (var q in queries)
|
||||
{
|
||||
CryptoData cryptoData = new CryptoData();
|
||||
cryptoData.CryptoCode = q.network.CryptoCode;
|
||||
cryptoData.FeeRate = (await q.getFeeRate);
|
||||
cryptoData.TxFee = storeBlob.NetworkFeeDisabled ? Money.Zero : cryptoData.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
cryptoData.Rate = await q.getRate;
|
||||
cryptoData.DepositAddress = (await q.getAddress).ToString();
|
||||
|
||||
PaymentMethod paymentMethod = new PaymentMethod();
|
||||
paymentMethod.SetId(new PaymentMethodId(q.network.CryptoCode, PaymentTypes.BTCLike));
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
onchainMethod.FeeRate = (await q.getFeeRate);
|
||||
onchainMethod.TxFee = GetTxFee(storeBlob, onchainMethod.FeeRate); // assume price for 100 bytes
|
||||
paymentMethod.Rate = await q.getRate;
|
||||
onchainMethod.DepositAddress = (await q.getAddress);
|
||||
paymentMethod.SetPaymentMethodDetails(onchainMethod);
|
||||
#pragma warning disable CS0618
|
||||
if (q.network.IsBTC)
|
||||
{
|
||||
entity.TxFee = cryptoData.TxFee;
|
||||
entity.Rate = cryptoData.Rate;
|
||||
entity.DepositAddress = cryptoData.DepositAddress;
|
||||
legacyBTCisSet = true;
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
cryptoDatas.Add(cryptoData.CryptoCode, cryptoData);
|
||||
paymentMethods.Add(paymentMethod);
|
||||
}
|
||||
entity.SetCryptoData(cryptoDatas);
|
||||
|
||||
if (!legacyBTCisSet)
|
||||
{
|
||||
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
|
||||
#pragma warning disable CS0618
|
||||
var btc = _NetworkProvider.BTC;
|
||||
var feeProvider = _FeeProviderFactory.CreateFeeProvider(btc);
|
||||
var rateProvider = storeBlob.ApplyRateRules(btc, _RateProviders.GetRateProvider(btc, false));
|
||||
if (feeProvider != null && rateProvider != null)
|
||||
{
|
||||
var gettingFee = feeProvider.GetFeeRateAsync();
|
||||
var gettingRate = rateProvider.GetRateAsync(invoice.Currency);
|
||||
entity.TxFee = GetTxFee(storeBlob, await gettingFee);
|
||||
entity.Rate = await gettingRate;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
|
||||
_EventAggregator.Publish(new Events.InvoiceCreatedEvent(entity.Id));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
|
||||
private static Money GetTxFee(StoreBlob storeBlob, FeeRate feeRate)
|
||||
{
|
||||
return storeBlob.NetworkFeeDisabled ? Money.Zero : feeRate.GetFee(100);
|
||||
}
|
||||
|
||||
private SpeedPolicy ParseSpeedPolicy(string transactionSpeed, SpeedPolicy defaultPolicy)
|
||||
{
|
||||
if (transactionSpeed == null)
|
||||
|
@ -20,6 +20,7 @@ using NBitcoin;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using System.Globalization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
@ -33,7 +34,6 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ILogger _logger;
|
||||
private readonly UrlEncoder _urlEncoder;
|
||||
TokenRepository _TokenRepository;
|
||||
private readonly BTCPayWallet _Wallet;
|
||||
IHostingEnvironment _Env;
|
||||
StoreRepository _StoreRepository;
|
||||
|
||||
@ -47,7 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
ILogger<ManageController> logger,
|
||||
UrlEncoder urlEncoder,
|
||||
TokenRepository tokenRepository,
|
||||
BTCPayWallet wallet,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
StoreRepository storeRepository,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
@ -57,7 +57,6 @@ namespace BTCPayServer.Controllers
|
||||
_logger = logger;
|
||||
_urlEncoder = urlEncoder;
|
||||
_TokenRepository = tokenRepository;
|
||||
_Wallet = wallet;
|
||||
_Env = env;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
@ -436,7 +435,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
// Strip spaces and hypens
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
|
||||
var verificationCode = model.Code.Replace(" ", string.Empty, StringComparison.InvariantCulture).Replace("-", string.Empty, StringComparison.InvariantCulture);
|
||||
|
||||
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
|
||||
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
|
||||
@ -526,7 +525,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
private string GenerateQrCodeUri(string email, string unformattedKey)
|
||||
{
|
||||
return string.Format(
|
||||
return string.Format(CultureInfo.InvariantCulture,
|
||||
AuthenicatorUriFormat,
|
||||
_urlEncoder.Encode("BTCPayServer"),
|
||||
_urlEncoder.Encode(email),
|
||||
|
@ -7,34 +7,68 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class RateController : Controller
|
||||
{
|
||||
IRateProvider _RateProvider;
|
||||
IRateProviderFactory _RateProviderFactory;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
CurrencyNameTable _CurrencyNameTable;
|
||||
public RateController(IRateProvider rateProvider, CurrencyNameTable currencyNameTable)
|
||||
StoreRepository _StoreRepo;
|
||||
public RateController(
|
||||
IRateProviderFactory rateProviderFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
StoreRepository storeRepo,
|
||||
CurrencyNameTable currencyNameTable)
|
||||
{
|
||||
_RateProvider = rateProvider ?? throw new ArgumentNullException(nameof(rateProvider));
|
||||
_RateProviderFactory = rateProviderFactory ?? throw new ArgumentNullException(nameof(rateProviderFactory));
|
||||
_NetworkProvider = networkProvider;
|
||||
_StoreRepo = storeRepo;
|
||||
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
}
|
||||
|
||||
[Route("rates")]
|
||||
[HttpGet]
|
||||
[BitpayAPIConstraint]
|
||||
public async Task<DataWrapper<NBitpayClient.Rate[]>> GetRates()
|
||||
public async Task<IActionResult> GetRates(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
var allRates = (await _RateProvider.GetRatesAsync());
|
||||
return new DataWrapper<NBitpayClient.Rate[]>
|
||||
(allRates.Select(r =>
|
||||
var result = await GetRates2(cryptoCode, storeId);
|
||||
var rates = (result as JsonResult)?.Value as NBitpayClient.Rate[];
|
||||
if(rates == null)
|
||||
return result;
|
||||
return Json(new DataWrapper<NBitpayClient.Rate[]>(rates));
|
||||
}
|
||||
|
||||
[Route("api/rates")]
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetRates2(string cryptoCode = null, string storeId = null)
|
||||
{
|
||||
cryptoCode = cryptoCode ?? "BTC";
|
||||
var network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return NotFound();
|
||||
var rateProvider = _RateProviderFactory.GetRateProvider(network, true);
|
||||
if (rateProvider == null)
|
||||
return NotFound();
|
||||
|
||||
if (storeId != null)
|
||||
{
|
||||
var store = await _StoreRepo.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
rateProvider = store.GetStoreBlob().ApplyRateRules(network, rateProvider);
|
||||
}
|
||||
|
||||
var allRates = (await rateProvider.GetRatesAsync());
|
||||
return Json(allRates.Select(r =>
|
||||
new NBitpayClient.Rate()
|
||||
{
|
||||
Code = r.Currency,
|
||||
Name = _CurrencyNameTable.GetCurrencyData(r.Currency)?.Name,
|
||||
Value = r.Value
|
||||
}).Where(n => n.Name != null).ToArray());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,31 @@
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Fees;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -28,27 +38,33 @@ namespace BTCPayServer.Controllers
|
||||
public class StoresController : Controller
|
||||
{
|
||||
public StoresController(
|
||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||
StoreRepository repo,
|
||||
TokenRepository tokenRepo,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
AccessTokenController tokenController,
|
||||
BTCPayWallet wallet,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
ExplorerClientProvider explorerProvider,
|
||||
IFeeProviderFactory feeRateProvider,
|
||||
IHostingEnvironment env)
|
||||
{
|
||||
_Repo = repo;
|
||||
_TokenRepository = tokenRepo;
|
||||
_UserManager = userManager;
|
||||
_TokenController = tokenController;
|
||||
_Wallet = wallet;
|
||||
_WalletProvider = walletProvider;
|
||||
_Env = env;
|
||||
_NetworkProvider = networkProvider;
|
||||
_ExplorerProvider = explorerProvider;
|
||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
||||
_FeeRateProvider = feeRateProvider;
|
||||
}
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
private ExplorerClientProvider _ExplorerProvider;
|
||||
BTCPayWallet _Wallet;
|
||||
private MvcJsonOptions _MvcJsonOptions;
|
||||
private IFeeProviderFactory _FeeRateProvider;
|
||||
BTCPayWalletProvider _WalletProvider;
|
||||
AccessTokenController _TokenController;
|
||||
StoreRepository _Repo;
|
||||
TokenRepository _TokenRepository;
|
||||
@ -87,6 +103,208 @@ namespace BTCPayServer.Controllers
|
||||
get; set;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/wallet")]
|
||||
public async Task<IActionResult> Wallet(string storeId)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
|
||||
public class GetInfoResult
|
||||
{
|
||||
public int RecommendedSatoshiPerByte { get; set; }
|
||||
public double Balance { get; set; }
|
||||
}
|
||||
|
||||
public class SendToAddressResult
|
||||
{
|
||||
public string TransactionId { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/ws/ledger")]
|
||||
public async Task<IActionResult> LedgerConnection(
|
||||
string storeId,
|
||||
string command,
|
||||
// getinfo
|
||||
string cryptoCode = null,
|
||||
// sendtoaddress
|
||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||
)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
var hw = new HardwareWalletService(webSocket);
|
||||
object result = null;
|
||||
try
|
||||
{
|
||||
BTCPayNetwork network = null;
|
||||
if (cryptoCode != null)
|
||||
{
|
||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
throw new FormatException("Invalid value for crypto code");
|
||||
}
|
||||
|
||||
BitcoinAddress destinationAddress = null;
|
||||
if (destination != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
destinationAddress = BitcoinAddress.Create(destination);
|
||||
}
|
||||
catch { }
|
||||
if (destinationAddress == null)
|
||||
throw new FormatException("Invalid value for destination");
|
||||
}
|
||||
|
||||
FeeRate feeRateValue = null;
|
||||
if (feeRate != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||
}
|
||||
catch { }
|
||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||
throw new FormatException("Invalid value for fee rate");
|
||||
}
|
||||
|
||||
Money amountBTC = null;
|
||||
if (amount != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
amountBTC = Money.Parse(amount);
|
||||
}
|
||||
catch { }
|
||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||
throw new FormatException("Invalid value for amount");
|
||||
}
|
||||
|
||||
bool subsctractFeesValue = false;
|
||||
if (substractFees != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
subsctractFeesValue = bool.Parse(substractFees);
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for substract fees"); }
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test();
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
result = await hw.GetExtPubKey(network);
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
if (strategy == null || !await hw.SupportDerivation(network, strategy))
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||
}
|
||||
|
||||
if (command == "sendtoaddress")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(store, network);
|
||||
var strategyBase = GetDerivationStrategy(store, network);
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
||||
var changeAddress = await change;
|
||||
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,
|
||||
new[] { (destinationAddress as IDestination, amountBTC, subsctractFeesValue) },
|
||||
feeRateValue,
|
||||
changeAddress.Item1,
|
||||
changeAddress.Item2);
|
||||
try
|
||||
{
|
||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||
if (!broadcastResult[0].Success)
|
||||
{
|
||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||
}
|
||||
wallet.InvalidateCache(strategyBase);
|
||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = "Timeout" }; }
|
||||
catch (Exception ex)
|
||||
{ result = new LedgerTestResult() { Success = false, Error = ex.Message }; }
|
||||
|
||||
try
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
UTF8Encoding UTF8NOBOM = new UTF8Encoding(false);
|
||||
var bytes = UTF8NOBOM.GetBytes(JsonConvert.SerializeObject(result, _MvcJsonOptions.SerializerSettings));
|
||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally
|
||||
{
|
||||
await webSocket.CloseSocket();
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = GetDerivationStrategy(store, network);
|
||||
var directStrategy = strategy as DirectDerivationStrategy;
|
||||
if (directStrategy == null)
|
||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||
if (!directStrategy.Segwit)
|
||||
return null;
|
||||
return directStrategy;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
||||
{
|
||||
var strategy = store.GetDerivationStrategies(_NetworkProvider).FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
||||
if (strategy == null)
|
||||
{
|
||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
||||
}
|
||||
|
||||
return strategy.DerivationStrategyBase;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListStores()
|
||||
{
|
||||
@ -95,7 +313,10 @@ namespace BTCPayServer.Controllers
|
||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||
var balances = stores
|
||||
.Select(s => s.GetDerivationStrategies(_NetworkProvider)
|
||||
.Select(async ss => (await _Wallet.GetBalance(ss)).ToString() + " " + ss.Network.CryptoCode))
|
||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
||||
DerivationStrategy: d.DerivationStrategyBase)))
|
||||
.Where(_ => _.Wallet != null)
|
||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
||||
.ToArray();
|
||||
|
||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
||||
@ -113,6 +334,21 @@ namespace BTCPayServer.Controllers
|
||||
return View(result);
|
||||
}
|
||||
|
||||
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
@ -160,6 +396,9 @@ namespace BTCPayServer.Controllers
|
||||
AddDerivationSchemes(store, vm);
|
||||
vm.StatusMessage = StatusMessage;
|
||||
vm.MonitoringExpiration = storeBlob.MonitoringExpiration;
|
||||
vm.InvoiceExpiration = storeBlob.InvoiceExpiration;
|
||||
vm.RateMultiplier = (double)storeBlob.GetRateMultiplier();
|
||||
vm.PreferredExchange = storeBlob.PreferredExchange.IsCoinAverage() ? "coinaverage" : storeBlob.PreferredExchange;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
@ -190,15 +429,17 @@ namespace BTCPayServer.Controllers
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
vm.SetCryptoCurrencies(_ExplorerProvider, selectedScheme);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/derivations")]
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string command, string selectedScheme = null)
|
||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string selectedScheme = null)
|
||||
{
|
||||
selectedScheme = selectedScheme ?? "BTC";
|
||||
vm.ServerUrl = GetStoreUrl(storeId);
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
@ -210,17 +451,38 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
var wallet = _WalletProvider.GetWallet(network);
|
||||
if (wallet == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.CryptoCurrency), "Invalid network");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (command == "Save")
|
||||
|
||||
DerivationStrategyBase strategy = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
store.SetDerivationStrategy(network, vm.DerivationScheme);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
vm.Confirmation = false;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
if (strategy == null || vm.Confirmation)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
await _Wallet.TrackAsync(strategy);
|
||||
vm.DerivationScheme = strategy.ToString();
|
||||
}
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy);
|
||||
store.SetDerivationStrategy(network, vm.DerivationScheme);
|
||||
}
|
||||
catch
|
||||
@ -230,29 +492,22 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = $"Derivation scheme for {vm.CryptoCurrency} has been modified.";
|
||||
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
|
||||
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(vm.DerivationScheme))
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
|
||||
var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
|
||||
var line = strategy.GetLineFor(DerivationFeature.Deposit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(scheme.Network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
catch
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme");
|
||||
var address = line.Derive((uint)i);
|
||||
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
|
||||
}
|
||||
}
|
||||
vm.Confirmation = true;
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
@ -265,6 +520,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
if (model.PreferredExchange != null)
|
||||
model.PreferredExchange = model.PreferredExchange.Trim().ToLowerInvariant();
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
@ -297,12 +554,31 @@ namespace BTCPayServer.Controllers
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
|
||||
blob.SetRateMultiplier(model.RateMultiplier);
|
||||
|
||||
if (store.SetStoreBlob(blob))
|
||||
{
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
if (!blob.PreferredExchange.IsCoinAverage() && newExchange)
|
||||
{
|
||||
using (HttpClient client = new HttpClient())
|
||||
{
|
||||
var rate = await client.GetAsync(model.RateSource);
|
||||
if (rate.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PreferredExchange), $"Unsupported exchange ({model.RateSource})");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
await _Repo.UpdateStore(store);
|
||||
@ -315,7 +591,7 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
|
||||
private DerivationStrategyBase ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
|
||||
{
|
||||
if (format == "Electrum")
|
||||
{
|
||||
@ -329,7 +605,7 @@ namespace BTCPayServer.Controllers
|
||||
var p2wpkh_p2sh = 0x049d7cb2U;
|
||||
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
|
||||
var p2wpkh = 0x4b24746U;
|
||||
electrumMapping.Add(p2wpkh, new string[] { });
|
||||
electrumMapping.Add(p2wpkh, Array.Empty<string>());
|
||||
|
||||
var data = Encoders.Base58Check.DecodeData(derivationScheme);
|
||||
if (data.Length < 4)
|
||||
@ -337,7 +613,7 @@ namespace BTCPayServer.Controllers
|
||||
var prefix = Utils.ToUInt32(data, false);
|
||||
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
|
||||
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
|
||||
var standardPrefix = Utils.ToBytes(network.NBitcoinNetwork == Network.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
data[i] = standardPrefix[i];
|
||||
@ -349,7 +625,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return DerivationStrategy.Parse(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme).ToString(), network);
|
||||
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -415,6 +691,7 @@ namespace BTCPayServer.Controllers
|
||||
pairingCode = ((DataWrapper<List<PairingCodeResponse>>)await _TokenController.Tokens(tokenRequest)).Data[0].PairingCode;
|
||||
}
|
||||
|
||||
GeneratedPairingCode = pairingCode;
|
||||
return RedirectToAction(nameof(RequestPairing), new
|
||||
{
|
||||
pairingCode = pairingCode,
|
||||
@ -422,6 +699,8 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
public string GeneratedPairingCode { get; set; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("/api-tokens")]
|
||||
[Route("{storeId}/Tokens/Create")]
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
@ -20,28 +21,30 @@ namespace BTCPayServer.Data
|
||||
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public ScriptId GetHash()
|
||||
public string GetAddress()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return new ScriptId(Address);
|
||||
return new ScriptId(Address.Substring(0, index));
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
}
|
||||
public AddressInvoiceData SetHash(ScriptId scriptId, string cryptoCode)
|
||||
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
Address = scriptId + "#" + cryptoCode;
|
||||
Address = address + "#" + paymentMethodId?.ToString();
|
||||
return this;
|
||||
}
|
||||
public string GetCryptoCode()
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.LastIndexOf("#", StringComparison.InvariantCulture);
|
||||
// Legacy AddressInvoiceData does not have the paymentMethodId attached to the Address
|
||||
if (index == -1)
|
||||
return "BTC";
|
||||
return Address.Substring(index + 1);
|
||||
return PaymentMethodId.Parse("BTC");
|
||||
/////////////////////////
|
||||
return PaymentMethodId.Parse(Address.Substring(index + 1));
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
|
@ -26,6 +26,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<InvoiceEventData> InvoiceEvents
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public DbSet<HistoricalAddressInvoiceData> HistoricalAddressInvoices
|
||||
{
|
||||
get; set;
|
||||
@ -132,6 +137,15 @@ namespace BTCPayServer.Data
|
||||
o.InvoiceDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.Address
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
|
||||
builder.Entity<InvoiceEventData>()
|
||||
.HasKey(o => new
|
||||
{
|
||||
o.InvoiceDataId,
|
||||
#pragma warning disable CS0618
|
||||
o.UniqueId
|
||||
#pragma warning restore CS0618
|
||||
});
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
if (Address == null)
|
||||
return null;
|
||||
var index = Address.IndexOf("#");
|
||||
var index = Address.IndexOf("#", StringComparison.InvariantCulture);
|
||||
if (index == -1)
|
||||
return Address;
|
||||
return Address.Substring(0, index);
|
||||
|
@ -32,6 +32,11 @@ namespace BTCPayServer.Data
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<InvoiceEventData> Events
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<RefundAddressesData> RefundAddresses
|
||||
{
|
||||
get; set;
|
||||
|
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
22
BTCPayServer/Data/InvoiceEventData.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
public class InvoiceEventData
|
||||
{
|
||||
public string InvoiceDataId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string UniqueId { get; internal set; }
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using System.Threading.Tasks;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Data
|
||||
{
|
||||
@ -99,10 +100,10 @@ namespace BTCPayServer.Data
|
||||
|
||||
if (!existing && string.IsNullOrEmpty(derivationScheme))
|
||||
{
|
||||
if(network.IsBTC)
|
||||
if (network.IsBTC)
|
||||
DerivationStrategy = null;
|
||||
}
|
||||
else if(!existing)
|
||||
else if (!existing)
|
||||
strategies.Add(new JProperty(network.CryptoCode, new JValue(derivationScheme)));
|
||||
// This is deprecated so we don't have to set anymore
|
||||
//if (network.IsBTC)
|
||||
@ -173,10 +174,27 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public class RateRule
|
||||
{
|
||||
public RateRule()
|
||||
{
|
||||
RuleName = "Multiplier";
|
||||
}
|
||||
public string RuleName { get; set; }
|
||||
|
||||
public double Multiplier { get; set; }
|
||||
|
||||
public decimal Apply(BTCPayNetwork network, decimal rate)
|
||||
{
|
||||
return rate * (decimal)Multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
{
|
||||
InvoiceExpiration = 15;
|
||||
MonitoringExpiration = 60;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
@ -190,5 +208,59 @@ namespace BTCPayServer.Data
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[DefaultValue(15)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public void SetRateMultiplier(double rate)
|
||||
{
|
||||
RateRules = new List<RateRule>();
|
||||
RateRules.Add(new RateRule() { Multiplier = rate });
|
||||
}
|
||||
public decimal GetRateMultiplier()
|
||||
{
|
||||
decimal rate = 1.0m;
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rate;
|
||||
foreach (var rule in RateRules)
|
||||
{
|
||||
rate = rule.Apply(null, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
|
||||
{
|
||||
if (!PreferredExchange.IsCoinAverage())
|
||||
{
|
||||
// If the original rateProvider is a cache, use the same inner provider as fallback, and same memory cache to wrap it all
|
||||
if (rateProvider is CachedRateProvider cachedRateProvider)
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
cachedRateProvider.Inner
|
||||
});
|
||||
rateProvider = new CachedRateProvider(network.CryptoCode, rateProvider, cachedRateProvider.MemoryCache) { AdditionalScope = PreferredExchange };
|
||||
}
|
||||
else
|
||||
{
|
||||
rateProvider = new FallbackRateProvider(new IRateProvider[] {
|
||||
new CoinAverageRateProvider(network.CryptoCode) { Exchange = PreferredExchange },
|
||||
rateProvider
|
||||
});
|
||||
}
|
||||
}
|
||||
if (RateRules == null || RateRules.Count == 0)
|
||||
return rateProvider;
|
||||
return new TweakRateProvider(network, rateProvider, RateRules.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public Task<GetInfoResponse> GetInfoAsync()
|
||||
{
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", new object[] { }));
|
||||
return SendCommandAsync<GetInfoResponse>(new RPCRequest("getinfo", Array.Empty<object>()));
|
||||
}
|
||||
|
||||
public async Task<T> SendCommandAsync<T>(RPCRequest request, bool throwIfRPCError = true)
|
||||
@ -104,7 +104,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public async Task<AllChannelResponse[]> AllChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<AllChannelResponse[]>(new RPCRequest("allchannels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string[] Channels()
|
||||
@ -114,7 +114,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public async Task<string[]> ChannelsAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("channels", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Close(string channelId)
|
||||
@ -155,7 +155,7 @@ namespace BTCPayServer.Eclair
|
||||
|
||||
public async Task<string[]> AllNodesAsync()
|
||||
{
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false);
|
||||
return await SendCommandAsync<string[]>(new RPCRequest("allnodes", Array.Empty<object>())).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Uri Address { get; private set; }
|
||||
|
@ -139,7 +139,7 @@ namespace BTCPayServer.Eclair
|
||||
public IEnumerable<LightMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
|
||||
throw new ArgumentOutOfRangeException(nameof(parts), "Parts should be more than 0");
|
||||
long remain;
|
||||
long result = DivRem(_MilliSatoshis, parts, out remain);
|
||||
|
||||
@ -431,7 +431,7 @@ namespace BTCPayServer.Eclair
|
||||
/// <returns></returns>
|
||||
public string ToString(bool fplus, bool trimExcessZero = true)
|
||||
{
|
||||
var fmt = string.Format("{{0:{0}{1}B}}",
|
||||
var fmt = string.Format(CultureInfo.InvariantCulture, "{{0:{0}{1}B}}",
|
||||
(fplus ? "+" : null),
|
||||
(trimExcessZero ? "2" : "11"));
|
||||
return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis);
|
||||
@ -479,7 +479,7 @@ namespace BTCPayServer.Eclair
|
||||
unitToUseInCalc = LightMoneyUnit.BTC;
|
||||
break;
|
||||
}
|
||||
var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc;
|
||||
var val = Convert.ToDecimal(arg, CultureInfo.InvariantCulture) / (long)unitToUseInCalc;
|
||||
var zeros = new string('0', decPos);
|
||||
var rest = new string('#', 11 - decPos);
|
||||
var fmt = plus && val > 0 ? "+" : string.Empty;
|
||||
|
@ -88,7 +88,9 @@ namespace BTCPayServer
|
||||
}
|
||||
}
|
||||
|
||||
Logs.Events.LogInformation($"New event: {evt.ToString()}");
|
||||
var log = evt.ToString();
|
||||
if(!String.IsNullOrEmpty(log))
|
||||
Logs.Events.LogInformation(log);
|
||||
foreach (var sub in actionList)
|
||||
{
|
||||
try
|
||||
|
@ -2,16 +2,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceDataChangedEvent
|
||||
{
|
||||
public InvoiceDataChangedEvent(InvoiceEntity invoice)
|
||||
{
|
||||
InvoiceId = invoice.Id;
|
||||
Status = invoice.Status;
|
||||
ExceptionStatus = invoice.ExceptionStatus;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string Status { get; internal set; }
|
||||
public string ExceptionStatus { get; internal set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} data changed";
|
||||
if (string.IsNullOrEmpty(ExceptionStatus) || ExceptionStatus == "false")
|
||||
{
|
||||
return $"Invoice status is {Status}";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"Invoice status is {Status} (Exception status: {ExceptionStatus})";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
31
BTCPayServer/Events/InvoiceEvent.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public InvoiceEvent(InvoiceEntity invoice, int code, string name) : this(invoice.Id, code, name)
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceEvent(string invoiceId, int code, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} new event: {Name} ({EventCode})";
|
||||
}
|
||||
}
|
||||
}
|
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
35
BTCPayServer/Events/InvoiceIPNEvent.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceIPNEvent
|
||||
{
|
||||
public InvoiceIPNEvent(string invoiceId, int? eventCode, string name)
|
||||
{
|
||||
InvoiceId = invoiceId;
|
||||
EventCode = eventCode;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
public string Error { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string ipnType = "IPN";
|
||||
if(EventCode.HasValue)
|
||||
{
|
||||
ipnType = $"IPN ({EventCode.Value} {Name})";
|
||||
}
|
||||
if (Error == null)
|
||||
return $"{ipnType} sent for invoice {InvoiceId}";
|
||||
return $"Error while sending {ipnType}: {Error}";
|
||||
}
|
||||
}
|
||||
}
|
@ -2,15 +2,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoicePaymentEvent
|
||||
public class InvoiceNeedUpdateEvent
|
||||
{
|
||||
|
||||
public InvoicePaymentEvent(string invoiceId)
|
||||
public InvoiceNeedUpdateEvent(string invoiceId)
|
||||
{
|
||||
if (invoiceId == null)
|
||||
throw new ArgumentNullException(nameof(invoiceId));
|
||||
InvoiceId = invoiceId;
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ namespace BTCPayServer.Events
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} received a payment";
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
25
BTCPayServer/Events/InvoiceNewAddressEvent.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceNewAddressEvent
|
||||
{
|
||||
public InvoiceNewAddressEvent(string invoiceId, string address, BTCPayNetwork network)
|
||||
{
|
||||
Address = address;
|
||||
InvoiceId = invoiceId;
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public string Address { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: New address {Address} for invoice {InvoiceId}";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceStatusChangedEvent
|
||||
{
|
||||
public InvoiceStatusChangedEvent()
|
||||
{
|
||||
|
||||
}
|
||||
public InvoiceStatusChangedEvent(InvoiceEntity invoice, string newState)
|
||||
{
|
||||
OldState = invoice.Status;
|
||||
InvoiceId = invoice.Id;
|
||||
NewState = newState;
|
||||
}
|
||||
public string InvoiceId { get; set; }
|
||||
public string OldState { get; set; }
|
||||
public string NewState { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} changed status: {OldState} => {NewState}";
|
||||
}
|
||||
}
|
||||
}
|
@ -5,18 +5,16 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceCreatedEvent
|
||||
public class InvoiceStopWatchedEvent
|
||||
{
|
||||
public InvoiceCreatedEvent(string id)
|
||||
public InvoiceStopWatchedEvent(string invoiceId)
|
||||
{
|
||||
InvoiceId = id;
|
||||
this.InvoiceId = invoiceId;
|
||||
}
|
||||
|
||||
public string InvoiceId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {InvoiceId} created";
|
||||
return $"Invoice {InvoiceId} is not monitored anymore.";
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,21 @@ using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class NBXplorerErrorEvent
|
||||
{
|
||||
public NBXplorerErrorEvent(BTCPayNetwork network, string errorMessage)
|
||||
{
|
||||
Message = errorMessage;
|
||||
Network = network;
|
||||
}
|
||||
public string Message { get; set; }
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Network.CryptoCode}: NBXplorer error `{Message}`";
|
||||
}
|
||||
}
|
||||
public class NBXplorerStateChangedEvent
|
||||
{
|
||||
public NBXplorerStateChangedEvent(BTCPayNetwork network, NBXplorerState old, NBXplorerState newState)
|
||||
|
@ -7,9 +7,10 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class NewBlockEvent
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public override string ToString()
|
||||
{
|
||||
return "New block";
|
||||
return $"{CryptoCode}: New block";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Events
|
||||
{
|
||||
public class TxOutReceivedEvent
|
||||
{
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public Script ScriptPubKey { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
String address = ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork)?.ToString() ?? ScriptPubKey.ToString();
|
||||
return $"{address} received a transaction";
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Logging;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
@ -13,47 +16,87 @@ namespace BTCPayServer
|
||||
BTCPayServerOptions _Options;
|
||||
|
||||
public BTCPayNetworkProvider NetworkProviders => _NetworkProviders;
|
||||
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options)
|
||||
NBXplorerDashboard _Dashboard;
|
||||
public ExplorerClientProvider(BTCPayNetworkProvider networkProviders, BTCPayServerOptions options, NBXplorerDashboard dashboard)
|
||||
{
|
||||
_Dashboard = dashboard;
|
||||
_NetworkProviders = networkProviders;
|
||||
_Options = options;
|
||||
|
||||
foreach (var setting in options.NBXplorerConnectionSettings)
|
||||
{
|
||||
var cookieFile = setting.CookieFile;
|
||||
if (cookieFile.Trim() == "0" || string.IsNullOrEmpty(cookieFile.Trim()))
|
||||
cookieFile = null;
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Explorer url is {(setting.ExplorerUri.AbsoluteUri ?? "not set")}");
|
||||
Logs.Configuration.LogInformation($"{setting.CryptoCode}: Cookie file is {(setting.CookieFile ?? "not set")}");
|
||||
if (setting.ExplorerUri != null)
|
||||
{
|
||||
_Clients.TryAdd(setting.CryptoCode, CreateExplorerClient(_NetworkProviders.GetNetwork(setting.CryptoCode), setting.ExplorerUri, setting.CookieFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ExplorerClient CreateExplorerClient(BTCPayNetwork n, Uri uri, string cookieFile)
|
||||
{
|
||||
var explorer = new ExplorerClient(n.NBXplorerNetwork, uri);
|
||||
if (cookieFile == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Not using cookie authentication");
|
||||
explorer.SetNoAuth();
|
||||
}
|
||||
if(!explorer.SetCookieAuth(cookieFile))
|
||||
{
|
||||
Logs.Configuration.LogWarning($"{n.CryptoCode}: Using cookie auth against NBXplorer, but {cookieFile} is not found");
|
||||
}
|
||||
return explorer;
|
||||
}
|
||||
|
||||
Dictionary<string, ExplorerClient> _Clients = new Dictionary<string, ExplorerClient>();
|
||||
|
||||
public ExplorerClient GetExplorerClient(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
if (_Options.ExplorerFactories.TryGetValue(network.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
|
||||
{
|
||||
return factory(network);
|
||||
}
|
||||
return null;
|
||||
_Clients.TryGetValue(network.CryptoCode, out ExplorerClient client);
|
||||
return client;
|
||||
}
|
||||
|
||||
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return GetExplorerClient(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return IsAvailable(network.CryptoCode);
|
||||
}
|
||||
|
||||
public bool IsAvailable(string cryptoCode)
|
||||
{
|
||||
return _Clients.ContainsKey(cryptoCode) && _Dashboard.IsFullySynched(cryptoCode);
|
||||
}
|
||||
|
||||
public BTCPayNetwork GetNetwork(string cryptoCode)
|
||||
{
|
||||
var network = _NetworkProviders.GetNetwork(cryptoCode);
|
||||
if (network == null)
|
||||
return null;
|
||||
if (_Options.ExplorerFactories.ContainsKey(network.CryptoCode))
|
||||
if (_Clients.ContainsKey(network.CryptoCode))
|
||||
return network;
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<(BTCPayNetwork, ExplorerClient)> GetAll()
|
||||
{
|
||||
foreach(var net in _NetworkProviders.GetAll())
|
||||
foreach (var net in _NetworkProviders.GetAll())
|
||||
{
|
||||
if(_Options.ExplorerFactories.TryGetValue(net.CryptoCode, out Func<BTCPayNetwork, ExplorerClient> factory))
|
||||
if (_Clients.TryGetValue(net.CryptoCode, out ExplorerClient explorer))
|
||||
{
|
||||
yield return (net, factory(net));
|
||||
yield return (net, explorer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,36 +19,58 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using System.IO;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.WebSockets;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitpayClient;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetDefaultExplorerUrl(this NBXplorer.Configuration.NetworkInformation networkInfo)
|
||||
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
|
||||
{
|
||||
return $"http://127.0.0.1:{networkInfo.DefaultExplorerPort}/";
|
||||
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));
|
||||
}
|
||||
public static string GetDefaultCookieFile(this NBXplorer.Configuration.NetworkInformation networkInfo)
|
||||
public static async Task CloseSocket(this WebSocket webSocket)
|
||||
{
|
||||
return Path.Combine(networkInfo.DefaultDataDirectory, ".cookie");
|
||||
try
|
||||
{
|
||||
if (webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(5000);
|
||||
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { webSocket.Dispose(); } catch { } }
|
||||
}
|
||||
public static bool SupportDropColumn(this Microsoft.EntityFrameworkCore.Migrations.Migration migration, string activeProvider)
|
||||
{
|
||||
return activeProvider != "Microsoft.EntityFrameworkCore.Sqlite";
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, BTCPayNetwork network, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
public static bool IsCoinAverage(this string exchangeName)
|
||||
{
|
||||
string[] coinAverages = new[] { "coinaverage", "bitcoinaverage" };
|
||||
return String.IsNullOrWhiteSpace(exchangeName) ? true : coinAverages.Contains(exchangeName, StringComparer.OrdinalIgnoreCase) ? true : false;
|
||||
}
|
||||
|
||||
public static async Task<Dictionary<uint256, TransactionResult>> GetTransactions(this BTCPayWallet client, uint256[] hashes, CancellationToken cts = default(CancellationToken))
|
||||
{
|
||||
hashes = hashes.Distinct().ToArray();
|
||||
var transactions = hashes
|
||||
.Select(async o => await client.GetTransactionAsync(network, o, cts))
|
||||
.Select(async o => await client.GetTransactionAsync(o, cts))
|
||||
.ToArray();
|
||||
await Task.WhenAll(transactions).ConfigureAwait(false);
|
||||
return transactions.Select(t => t.Result).Where(t => t != null).ToDictionary(o => o.Transaction.GetHash());
|
||||
}
|
||||
public static string WithTrailingSlash(this string str)
|
||||
{
|
||||
if (str.EndsWith("/"))
|
||||
if (str.EndsWith("/", StringComparison.InvariantCulture))
|
||||
return str;
|
||||
return str + "/";
|
||||
}
|
||||
|
@ -37,6 +37,9 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public int? EventCode { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public ILogger Logger
|
||||
@ -63,18 +66,32 @@ namespace BTCPayServer.HostedServices
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
async Task Notify(InvoiceEntity invoice)
|
||||
async Task Notify(InvoiceEntity invoice, int? eventCode = null, string name = null)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
await SendNotification(invoice, cts.Token);
|
||||
if (string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
return;
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name));
|
||||
await SendNotification(invoice, eventCode, name, cts.Token);
|
||||
return;
|
||||
}
|
||||
catch // It fails, it is OK because we try with hangfire after
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice });
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(invoice.Id, eventCode, name)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
var invoiceStr = NBitcoin.JsonConverters.Serializer.ToString(new ScheduledJob() { TryCount = 0, Invoice = invoice, EventCode = eventCode, Message = name });
|
||||
if (!string.IsNullOrEmpty(invoice.NotificationURL))
|
||||
_JobClient.Schedule(() => NotifyHttp(invoiceStr), TimeSpan.Zero);
|
||||
}
|
||||
@ -93,14 +110,45 @@ namespace BTCPayServer.HostedServices
|
||||
CancellationTokenSource cts = new CancellationTokenSource(10000);
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, cts.Token);
|
||||
HttpResponseMessage response = await SendNotification(job.Invoice, job.EventCode, job.Message, cts.Token);
|
||||
reschedule = response.StatusCode != System.Net.HttpStatusCode.OK;
|
||||
Logger.LogInformation("Job " + jobId + " returned " + response.StatusCode);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = reschedule ? $"Unexpected return code: {(int)response.StatusCode}" : null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = "Timeout"
|
||||
});
|
||||
reschedule = true;
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + ex.Message);
|
||||
Logger.LogInformation("Job " + jobId + " timed out");
|
||||
}
|
||||
catch (Exception ex) // It fails, it is OK because we try with hangfire after
|
||||
{
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
reschedule = true;
|
||||
|
||||
List<string> messages = new List<string>();
|
||||
while (ex != null)
|
||||
{
|
||||
messages.Add(ex.Message);
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
string message = String.Join(',', messages.ToArray());
|
||||
Logger.LogInformation("Job " + jobId + " threw exception " + message);
|
||||
|
||||
_EventAggregator.Publish<InvoiceIPNEvent>(new InvoiceIPNEvent(job.Invoice.Id, job.EventCode, job.Message)
|
||||
{
|
||||
Error = $"Unexpected error: {message}"
|
||||
});
|
||||
}
|
||||
finally { cts.Dispose(); _Executing.TryRemove(jobId, out jobId); }
|
||||
|
||||
@ -115,8 +163,23 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoicePaymentNotificationEvent
|
||||
{
|
||||
[JsonProperty("code")]
|
||||
public int Code { get; set; }
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
public class InvoicePaymentNotificationEventWrapper
|
||||
{
|
||||
[JsonProperty("event")]
|
||||
public InvoicePaymentNotificationEvent Event { get; set; }
|
||||
[JsonProperty("data")]
|
||||
public InvoicePaymentNotification Data { get; set; }
|
||||
}
|
||||
|
||||
Encoding UTF8 = new UTF8Encoding(false);
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, CancellationToken cancellation)
|
||||
private async Task<HttpResponseMessage> SendNotification(InvoiceEntity invoice, int? eventCode, string name, CancellationToken cancellation)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
request.Method = HttpMethod.Post;
|
||||
@ -138,8 +201,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
// We keep backward compatibility with bitpay by passing BTC info to the notification
|
||||
// we don't pass other info, as it is a bad idea to use IPN data for logic processing (can be faked)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.CryptoCode == "BTC");
|
||||
if(btcCryptoInfo != null)
|
||||
var btcCryptoInfo = dto.CryptoInfo.FirstOrDefault(c => c.GetpaymentMethodId() == new PaymentMethodId("BTC", Payments.PaymentTypes.BTCLike));
|
||||
if (btcCryptoInfo != null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
notification.Rate = (double)dto.Rate;
|
||||
@ -149,12 +212,84 @@ namespace BTCPayServer.HostedServices
|
||||
notification.BTCPrice = dto.BTCPrice;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
string notificationString = null;
|
||||
if (eventCode.HasValue)
|
||||
{
|
||||
var wrapper = new InvoicePaymentNotificationEventWrapper();
|
||||
wrapper.Data = notification;
|
||||
wrapper.Event = new InvoicePaymentNotificationEvent() { Code = eventCode.Value, Name = name };
|
||||
notificationString = JsonConvert.SerializeObject(wrapper);
|
||||
}
|
||||
else
|
||||
{
|
||||
notificationString = JsonConvert.SerializeObject(notification);
|
||||
}
|
||||
|
||||
request.RequestUri = new Uri(invoice.NotificationURL, UriKind.Absolute);
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(notification), UTF8, "application/json");
|
||||
var response = await _Client.SendAsync(request, cancellation);
|
||||
request.Content = new StringContent(notificationString, UTF8, "application/json");
|
||||
var response = await Enqueue(invoice.Id, async () => await _Client.SendAsync(request, cancellation));
|
||||
return response;
|
||||
}
|
||||
|
||||
Dictionary<string, Task> _SendingRequestsByInvoiceId = new Dictionary<string, Task>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Will make sure only one callback is called at once on the same invoiceId
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="sendRequest"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<T> Enqueue<T>(string id, Func<Task<T>> sendRequest)
|
||||
{
|
||||
Task<T> sending = null;
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
if (_SendingRequestsByInvoiceId.TryGetValue(id, out var executing))
|
||||
{
|
||||
var completion = new TaskCompletionSource<T>();
|
||||
sending = completion.Task;
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
executing.ContinueWith(_ =>
|
||||
{
|
||||
sendRequest()
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
if(t.Status == TaskStatus.RanToCompletion)
|
||||
{
|
||||
completion.TrySetResult(t.Result);
|
||||
}
|
||||
if(t.Status == TaskStatus.Faulted)
|
||||
{
|
||||
completion.TrySetException(t.Exception);
|
||||
}
|
||||
if(t.Status == TaskStatus.Canceled)
|
||||
{
|
||||
completion.TrySetCanceled();
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
sending = sendRequest();
|
||||
_SendingRequestsByInvoiceId.Add(id, sending);
|
||||
}
|
||||
sending.ContinueWith(o =>
|
||||
{
|
||||
lock (_SendingRequestsByInvoiceId)
|
||||
{
|
||||
_SendingRequestsByInvoiceId.TryGetValue(id, out var executing2);
|
||||
if(executing2 == sending)
|
||||
_SendingRequestsByInvoiceId.Remove(id);
|
||||
}
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
return await sending;
|
||||
}
|
||||
|
||||
int MaxTry = 6;
|
||||
|
||||
private static string GetHttpJobId(InvoiceEntity invoice)
|
||||
@ -165,29 +300,60 @@ namespace BTCPayServer.HostedServices
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStatusChangedEvent>(async e =>
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceEvent>(async e =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, e.InvoiceId);
|
||||
await SaveEvent(invoice.Id, e);
|
||||
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.NewState == "expired" ||
|
||||
e.NewState == "paid" ||
|
||||
e.NewState == "invalid" ||
|
||||
e.NewState == "complete"
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed"
|
||||
)
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if(e.NewState == "confirmed")
|
||||
|
||||
if (e.Name == "invoice_confirmed")
|
||||
{
|
||||
await Notify(invoice);
|
||||
}
|
||||
|
||||
if (invoice.ExtendedNotifications)
|
||||
{
|
||||
await Notify(invoice, e.EventCode, e.Name);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceDataChangedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceStopWatchedEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<InvoiceIPNEvent>(async e =>
|
||||
{
|
||||
await SaveEvent(e.InvoiceId, e);
|
||||
}));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task SaveEvent(string invoiceId, object evt)
|
||||
{
|
||||
return _InvoiceRepository.AddInvoiceEvent(invoiceId, evt);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Events;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
@ -24,13 +25,10 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
class UpdateInvoiceContext
|
||||
{
|
||||
public UpdateInvoiceContext()
|
||||
public UpdateInvoiceContext(InvoiceEntity invoice)
|
||||
{
|
||||
|
||||
Invoice = invoice;
|
||||
}
|
||||
|
||||
public Dictionary<BTCPayNetwork, KnownState> KnownStates { get; set; }
|
||||
public Dictionary<BTCPayNetwork, KnownState> ModifiedKnownStates { get; set; } = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
public InvoiceEntity Invoice { get; set; }
|
||||
public List<object> Events { get; set; } = new List<object>();
|
||||
|
||||
@ -45,285 +43,131 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
EventAggregator _EventAggregator;
|
||||
BTCPayWallet _Wallet;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
|
||||
public InvoiceWatcher(
|
||||
IHostingEnvironment env,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayWallet wallet)
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
|
||||
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
|
||||
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_NetworkProvider = networkProvider;
|
||||
}
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
|
||||
async Task NotifyReceived(Script scriptPubKey, BTCPayNetwork network)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceIdFromScriptPubKey(scriptPubKey, network.CryptoCode);
|
||||
if (invoice != null)
|
||||
_WatchRequests.Add(invoice);
|
||||
}
|
||||
|
||||
async Task NotifyBlock()
|
||||
{
|
||||
foreach (var invoice in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
_WatchRequests.Add(invoice);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateInvoice(string invoiceId)
|
||||
{
|
||||
Dictionary<BTCPayNetwork, KnownState> changes = new Dictionary<BTCPayNetwork, KnownState>();
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true).ConfigureAwait(false);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var stateBefore = invoice.Status;
|
||||
var updateContext = new UpdateInvoiceContext()
|
||||
{
|
||||
Invoice = invoice,
|
||||
KnownStates = changes
|
||||
};
|
||||
await UpdateInvoice(updateContext).ConfigureAwait(false);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus).ConfigureAwait(false);
|
||||
_EventAggregator.Publish(new InvoiceDataChangedEvent() { InvoiceId = invoice.Id });
|
||||
}
|
||||
|
||||
var changed = stateBefore != invoice.Status;
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
foreach (var modifiedKnownState in updateContext.ModifiedKnownStates)
|
||||
{
|
||||
changes.AddOrReplace(modifiedKnownState.Key, modifiedKnownState.Value);
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id).ConfigureAwait(false))
|
||||
Logs.PayServer.LogInformation("Stopped watching invoice " + invoiceId);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!changed || _Cts.Token.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
await Task.Delay(10000, _Cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task UpdateInvoice(UpdateInvoiceContext context)
|
||||
{
|
||||
var invoice = context.Invoice;
|
||||
//Fetch unknown payments
|
||||
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var getCoinsResponsesAsync = strategies
|
||||
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network), _Cts.Token))
|
||||
.ToArray();
|
||||
await Task.WhenAll(getCoinsResponsesAsync);
|
||||
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
|
||||
foreach (var response in getCoinsResponses)
|
||||
{
|
||||
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
||||
}
|
||||
var coins = getCoinsResponses.Where(s => s.Coins.Length != 0).FirstOrDefault();
|
||||
bool dirtyAddress = false;
|
||||
if (coins != null)
|
||||
{
|
||||
context.ModifiedKnownStates.Add(coins.Strategy.Network, coins.State);
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
||||
invoice.Payments.Add(payment);
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
}
|
||||
//////
|
||||
var network = coins?.Strategy?.Network ?? _NetworkProvider.GetNetwork(invoice.GetCryptoData().First().Key);
|
||||
var cryptoData = invoice.GetCryptoData(network);
|
||||
var cryptoDataAll = invoice.GetCryptoData();
|
||||
var accounting = cryptoData.Calculate();
|
||||
if (invoice.Status == "new" && invoice.ExpirationTime < DateTimeOffset.UtcNow)
|
||||
{
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1004, "invoice_expired"));
|
||||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
var allPaymentMethods = invoice.GetPaymentMethods(_NetworkProvider);
|
||||
foreach (var paymentMethod in allPaymentMethods.Select(c => c))
|
||||
{
|
||||
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalPaid >= accounting.TotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "paid"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
if (dirtyAddress)
|
||||
{
|
||||
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
|
||||
Logs.PayServer.LogInformation("Generate new " + address);
|
||||
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1);
|
||||
}
|
||||
else if (invoice.SpeedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
}
|
||||
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(totalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "invalid"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceStatusChangedEvent(invoice, "complete"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
|
||||
{
|
||||
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
|
||||
|
||||
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
|
||||
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
|
||||
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
|
||||
foreach (var payment in invoice.Payments)
|
||||
{
|
||||
TransactionResult tx;
|
||||
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
|
||||
{
|
||||
result.Remove(payment.Outpoint);
|
||||
var accounting = paymentMethod.Calculate();
|
||||
var network = _NetworkProvider.GetNetwork(paymentMethod.GetId().CryptoCode);
|
||||
if (network == null)
|
||||
continue;
|
||||
}
|
||||
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
|
||||
var totalPaid = payments.Select(p => p.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
Confirmations = tx.Confirmations,
|
||||
Transaction = tx.Transaction,
|
||||
Payment = payment
|
||||
};
|
||||
payments.Add(accountedPayment);
|
||||
foreach (var txin in tx.Transaction.Inputs)
|
||||
{
|
||||
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
|
||||
if (totalPaid >= accounting.TotalDue)
|
||||
{
|
||||
//We get a double spend
|
||||
var existing = spentTxIn[txin.PrevOut];
|
||||
|
||||
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
|
||||
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
|
||||
if (invoice.Status == "new")
|
||||
{
|
||||
spentTxIn[txin.PrevOut] = accountedPayment;
|
||||
result.Remove(existing.Payment.Outpoint);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1003, "invoice_paidInFull"));
|
||||
invoice.Status = "paid";
|
||||
invoice.ExceptionStatus = totalPaid > accounting.TotalDue ? "paidOver" : null;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (invoice.Status == "expired" && invoice.ExceptionStatus != "paidLate")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidLate";
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1009, "invoice_paidAfterExpiration"));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// Just make sure RBF did not cancelled a payment
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
if (totalPaid == accounting.TotalDue && invoice.ExceptionStatus == "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = null;
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid > accounting.TotalDue && invoice.ExceptionStatus != "paidOver")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidOver";
|
||||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue)
|
||||
{
|
||||
invoice.Status = "new";
|
||||
invoice.ExceptionStatus = totalPaid == Money.Zero ? null : "paidPartial";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentConfirmed(p, invoice.SpeedPolicy, network));
|
||||
|
||||
var totalConfirmed = transactions.Select(t => t.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
|
||||
|
||||
if (// Is after the monitoring deadline
|
||||
(invoice.MonitoringExpiration < DateTimeOffset.UtcNow)
|
||||
&&
|
||||
// And not enough amount confirmed
|
||||
(totalConfirmed < accounting.TotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1013, "invoice_failedToConfirm"));
|
||||
invoice.Status = "invalid";
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1005, "invoice_confirmed"));
|
||||
invoice.Status = "confirmed";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var transactions = payments.Where(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
var totalConfirmed = transactions.Select(t => t.GetValue(allPaymentMethods, paymentMethod.GetId())).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice, 1006, "invoice_completed"));
|
||||
invoice.Status = "complete";
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PaymentEntity> updated = new List<PaymentEntity>();
|
||||
var accountedPayments = payments.Where(p =>
|
||||
{
|
||||
var accounted = result.Contains(p.Payment.Outpoint);
|
||||
if (p.Payment.Accounted != accounted)
|
||||
{
|
||||
p.Payment.Accounted = accounted;
|
||||
updated.Add(p.Payment);
|
||||
}
|
||||
return accounted;
|
||||
}).ToArray();
|
||||
|
||||
await _InvoiceRepository.UpdatePayments(payments);
|
||||
return accountedPayments;
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
@ -336,10 +180,6 @@ namespace BTCPayServer.HostedServices
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_UpdatePendingInvoices != null)
|
||||
{
|
||||
_UpdatePendingInvoices.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,87 +190,138 @@ namespace BTCPayServer.HostedServices
|
||||
_WatchRequests.Add(invoiceId);
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
public void Dispose()
|
||||
private async Task Wait(string invoiceId)
|
||||
{
|
||||
_Cts.Cancel();
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
try
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (invoice.ExpirationTime > now)
|
||||
{
|
||||
await Task.Delay(invoice.ExpirationTime - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
now = DateTimeOffset.UtcNow;
|
||||
if (invoice.MonitoringExpiration > now)
|
||||
{
|
||||
await Task.Delay(invoice.MonitoringExpiration - now, _Cts.Token);
|
||||
}
|
||||
Watch(invoiceId);
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested)
|
||||
{ }
|
||||
|
||||
}
|
||||
|
||||
BlockingCollection<string> _WatchRequests = new BlockingCollection<string>(new ConcurrentQueue<string>());
|
||||
|
||||
Thread _Thread;
|
||||
TaskCompletionSource<bool> _RunningTask;
|
||||
Task _Loop;
|
||||
Task _WaitingInvoices;
|
||||
CancellationTokenSource _Cts;
|
||||
Timer _UpdatePendingInvoices;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Thread = new Thread(Run) { Name = "InvoiceWatcher" };
|
||||
_Thread.Start();
|
||||
_UpdatePendingInvoices = new Timer(async s =>
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
_WaitingInvoices = WaitPendingInvoices();
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceNeedUpdateEvent>(b =>
|
||||
{
|
||||
foreach (var pending in await _InvoiceRepository.GetPendingInvoices())
|
||||
Watch(b.InvoiceId);
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
{
|
||||
_WatchRequests.Add(pending);
|
||||
Watch(b.InvoiceId);
|
||||
await Wait(b.InvoiceId);
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
|
||||
leases.Add(_EventAggregator.Subscribe<Events.NewBlockEvent>(async b => { await NotifyBlock(); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.TxOutReceivedEvent>(async b => { await NotifyReceived(b.ScriptPubKey, b.Network); }));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceCreatedEvent>(b => { Watch(b.InvoiceId); }));
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
{
|
||||
Watch(b.InvoiceId);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Run()
|
||||
private async Task WaitPendingInvoices()
|
||||
{
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(id => Wait(id)).ToArray());
|
||||
_WaitingInvoices = null;
|
||||
}
|
||||
|
||||
async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Start watching invoices");
|
||||
ConcurrentDictionary<string, Lazy<Task>> updating = new ConcurrentDictionary<string, Lazy<Task>>();
|
||||
await Task.Delay(1).ConfigureAwait(false); // Small hack so that the caller does not block on GetConsumingEnumerable
|
||||
try
|
||||
{
|
||||
foreach (var item in _WatchRequests.GetConsumingEnumerable(_Cts.Token))
|
||||
foreach (var invoiceId in _WatchRequests.GetConsumingEnumerable(cancellation))
|
||||
{
|
||||
try
|
||||
int maxLoop = 5;
|
||||
int loopCount = -1;
|
||||
while (!cancellation.IsCancellationRequested && loopCount < maxLoop)
|
||||
{
|
||||
_Cts.Token.ThrowIfCancellationRequested();
|
||||
var localItem = item;
|
||||
// If the invoice is already updating, ignore
|
||||
Lazy<Task> updateInvoice = new Lazy<Task>(() => UpdateInvoice(localItem), false);
|
||||
if (updating.TryAdd(item, updateInvoice))
|
||||
loopCount++;
|
||||
try
|
||||
{
|
||||
updateInvoice.Value.ContinueWith(i => updating.TryRemove(item, out updateInvoice));
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
if (invoice == null)
|
||||
break;
|
||||
var updateContext = new UpdateInvoiceContext(invoice);
|
||||
await UpdateInvoice(updateContext);
|
||||
if (updateContext.Dirty)
|
||||
{
|
||||
await _InvoiceRepository.UpdateInvoiceStatus(invoice.Id, invoice.Status, invoice.ExceptionStatus);
|
||||
updateContext.Events.Add(new InvoiceDataChangedEvent(invoice));
|
||||
}
|
||||
|
||||
foreach (var evt in updateContext.Events)
|
||||
{
|
||||
_EventAggregator.Publish(evt, evt.GetType());
|
||||
}
|
||||
|
||||
if (invoice.Status == "complete" ||
|
||||
((invoice.Status == "invalid" || invoice.Status == "expired") && invoice.MonitoringExpiration < DateTimeOffset.UtcNow))
|
||||
{
|
||||
if (await _InvoiceRepository.RemovePendingInvoice(invoice.Id))
|
||||
_EventAggregator.Publish(new InvoiceStopWatchedEvent(invoice.Id));
|
||||
break;
|
||||
}
|
||||
|
||||
if (updateContext.Events.Count == 0 || cancellation.IsCancellationRequested)
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Unhandled error on watching invoice " + invoiceId);
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
Task.Delay(10000, cancellation)
|
||||
.ContinueWith(t => _WatchRequests.Add(invoiceId), TaskScheduler.Default);
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (!_Cts.Token.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogCritical(ex, $"Error in the InvoiceWatcher loop (Invoice {item})");
|
||||
_Cts.Token.WaitHandle.WaitOne(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch when (cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.WaitAll(updating.Select(c => c.Value.Value).ToArray());
|
||||
}
|
||||
catch (AggregateException) { }
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
Logs.PayServer.LogInformation("Stop watching invoices");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_UpdatePendingInvoices.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
var waitingPendingInvoices = _WaitingInvoices ?? Task.CompletedTask;
|
||||
return Task.WhenAll(waitingPendingInvoices, _Loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,204 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using System.Collections.Concurrent;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Events;
|
||||
|
||||
namespace BTCPayServer.HostedServices
|
||||
{
|
||||
public class NBXplorerListener : IHostedService
|
||||
{
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
IApplicationLifetime _Lifetime;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
private TaskCompletionSource<bool> _RunningTask;
|
||||
private CancellationTokenSource _Cts;
|
||||
NBXplorerDashboard _Dashboards;
|
||||
|
||||
public NBXplorerListener(ExplorerClientProvider explorerClients,
|
||||
NBXplorerDashboard dashboard,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator aggregator, IApplicationLifetime lifetime)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Dashboards = dashboard;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_ExplorerClients = explorerClients;
|
||||
_Aggregator = aggregator;
|
||||
_Lifetime = lifetime;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_ListenPoller != null)
|
||||
{
|
||||
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
|
||||
{
|
||||
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
|
||||
{
|
||||
await Listen(nbxplorerEvent.Network);
|
||||
}
|
||||
}));
|
||||
|
||||
_ListenPoller = new Timer(async s =>
|
||||
{
|
||||
foreach (var nbxplorerState in _Dashboards.GetAll())
|
||||
{
|
||||
if (nbxplorerState.Status != null && nbxplorerState.Status.IsFullySynched)
|
||||
{
|
||||
await Listen(nbxplorerState.Network);
|
||||
}
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceCreatedEvent>(async inv =>
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
|
||||
List<Task> listeningDerivations = new List<Task>();
|
||||
foreach (var notificationSessions in _Sessions)
|
||||
{
|
||||
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
|
||||
if (derivationStrategy != null)
|
||||
{
|
||||
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
|
||||
}
|
||||
}
|
||||
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Listen(BTCPayNetwork network)
|
||||
{
|
||||
bool cleanup = false;
|
||||
try
|
||||
{
|
||||
if (_Sessions.ContainsKey(network.CryptoCode))
|
||||
return;
|
||||
var client = _ExplorerClients.GetExplorerClient(network);
|
||||
if (client == null)
|
||||
return;
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
|
||||
if (!_Sessions.TryAdd(network.CryptoCode, session))
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
cleanup = true;
|
||||
using (session)
|
||||
{
|
||||
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
|
||||
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
|
||||
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
|
||||
switch (newEvent)
|
||||
{
|
||||
case NBXplorer.Models.NewBlockEvent evt:
|
||||
_Aggregator.Publish(new Events.NewBlockEvent());
|
||||
break;
|
||||
case NBXplorer.Models.NewTransactionEvent evt:
|
||||
foreach (var txout in evt.Match.Outputs)
|
||||
{
|
||||
_Aggregator.Publish(new Events.TxOutReceivedEvent()
|
||||
{
|
||||
Network = network,
|
||||
ScriptPubKey = txout.ScriptPubKey
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (cleanup)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
|
||||
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
|
||||
{
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
|
||||
{
|
||||
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
|
||||
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var strategy = GetStrategy(network.CryptoCode, invoice);
|
||||
if (strategy != null)
|
||||
strategies.Add(strategy);
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
|
||||
{
|
||||
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
|
||||
{
|
||||
if (derivationStrategy.Network.CryptoCode == cryptoCode)
|
||||
{
|
||||
return derivationStrategy.DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
@ -27,11 +27,12 @@ namespace BTCPayServer.HostedServices
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public NBXplorerState State { get; set; }
|
||||
public StatusResult Status { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
ConcurrentDictionary<string, NBXplorerSummary> _Summaries = new ConcurrentDictionary<string, NBXplorerSummary>();
|
||||
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status)
|
||||
public void Publish(BTCPayNetwork network, NBXplorerState state, StatusResult status, string error)
|
||||
{
|
||||
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status };
|
||||
var summary = new NBXplorerSummary() { Network = network, State = state, Status = status, Error = error };
|
||||
_Summaries.AddOrUpdate(network.CryptoCode, summary, (k, v) => summary);
|
||||
}
|
||||
|
||||
@ -40,6 +41,11 @@ namespace BTCPayServer.HostedServices
|
||||
return _Summaries.All(s => s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public bool IsFullySynched(string cryptoCode)
|
||||
{
|
||||
return _Summaries.Any(s => s.Key.Equals(cryptoCode, StringComparison.OrdinalIgnoreCase) && s.Value.Status != null && s.Value.Status.IsFullySynched);
|
||||
}
|
||||
|
||||
public IEnumerable<NBXplorerSummary> GetAll()
|
||||
{
|
||||
return _Summaries.Values;
|
||||
@ -82,139 +88,135 @@ namespace BTCPayServer.HostedServices
|
||||
BTCPayNetwork _Network;
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClient _Client;
|
||||
Timer _Timer;
|
||||
ManualResetEventSlim _Idle = new ManualResetEventSlim(true);
|
||||
|
||||
CancellationTokenSource _Cts;
|
||||
Task _Loop;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer = new Timer(Callback, null, 0, (int)TimeSpan.FromMinutes(1.0).TotalMilliseconds);
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Callback(object state)
|
||||
private async Task StartLoop(CancellationToken cancellation)
|
||||
{
|
||||
if (!_Idle.IsSet)
|
||||
return;
|
||||
Logs.PayServer.LogInformation($"Starting listening NBXplorer ({_Network.CryptoCode})");
|
||||
try
|
||||
{
|
||||
_Idle.Reset();
|
||||
CheckStatus().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, "Error while checking NBXplorer state");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_Idle.Set();
|
||||
}
|
||||
}
|
||||
|
||||
async Task CheckStatus()
|
||||
{
|
||||
while (await StepAsync())
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync()
|
||||
{
|
||||
var oldState = State;
|
||||
|
||||
StatusResult status = null;
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await GetStatusWithTimeout();
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (oldState != State)
|
||||
{
|
||||
if (State == NBXplorerState.Synching)
|
||||
{
|
||||
SetInterval(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
else
|
||||
{
|
||||
SetInterval(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
_Dashboard.Publish(_Network, State, status);
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
private void SetInterval(TimeSpan interval)
|
||||
{
|
||||
try
|
||||
{
|
||||
_Timer.Change(0, (int)interval.TotalMilliseconds);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task<StatusResult> GetStatusWithTimeout()
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource();
|
||||
using (cts)
|
||||
{
|
||||
var cancellation = cts.Token;
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var status = await _Client.GetStatusAsync(cancellation).ConfigureAwait(false);
|
||||
return status;
|
||||
while (await StepAsync(cancellation))
|
||||
{
|
||||
|
||||
}
|
||||
await Task.Delay(PollInterval, cancellation);
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Unhandled exception in NBXplorerWaiter ({_Network.CryptoCode})");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellation);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
catch when (cancellation.IsCancellationRequested) { }
|
||||
}
|
||||
|
||||
private async Task<bool> StepAsync(CancellationToken cancellation)
|
||||
{
|
||||
var oldState = State;
|
||||
string error = null;
|
||||
StatusResult status = null;
|
||||
try
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case NBXplorerState.NotConnected:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status != null)
|
||||
{
|
||||
if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Synching:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Ready;
|
||||
}
|
||||
break;
|
||||
case NBXplorerState.Ready:
|
||||
status = await _Client.GetStatusAsync(cancellation);
|
||||
if (status == null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
}
|
||||
else if (!status.IsFullySynched)
|
||||
{
|
||||
State = NBXplorerState.Synching;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception ex) when (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
error = ex.Message;
|
||||
}
|
||||
|
||||
|
||||
if(status == null && error == null)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer does not support this cryptocurrency";
|
||||
|
||||
if(status != null && error == null)
|
||||
{
|
||||
if(status.ChainType != _Network.NBXplorerNetwork.DefaultSettings.ChainType)
|
||||
error = $"{_Network.CryptoCode}: NBXplorer is on a different ChainType (actual: {status.ChainType}, expected: {_Network.NBXplorerNetwork.DefaultSettings.ChainType})";
|
||||
}
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
State = NBXplorerState.NotConnected;
|
||||
status = null;
|
||||
_Aggregator.Publish(new NBXplorerErrorEvent(_Network, error));
|
||||
}
|
||||
|
||||
_Dashboard.Publish(_Network, State, status, error);
|
||||
if (oldState != State)
|
||||
{
|
||||
if (State == NBXplorerState.Synching)
|
||||
{
|
||||
PollInterval = TimeSpan.FromSeconds(10);
|
||||
}
|
||||
else
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1);
|
||||
}
|
||||
_Aggregator.Publish(new NBXplorerStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
return oldState != State;
|
||||
}
|
||||
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
public NBXplorerState State { get; private set; }
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Timer.Dispose();
|
||||
_Timer = null;
|
||||
_Idle.Wait();
|
||||
return Task.CompletedTask;
|
||||
_Cts.Cancel();
|
||||
return _Loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ namespace BTCPayServer.Hosting
|
||||
var dbpath = Path.Combine(opts.DataDir, "InvoiceDB");
|
||||
if (!Directory.Exists(dbpath))
|
||||
Directory.CreateDirectory(dbpath);
|
||||
return new InvoiceRepository(dbContext, dbpath, opts.Network);
|
||||
return new InvoiceRepository(dbContext, dbpath);
|
||||
});
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
services.TryAddSingleton<TokenRepository>();
|
||||
@ -129,12 +129,12 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<BTCPayNetworkProvider>(o =>
|
||||
{
|
||||
var opts = o.GetRequiredService<BTCPayServerOptions>();
|
||||
return new BTCPayNetworkProvider(opts.Network);
|
||||
return new BTCPayNetworkProvider(opts.ChainType);
|
||||
});
|
||||
|
||||
services.TryAddSingleton<NBXplorerDashboard>();
|
||||
services.TryAddSingleton<StoreRepository>();
|
||||
services.TryAddSingleton<BTCPayWallet>();
|
||||
services.TryAddSingleton<BTCPayWalletProvider>();
|
||||
services.TryAddSingleton<CurrencyNameTable>();
|
||||
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
|
||||
{
|
||||
@ -143,14 +143,14 @@ namespace BTCPayServer.Hosting
|
||||
});
|
||||
|
||||
services.AddSingleton<IHostedService, NBXplorerWaiters>();
|
||||
services.AddSingleton<IHostedService, NBXplorerListener>();
|
||||
services.AddSingleton<IHostedService, Payments.Bitcoin.NBXplorerListener>();
|
||||
services.AddSingleton<IHostedService, InvoiceNotificationManager>();
|
||||
services.AddSingleton<IHostedService, InvoiceWatcher>();
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.TryAddSingleton<Bitpay>(o =>
|
||||
{
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().Network == Network.Main)
|
||||
if (o.GetRequiredService<BTCPayServerOptions>().ChainType == ChainType.Main)
|
||||
return new Bitpay(new Key(), new Uri("https://bitpay.com/"));
|
||||
else
|
||||
return new Bitpay(new Key(), new Uri("https://test.bitpay.com/"));
|
||||
@ -205,11 +205,9 @@ namespace BTCPayServer.Hosting
|
||||
act();
|
||||
return;
|
||||
}
|
||||
catch
|
||||
catch when(!cts.IsCancellationRequested)
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
throw;
|
||||
Thread.Sleep(1000);
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -69,7 +70,7 @@ namespace BTCPayServer.Hosting
|
||||
if (BitIdExtensions.CheckBitIDSignature(key, sig, url, body))
|
||||
{
|
||||
var bitid = new BitIdentity(key);
|
||||
httpContext.User = new GenericPrincipal(bitid, new string[0]);
|
||||
httpContext.User = new GenericPrincipal(bitid, Array.Empty<string>());
|
||||
Logs.PayServer.LogDebug($"BitId signature check success for SIN {bitid.SIN}");
|
||||
}
|
||||
}
|
||||
@ -82,6 +83,8 @@ namespace BTCPayServer.Hosting
|
||||
{
|
||||
await _Next(httpContext);
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{ }
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
await HandleBitpayHttpException(httpContext, new BitpayHttpException(401, ex.Message));
|
||||
|
@ -6,6 +6,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
@ -36,6 +37,8 @@ using System.Threading;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
@ -88,6 +91,15 @@ namespace BTCPayServer.Hosting
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
});
|
||||
|
||||
// Needed to debug U2F for ledger support
|
||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
||||
//{
|
||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||
// {
|
||||
// l.UseHttps("devtest.pfx", "toto");
|
||||
// });
|
||||
//});
|
||||
}
|
||||
|
||||
// Big hack, tests fails if only call AddHangfire because Hangfire fail at initializing at the second test run
|
||||
|
@ -6,13 +6,18 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Logging
|
||||
{
|
||||
public class CustomConsoleLogProvider : ILoggerProvider
|
||||
{
|
||||
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
|
||||
ConsoleLoggerProcessor _Processor;
|
||||
public CustomConsoleLogProvider(ConsoleLoggerProcessor processor)
|
||||
{
|
||||
_Processor = processor;
|
||||
}
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new CustomConsoleLogger(categoryName, (a, b) => true, false, _Processor);
|
||||
@ -329,7 +334,8 @@ namespace BTCPayServer.Logging
|
||||
_outputTask = Task.Factory.StartNew(
|
||||
ProcessLogQueue,
|
||||
this,
|
||||
TaskCreationOptions.LongRunning);
|
||||
default(CancellationToken),
|
||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
public virtual void EnqueueMessage(LogMessageEntry message)
|
||||
|
508
BTCPayServer/Migrations/20180114123253_events.Designer.cs
generated
Normal file
508
BTCPayServer/Migrations/20180114123253_events.Designer.cs
generated
Normal file
@ -0,0 +1,508 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20180114123253_events")]
|
||||
partial class events
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "2.0.1-rtm-125");
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Address")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTimeOffset?>("CreatedTime");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Address");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("AddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("Address");
|
||||
|
||||
b.Property<DateTimeOffset>("Assigned");
|
||||
|
||||
b.Property<string>("CryptoCode");
|
||||
|
||||
b.Property<DateTimeOffset?>("UnAssigned");
|
||||
|
||||
b.HasKey("InvoiceDataId", "Address");
|
||||
|
||||
b.ToTable("HistoricalAddressInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<DateTimeOffset>("Created");
|
||||
|
||||
b.Property<string>("CustomerEmail");
|
||||
|
||||
b.Property<string>("ExceptionStatus");
|
||||
|
||||
b.Property<string>("ItemCode");
|
||||
|
||||
b.Property<string>("OrderId");
|
||||
|
||||
b.Property<string>("Status");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<DateTimeOffset>("PairingTime");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SIN");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("PairedSINData");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<DateTime>("DateCreated");
|
||||
|
||||
b.Property<DateTimeOffset>("Expiration");
|
||||
|
||||
b.Property<string>("Facade");
|
||||
|
||||
b.Property<string>("Label");
|
||||
|
||||
b.Property<string>("SIN");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("TokenValue");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PairingCodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<bool>("Accounted");
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("Payments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("PendingInvoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<byte[]>("Blob");
|
||||
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InvoiceDataId");
|
||||
|
||||
b.ToTable("RefundAddresses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("DefaultCrypto");
|
||||
|
||||
b.Property<string>("DerivationStrategies");
|
||||
|
||||
b.Property<string>("DerivationStrategy");
|
||||
|
||||
b.Property<int>("SpeedPolicy");
|
||||
|
||||
b.Property<byte[]>("StoreBlob");
|
||||
|
||||
b.Property<byte[]>("StoreCertificate");
|
||||
|
||||
b.Property<string>("StoreName");
|
||||
|
||||
b.Property<string>("StoreWebsite");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Stores");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.Property<string>("ApplicationUserId");
|
||||
|
||||
b.Property<string>("StoreDataId");
|
||||
|
||||
b.Property<string>("Role");
|
||||
|
||||
b.HasKey("ApplicationUserId", "StoreDataId");
|
||||
|
||||
b.HasIndex("StoreDataId");
|
||||
|
||||
b.ToTable("UserStore");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<int>("AccessFailedCount");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<bool>("EmailConfirmed");
|
||||
|
||||
b.Property<bool>("LockoutEnabled");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("PasswordHash");
|
||||
|
||||
b.Property<string>("PhoneNumber");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed");
|
||||
|
||||
b.Property<bool>("RequiresEmailConfirmation");
|
||||
|
||||
b.Property<string>("SecurityStamp");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken();
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256);
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b.Property<string>("ClaimType");
|
||||
|
||||
b.Property<string>("ClaimValue");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("ProviderKey");
|
||||
|
||||
b.Property<string>("ProviderDisplayName");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.IsRequired();
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("RoleId");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.Property<string>("UserId");
|
||||
|
||||
b.Property<string>("LoginProvider");
|
||||
|
||||
b.Property<string>("Name");
|
||||
|
||||
b.Property<string>("Value");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("AddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("HistoricalAddressInvoices")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany()
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("Payments")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
.WithMany("RefundAddresses")
|
||||
.HasForeignKey("InvoiceDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("ApplicationUserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
|
||||
.WithMany("UserStores")
|
||||
.HasForeignKey("StoreDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||
{
|
||||
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Models.ApplicationUser")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
38
BTCPayServer/Migrations/20180114123253_events.cs
Normal file
38
BTCPayServer/Migrations/20180114123253_events.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
public partial class events : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "InvoiceEvents",
|
||||
columns: table => new
|
||||
{
|
||||
InvoiceDataId = table.Column<string>(nullable: false),
|
||||
UniqueId = table.Column<string>(nullable: false),
|
||||
Message = table.Column<string>(nullable: true),
|
||||
Timestamp = table.Column<DateTimeOffset>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_InvoiceEvents", x => new { x.InvoiceDataId, x.UniqueId });
|
||||
table.ForeignKey(
|
||||
name: "FK_InvoiceEvents_Invoices_InvoiceDataId",
|
||||
column: x => x.InvoiceDataId,
|
||||
principalTable: "Invoices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "InvoiceEvents");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,9 @@
|
||||
// <auto-generated />
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Microsoft.EntityFrameworkCore.Storage.Internal;
|
||||
using System;
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
@ -81,6 +78,21 @@ namespace BTCPayServer.Migrations
|
||||
b.ToTable("Invoices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.Property<string>("InvoiceDataId");
|
||||
|
||||
b.Property<string>("UniqueId");
|
||||
|
||||
b.Property<string>("Message");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp");
|
||||
|
||||
b.HasKey("InvoiceDataId", "UniqueId");
|
||||
|
||||
b.ToTable("InvoiceEvents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
@ -407,6 +419,14 @@ namespace BTCPayServer.Migrations
|
||||
.HasForeignKey("StoreDataId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("InvoiceDataId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
|
||||
{
|
||||
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
|
||||
|
@ -37,81 +37,6 @@ namespace BTCPayServer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public class InvoiceCryptoInfo
|
||||
{
|
||||
[JsonProperty("cryptoCode")]
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
[JsonProperty("rate")]
|
||||
public decimal Rate { get; set; }
|
||||
|
||||
//"exRates":{"USD":4320.02}
|
||||
[JsonProperty("exRates")]
|
||||
public Dictionary<string, double> ExRates
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"btcPaid":"0.000000"
|
||||
[JsonProperty("paid")]
|
||||
public string Paid
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"btcPrice":"0.001157"
|
||||
[JsonProperty("price")]
|
||||
public string Price
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"btcDue":"0.001160"
|
||||
/// <summary>
|
||||
/// Amount of crypto remaining to pay this invoice
|
||||
/// </summary>
|
||||
[JsonProperty("due")]
|
||||
public string Due
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty("paymentUrls")]
|
||||
public NBitpayClient.InvoicePaymentUrls PaymentUrls
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonProperty("address")]
|
||||
public string Address { get; set; }
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of this invoice
|
||||
/// </summary>
|
||||
[JsonProperty("totalDue")]
|
||||
public string TotalDue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of network fee to pay to the invoice
|
||||
/// </summary>
|
||||
[JsonProperty("networkFee")]
|
||||
public string NetworkFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of transactions required to pay
|
||||
/// </summary>
|
||||
[JsonProperty("txCount")]
|
||||
public int TxCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total amount of the invoice paid in this crypto
|
||||
/// </summary>
|
||||
[JsonProperty("cryptoPaid")]
|
||||
public Money CryptoPaid { get; set; }
|
||||
}
|
||||
|
||||
//{"facade":"pos/invoice","data":{,}}
|
||||
public class InvoiceResponse
|
||||
{
|
||||
@ -151,7 +76,7 @@ namespace BTCPayServer.Models
|
||||
}
|
||||
|
||||
[JsonProperty("cryptoInfo")]
|
||||
public List<InvoiceCryptoInfo> CryptoInfo { get; set; }
|
||||
public List<NBitpayClient.InvoiceCryptoInfo> CryptoInfo { get; set; }
|
||||
|
||||
//"price":5
|
||||
[JsonProperty("price")]
|
||||
|
@ -22,7 +22,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public class Payment
|
||||
{
|
||||
public string CryptoCode { get; set; }
|
||||
public int Confirmations
|
||||
public string Confirmations
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -48,6 +48,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Replaced { get; set; }
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
@ -70,7 +72,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StatusException { get; set; }
|
||||
public DateTimeOffset CreatedDate
|
||||
{
|
||||
get; set;
|
||||
@ -95,6 +97,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string TransactionSpeed { get; set; }
|
||||
public object StoreName
|
||||
{
|
||||
get;
|
||||
@ -110,6 +114,8 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
get;
|
||||
internal set;
|
||||
}
|
||||
|
||||
public string RedirectUrl { get; set; }
|
||||
public string Fiat
|
||||
{
|
||||
get;
|
||||
@ -122,5 +128,6 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
}
|
||||
public HistoricalAddressInvoiceData[] Addresses { get; set; }
|
||||
public DateTimeOffset MonitoringDate { get; internal set; }
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,13 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
{
|
||||
public class PaymentModel
|
||||
{
|
||||
public class AvailableCrypto
|
||||
{
|
||||
public string PaymentMethodId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
|
||||
public string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
@ -23,15 +30,17 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string ItemDesc { get; set; }
|
||||
public string TimeLeft { get; set; }
|
||||
public string Rate { get; set; }
|
||||
public string BtcAmount { get; set; }
|
||||
public string TxFees { get; set; }
|
||||
public string OrderAmount { get; set; }
|
||||
public string InvoiceBitcoinUrl { get; set; }
|
||||
public string BtcTotalDue { get; set; }
|
||||
public int TxCount { get; set; }
|
||||
public string BtcPaid { get; set; }
|
||||
public string StoreEmail { get; set; }
|
||||
|
||||
public string OrderId { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
public string NetworkFeeDescription { get; internal set; }
|
||||
public int MaxTimeMinutes { get; internal set; }
|
||||
public string PaymentType { get; internal set; }
|
||||
public string PaymentMethodId { get; internal set; }
|
||||
}
|
||||
}
|
||||
|
@ -48,10 +48,13 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
set;
|
||||
}
|
||||
|
||||
public bool Confirmation { get; set; }
|
||||
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
public SelectList DerivationSchemeFormats { get; set; }
|
||||
|
||||
|
||||
public string ServerUrl { get; set; }
|
||||
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
|
@ -47,7 +47,34 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public List<StoreViewModel.DerivationScheme> DerivationSchemes { get; set; } = new List<StoreViewModel.DerivationScheme>();
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm after ... minutes")]
|
||||
[Display(Name = "Preferred price source (eg. bitfinex, bitstamp...)")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public string RateSource
|
||||
{
|
||||
get
|
||||
{
|
||||
return PreferredExchange.IsCoinAverage() ? "https://apiv2.bitcoinaverage.com/indices/global/ticker/short" : $"https://apiv2.bitcoinaverage.com/exchanges/{PreferredExchange}";
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Multiply the original rate by ...")]
|
||||
[Range(0.01, 10.0)]
|
||||
public double RateMultiplier
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Invoice expires if the full amount has not been paid after ... minutes")]
|
||||
[Range(1, 60 * 24 * 31)]
|
||||
public int InvoiceExpiration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Display(Name = "Payment invalid if transactions fails to confirm ... minutes after invoice expiration")]
|
||||
[Range(10, 60 * 24 * 31)]
|
||||
public int MonitoringExpiration
|
||||
{
|
||||
|
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal file
34
BTCPayServer/Models/StoreViewModels/WalletModel.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class WalletModel
|
||||
{
|
||||
public string ServerUrl { get; set; }
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public class BitcoinLikeOnChainPaymentMethod : IPaymentMethodDetails
|
||||
{
|
||||
public PaymentTypes GetPaymentType()
|
||||
{
|
||||
return PaymentTypes.BTCLike;
|
||||
}
|
||||
|
||||
public string GetPaymentDestination()
|
||||
{
|
||||
return DepositAddress?.ToString();
|
||||
}
|
||||
|
||||
public Money GetTxFee()
|
||||
{
|
||||
return TxFee;
|
||||
}
|
||||
|
||||
public void SetPaymentDestination(string newPaymentDestination)
|
||||
{
|
||||
if (newPaymentDestination == null)
|
||||
DepositAddress = null;
|
||||
else
|
||||
DepositAddress = BitcoinAddress.Create(newPaymentDestination, DepositAddress.Network);
|
||||
}
|
||||
|
||||
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
|
||||
[JsonIgnore]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
[JsonIgnore]
|
||||
public Money TxFee { get; set; }
|
||||
[JsonIgnore]
|
||||
public BitcoinAddress DepositAddress { get; set; }
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
}
|
78
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs
Normal file
78
BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
|
||||
public class BitcoinLikePaymentData : CryptoPaymentData
|
||||
{
|
||||
public PaymentTypes GetPaymentType()
|
||||
{
|
||||
return PaymentTypes.BTCLike;
|
||||
}
|
||||
public BitcoinLikePaymentData()
|
||||
{
|
||||
|
||||
}
|
||||
public BitcoinLikePaymentData(Coin coin, bool rbf)
|
||||
{
|
||||
Outpoint = coin.Outpoint;
|
||||
Output = coin.TxOut;
|
||||
ConfirmationCount = 0;
|
||||
RBF = rbf;
|
||||
}
|
||||
[JsonIgnore]
|
||||
public OutPoint Outpoint { get; set; }
|
||||
[JsonIgnore]
|
||||
public TxOut Output { get; set; }
|
||||
public int ConfirmationCount { get; set; }
|
||||
public bool RBF { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
|
||||
/// </summary>
|
||||
public bool Legacy { get; set; }
|
||||
|
||||
public string GetPaymentId()
|
||||
{
|
||||
return Outpoint.ToString();
|
||||
}
|
||||
|
||||
public string[] GetSearchTerms()
|
||||
{
|
||||
return new[] { Outpoint.Hash.ToString() };
|
||||
}
|
||||
|
||||
public Money GetValue()
|
||||
{
|
||||
return Output.Value;
|
||||
}
|
||||
|
||||
public bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network)
|
||||
{
|
||||
return ConfirmationCount >= network.MaxTrackedConfirmation;
|
||||
}
|
||||
|
||||
public bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network)
|
||||
{
|
||||
if (speedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 1 || !RBF;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.MediumSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 1;
|
||||
}
|
||||
else if (speedPolicy == SpeedPolicy.LowSpeed)
|
||||
{
|
||||
return ConfirmationCount >= 6;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
407
BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs
Normal file
407
BTCPayServer/Payments/Bitcoin/NBXplorerListener.cs
Normal file
@ -0,0 +1,407 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer;
|
||||
using System.Collections.Concurrent;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.HostedServices;
|
||||
|
||||
namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
public class NBXplorerListener : IHostedService
|
||||
{
|
||||
EventAggregator _Aggregator;
|
||||
ExplorerClientProvider _ExplorerClients;
|
||||
IApplicationLifetime _Lifetime;
|
||||
InvoiceRepository _InvoiceRepository;
|
||||
private TaskCompletionSource<bool> _RunningTask;
|
||||
private CancellationTokenSource _Cts;
|
||||
BTCPayWalletProvider _Wallets;
|
||||
|
||||
public NBXplorerListener(ExplorerClientProvider explorerClients,
|
||||
BTCPayWalletProvider wallets,
|
||||
InvoiceRepository invoiceRepository,
|
||||
EventAggregator aggregator, IApplicationLifetime lifetime)
|
||||
{
|
||||
PollInterval = TimeSpan.FromMinutes(1.0);
|
||||
_Wallets = wallets;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_ExplorerClients = explorerClients;
|
||||
_Aggregator = aggregator;
|
||||
_Lifetime = lifetime;
|
||||
}
|
||||
|
||||
CompositeDisposable leases = new CompositeDisposable();
|
||||
ConcurrentDictionary<string, NotificationSession> _Sessions = new ConcurrentDictionary<string, NotificationSession>();
|
||||
private Timer _ListenPoller;
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
get
|
||||
{
|
||||
return _PollInterval;
|
||||
}
|
||||
set
|
||||
{
|
||||
_PollInterval = value;
|
||||
if (_ListenPoller != null)
|
||||
{
|
||||
_ListenPoller.Change(0, (int)value.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_RunningTask = new TaskCompletionSource<bool>();
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
leases.Add(_Aggregator.Subscribe<Events.NBXplorerStateChangedEvent>(async nbxplorerEvent =>
|
||||
{
|
||||
if (nbxplorerEvent.NewState == NBXplorerState.Ready)
|
||||
{
|
||||
var wallet = _Wallets.GetWallet(nbxplorerEvent.Network);
|
||||
if (_Wallets.IsAvailable(wallet.Network))
|
||||
{
|
||||
await Listen(wallet);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
_ListenPoller = new Timer(async s =>
|
||||
{
|
||||
foreach (var wallet in _Wallets.GetWallets())
|
||||
{
|
||||
if (_Wallets.IsAvailable(wallet.Network))
|
||||
{
|
||||
await Listen(wallet);
|
||||
}
|
||||
}
|
||||
}, null, 0, (int)PollInterval.TotalMilliseconds);
|
||||
leases.Add(_ListenPoller);
|
||||
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, inv.InvoiceId);
|
||||
List<Task> listeningDerivations = new List<Task>();
|
||||
foreach (var notificationSessions in _Sessions)
|
||||
{
|
||||
var derivationStrategy = GetStrategy(notificationSessions.Key, invoice);
|
||||
if (derivationStrategy != null)
|
||||
{
|
||||
listeningDerivations.Add(notificationSessions.Value.ListenDerivationSchemesAsync(new[] { derivationStrategy }, _Cts.Token));
|
||||
}
|
||||
}
|
||||
await Task.WhenAll(listeningDerivations.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
}));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Listen(BTCPayWallet wallet)
|
||||
{
|
||||
var network = wallet.Network;
|
||||
bool cleanup = false;
|
||||
try
|
||||
{
|
||||
if (_Sessions.ContainsKey(network.CryptoCode))
|
||||
return;
|
||||
var client = _ExplorerClients.GetExplorerClient(network);
|
||||
if (client == null)
|
||||
return;
|
||||
if (_Cts.IsCancellationRequested)
|
||||
return;
|
||||
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
|
||||
if (!_Sessions.TryAdd(network.CryptoCode, session))
|
||||
{
|
||||
await session.DisposeAsync();
|
||||
return;
|
||||
}
|
||||
cleanup = true;
|
||||
|
||||
using (session)
|
||||
{
|
||||
await session.ListenNewBlockAsync(_Cts.Token).ConfigureAwait(false);
|
||||
await session.ListenDerivationSchemesAsync((await GetStrategies(network)).ToArray(), _Cts.Token).ConfigureAwait(false);
|
||||
|
||||
Logs.PayServer.LogInformation($"{network.CryptoCode}: Checking if any pending invoice got paid while offline...");
|
||||
int paymentCount = await FindPaymentViaPolling(wallet, network);
|
||||
Logs.PayServer.LogInformation($"{network.CryptoCode}: {paymentCount} payments happened while offline");
|
||||
|
||||
Logs.PayServer.LogInformation($"Connected to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
while (!_Cts.IsCancellationRequested)
|
||||
{
|
||||
var newEvent = await session.NextEventAsync(_Cts.Token).ConfigureAwait(false);
|
||||
switch (newEvent)
|
||||
{
|
||||
case NBXplorer.Models.NewBlockEvent evt:
|
||||
|
||||
await Task.WhenAll((await _InvoiceRepository.GetPendingInvoices())
|
||||
.Select(invoiceId => UpdatePaymentStates(wallet, invoiceId))
|
||||
.ToArray());
|
||||
_Aggregator.Publish(new Events.NewBlockEvent() { CryptoCode = evt.CryptoCode });
|
||||
break;
|
||||
case NBXplorer.Models.NewTransactionEvent evt:
|
||||
wallet.InvalidateCache(evt.DerivationStrategy);
|
||||
foreach (var output in evt.Outputs)
|
||||
{
|
||||
foreach (var txCoin in evt.TransactionData.Transaction.Outputs.AsCoins()
|
||||
.Where(o => o.ScriptPubKey == output.ScriptPubKey)
|
||||
.Select(o => output.Redeem == null ? o : o.ToScriptCoin(output.Redeem)))
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoiceFromScriptPubKey(output.ScriptPubKey, network.CryptoCode);
|
||||
if (invoice != null)
|
||||
{
|
||||
var paymentData = new BitcoinLikePaymentData(txCoin, evt.TransactionData.Transaction.RBF);
|
||||
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
||||
if (!alreadyExist)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
await ReceivedPayment(wallet, invoice.Id, payment, evt.DerivationStrategy);
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdatePaymentStates(wallet, invoice.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Logs.PayServer.LogWarning("Received unknown message from NBXplorer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (_Cts.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.PayServer.LogError(ex, $"Error while connecting to WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (cleanup)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
|
||||
_Sessions.TryRemove(network.CryptoCode, out NotificationSession unused);
|
||||
if (_Sessions.Count == 0 && _Cts.IsCancellationRequested)
|
||||
{
|
||||
_RunningTask.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<BitcoinLikePaymentData> GetAllBitcoinPaymentData(InvoiceEntity invoice)
|
||||
{
|
||||
return invoice.GetPayments()
|
||||
.Where(p => p.GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
.Select(p => (BitcoinLikePaymentData)p.GetCryptoPaymentData());
|
||||
}
|
||||
|
||||
async Task<InvoiceEntity> UpdatePaymentStates(BTCPayWallet wallet, string invoiceId)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, false);
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
var transactions = await wallet.GetTransactions(GetAllBitcoinPaymentData(invoice)
|
||||
.Select(p => p.Outpoint.Hash)
|
||||
.ToArray());
|
||||
var conflicts = GetConflicts(transactions.Select(t => t.Value));
|
||||
foreach (var payment in invoice.GetPayments(wallet.Network))
|
||||
{
|
||||
if (payment.GetpaymentMethodId().PaymentType != PaymentTypes.BTCLike)
|
||||
continue;
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
if (!transactions.TryGetValue(paymentData.Outpoint.Hash, out TransactionResult tx))
|
||||
continue;
|
||||
var txId = tx.Transaction.GetHash();
|
||||
var txConflict = conflicts.GetConflict(txId);
|
||||
var accounted = txConflict == null || txConflict.IsWinner(txId);
|
||||
|
||||
bool updated = false;
|
||||
if (accounted != payment.Accounted)
|
||||
{
|
||||
updated = true;
|
||||
payment.Accounted = accounted;
|
||||
}
|
||||
|
||||
if (paymentData.ConfirmationCount != tx.Confirmations)
|
||||
{
|
||||
if(wallet.Network.MaxTrackedConfirmation >= paymentData.ConfirmationCount)
|
||||
{
|
||||
paymentData.ConfirmationCount = tx.Confirmations;
|
||||
payment.SetCryptoPaymentData(paymentData);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated)
|
||||
updatedPaymentEntities.Add(payment);
|
||||
}
|
||||
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
|
||||
if (updatedPaymentEntities.Count != 0)
|
||||
_Aggregator.Publish(new Events.InvoiceNeedUpdateEvent(invoice.Id));
|
||||
return invoice;
|
||||
}
|
||||
|
||||
class TransactionConflict
|
||||
{
|
||||
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
|
||||
|
||||
|
||||
uint256 _Winner;
|
||||
public bool IsWinner(uint256 txId)
|
||||
{
|
||||
if (_Winner == null)
|
||||
{
|
||||
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
|
||||
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
|
||||
{
|
||||
_Winner = confirmed.Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
|
||||
_Winner = Transactions
|
||||
.OrderByDescending(t => t.Value.Timestamp)
|
||||
.First()
|
||||
.Key;
|
||||
}
|
||||
}
|
||||
return _Winner == txId;
|
||||
}
|
||||
}
|
||||
class TransactionConflicts : List<TransactionConflict>
|
||||
{
|
||||
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TransactionConflict GetConflict(uint256 txId)
|
||||
{
|
||||
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
|
||||
}
|
||||
}
|
||||
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
|
||||
{
|
||||
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
var hash = tx.Transaction.GetHash();
|
||||
foreach (var input in tx.Transaction.Inputs)
|
||||
{
|
||||
TransactionConflict conflict = new TransactionConflict();
|
||||
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
|
||||
{
|
||||
conflict = conflictsByOutpoint[input.PrevOut];
|
||||
}
|
||||
if (!conflict.Transactions.ContainsKey(hash))
|
||||
conflict.Transactions.Add(hash, tx);
|
||||
}
|
||||
}
|
||||
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
|
||||
}
|
||||
|
||||
private async Task<int> FindPaymentViaPolling(BTCPayWallet wallet, BTCPayNetwork network)
|
||||
{
|
||||
int totalPayment = 0;
|
||||
var invoices = await _InvoiceRepository.GetPendingInvoices();
|
||||
foreach (var invoiceId in invoices)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId, true);
|
||||
var alreadyAccounted = GetAllBitcoinPaymentData(invoice).Select(p => p.Outpoint).ToHashSet();
|
||||
var strategy = invoice.GetDerivationStrategy(network);
|
||||
if (strategy == null)
|
||||
continue;
|
||||
var cryptoId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike);
|
||||
if (!invoice.Support(cryptoId))
|
||||
continue;
|
||||
var coins = (await wallet.GetUnspentCoins(strategy))
|
||||
.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + cryptoId))
|
||||
.ToArray();
|
||||
foreach (var coin in coins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
|
||||
{
|
||||
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
invoice = await ReceivedPayment(wallet, invoice.Id, payment, strategy);
|
||||
totalPayment++;
|
||||
}
|
||||
}
|
||||
return totalPayment;
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity> ReceivedPayment(BTCPayWallet wallet, string invoiceId, PaymentEntity payment, DerivationStrategyBase strategy)
|
||||
{
|
||||
var paymentData = (BitcoinLikePaymentData)payment.GetCryptoPaymentData();
|
||||
var invoice = (await UpdatePaymentStates(wallet, invoiceId));
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike, _ExplorerClients.NetworkProviders);
|
||||
if (paymentMethod != null &&
|
||||
paymentMethod.GetPaymentMethodDetails() is BitcoinLikeOnChainPaymentMethod btc &&
|
||||
btc.DepositAddress.ScriptPubKey == paymentData.Output.ScriptPubKey &&
|
||||
paymentMethod.Calculate().Due > Money.Zero)
|
||||
{
|
||||
var address = await wallet.ReserveAddressAsync(strategy);
|
||||
btc.DepositAddress = address;
|
||||
await _InvoiceRepository.NewAddress(invoiceId, btc, wallet.Network);
|
||||
_Aggregator.Publish(new InvoiceNewAddressEvent(invoiceId, address.ToString(), wallet.Network));
|
||||
paymentMethod.SetPaymentMethodDetails(btc);
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoiceId, 1002, "invoice_receivedPayment"));
|
||||
return invoice;
|
||||
}
|
||||
|
||||
private async Task<List<DerivationStrategyBase>> GetStrategies(BTCPayNetwork network)
|
||||
{
|
||||
List<DerivationStrategyBase> strategies = new List<DerivationStrategyBase>();
|
||||
foreach (var invoiceId in await _InvoiceRepository.GetPendingInvoices())
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
|
||||
var strategy = GetStrategy(network.CryptoCode, invoice);
|
||||
if (strategy != null)
|
||||
strategies.Add(strategy);
|
||||
}
|
||||
|
||||
return strategies;
|
||||
}
|
||||
|
||||
private DerivationStrategyBase GetStrategy(string cryptoCode, InvoiceEntity invoice)
|
||||
{
|
||||
foreach (var derivationStrategy in invoice.GetDerivationStrategies(_ExplorerClients.NetworkProviders))
|
||||
{
|
||||
if (derivationStrategy.Network.CryptoCode == cryptoCode)
|
||||
{
|
||||
return derivationStrategy.DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
leases.Dispose();
|
||||
_Cts.Cancel();
|
||||
return Task.WhenAny(_RunningTask.Task, Task.Delay(-1, cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
16
BTCPayServer/Payments/IPaymentMethodDetails.cs
Normal file
16
BTCPayServer/Payments/IPaymentMethodDetails.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
public interface IPaymentMethodDetails
|
||||
{
|
||||
string GetPaymentDestination();
|
||||
PaymentTypes GetPaymentType();
|
||||
Money GetTxFee();
|
||||
void SetPaymentDestination(string newPaymentDestination);
|
||||
}
|
||||
}
|
12
BTCPayServer/Payments/PaymentTypes.cs
Normal file
12
BTCPayServer/Payments/PaymentTypes.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Payments
|
||||
{
|
||||
public enum PaymentTypes
|
||||
{
|
||||
BTCLike
|
||||
}
|
||||
}
|
@ -24,8 +24,8 @@ namespace BTCPayServer
|
||||
{
|
||||
ServicePointManager.DefaultConnectionLimit = 100;
|
||||
IWebHost host = null;
|
||||
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider();
|
||||
|
||||
var processor = new ConsoleLoggerProcessor();
|
||||
CustomConsoleLogProvider loggerProvider = new CustomConsoleLogProvider(processor);
|
||||
var loggerFactory = new LoggerFactory();
|
||||
loggerFactory.AddProvider(loggerProvider);
|
||||
var logger = loggerFactory.CreateLogger("Configuration");
|
||||
@ -44,7 +44,8 @@ namespace BTCPayServer
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
l.AddFilter("Microsoft", LogLevel.Error);
|
||||
l.AddProvider(new CustomConsoleLogProvider());
|
||||
l.AddFilter("Microsoft.AspNetCore.Antiforgery.Internal", LogLevel.Critical);
|
||||
l.AddProvider(new CustomConsoleLogProvider(processor));
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.Build();
|
||||
@ -61,13 +62,9 @@ namespace BTCPayServer
|
||||
if (!string.IsNullOrEmpty(ex.Message))
|
||||
Logs.Configuration.LogError(ex.Message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
logger.LogError("Exception thrown while running the server");
|
||||
logger.LogError(exception.ToString());
|
||||
}
|
||||
finally
|
||||
{
|
||||
processor.Dispose();
|
||||
if (host != null)
|
||||
host.Dispose();
|
||||
loggerProvider.Dispose();
|
||||
|
@ -1,27 +1,29 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:14139/",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Default": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"Docker-Regtest": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32839/",
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14142/"
|
||||
}
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:14139/",
|
||||
"sslPort": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Default": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "--network testnet --chains ltc --ltcexplorerurl http://127.0.0.1:2727/"
|
||||
},
|
||||
"Docker-Regtest": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"BTCPAY_NETWORK": "regtest",
|
||||
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
},
|
||||
"applicationUrl": "http://localhost:14142/"
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ namespace BTCPayServer
|
||||
{
|
||||
if(filter.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries).Length == 2)
|
||||
{
|
||||
TextSearch = TextSearch.Replace(filter, string.Empty);
|
||||
TextSearch = TextSearch.Replace(filter, string.Empty, StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
||||
TextSearch = TextSearch.Trim();
|
||||
|
@ -5,12 +5,13 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using NBXplorer;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class BTCPayServerEnvironment
|
||||
{
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env)
|
||||
public BTCPayServerEnvironment(IHostingEnvironment env, BTCPayNetworkProvider provider)
|
||||
{
|
||||
Version = typeof(BTCPayServerEnvironment).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version;
|
||||
#if DEBUG
|
||||
@ -19,11 +20,13 @@ namespace BTCPayServer.Services
|
||||
Build = "Release";
|
||||
#endif
|
||||
Environment = env;
|
||||
ChainType = provider.NBXplorerNetworkProvider.ChainType;
|
||||
}
|
||||
public IHostingEnvironment Environment
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public ChainType ChainType { get; set; }
|
||||
public string Version
|
||||
{
|
||||
get; set;
|
||||
|
233
BTCPayServer/Services/HardwareWalletService.cs
Normal file
233
BTCPayServer/Services/HardwareWalletService.cs
Normal file
@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using LedgerWallet;
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
|
||||
public class HardwareWalletException : Exception
|
||||
{
|
||||
public HardwareWalletException() { }
|
||||
public HardwareWalletException(string message) : base(message) { }
|
||||
public HardwareWalletException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
public class HardwareWalletService
|
||||
{
|
||||
class WebSocketTransport : LedgerWallet.Transports.ILedgerTransport
|
||||
{
|
||||
private readonly WebSocket webSocket;
|
||||
|
||||
public WebSocketTransport(System.Net.WebSockets.WebSocket webSocket)
|
||||
{
|
||||
if (webSocket == null)
|
||||
throw new ArgumentNullException(nameof(webSocket));
|
||||
this.webSocket = webSocket;
|
||||
}
|
||||
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public async Task<byte[][]> Exchange(byte[][] apdus)
|
||||
{
|
||||
List<byte[]> responses = new List<byte[]>();
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(Timeout))
|
||||
{
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
await this.webSocket.SendAsync(new ArraySegment<byte>(apdu), WebSocketMessageType.Binary, true, cts.Token);
|
||||
}
|
||||
foreach (var apdu in apdus)
|
||||
{
|
||||
byte[] response = new byte[300];
|
||||
var result = await this.webSocket.ReceiveAsync(new ArraySegment<byte>(response), cts.Token);
|
||||
Array.Resize(ref response, result.Count);
|
||||
responses.Add(response);
|
||||
}
|
||||
}
|
||||
return responses.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LedgerClient _Ledger;
|
||||
public LedgerClient Ledger
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Ledger;
|
||||
}
|
||||
}
|
||||
WebSocketTransport _Transport = null;
|
||||
public HardwareWalletService(System.Net.WebSockets.WebSocket ledgerWallet)
|
||||
{
|
||||
if (ledgerWallet == null)
|
||||
throw new ArgumentNullException(nameof(ledgerWallet));
|
||||
_Transport = new WebSocketTransport(ledgerWallet);
|
||||
_Ledger = new LedgerClient(_Transport);
|
||||
}
|
||||
|
||||
public async Task<LedgerTestResult> Test()
|
||||
{
|
||||
var version = await _Ledger.GetFirmwareVersionAsync();
|
||||
return new LedgerTestResult() { Success = true };
|
||||
}
|
||||
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
|
||||
var pubkey = await GetExtPubKey(_Ledger, network, new KeyPath("49'").Derive(network.CoinType).Derive(0, true), false);
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = true,
|
||||
Legacy = false
|
||||
});
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString() };
|
||||
}
|
||||
|
||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pubKey = await ledger.GetWalletPubKeyAsync(account);
|
||||
if (pubKey.Address.Network != network.NBitcoinNetwork)
|
||||
{
|
||||
if (network.DefaultSettings.ChainType == NBXplorer.ChainType.Main)
|
||||
throw new Exception($"The opened ledger app should be for {network.NBitcoinNetwork.Name}, not for {pubKey.Address.Network}");
|
||||
}
|
||||
var fingerprint = onlyChaincode ? new byte[4] : (await ledger.GetWalletPubKeyAsync(account.Parent)).UncompressedPublicKey.Compress().Hash.ToBytes().Take(4).ToArray();
|
||||
var extpubkey = new ExtPubKey(pubKey.UncompressedPublicKey.Compress(), pubKey.ChainCode, (byte)account.Indexes.Length, fingerprint, account.Indexes.Last()).GetWif(network.NBitcoinNetwork);
|
||||
return extpubkey;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new HardwareWalletException("Unsupported ledger app");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SupportDerivation(BTCPayNetwork network, DirectDerivationStrategy strategy)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
if (!strategy.Segwit)
|
||||
return false;
|
||||
return await GetKeyPath(_Ledger, network, strategy) != null;
|
||||
}
|
||||
|
||||
private static async Task<KeyPath> GetKeyPath(LedgerClient ledger, BTCPayNetwork network, DirectDerivationStrategy directStrategy)
|
||||
{
|
||||
KeyPath foundKeyPath = null;
|
||||
foreach (var account in
|
||||
new[] { new KeyPath("49'"), new KeyPath("44'") }
|
||||
.Select(purpose => purpose.Derive(network.CoinType))
|
||||
.SelectMany(coinType => Enumerable.Range(0, 5).Select(i => coinType.Derive(i, true))))
|
||||
{
|
||||
try
|
||||
{
|
||||
var extpubkey = await GetExtPubKey(ledger, network, account, true);
|
||||
if (directStrategy.Root.PubKey == extpubkey.ExtPubKey.PubKey)
|
||||
{
|
||||
foundKeyPath = account;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
throw new Exception($"The opened ledger app does not support {network.NBitcoinNetwork.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
return foundKeyPath;
|
||||
}
|
||||
|
||||
public async Task<Transaction> SendToAddress(DirectDerivationStrategy strategy,
|
||||
ReceivedCoin[] coins, BTCPayNetwork network,
|
||||
(IDestination destination, Money amount, bool substractFees)[] send,
|
||||
FeeRate feeRate,
|
||||
IDestination changeAddress,
|
||||
KeyPath changeKeyPath)
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (feeRate == null)
|
||||
throw new ArgumentNullException(nameof(feeRate));
|
||||
if (changeAddress == null)
|
||||
throw new ArgumentNullException(nameof(changeAddress));
|
||||
if (feeRate.FeePerK <= Money.Zero)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(feeRate), "The fee rate should be above zero");
|
||||
}
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
if (element.destination == null)
|
||||
throw new ArgumentNullException(nameof(element.destination));
|
||||
if (element.amount == null)
|
||||
throw new ArgumentNullException(nameof(element.amount));
|
||||
if (element.amount <= Money.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await GetKeyPath(Ledger, network, strategy);
|
||||
|
||||
if (foundKeyPath == null)
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
}
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder();
|
||||
builder.AddCoins(coins.Select(c=>c.Coin).ToArray());
|
||||
|
||||
foreach (var element in send)
|
||||
{
|
||||
builder.Send(element.destination, element.amount);
|
||||
if (element.substractFees)
|
||||
builder.SubtractFees();
|
||||
}
|
||||
builder.SetChange(changeAddress);
|
||||
builder.SendEstimatedFees(feeRate);
|
||||
builder.Shuffle();
|
||||
var unsigned = builder.BuildTransaction(false);
|
||||
|
||||
var keypaths = new Dictionary<Script, KeyPath>();
|
||||
foreach(var c in coins)
|
||||
{
|
||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||
}
|
||||
|
||||
var hasChange = unsigned.Outputs.Count == 2;
|
||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||
_Transport.Timeout = TimeSpan.FromMinutes(5);
|
||||
var fullySigned = await Ledger.SignTransactionAsync(
|
||||
usedCoins.Select(c => new SignatureRequest
|
||||
{
|
||||
InputCoin = c,
|
||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||
}).ToArray(),
|
||||
unsigned,
|
||||
hasChange ? foundKeyPath.Derive(changeKeyPath) : null);
|
||||
return fullySigned;
|
||||
}
|
||||
}
|
||||
|
||||
public class LedgerTestResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
public class GetXPubResult
|
||||
{
|
||||
public string ExtPubKey { get; set; }
|
||||
}
|
||||
}
|
81
BTCPayServer/Services/Invoices/CryptoDataDictionary.cs
Normal file
81
BTCPayServer/Services/Invoices/CryptoDataDictionary.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
public class PaymentMethodDictionary : IEnumerable<PaymentMethod>
|
||||
{
|
||||
Dictionary<PaymentMethodId, PaymentMethod> _Inner = new Dictionary<PaymentMethodId, PaymentMethod>();
|
||||
public PaymentMethodDictionary()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public PaymentMethodDictionary(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public PaymentMethod this[PaymentMethodId index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Inner[index];
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(PaymentMethod paymentMethod)
|
||||
{
|
||||
_Inner.Add(paymentMethod.GetId(), paymentMethod);
|
||||
}
|
||||
|
||||
public void Remove(PaymentMethod paymentMethod)
|
||||
{
|
||||
_Inner.Remove(paymentMethod.GetId());
|
||||
}
|
||||
public bool TryGetValue(PaymentMethodId paymentMethodId, out PaymentMethod data)
|
||||
{
|
||||
if (paymentMethodId == null)
|
||||
throw new ArgumentNullException(nameof(paymentMethodId));
|
||||
return _Inner.TryGetValue(paymentMethodId, out data);
|
||||
}
|
||||
|
||||
public void AddOrReplace(PaymentMethod paymentMethod)
|
||||
{
|
||||
var key = paymentMethod.GetId();
|
||||
_Inner.Remove(key);
|
||||
_Inner.Add(key, paymentMethod);
|
||||
}
|
||||
|
||||
public IEnumerator<PaymentMethod> GetEnumerator()
|
||||
{
|
||||
return _Inner.Values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public PaymentMethod TryGet(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (paymentMethodId == null)
|
||||
throw new ArgumentNullException(nameof(paymentMethodId));
|
||||
_Inner.TryGetValue(paymentMethodId, out var value);
|
||||
return value;
|
||||
}
|
||||
public PaymentMethod TryGet(string network, PaymentTypes paymentType)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
var id = new PaymentMethodId(network, paymentType);
|
||||
return TryGet(id);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using BTCPayServer.Models;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.DataEncoders;
|
||||
using BTCPayServer.Data;
|
||||
using NBXplorer.Models;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
@ -121,7 +122,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
[Obsolete("Use GetCryptoData(network).Rate instead")]
|
||||
[Obsolete("Use GetPaymentMethod(network) instead")]
|
||||
public decimal Rate
|
||||
{
|
||||
get; set;
|
||||
@ -135,7 +136,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetCryptoData(network).DepositAddress instead")]
|
||||
[Obsolete("Use GetPaymentMethod(network).GetPaymentMethodDetails().GetDestinationAddress() instead")]
|
||||
public string DepositAddress
|
||||
{
|
||||
get; set;
|
||||
@ -168,6 +169,30 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase GetDerivationStrategy(BTCPayNetwork network)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
if (!string.IsNullOrEmpty(DerivationStrategies))
|
||||
{
|
||||
JObject strategies = JObject.Parse(DerivationStrategies);
|
||||
#pragma warning restore CS0618
|
||||
foreach (var strat in strategies.Properties())
|
||||
{
|
||||
if (strat.Name == network.CryptoCode)
|
||||
{
|
||||
return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network).DerivationStrategyBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
if (network.IsBTC && !string.IsNullOrEmpty(DerivationStrategy))
|
||||
{
|
||||
return BTCPayServer.DerivationStrategy.Parse(DerivationStrategy, network).DerivationStrategyBase;
|
||||
}
|
||||
return null;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public IEnumerable<DerivationStrategy> GetDerivationStrategies(BTCPayNetworkProvider networks)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
@ -180,7 +205,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
var network = networks.GetNetwork(strat.Name);
|
||||
if (network != null)
|
||||
{
|
||||
if (network == networks.BTC && btcReturned)
|
||||
if (network == networks.BTC)
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
|
||||
}
|
||||
@ -220,10 +245,27 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetPayments instead")]
|
||||
public List<PaymentEntity> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public List<PaymentEntity> GetPayments()
|
||||
{
|
||||
return Payments.ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode)
|
||||
{
|
||||
return Payments.Where(p => p.CryptoCode == cryptoCode).ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(BTCPayNetwork network)
|
||||
{
|
||||
return GetPayments(network.CryptoCode);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
public bool Refundable
|
||||
{
|
||||
get;
|
||||
@ -240,7 +282,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetCryptoData(network).TxFee instead")]
|
||||
[Obsolete("Use GetPaymentMethod(network).GetTxFee() instead")]
|
||||
public Money TxFee
|
||||
{
|
||||
get;
|
||||
@ -262,8 +304,9 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
|
||||
[Obsolete("Use Set/GetCryptoData() instead")]
|
||||
public JObject CryptoData { get; set; }
|
||||
[Obsolete("Use Set/GetPaymentMethod() instead")]
|
||||
[JsonProperty(PropertyName = "cryptoData")]
|
||||
public JObject PaymentMethod { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public DateTimeOffset MonitoringExpiration
|
||||
@ -283,6 +326,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
public bool ExtendedNotifications { get; set; }
|
||||
public List<InvoiceEventData> Events { get; internal set; }
|
||||
|
||||
public bool IsExpired()
|
||||
{
|
||||
@ -306,12 +350,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
Flags = new Flags() { Refundable = Refundable }
|
||||
};
|
||||
|
||||
dto.CryptoInfo = new List<InvoiceCryptoInfo>();
|
||||
foreach (var info in this.GetCryptoData().Values)
|
||||
dto.CryptoInfo = new List<NBitpayClient.InvoiceCryptoInfo>();
|
||||
foreach (var info in this.GetPaymentMethods(networkProvider, true))
|
||||
{
|
||||
var accounting = info.Calculate();
|
||||
var cryptoInfo = new InvoiceCryptoInfo();
|
||||
cryptoInfo.CryptoCode = info.CryptoCode;
|
||||
var cryptoInfo = new NBitpayClient.InvoiceCryptoInfo();
|
||||
cryptoInfo.CryptoCode = info.GetId().CryptoCode;
|
||||
cryptoInfo.PaymentType = info.GetId().PaymentType.ToString();
|
||||
cryptoInfo.Rate = info.Rate;
|
||||
cryptoInfo.Price = Money.Coins(ProductInformation.Price / cryptoInfo.Rate).ToString();
|
||||
|
||||
@ -320,20 +365,20 @@ namespace BTCPayServer.Services.Invoices
|
||||
cryptoInfo.TotalDue = accounting.TotalDue.ToString();
|
||||
cryptoInfo.NetworkFee = accounting.NetworkFee.ToString();
|
||||
cryptoInfo.TxCount = accounting.TxCount;
|
||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid;
|
||||
cryptoInfo.CryptoPaid = accounting.CryptoPaid.ToString();
|
||||
|
||||
cryptoInfo.Address = info.DepositAddress;
|
||||
cryptoInfo.Address = info.GetPaymentMethodDetails()?.GetPaymentDestination();
|
||||
cryptoInfo.ExRates = new Dictionary<string, double>
|
||||
{
|
||||
{ ProductInformation.Currency, (double)cryptoInfo.Rate }
|
||||
};
|
||||
|
||||
var scheme = networkProvider.GetNetwork(info.CryptoCode)?.UriScheme ?? "BTC";
|
||||
var scheme = info.Network.UriScheme;
|
||||
var cryptoSuffix = cryptoInfo.CryptoCode == "BTC" ? "" : "/" + cryptoInfo.CryptoCode;
|
||||
cryptoInfo.Url = ServerUrl.WithTrailingSlash() + $"invoice{cryptoSuffix}?id=" + Id;
|
||||
|
||||
|
||||
cryptoInfo.PaymentUrls = new InvoicePaymentUrls()
|
||||
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
|
||||
{
|
||||
BIP72 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}&r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
BIP72b = $"{scheme}:?r={ServerUrl.WithTrailingSlash() + ($"i/{Id}{cryptoSuffix}")}",
|
||||
@ -353,8 +398,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
dto.PaymentUrls = cryptoInfo.PaymentUrls;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
if (!info.IsPhantomBTC)
|
||||
dto.CryptoInfo.Add(cryptoInfo);
|
||||
}
|
||||
|
||||
Populate(ProductInformation, dto);
|
||||
@ -373,42 +418,50 @@ namespace BTCPayServer.Services.Invoices
|
||||
JsonConvert.PopulateObject(str, dest);
|
||||
}
|
||||
|
||||
internal bool Support(BTCPayNetwork network)
|
||||
internal bool Support(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
var rates = GetCryptoData();
|
||||
return rates.TryGetValue(network.CryptoCode, out var data);
|
||||
var rates = GetPaymentMethods(null);
|
||||
return rates.TryGet(paymentMethodId) != null;
|
||||
}
|
||||
|
||||
public CryptoData GetCryptoData(string cryptoCode)
|
||||
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
GetCryptoData().TryGetValue(cryptoCode, out var data);
|
||||
GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data);
|
||||
return data;
|
||||
}
|
||||
|
||||
public CryptoData GetCryptoData(BTCPayNetwork network)
|
||||
public PaymentMethod GetPaymentMethod(BTCPayNetwork network, PaymentTypes paymentType, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
GetCryptoData().TryGetValue(network.CryptoCode, out var data);
|
||||
return data;
|
||||
return GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentType), networkProvider);
|
||||
}
|
||||
|
||||
public Dictionary<string, CryptoData> GetCryptoData()
|
||||
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider, bool alwaysIncludeBTC = false)
|
||||
{
|
||||
Dictionary<string, CryptoData> rates = new Dictionary<string, CryptoData>();
|
||||
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
|
||||
var serializer = new Serializer(Dummy);
|
||||
PaymentMethod phantom = null;
|
||||
#pragma warning disable CS0618
|
||||
// Legacy
|
||||
if (Rate != 0.0m)
|
||||
if (alwaysIncludeBTC)
|
||||
{
|
||||
rates.TryAdd("BTC", new CryptoData() { ParentEntity = this, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress });
|
||||
var btcNetwork = networkProvider?.GetNetwork("BTC");
|
||||
phantom = new PaymentMethod() { ParentEntity = this, IsPhantomBTC = true, Rate = Rate, CryptoCode = "BTC", TxFee = TxFee, FeeRate = new FeeRate(TxFee, 100), DepositAddress = DepositAddress, Network = btcNetwork };
|
||||
if (btcNetwork != null || networkProvider == null)
|
||||
rates.Add(phantom);
|
||||
}
|
||||
if (CryptoData != null)
|
||||
if (PaymentMethod != null)
|
||||
{
|
||||
foreach (var prop in CryptoData.Properties())
|
||||
foreach (var prop in PaymentMethod.Properties())
|
||||
{
|
||||
var r = serializer.ToObject<CryptoData>(prop.Value.ToString());
|
||||
r.CryptoCode = prop.Name;
|
||||
if (prop.Name == "BTC" && phantom != null)
|
||||
rates.Remove(phantom);
|
||||
var r = serializer.ToObject<PaymentMethod>(prop.Value.ToString());
|
||||
var paymentMethodId = PaymentMethodId.Parse(prop.Name);
|
||||
r.CryptoCode = paymentMethodId.CryptoCode;
|
||||
r.PaymentType = paymentMethodId.PaymentType.ToString();
|
||||
r.ParentEntity = this;
|
||||
rates.TryAdd(r.CryptoCode, r);
|
||||
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
|
||||
if(r.Network != null || networkProvider == null)
|
||||
rates.Add(r);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
@ -416,23 +469,34 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
|
||||
Network Dummy = Network.Main;
|
||||
public void SetCryptoData(Dictionary<string, CryptoData> cryptoData)
|
||||
|
||||
public void SetPaymentMethod(PaymentMethod paymentMethod)
|
||||
{
|
||||
var dict = GetPaymentMethods(null);
|
||||
dict.AddOrReplace(paymentMethod);
|
||||
SetPaymentMethods(dict);
|
||||
}
|
||||
|
||||
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
|
||||
{
|
||||
if (paymentMethods.NetworkProvider != null)
|
||||
throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null");
|
||||
var obj = new JObject();
|
||||
var serializer = new Serializer(Dummy);
|
||||
foreach (var kv in cryptoData)
|
||||
{
|
||||
var clone = serializer.ToObject<CryptoData>(serializer.ToString(kv.Value));
|
||||
clone.CryptoCode = null;
|
||||
obj.Add(new JProperty(kv.Key, JObject.Parse(serializer.ToString(clone))));
|
||||
}
|
||||
#pragma warning disable CS0618
|
||||
CryptoData = obj;
|
||||
foreach (var v in paymentMethods)
|
||||
{
|
||||
var clone = serializer.ToObject<PaymentMethod>(serializer.ToString(v));
|
||||
clone.CryptoCode = null;
|
||||
clone.PaymentType = null;
|
||||
obj.Add(new JProperty(v.GetId().ToString(), JObject.Parse(serializer.ToString(clone))));
|
||||
}
|
||||
PaymentMethod = obj;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
|
||||
public class CryptoDataAccounting
|
||||
public class PaymentMethodAccounting
|
||||
{
|
||||
/// <summary>
|
||||
/// Total amount of this invoice
|
||||
@ -464,24 +528,168 @@ namespace BTCPayServer.Services.Invoices
|
||||
public Money NetworkFee { get; set; }
|
||||
}
|
||||
|
||||
public class CryptoData
|
||||
public class PaymentMethodId
|
||||
{
|
||||
public PaymentMethodId(string cryptoCode, PaymentTypes paymentType)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
PaymentType = paymentType;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public string CryptoCode { get; private set; }
|
||||
public PaymentTypes PaymentType { get; private set; }
|
||||
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
PaymentMethodId item = obj as PaymentMethodId;
|
||||
if (item == null)
|
||||
return false;
|
||||
return ToString().Equals(item.ToString(), StringComparison.InvariantCulture);
|
||||
}
|
||||
public static bool operator ==(PaymentMethodId a, PaymentMethodId b)
|
||||
{
|
||||
if (System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.ToString() == b.ToString();
|
||||
}
|
||||
|
||||
public static bool operator !=(PaymentMethodId a, PaymentMethodId b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
#pragma warning disable CA1307 // Specify StringComparison
|
||||
return ToString().GetHashCode();
|
||||
#pragma warning restore CA1307 // Specify StringComparison
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (PaymentType == PaymentTypes.BTCLike)
|
||||
return CryptoCode;
|
||||
return CryptoCode + "_" + PaymentType.ToString();
|
||||
}
|
||||
|
||||
public static PaymentMethodId Parse(string str)
|
||||
{
|
||||
var parts = str.Split('_');
|
||||
return new PaymentMethodId(parts[0], parts.Length == 1 ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentMethod
|
||||
{
|
||||
[JsonIgnore]
|
||||
public InvoiceEntity ParentEntity { get; set; }
|
||||
[JsonIgnore]
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
[JsonProperty(PropertyName = "cryptoCode", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().CryptoCode instead")]
|
||||
public string CryptoCode { get; set; }
|
||||
[JsonProperty(PropertyName = "paymentType", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[Obsolete("Use GetId().PaymentType instead")]
|
||||
public string PaymentType { get; set; }
|
||||
|
||||
|
||||
public PaymentMethodId GetId()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return new PaymentMethodId(CryptoCode, string.IsNullOrEmpty(PaymentType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(PaymentType));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public void SetId(PaymentMethodId id)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
CryptoCode = id.CryptoCode;
|
||||
PaymentType = id.PaymentType.ToString();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "rate")]
|
||||
public decimal Rate { get; set; }
|
||||
|
||||
[Obsolete("Use GetPaymentMethodDetails() instead")]
|
||||
[JsonProperty(PropertyName = "paymentMethod")]
|
||||
public JObject PaymentMethodDetails { get; set; }
|
||||
public IPaymentMethodDetails GetPaymentMethodDetails()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// Legacy, old code does not have PaymentMethods
|
||||
if (string.IsNullOrEmpty(PaymentType) || PaymentMethodDetails == null)
|
||||
{
|
||||
return new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
FeeRate = FeeRate,
|
||||
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork),
|
||||
TxFee = TxFee
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (GetId().PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var method = DeserializePaymentMethodDetails<Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod>(PaymentMethodDetails);
|
||||
method.TxFee = TxFee;
|
||||
method.DepositAddress = BitcoinAddress.Create(DepositAddress, Network?.NBitcoinNetwork);
|
||||
method.FeeRate = FeeRate;
|
||||
return method;
|
||||
}
|
||||
}
|
||||
throw new NotSupportedException(PaymentType);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
private T DeserializePaymentMethodDetails<T>(JObject jobj) where T : class, IPaymentMethodDetails
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(jobj.ToString());
|
||||
}
|
||||
|
||||
public void SetPaymentMethodDetails(IPaymentMethodDetails paymentMethod)
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// Legacy, need to fill the old fields
|
||||
|
||||
if (PaymentType == null)
|
||||
PaymentType = paymentMethod.GetPaymentType().ToString();
|
||||
else if (PaymentType != paymentMethod.GetPaymentType().ToString())
|
||||
throw new InvalidOperationException("Invalid payment method affected");
|
||||
|
||||
if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod)
|
||||
{
|
||||
TxFee = bitcoinPaymentMethod.TxFee;
|
||||
FeeRate = bitcoinPaymentMethod.FeeRate;
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress.ToString();
|
||||
}
|
||||
var jobj = JObject.Parse(JsonConvert.SerializeObject(paymentMethod));
|
||||
PaymentMethodDetails = jobj;
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
[JsonProperty(PropertyName = "feeRate")]
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
[JsonProperty(PropertyName = "txFee")]
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")]
|
||||
public Money TxFee { get; set; }
|
||||
[JsonProperty(PropertyName = "depositAddress")]
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||
public string DepositAddress { get; set; }
|
||||
|
||||
public CryptoDataAccounting Calculate()
|
||||
[JsonIgnore]
|
||||
public bool IsPhantomBTC { get; set; }
|
||||
|
||||
public PaymentMethodAccounting Calculate()
|
||||
{
|
||||
var cryptoData = ParentEntity.GetCryptoData();
|
||||
var paymentMethods = ParentEntity.GetPaymentMethods(null, IsPhantomBTC);
|
||||
var totalDue = Money.Coins(ParentEntity.ProductInformation.Price / Rate);
|
||||
var paid = Money.Zero;
|
||||
var cryptoPaid = Money.Zero;
|
||||
@ -490,19 +698,22 @@ namespace BTCPayServer.Services.Invoices
|
||||
bool paidEnough = totalDue <= paid;
|
||||
int txCount = 0;
|
||||
var payments =
|
||||
ParentEntity.Payments
|
||||
ParentEntity.GetPayments()
|
||||
.Where(p => p.Accounted)
|
||||
.OrderByDescending(p => p.ReceivedTime)
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
paidTxFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
|
||||
paid += _.GetValue(cryptoData, CryptoCode);
|
||||
if(!paidEnough)
|
||||
totalDue += paidTxFee;
|
||||
paidEnough |= totalDue <= paid;
|
||||
if (CryptoCode == _.GetCryptoCode())
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetpaymentMethodId()].GetTxFee());
|
||||
paid += _.GetValue(paymentMethods, GetId());
|
||||
if (!paidEnough)
|
||||
{
|
||||
cryptoPaid += _.GetValue();
|
||||
totalDue += txFee;
|
||||
paidTxFee += txFee;
|
||||
}
|
||||
paidEnough |= totalDue <= paid;
|
||||
if (GetId() == _.GetpaymentMethodId())
|
||||
{
|
||||
cryptoPaid += _.GetCryptoPaymentData().GetValue();
|
||||
txCount++;
|
||||
}
|
||||
return _;
|
||||
@ -512,29 +723,26 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (!paidEnough)
|
||||
{
|
||||
txCount++;
|
||||
totalDue += TxFee;
|
||||
totalDue += GetTxFee();
|
||||
paidTxFee += GetTxFee();
|
||||
}
|
||||
var accounting = new CryptoDataAccounting();
|
||||
var accounting = new PaymentMethodAccounting();
|
||||
accounting.TotalDue = totalDue;
|
||||
accounting.Paid = paid;
|
||||
accounting.TxCount = txCount;
|
||||
accounting.CryptoPaid = cryptoPaid;
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.NetworkFee = TxFee * txCount;
|
||||
accounting.NetworkFee = paidTxFee;
|
||||
return accounting;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AccountedPaymentEntity
|
||||
{
|
||||
public int Confirmations
|
||||
private Money GetTxFee()
|
||||
{
|
||||
get;
|
||||
set;
|
||||
var method = GetPaymentMethodDetails();
|
||||
if (method == null)
|
||||
return Money.Zero;
|
||||
return method.GetTxFee();
|
||||
}
|
||||
public PaymentEntity Payment { get; set; }
|
||||
public Transaction Transaction { get; set; }
|
||||
}
|
||||
|
||||
public class PaymentEntity
|
||||
@ -543,64 +751,133 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
|
||||
public OutPoint Outpoint
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetValue() or GetScriptPubKey() instead")]
|
||||
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Output")]
|
||||
public TxOut Output
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public Script GetScriptPubKey()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
return Output.ScriptPubKey;
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public bool Accounted
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetCryptoCode() instead")]
|
||||
|
||||
[Obsolete("Use GetpaymentMethodId().CryptoCode instead")]
|
||||
public string CryptoCode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Money GetValue()
|
||||
|
||||
[Obsolete("Use GetCryptoPaymentData() instead")]
|
||||
public string CryptoPaymentData { get; set; }
|
||||
[Obsolete("Use GetpaymentMethodId().PaymentType instead")]
|
||||
public string CryptoPaymentDataType { get; set; }
|
||||
|
||||
|
||||
public CryptoPaymentData GetCryptoPaymentData()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
return Output.Value;
|
||||
if (string.IsNullOrEmpty(CryptoPaymentDataType))
|
||||
{
|
||||
// In case this is a payment done before this update, consider it unconfirmed with RBF for safety
|
||||
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
|
||||
paymentData.Outpoint = Outpoint;
|
||||
paymentData.Output = Output;
|
||||
paymentData.RBF = true;
|
||||
paymentData.ConfirmationCount = 0;
|
||||
paymentData.Legacy = true;
|
||||
return paymentData;
|
||||
}
|
||||
if (GetpaymentMethodId().PaymentType == PaymentTypes.BTCLike)
|
||||
{
|
||||
var paymentData = JsonConvert.DeserializeObject<Payments.Bitcoin.BitcoinLikePaymentData>(CryptoPaymentData);
|
||||
// legacy
|
||||
paymentData.Output = Output;
|
||||
paymentData.Outpoint = Outpoint;
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
throw new NotSupportedException(nameof(CryptoPaymentDataType) + " does not support " + CryptoPaymentDataType);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
public Money GetValue(Dictionary<string, CryptoData> cryptoData, string cryptoCode, Money value = null)
|
||||
|
||||
public void SetCryptoPaymentData(CryptoPaymentData cryptoPaymentData)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
if (cryptoPaymentData is Payments.Bitcoin.BitcoinLikePaymentData paymentData)
|
||||
{
|
||||
// Legacy
|
||||
Outpoint = paymentData.Outpoint;
|
||||
Output = paymentData.Output;
|
||||
///
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException(cryptoPaymentData.ToString());
|
||||
CryptoPaymentDataType = paymentData.GetPaymentType().ToString();
|
||||
CryptoPaymentData = JsonConvert.SerializeObject(cryptoPaymentData);
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
public Money GetValue(PaymentMethodDictionary paymentMethods, PaymentMethodId paymentMethodId, Money value = null)
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
value = value ?? Output.Value;
|
||||
#pragma warning restore CS0618
|
||||
var to = cryptoCode;
|
||||
var from = GetCryptoCode();
|
||||
var to = paymentMethodId;
|
||||
var from = this.GetpaymentMethodId();
|
||||
if (to == from)
|
||||
return value;
|
||||
var fromRate = cryptoData[from].Rate;
|
||||
var toRate = cryptoData[to].Rate;
|
||||
var fromRate = paymentMethods[from].Rate;
|
||||
var toRate = paymentMethods[to].Rate;
|
||||
|
||||
var fiatValue = fromRate * value.ToDecimal(MoneyUnit.BTC);
|
||||
var otherCurrencyValue = toRate == 0 ? 0.0m : fiatValue / toRate;
|
||||
return Money.Coins(otherCurrencyValue);
|
||||
}
|
||||
|
||||
public PaymentMethodId GetpaymentMethodId()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return new PaymentMethodId(CryptoCode ?? "BTC", string.IsNullOrEmpty(CryptoPaymentDataType) ? PaymentTypes.BTCLike : Enum.Parse<PaymentTypes>(CryptoPaymentDataType));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public string GetCryptoCode()
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
return CryptoCode ?? "BTC";
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
|
||||
public interface CryptoPaymentData
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an identifier which uniquely identify the payment
|
||||
/// </summary>
|
||||
/// <returns>The payment id</returns>
|
||||
string GetPaymentId();
|
||||
|
||||
/// <summary>
|
||||
/// Returns terms which will be indexed and searchable in the search bar of invoice
|
||||
/// </summary>
|
||||
/// <returns>The search terms</returns>
|
||||
string[] GetSearchTerms();
|
||||
/// <summary>
|
||||
/// Get value of what as been paid
|
||||
/// </summary>
|
||||
/// <returns>The amount paid</returns>
|
||||
Money GetValue();
|
||||
bool PaymentCompleted(PaymentEntity entity, BTCPayNetwork network);
|
||||
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using DBreeze;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@ -16,6 +17,8 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using System.Globalization;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
|
||||
namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
@ -32,32 +35,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Network _Network;
|
||||
public Network Network
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Network;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Network = value;
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private CustomThreadPool _IndexerThread;
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath, Network network)
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
|
||||
{
|
||||
_Engine = new DBreezeEngine(dbreezePath);
|
||||
_IndexerThread = new CustomThreadPool(1, "Invoice Indexer");
|
||||
_Network = network;
|
||||
_ContextFactory = contextFactory;
|
||||
}
|
||||
|
||||
public async Task<bool> RemovePendingInvoice(string invoiceId)
|
||||
{
|
||||
Logs.PayServer.LogInformation($"Remove pending invoice {invoiceId}");
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.PendingInvoices.Remove(new PendingInvoiceData() { Id = invoiceId });
|
||||
@ -70,12 +59,22 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetInvoiceIdFromScriptPubKey(Script scriptPubKey, string cryptoCode)
|
||||
public async Task<InvoiceEntity> GetInvoiceFromScriptPubKey(Script scriptPubKey, string cryptoCode)
|
||||
{
|
||||
using (var db = _ContextFactory.CreateContext())
|
||||
{
|
||||
var result = await db.AddressInvoices.FindAsync(scriptPubKey.Hash.ToString() + "#" + cryptoCode);
|
||||
return result?.InvoiceDataId;
|
||||
var key = scriptPubKey.Hash.ToString() + "#" + cryptoCode;
|
||||
var result = await db.AddressInvoices
|
||||
#pragma warning disable CS0618
|
||||
.Where(a => a.Address == key)
|
||||
#pragma warning restore CS0618
|
||||
.Select(a => a.InvoiceData)
|
||||
.Include(a => a.Payments)
|
||||
.Include(a => a.RefundAddresses)
|
||||
.FirstOrDefaultAsync();
|
||||
if (result == null)
|
||||
return null;
|
||||
return ToEntity(result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,9 +104,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
public async Task<InvoiceEntity> CreateInvoiceAsync(string storeId, InvoiceEntity invoice, BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
List<string> textSearch = new List<string>();
|
||||
invoice = Clone(invoice);
|
||||
invoice = Clone(invoice, null);
|
||||
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
#pragma warning disable CS0618
|
||||
invoice.Payments = new List<PaymentEntity>();
|
||||
#pragma warning restore CS0618
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
@ -116,30 +117,33 @@ namespace BTCPayServer.Services.Invoices
|
||||
StoreDataId = storeId,
|
||||
Id = invoice.Id,
|
||||
Created = invoice.InvoiceTime,
|
||||
Blob = ToBytes(invoice),
|
||||
Blob = ToBytes(invoice, null),
|
||||
OrderId = invoice.OrderId,
|
||||
Status = invoice.Status,
|
||||
ItemCode = invoice.ProductInformation.ItemCode,
|
||||
CustomerEmail = invoice.RefundMail
|
||||
});
|
||||
|
||||
foreach (var cryptoData in invoice.GetCryptoData().Values)
|
||||
foreach (var paymentMethod in invoice.GetPaymentMethods(networkProvider))
|
||||
{
|
||||
var network = networkProvider.GetNetwork(cryptoData.CryptoCode);
|
||||
if (network == null)
|
||||
if (paymentMethod.Network == null)
|
||||
throw new InvalidOperationException("CryptoCode unsupported");
|
||||
var paymentDestination = paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||
|
||||
string address = GetDestination(paymentMethod);
|
||||
context.AddressInvoices.Add(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
CreatedTime = DateTimeOffset.UtcNow,
|
||||
}.SetHash(BitcoinAddress.Create(cryptoData.DepositAddress, network.NBitcoinNetwork).ScriptPubKey.Hash, network.CryptoCode));
|
||||
}.Set(address, paymentMethod.GetId()));
|
||||
|
||||
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
Assigned = DateTimeOffset.UtcNow
|
||||
}.SetAddress(cryptoData.DepositAddress, cryptoData.CryptoCode));
|
||||
textSearch.Add(cryptoData.DepositAddress);
|
||||
textSearch.Add(cryptoData.Calculate().TotalDue.ToString());
|
||||
}.SetAddress(paymentDestination, paymentMethod.GetId().ToString()));
|
||||
textSearch.Add(paymentDestination);
|
||||
textSearch.Add(paymentMethod.Calculate().TotalDue.ToString());
|
||||
}
|
||||
context.PendingInvoices.Add(new PendingInvoiceData() { Id = invoice.Id });
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
@ -149,8 +153,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
textSearch.Add(invoice.InvoiceTime.ToString(CultureInfo.InvariantCulture));
|
||||
textSearch.Add(invoice.ProductInformation.Price.ToString(CultureInfo.InvariantCulture));
|
||||
textSearch.Add(invoice.OrderId);
|
||||
textSearch.Add(ToString(invoice.BuyerInformation));
|
||||
textSearch.Add(ToString(invoice.ProductInformation));
|
||||
textSearch.Add(ToString(invoice.BuyerInformation, null));
|
||||
textSearch.Add(ToString(invoice.ProductInformation, null));
|
||||
textSearch.Add(invoice.StoreId);
|
||||
|
||||
AddToTextSearch(invoice.Id, textSearch.ToArray());
|
||||
@ -158,7 +162,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
return invoice;
|
||||
}
|
||||
|
||||
public async Task<bool> NewAddress(string invoiceId, BitcoinAddress bitcoinAddress, BTCPayNetwork network)
|
||||
private static string GetDestination(PaymentMethod paymentMethod)
|
||||
{
|
||||
// For legacy reason, BitcoinLikeOnChain is putting the hashes of addresses in database
|
||||
if (paymentMethod.GetId().PaymentType == Payments.PaymentTypes.BTCLike)
|
||||
{
|
||||
return ((Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails()).DepositAddress.ScriptPubKey.Hash.ToString();
|
||||
}
|
||||
///////////////
|
||||
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||
}
|
||||
|
||||
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
@ -166,52 +181,71 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (invoice == null)
|
||||
return false;
|
||||
|
||||
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob);
|
||||
var cryptoData = invoiceEntity.GetCryptoData();
|
||||
var currencyData = cryptoData.Where(c => c.Value.CryptoCode == network.CryptoCode).Select(f => f.Value).FirstOrDefault();
|
||||
var invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
|
||||
var currencyData = invoiceEntity.GetPaymentMethod(network, paymentMethod.GetPaymentType(), null);
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
|
||||
if (currencyData.DepositAddress != null)
|
||||
var existingPaymentMethod = currencyData.GetPaymentMethodDetails();
|
||||
if (existingPaymentMethod.GetPaymentDestination() != null)
|
||||
{
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, network.CryptoCode);
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
|
||||
}
|
||||
|
||||
currencyData.DepositAddress = bitcoinAddress.ToString();
|
||||
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
|
||||
|
||||
currencyData.SetPaymentMethodDetails(existingPaymentMethod);
|
||||
#pragma warning disable CS0618
|
||||
if (network.IsBTC)
|
||||
{
|
||||
invoiceEntity.DepositAddress = currencyData.DepositAddress;
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
invoiceEntity.SetCryptoData(cryptoData);
|
||||
invoice.Blob = ToBytes(invoiceEntity);
|
||||
invoiceEntity.SetPaymentMethod(currencyData);
|
||||
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
|
||||
|
||||
context.AddressInvoices.Add(new AddressInvoiceData() {
|
||||
InvoiceDataId = invoiceId, CreatedTime = DateTimeOffset.UtcNow }
|
||||
.SetHash(bitcoinAddress.ScriptPubKey.Hash, network.CryptoCode));
|
||||
context.AddressInvoices.Add(new AddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
CreatedTime = DateTimeOffset.UtcNow
|
||||
}
|
||||
.Set(GetDestination(currencyData), currencyData.GetId()));
|
||||
context.HistoricalAddressInvoices.Add(new HistoricalAddressInvoiceData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
Assigned = DateTimeOffset.UtcNow
|
||||
}.SetAddress(bitcoinAddress.ToString(), network.CryptoCode));
|
||||
}.SetAddress(paymentMethod.GetPaymentDestination(), network.CryptoCode));
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
AddToTextSearch(invoice.Id, bitcoinAddress.ToString());
|
||||
AddToTextSearch(invoice.Id, paymentMethod.GetPaymentDestination());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, string cryptoCode)
|
||||
public async Task AddInvoiceEvent(string invoiceId, object evt)
|
||||
{
|
||||
foreach (var address in entity.GetCryptoData())
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
if (cryptoCode != null && cryptoCode != address.Value.CryptoCode)
|
||||
context.InvoiceEvents.Add(new InvoiceEventData()
|
||||
{
|
||||
InvoiceDataId = invoiceId,
|
||||
Message = evt.ToString(),
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
UniqueId = Encoders.Hex.EncodeData(RandomUtils.GetBytes(10))
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, PaymentMethodId paymentMethodId)
|
||||
{
|
||||
foreach (var address in entity.GetPaymentMethods(null))
|
||||
{
|
||||
if (paymentMethodId != null && paymentMethodId != address.GetId())
|
||||
continue;
|
||||
var historical = new HistoricalAddressInvoiceData();
|
||||
historical.InvoiceDataId = invoiceId;
|
||||
historical.SetAddress(address.Value.DepositAddress, cryptoCode);
|
||||
historical.SetAddress(address.GetPaymentMethodDetails().GetPaymentDestination(), address.GetId().ToString());
|
||||
historical.UnAssigned = DateTimeOffset.UtcNow;
|
||||
context.Attach(historical);
|
||||
context.Entry(historical).Property(o => o.UnAssigned).IsModified = true;
|
||||
@ -225,7 +259,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
var invoiceData = await context.FindAsync<InvoiceData>(invoiceId).ConfigureAwait(false);
|
||||
if (invoiceData == null)
|
||||
return;
|
||||
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob);
|
||||
var invoiceEntity = ToObject<InvoiceEntity>(invoiceData.Blob, null);
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, null);
|
||||
try
|
||||
{
|
||||
@ -308,13 +342,15 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
private InvoiceEntity ToEntity(InvoiceData invoice)
|
||||
{
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob);
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = invoice.Payments.Select(p =>
|
||||
{
|
||||
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
|
||||
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
|
||||
paymentEntity.Accounted = p.Accounted;
|
||||
return paymentEntity;
|
||||
}).ToList();
|
||||
#pragma warning restore CS0618
|
||||
entity.ExceptionStatus = invoice.ExceptionStatus;
|
||||
entity.Status = invoice.Status;
|
||||
entity.RefundMail = invoice.CustomerEmail;
|
||||
@ -325,7 +361,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
}
|
||||
if (invoice.AddressInvoices != null)
|
||||
{
|
||||
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetHash() + a.GetCryptoCode()).ToHashSet();
|
||||
entity.AvailableAddressHashes = invoice.AddressInvoices.Select(a => a.GetAddress() + a.GetpaymentMethodId().ToString()).ToHashSet();
|
||||
}
|
||||
if(invoice.Events != null)
|
||||
{
|
||||
entity.Events = invoice.Events.OrderBy(c => c.Timestamp).ToList();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
@ -339,8 +379,10 @@ namespace BTCPayServer.Services.Invoices
|
||||
.Invoices
|
||||
.Include(o => o.Payments)
|
||||
.Include(o => o.RefundAddresses);
|
||||
if(queryObject.IncludeAddresses)
|
||||
if (queryObject.IncludeAddresses)
|
||||
query = query.Include(o => o.HistoricalAddressInvoices).Include(o => o.AddressInvoices);
|
||||
if (queryObject.IncludeEvents)
|
||||
query = query.Include(o => o.Events);
|
||||
if (!string.IsNullOrEmpty(queryObject.InvoiceId))
|
||||
{
|
||||
query = query.Where(i => i.Id == queryObject.InvoiceId);
|
||||
@ -360,7 +402,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
var ids = new HashSet<string>(SearchInvoice(queryObject.TextSearch));
|
||||
if (ids.Count == 0)
|
||||
return new InvoiceEntity[0];
|
||||
return Array.Empty<InvoiceEntity>();
|
||||
query = query.Where(i => ids.Contains(i.Id));
|
||||
}
|
||||
|
||||
@ -394,7 +436,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
}
|
||||
|
||||
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs)
|
||||
public async Task AddRefundsAsync(string invoiceId, TxOut[] outputs, Network network)
|
||||
{
|
||||
if (outputs.Length == 0)
|
||||
return;
|
||||
@ -408,47 +450,49 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
Id = invoiceId + "-" + i,
|
||||
InvoiceDataId = invoiceId,
|
||||
Blob = ToBytes(output)
|
||||
Blob = ToBytes(output, network)
|
||||
});
|
||||
i++;
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(_Network)).Where(a => a != null).ToArray();
|
||||
var addresses = outputs.Select(o => o.ScriptPubKey.GetDestinationAddress(network)).Where(a => a != null).ToArray();
|
||||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
||||
}
|
||||
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin, string cryptoCode)
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
PaymentEntity entity = new PaymentEntity
|
||||
{
|
||||
Outpoint = receivedCoin.Outpoint,
|
||||
#pragma warning disable CS0618
|
||||
Output = receivedCoin.TxOut,
|
||||
CryptoCode = cryptoCode,
|
||||
#pragma warning restore CS0618
|
||||
ReceivedTime = DateTime.UtcNow
|
||||
ReceivedTime = date.UtcDateTime,
|
||||
Accounted = false
|
||||
};
|
||||
entity.SetCryptoPaymentData(paymentData);
|
||||
|
||||
|
||||
PaymentData data = new PaymentData
|
||||
{
|
||||
Id = receivedCoin.Outpoint.ToString(),
|
||||
Blob = ToBytes(entity),
|
||||
InvoiceDataId = invoiceId
|
||||
Id = paymentData.GetPaymentId(),
|
||||
Blob = ToBytes(entity, null),
|
||||
InvoiceDataId = invoiceId,
|
||||
Accounted = false
|
||||
};
|
||||
|
||||
context.Payments.Add(data);
|
||||
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
AddToTextSearch(invoiceId, receivedCoin.Outpoint.Hash.ToString());
|
||||
AddToTextSearch(invoiceId, paymentData.GetSearchTerms());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePayments(List<AccountedPaymentEntity> payments)
|
||||
public async Task UpdatePayments(List<PaymentEntity> payments)
|
||||
{
|
||||
if (payments.Count == 0)
|
||||
return;
|
||||
@ -456,34 +500,37 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
foreach (var payment in payments)
|
||||
{
|
||||
var paymentData = payment.GetCryptoPaymentData();
|
||||
var data = new PaymentData();
|
||||
data.Id = payment.Payment.Outpoint.ToString();
|
||||
data.Accounted = payment.Payment.Accounted;
|
||||
data.Id = paymentData.GetPaymentId();
|
||||
data.Accounted = payment.Accounted;
|
||||
data.Blob = ToBytes(payment, null);
|
||||
context.Attach(data);
|
||||
context.Entry(data).Property(o => o.Accounted).IsModified = true;
|
||||
context.Entry(data).Property(o => o.Blob).IsModified = true;
|
||||
}
|
||||
await context.SaveChangesAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private T ToObject<T>(byte[] value)
|
||||
private T ToObject<T>(byte[] value, Network network)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), Network);
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ZipUtils.Unzip(value), network);
|
||||
}
|
||||
|
||||
private byte[] ToBytes<T>(T obj)
|
||||
private byte[] ToBytes<T>(T obj, Network network)
|
||||
{
|
||||
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj));
|
||||
return ZipUtils.Zip(NBitcoin.JsonConverters.Serializer.ToString(obj, network));
|
||||
}
|
||||
|
||||
private T Clone<T>(T invoice)
|
||||
private T Clone<T>(T invoice, Network network)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice), Network);
|
||||
return NBitcoin.JsonConverters.Serializer.ToObject<T>(ToString(invoice, network), network);
|
||||
}
|
||||
|
||||
private string ToString<T>(T data)
|
||||
private string ToString<T>(T data, Network network)
|
||||
{
|
||||
return NBitcoin.JsonConverters.Serializer.ToString(data, Network);
|
||||
return NBitcoin.JsonConverters.Serializer.ToString(data, network);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -549,5 +596,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
public bool IncludeAddresses { get; set; }
|
||||
|
||||
public bool IncludeEvents { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Hangfire;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Hangfire;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -20,10 +22,16 @@ namespace BTCPayServer.Services.Mails
|
||||
_JobClient = jobClient;
|
||||
_Repository = repository;
|
||||
}
|
||||
public Task SendEmailAsync(string email, string subject, string message)
|
||||
public async Task SendEmailAsync(string email, string subject, string message)
|
||||
{
|
||||
var settings = await _Repository.GetSettingAsync<EmailSettings>();
|
||||
if (settings == null)
|
||||
{
|
||||
Logs.Configuration.LogWarning("Should have sent email, but email settings are not configured");
|
||||
return;
|
||||
}
|
||||
_JobClient.Schedule(() => SendMailCore(email, subject, message), TimeSpan.Zero);
|
||||
return Task.CompletedTask;
|
||||
return;
|
||||
}
|
||||
|
||||
public async Task SendMailCore(string email, string subject, string message)
|
||||
|
@ -11,6 +11,16 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
IMemoryCache _Cache;
|
||||
ConcurrentDictionary<string, IRateProvider> _Providers = new ConcurrentDictionary<string, IRateProvider>();
|
||||
ConcurrentDictionary<string, IRateProvider> _LongCacheProviders = new ConcurrentDictionary<string, IRateProvider>();
|
||||
|
||||
public IMemoryCache Cache
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Cache;
|
||||
}
|
||||
}
|
||||
|
||||
public CachedDefaultRateProviderFactory(IMemoryCache cache)
|
||||
{
|
||||
if (cache == null)
|
||||
@ -18,10 +28,13 @@ namespace BTCPayServer.Services.Rates
|
||||
_Cache = cache;
|
||||
}
|
||||
|
||||
public IRateProvider RateProvider { get; set; }
|
||||
|
||||
public TimeSpan CacheSpan { get; set; } = TimeSpan.FromMinutes(1.0);
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network)
|
||||
public TimeSpan LongCacheSpan { get; set; } = TimeSpan.FromMinutes(15.0);
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
|
||||
{
|
||||
return _Providers.GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, network.DefaultRateProvider, _Cache) { CacheSpan = CacheSpan });
|
||||
return (longCache ? _LongCacheProviders : _Providers).GetOrAdd(network.CryptoCode, new CachedRateProvider(network.CryptoCode, RateProvider ?? network.DefaultRateProvider, _Cache) { CacheSpan = longCache ? LongCacheSpan : CacheSpan, AdditionalScope = longCache ? "LONG" : "SHORT" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,38 +19,43 @@ namespace BTCPayServer.Services.Rates
|
||||
if (memoryCache == null)
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
this._Inner = inner;
|
||||
this._MemoryCache = memoryCache;
|
||||
this.MemoryCache = memoryCache;
|
||||
this._CryptoCode = cryptoCode;
|
||||
}
|
||||
|
||||
public IRateProvider Inner
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Inner;
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan CacheSpan
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
public IMemoryCache MemoryCache { get => _MemoryCache; private set => _MemoryCache = value; }
|
||||
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
return _MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode, (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("CURR_" + currency + "_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRateAsync(currency);
|
||||
});
|
||||
}
|
||||
|
||||
private bool TryGetFromCache(string key, out object obj)
|
||||
{
|
||||
obj = _MemoryCache.Get(key);
|
||||
return obj != null;
|
||||
}
|
||||
|
||||
|
||||
public Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
return _MemoryCache.GetOrCreateAsync("GLOBAL_RATES", (ICacheEntry entry) =>
|
||||
return MemoryCache.GetOrCreateAsync("GLOBAL_RATES_" + _CryptoCode + "_" + AdditionalScope, (ICacheEntry entry) =>
|
||||
{
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return _Inner.GetRatesAsync();
|
||||
});
|
||||
}
|
||||
|
||||
public string AdditionalScope { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
@ -24,6 +25,8 @@ namespace BTCPayServer.Services.Rates
|
||||
CryptoCode = cryptoCode ?? "BTC";
|
||||
}
|
||||
|
||||
public string Exchange { get; set; }
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
|
||||
public string Market
|
||||
@ -45,7 +48,15 @@ namespace BTCPayServer.Services.Rates
|
||||
|
||||
private async Task<Dictionary<string, decimal>> GetRatesCore()
|
||||
{
|
||||
var resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
|
||||
HttpResponseMessage resp = null;
|
||||
if (Exchange == null)
|
||||
{
|
||||
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/indices/{Market}/ticker/short");
|
||||
}
|
||||
else
|
||||
{
|
||||
resp = await _Client.GetAsync($"https://apiv2.bitcoinaverage.com/exchanges/{Exchange}");
|
||||
}
|
||||
using (resp)
|
||||
{
|
||||
|
||||
@ -57,15 +68,24 @@ namespace BTCPayServer.Services.Rates
|
||||
throw new CoinAverageException("Unauthorized access to the API, premium plan needed");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var rates = JObject.Parse(await resp.Content.ReadAsStringAsync());
|
||||
if(Exchange != null)
|
||||
{
|
||||
rates = (JObject)rates["symbols"];
|
||||
}
|
||||
return rates.Properties()
|
||||
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase))
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p => ToDecimal(p.Value["last"]));
|
||||
.Where(p => p.Name.StartsWith(CryptoCode, StringComparison.OrdinalIgnoreCase) && TryToDecimal(p, out decimal unused))
|
||||
.ToDictionary(p => p.Name.Substring(CryptoCode.Length, p.Name.Length - CryptoCode.Length), p =>
|
||||
{
|
||||
TryToDecimal(p, out decimal v);
|
||||
return v;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private decimal ToDecimal(JToken token)
|
||||
private bool TryToDecimal(JProperty p, out decimal v)
|
||||
{
|
||||
return decimal.Parse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint);
|
||||
JToken token = p.Value[Exchange == null ? "last" : "bid"];
|
||||
return decimal.TryParse(token.Value<string>(), System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out v);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
|
@ -7,6 +7,6 @@ namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public interface IRateProviderFactory
|
||||
{
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network);
|
||||
IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache);
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,38 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class MockRateProviderFactory : IRateProviderFactory
|
||||
{
|
||||
List<MockRateProvider> _Mocks = new List<MockRateProvider>();
|
||||
public MockRateProviderFactory()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void AddMock(MockRateProvider mock)
|
||||
{
|
||||
_Mocks.Add(mock);
|
||||
}
|
||||
public IRateProvider GetRateProvider(BTCPayNetwork network, bool longCache)
|
||||
{
|
||||
return _Mocks.FirstOrDefault(m => m.CryptoCode == network.CryptoCode);
|
||||
}
|
||||
}
|
||||
public class MockRateProvider : IRateProvider
|
||||
{
|
||||
List<Rate> _Rates;
|
||||
|
||||
public MockRateProvider(params Rate[] rates)
|
||||
public string CryptoCode { get; }
|
||||
|
||||
public MockRateProvider(string cryptoCode, params Rate[] rates)
|
||||
{
|
||||
_Rates = new List<Rate>(rates);
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public MockRateProvider(List<Rate> rates)
|
||||
public MockRateProvider(string cryptoCode, List<Rate> rates)
|
||||
{
|
||||
_Rates = rates;
|
||||
CryptoCode = cryptoCode;
|
||||
}
|
||||
public Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
|
53
BTCPayServer/Services/Rates/TweakRateProvider.cs
Normal file
53
BTCPayServer/Services/Rates/TweakRateProvider.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Services.Rates
|
||||
{
|
||||
public class TweakRateProvider : IRateProvider
|
||||
{
|
||||
private BTCPayNetwork network;
|
||||
private IRateProvider rateProvider;
|
||||
private List<RateRule> rateRules;
|
||||
|
||||
public TweakRateProvider(BTCPayNetwork network, IRateProvider rateProvider, List<RateRule> rateRules)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (rateProvider == null)
|
||||
throw new ArgumentNullException(nameof(rateProvider));
|
||||
if (rateRules == null)
|
||||
throw new ArgumentNullException(nameof(rateRules));
|
||||
this.network = network;
|
||||
this.rateProvider = rateProvider;
|
||||
this.rateRules = rateRules;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetRateAsync(string currency)
|
||||
{
|
||||
var rate = await rateProvider.GetRateAsync(currency);
|
||||
foreach(var rule in rateRules)
|
||||
{
|
||||
rate = rule.Apply(network, rate);
|
||||
}
|
||||
return rate;
|
||||
}
|
||||
|
||||
public async Task<ICollection<Rate>> GetRatesAsync()
|
||||
{
|
||||
List<Rate> rates = new List<Rate>();
|
||||
foreach (var rate in await rateProvider.GetRatesAsync())
|
||||
{
|
||||
var localRate = rate.Value;
|
||||
foreach (var rule in rateRules)
|
||||
{
|
||||
localRate = rule.Apply(network, localRate);
|
||||
}
|
||||
rates.Add(new Rate(rate.Currency, localRate));
|
||||
}
|
||||
return rates;
|
||||
}
|
||||
}
|
||||
}
|
@ -68,7 +68,8 @@ namespace BTCPayServer.Services.Stores
|
||||
StoreData store = new StoreData
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(32)),
|
||||
StoreName = name
|
||||
StoreName = name,
|
||||
SpeedPolicy = Invoices.SpeedPolicy.MediumSpeed
|
||||
};
|
||||
var userStore = new UserStore
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using NBitcoin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
@ -9,82 +10,172 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using System.Threading;
|
||||
using NBXplorer.Models;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using BTCPayServer.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets
|
||||
{
|
||||
public class KnownState
|
||||
public class ReceivedCoin
|
||||
{
|
||||
public uint256 UnconfirmedHash { get; set; }
|
||||
public uint256 ConfirmedHash { get; set; }
|
||||
public Coin Coin { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public KeyPath KeyPath { get; set; }
|
||||
}
|
||||
public class GetCoinsResult
|
||||
public class NetworkCoins
|
||||
{
|
||||
public Coin[] Coins { get; set; }
|
||||
public KnownState State { get; set; }
|
||||
public DerivationStrategy Strategy { get; set; }
|
||||
public class TimestampedCoin
|
||||
{
|
||||
public DateTimeOffset DateTime { get; set; }
|
||||
public Coin Coin { get; set; }
|
||||
}
|
||||
public TimestampedCoin[] TimestampedCoins { get; set; }
|
||||
public DerivationStrategyBase Strategy { get; set; }
|
||||
public BTCPayWallet Wallet { get; set; }
|
||||
}
|
||||
public class BTCPayWallet
|
||||
{
|
||||
private ExplorerClientProvider _Client;
|
||||
ApplicationDbContextFactory _DBFactory;
|
||||
|
||||
public BTCPayWallet(ExplorerClientProvider client, ApplicationDbContextFactory factory)
|
||||
private ExplorerClient _Client;
|
||||
private IMemoryCache _MemoryCache;
|
||||
public BTCPayWallet(ExplorerClient client, IMemoryCache memoryCache, BTCPayNetwork network)
|
||||
{
|
||||
if (client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
if (memoryCache == null)
|
||||
throw new ArgumentNullException(nameof(memoryCache));
|
||||
_Client = client;
|
||||
_DBFactory = factory;
|
||||
_Network = network;
|
||||
_MemoryCache = memoryCache;
|
||||
}
|
||||
|
||||
|
||||
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategy derivationStrategy)
|
||||
private readonly BTCPayNetwork _Network;
|
||||
public BTCPayNetwork Network
|
||||
{
|
||||
var client = _Client.GetExplorerClient(derivationStrategy.Network);
|
||||
var pathInfo = await client.GetUnusedAsync(derivationStrategy.DerivationStrategyBase, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
|
||||
return pathInfo.ScriptPubKey.GetDestinationAddress(client.Network);
|
||||
}
|
||||
|
||||
public async Task TrackAsync(DerivationStrategy derivationStrategy)
|
||||
{
|
||||
var client = _Client.GetExplorerClient(derivationStrategy.Network);
|
||||
await client.TrackAsync(derivationStrategy.DerivationStrategyBase);
|
||||
}
|
||||
|
||||
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var client = _Client.GetExplorerClient(network);
|
||||
return client.GetTransactionAsync(txId, cancellation);
|
||||
}
|
||||
|
||||
public async Task<GetCoinsResult> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var client = _Client.GetExplorerClient(strategy.Network);
|
||||
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
|
||||
return new GetCoinsResult()
|
||||
get
|
||||
{
|
||||
Coins = utxos,
|
||||
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
|
||||
Strategy = strategy,
|
||||
};
|
||||
return _Network;
|
||||
}
|
||||
}
|
||||
|
||||
public Task BroadcastTransactionsAsync(BTCPayNetwork network, List<Transaction> transactions)
|
||||
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
var client = _Client.GetExplorerClient(network);
|
||||
var tasks = transactions.Select(t => client.BroadcastAsync(t)).ToArray();
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
|
||||
// Might happen on some broken install
|
||||
if (pathInfo == null)
|
||||
{
|
||||
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
|
||||
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
|
||||
}
|
||||
return pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
public async Task<(BitcoinAddress, KeyPath)> GetChangeAddressAsync(DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
|
||||
// Might happen on some broken install
|
||||
if (pathInfo == null)
|
||||
{
|
||||
await _Client.TrackAsync(derivationStrategy).ConfigureAwait(false);
|
||||
pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Change, 0, false).ConfigureAwait(false);
|
||||
}
|
||||
return (pathInfo.ScriptPubKey.GetDestinationAddress(Network.NBitcoinNetwork), pathInfo.KeyPath);
|
||||
}
|
||||
|
||||
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
await _Client.TrackAsync(derivationStrategy);
|
||||
}
|
||||
|
||||
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
if (txId == null)
|
||||
throw new ArgumentNullException(nameof(txId));
|
||||
var tx = await _Client.GetTransactionAsync(txId, cancellation);
|
||||
return tx;
|
||||
}
|
||||
|
||||
public void InvalidateCache(DerivationStrategyBase strategy)
|
||||
{
|
||||
_MemoryCache.Remove("CACHEDCOINS_" + strategy.ToString());
|
||||
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
|
||||
}
|
||||
ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
|
||||
|
||||
private async Task<UTXOChanges> GetUTXOChanges(DerivationStrategyBase strategy, CancellationToken cancellation)
|
||||
{
|
||||
var thisCompletionSource = new TaskCompletionSource<UTXOChanges>();
|
||||
var completionSource = _FetchingUTXOs.GetOrAdd(strategy.ToString(), (s) => thisCompletionSource);
|
||||
if (thisCompletionSource != completionSource)
|
||||
return await completionSource.Task;
|
||||
try
|
||||
{
|
||||
var utxos = await _MemoryCache.GetOrCreateAsync("CACHEDCOINS_" + strategy.ToString(), async entry =>
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
UTXOChanges result = null;
|
||||
try
|
||||
{
|
||||
result = await _Client.GetUTXOsAsync(strategy, null, false, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logs.PayServer.LogError("Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers");
|
||||
throw;
|
||||
}
|
||||
var spentTime = DateTimeOffset.UtcNow - now;
|
||||
if (spentTime.TotalSeconds > 30)
|
||||
{
|
||||
Logs.PayServer.LogWarning($"NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers");
|
||||
}
|
||||
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
|
||||
return result;
|
||||
});
|
||||
completionSource.TrySetResult(utxos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completionSource.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
|
||||
}
|
||||
return await completionSource.Task;
|
||||
}
|
||||
|
||||
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
|
||||
{
|
||||
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
|
||||
return Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategy derivationStrategy)
|
||||
|
||||
|
||||
public async Task<ReceivedCoin[]> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
var client = _Client.GetExplorerClient(derivationStrategy.Network);
|
||||
var result = await client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true);
|
||||
return result.Confirmed.UTXOs.Select(u => u.Value)
|
||||
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Value))
|
||||
.Sum();
|
||||
if (derivationStrategy == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
return (await GetUTXOChanges(derivationStrategy, cancellation))
|
||||
.GetUnspentUTXOs()
|
||||
.Select(c => new ReceivedCoin()
|
||||
{
|
||||
Coin = c.AsCoin(derivationStrategy),
|
||||
KeyPath = c.KeyPath,
|
||||
Timestamp = c.Timestamp
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
UTXOChanges changes = await GetUTXOChanges(derivationStrategy, cancellation);
|
||||
return changes.GetUnspentUTXOs().Select(c => c.Value).Sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
61
BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs
Normal file
61
BTCPayServer/Services/Wallets/BTCPayWalletProvider.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BTCPayServer.Services.Wallets
|
||||
{
|
||||
public class BTCPayWalletProvider
|
||||
{
|
||||
private ExplorerClientProvider _Client;
|
||||
BTCPayNetworkProvider _NetworkProvider;
|
||||
IOptions<MemoryCacheOptions> _Options;
|
||||
public BTCPayWalletProvider(ExplorerClientProvider client,
|
||||
IOptions<MemoryCacheOptions> memoryCacheOption,
|
||||
BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
if (client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
_Client = client;
|
||||
_NetworkProvider = networkProvider;
|
||||
_Options = memoryCacheOption;
|
||||
|
||||
foreach(var network in networkProvider.GetAll())
|
||||
{
|
||||
var explorerClient = _Client.GetExplorerClient(network.CryptoCode);
|
||||
if (explorerClient == null)
|
||||
continue;
|
||||
_Wallets.Add(network.CryptoCode, new BTCPayWallet(explorerClient, new MemoryCache(_Options), network));
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, BTCPayWallet> _Wallets = new Dictionary<string, BTCPayWallet>();
|
||||
|
||||
public BTCPayWallet GetWallet(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return GetWallet(network.CryptoCode);
|
||||
}
|
||||
public BTCPayWallet GetWallet(string cryptoCode)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
_Wallets.TryGetValue(cryptoCode, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsAvailable(BTCPayNetwork network)
|
||||
{
|
||||
return _Client.IsAvailable(network);
|
||||
}
|
||||
|
||||
public IEnumerable<BTCPayWallet> GetWallets()
|
||||
{
|
||||
foreach (var w in _Wallets)
|
||||
yield return w.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -57,10 +57,10 @@
|
||||
<h2>Video tutorials</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6 text-center">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
|
||||
<iframe width="400" height="225" src="https://www.youtube.com/embed/xh3Eac66qc4" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
<div class="col-md-6 text-center">
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
|
||||
<iframe width="400" height="225" src="https://www.youtube.com/embed/Xo_vApXTZBU" frameborder="0" allowfullscreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -122,10 +122,10 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 ml-auto text-center">
|
||||
<a href="http://52.191.212.129:3000/">
|
||||
<a href="http://slack.forkbitpay.ninja/">
|
||||
<img src="~/img/slack.png" height="100" />
|
||||
</a>
|
||||
<p><a href="http://52.191.212.129:3000/">On Slack</a></p>
|
||||
<p><a href="http://slack.forkbitpay.ninja/">On Slack</a></p>
|
||||
</div>
|
||||
<div class="col-lg-4 mr-auto text-center">
|
||||
<a href="https://twitter.com/BtcpayServer">
|
||||
|
@ -21,7 +21,7 @@
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.7.1/clipboard.min.js"></script>
|
||||
<script src="~/js/vue.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/vue.min.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/vue-qrcode.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/core.js" type="text/javascript" defer="defer"></script>
|
||||
<!-- <script src="img/Intl.js" type="text/javascript" defer="defer"></script>
|
||||
@ -123,14 +123,14 @@
|
||||
<div class="line-items">
|
||||
<!---->
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label" i18n="">Payment Amount</div>
|
||||
<div class="line-items__item__value">{{srvModel.btcAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
<div class="line-items__item__label" i18n="">Order Amount</div>
|
||||
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
<span i18n="">Network Cost</span>
|
||||
</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.txCount }} transaction x {{ srvModel.txFees}} {{ srvModel.cryptoCode }}</div>
|
||||
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
|
||||
</div>
|
||||
<div class="line-items__item">
|
||||
<div class="line-items__item__label">
|
||||
@ -158,6 +158,7 @@
|
||||
<div adjust-height="" class="payment-box">
|
||||
<div class="bp-view payment scan" id="scan">
|
||||
<div class="payment__scan">
|
||||
<img :src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
|
||||
<qrcode :val="srvModel.invoiceBitcoinUrl" :size="256" bg-color="#f5f5f7" fg-color="#000" />
|
||||
</div>
|
||||
<div class="payment__details__instruction__open-wallet">
|
||||
@ -360,7 +361,7 @@
|
||||
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
|
||||
<div class="manual-box__address__wrapper">
|
||||
<div class="manual-box__address__wrapper__logo">
|
||||
<img src="@Model.CryptoImage">
|
||||
<img :src="srvModel.cryptoImage" />
|
||||
</div>
|
||||
<div class="manual-box__address__wrapper__value">{{srvModel.btcAddress}}</div>
|
||||
</div>
|
||||
@ -491,7 +492,7 @@
|
||||
<div>
|
||||
<div class="expired__body">
|
||||
<div class="expired__header" i18n="">What happened?</div>
|
||||
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for 15 minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
|
||||
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
|
||||
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
|
||||
<div class="expired__text" i18n="">
|
||||
If the transaction
|
||||
@ -503,7 +504,7 @@
|
||||
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
|
||||
<!---->
|
||||
<span>
|
||||
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.OrderId}}
|
||||
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -605,8 +606,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px; display: none;">
|
||||
<div></div>
|
||||
<div class="footer__item no-hover" style="opacity: 1; padding-left: 0; max-height: 21px;">
|
||||
@if(Model.AvailableCryptos.Count > 1)
|
||||
{
|
||||
<div style="text-align:center">Accepted here</div>
|
||||
<div style="text-align:center">
|
||||
@foreach(var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.paymentMethodId='@crypto.paymentMethodId'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.paymentMethodId" src="@crypto.CryptoImage" /></a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,10 +53,18 @@
|
||||
<th>Monitoring date</th>
|
||||
<td>@Model.MonitoringDate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Transaction speed</th>
|
||||
<td>@Model.TransactionSpeed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>@Model.Status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status Exception</th>
|
||||
<td>@Model.StatusException</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Refund email</th>
|
||||
<td>@Model.RefundEmail</td>
|
||||
@ -73,6 +81,10 @@
|
||||
<th>Notification Url</th>
|
||||
<td>@Model.NotificationUrl</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Redirect Url</th>
|
||||
<td>@Model.RedirectUrl</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -174,6 +186,7 @@
|
||||
<th>Deposit address</th>
|
||||
<th>Transaction Id</th>
|
||||
<th>Confirmations</th>
|
||||
<th>Replaced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -185,6 +198,7 @@
|
||||
<td>@payment.DepositAddress</td>
|
||||
<td><a href="@payment.TransactionLink" target="_blank">@payment.TransactionId</a></td>
|
||||
<td>@payment.Confirmations</td>
|
||||
<td>@payment.Replaced</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@ -215,5 +229,28 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Events</h3>
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var evt in Model.Events)
|
||||
{
|
||||
<tr>
|
||||
<td>@evt.Timestamp</td>
|
||||
<td>@evt.Message</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
94
BTCPayServer/Views/Shared/SyncModal.cshtml
Normal file
94
BTCPayServer/Views/Shared/SyncModal.cshtml
Normal file
@ -0,0 +1,94 @@
|
||||
@inject BTCPayServer.HostedServices.NBXplorerDashboard dashboard
|
||||
<!-- Modal -->
|
||||
<div id="modalDialog" class="modal-dialog animated bounceInRight">
|
||||
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Your nodes are synching...</h4>
|
||||
<button type="button" class="close" onclick="dismissSyncModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Some of your nodes are still synching...<br />
|
||||
BTCPay Server will not work correctly until it is over.
|
||||
</p>
|
||||
@foreach (var line in dashboard.GetAll())
|
||||
{
|
||||
<h4>@line.Network.CryptoCode</h4>
|
||||
@if (line.Status == null)
|
||||
{
|
||||
<ul>
|
||||
<li>The node is offline</li>
|
||||
@if (line.Error != null)
|
||||
{
|
||||
<li>Last error: @line.Error</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul>
|
||||
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
|
||||
@if (line.Status.BitcoinStatus == null)
|
||||
{
|
||||
if (line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
|
||||
{
|
||||
<li>The node is starting...</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li>The node is offline</li>
|
||||
@if (line.Error != null)
|
||||
{
|
||||
<li>Last error: @line.Error</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (line.Status.BitcoinStatus.IsSynched)
|
||||
{
|
||||
<li>The node is synched (Height: @line.Status.BitcoinStatus.Headers)</li>
|
||||
@if (line.Status.BitcoinStatus.IsSynched &&
|
||||
line.Status.SyncHeight.HasValue &&
|
||||
line.Status.SyncHeight.Value < line.Status.BitcoinStatus.Headers)
|
||||
{
|
||||
<li>NBXplorer is synching... (Height: @line.Status.SyncHeight.Value)</li>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
|
||||
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
|
||||
}
|
||||
</ul>
|
||||
@if (!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
|
||||
{
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%">
|
||||
@((int)(line.Status.BitcoinStatus.VerificationProgress * 100))%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<link href="~/vendor/animatecss/animate.css" rel="stylesheet" />
|
||||
<script type="text/javascript">
|
||||
function dismissSyncModal() {
|
||||
$("#modalDialog").addClass('animated bounceOutRight')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
#modalDialog {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
margin: 0px;
|
||||
}
|
||||
</style>
|
@ -38,7 +38,7 @@
|
||||
<body id="page-top">
|
||||
|
||||
@{
|
||||
if(ViewBag.AlwaysShrinkNavBar == null)
|
||||
if (ViewBag.AlwaysShrinkNavBar == null)
|
||||
{
|
||||
ViewBag.AlwaysShrinkNavBar = true;
|
||||
}
|
||||
@ -48,30 +48,36 @@
|
||||
<!-- Navigation -->
|
||||
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
|
||||
<div class="container">
|
||||
<a class="navbar-brand js-scroll-trigger" href="~/"><img src="~/img/logo.png" height="45"></a>
|
||||
<a class="navbar-brand js-scroll-trigger" href="~/">
|
||||
<img src="~/img/logo.png" height="45">
|
||||
@if(env.ChainType != NBXplorer.ChainType.Main)
|
||||
{
|
||||
<span class="badge badge-warning" style="font-size:10px;">@env.ChainType.ToString()</span>
|
||||
}
|
||||
</a>
|
||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||
<ul class="navbar-nav ml-auto">
|
||||
@if(SignInManager.IsSignedIn(User))
|
||||
{
|
||||
@if(User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
{
|
||||
@if(User.IsInRole(Roles.ServerAdmin))
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
|
||||
}
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a asp-area="" asp-controller="Account" asp- asp-action="Logout" title="Manage" class="nav-link js-scroll-trigger">Log out</a>
|
||||
</li>}
|
||||
else
|
||||
{
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Register" class="nav-link js-scroll-trigger">Register</a></li>
|
||||
<li class="nav-item"><a asp-area="" asp-controller="Account" asp-action="Login" class="nav-link js-scroll-trigger">Log in</a></li>}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
@ -80,77 +86,6 @@
|
||||
|
||||
@RenderBody()
|
||||
|
||||
|
||||
@if(!dashboard.IsFullySynched())
|
||||
{
|
||||
<!-- Modal -->
|
||||
<div id="synching-modal" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
|
||||
<!-- Modal content-->
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Your nodes are synching...</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Some of your nodes are still synching...<br />
|
||||
BTCPay Server will not work correctly until it is over.
|
||||
</p>
|
||||
@foreach(var line in dashboard.GetAll())
|
||||
{
|
||||
<h4>@line.Network.CryptoCode</h4>
|
||||
@if(line.Status == null)
|
||||
{
|
||||
<p>NBXplorer is offline</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul>
|
||||
<li>NBXplorer headers height: @line.Status.ChainHeight</li>
|
||||
@if(line.Status.BitcoinStatus == null)
|
||||
{
|
||||
if(line.State == BTCPayServer.HostedServices.NBXplorerState.Synching)
|
||||
{
|
||||
<li>The node is starting...</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li>The node is offline</li>
|
||||
}
|
||||
}
|
||||
else if(line.Status.BitcoinStatus.IsSynched)
|
||||
{
|
||||
<li>The node is synched</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li>Node headers height: @line.Status.BitcoinStatus.Headers</li>
|
||||
<li>Validated blocks: @line.Status.BitcoinStatus.Blocks</li>
|
||||
}
|
||||
</ul>
|
||||
@if(!line.Status.IsFullySynched && line.Status.BitcoinStatus != null)
|
||||
{
|
||||
<div class="progress">
|
||||
<div class="progress-bar" role="progressbar" aria-valuenow="@((int)line.Status.BitcoinStatus.VerificationProgress * 100)"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width:@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%">
|
||||
@((int)line.Status.BitcoinStatus.VerificationProgress * 100)%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<footer class="bg-dark">
|
||||
<div class="container text-right"><span style="font-size:8px;">@env.ToString()</span></div>
|
||||
</footer>
|
||||
@ -168,13 +103,10 @@
|
||||
<!-- Custom scripts for this template -->
|
||||
<script src="~/js/creative.js"></script>
|
||||
|
||||
@if(!dashboard.IsFullySynched())
|
||||
|
||||
@if (!dashboard.IsFullySynched())
|
||||
{
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
$("#synching-modal").modal();
|
||||
});
|
||||
</script>
|
||||
@Html.Partial("SyncModal")
|
||||
}
|
||||
|
||||
@RenderSection("Scripts", required: false)
|
||||
|
@ -13,31 +13,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<h5>Derivation Scheme</h5>
|
||||
@if(Model.AddressSamples.Count == 0)
|
||||
{
|
||||
@if(!Model.Confirmation)
|
||||
{
|
||||
<div class="form-group">
|
||||
<h5>Derivation Scheme</h5>
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||
}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationSchemeFormat"></label>
|
||||
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@if(Model.AddressSamples.Count == 0)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
<span asp-validation-for="DerivationScheme" class="text-danger"></span>
|
||||
<p id="no-ledger-info" class="form-text text-muted" style="display: none;">
|
||||
No ledger wallet detected. If you own one, use chrome, open the app, activate the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a>, and refresh this page.
|
||||
</p>
|
||||
<p id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||
<span>A ledger wallet is detected, please use our <a id="ledger-info-recommended" href="#">recommended choice</a></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationSchemeFormat"></label>
|
||||
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span>BTCPay format memo</span>
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
@ -73,34 +77,49 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-info">Continue</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<div class="form-group">
|
||||
<h5>Confirm the addresses (@Model.CryptoCurrency)</h5>
|
||||
<span>Please check that your @Model.CryptoCurrency wallet is generating the same addresses as below.</span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="CryptoCurrency" />
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<input type="hidden" asp-for="DerivationSchemeFormat" />
|
||||
<div class="form-group">
|
||||
<table class="table">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var sample in Model.AddressSamples)
|
||||
{
|
||||
<tr>
|
||||
<th>Key path</th>
|
||||
<th>Address</th>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach(var sample in Model.AddressSamples)
|
||||
{
|
||||
<tr>
|
||||
<td>@sample.KeyPath</td>
|
||||
<td>@sample.Address</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||
<button name="command" type="submit" class="btn btn-default" value="Check">Check ExtPubKey</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
<script type="text/javascript">
|
||||
@Model.ServerUrl.ToJSVariableModel("srvModel");
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreAddDerivationScheme.js" type="text/javascript" defer="defer"></script>
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ namespace BTCPayServer.Views.Stores
|
||||
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
public static string Wallet => "Wallet";
|
||||
|
||||
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
|
||||
public static string WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
|
@ -25,11 +25,6 @@
|
||||
<input asp-for="StoreName" class="form-control" />
|
||||
<span asp-validation-for="StoreName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreName"></label>
|
||||
<input asp-for="StoreName" class="form-control" />
|
||||
<span asp-validation-for="StoreName" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StoreWebsite"></label>
|
||||
<input asp-for="StoreWebsite" class="form-control" />
|
||||
@ -43,6 +38,24 @@
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange"></label>
|
||||
<input asp-for="PreferredExchange" class="form-control" />
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
<p id="PreferredExchangeHelpBlock" class="form-text text-muted">
|
||||
Current price source is <a href="@Model.RateSource" target="_blank">@Model.PreferredExchange</a>.<small> (using 1 minute cache)</small>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="RateMultiplier"></label>
|
||||
<input asp-for="RateMultiplier" class="form-control" />
|
||||
<span asp-validation-for="RateMultiplier" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="InvoiceExpiration"></label>
|
||||
<input asp-for="InvoiceExpiration" class="form-control" />
|
||||
<span asp-validation-for="InvoiceExpiration" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="MonitoringExpiration"></label>
|
||||
<input asp-for="MonitoringExpiration" class="form-control" />
|
||||
|
74
BTCPayServer/Views/Stores/Wallet.cshtml
Normal file
74
BTCPayServer/Views/Stores/Wallet.cshtml
Normal file
@ -0,0 +1,74 @@
|
||||
@model WalletModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Manage wallet";
|
||||
ViewData.AddActivePage(StoreNavPages.Wallet);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<div class="alert alert-danger alert-dismissible" style="display:none;" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<span id="alertMessage"></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<p>
|
||||
You can send money received by this store to an address with the help of your Ledger Wallet. <br />
|
||||
If you don't have a Ledger Wallet, use Electrum with your favorite hardware wallet to transfer crypto. <br />
|
||||
If your Ledger wallet is not detected:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Activate <i class="icon-upload icon-large"></i> the <a href="https://support.ledgerwallet.com/hc/en-us/articles/115005198565-What-is-the-Browser-support-option-made-for-">Browser support</a> and refresh this page</li>
|
||||
<li>Use a browser supporting the <a href="https://www.yubico.com/support/knowledge-base/categories/articles/browsers-support-u2f/">U2F protocol</a></li>
|
||||
</ul>
|
||||
<p id="hw-loading"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||
<p id="hw-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
|
||||
<p id="hw-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
|
||||
|
||||
<p id="check-loading" style="display:none;"><span class="glyphicon glyphicon-question-sign" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
||||
<p id="check-error" style="display:none;"><span class="glyphicon glyphicon-remove-sign" style="color:red;"></span> <span class="check-label">An error happened</span></p>
|
||||
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<form id="sendform" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Destination</label>
|
||||
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
|
||||
<span id="Destination-Error" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Amount</label>
|
||||
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
|
||||
<span id="Amount-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Fee rate (satoshi per byte)</label>
|
||||
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
|
||||
<span id="FeeRate-Error" class="text-danger"></span>
|
||||
<p class="form-text text-muted crypto-info" style="display: none;">
|
||||
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Substract fees from amount</label>
|
||||
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
|
||||
</div>
|
||||
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
|
||||
</form>
|
||||
</div>
|
||||
@section Scripts
|
||||
{
|
||||
<script type="text/javascript">
|
||||
@Model.ToJSVariableModel("srvModel")
|
||||
</script>
|
||||
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
|
||||
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
|
||||
}
|
@ -4,5 +4,6 @@
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
|
||||
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
|
||||
<li class="@StoreNavPages.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
|
||||
</ul>
|
||||
|
||||
|
@ -43,7 +43,7 @@ h6 {
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
/*font-size: 16px;*/
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
77
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal file
77
BTCPayServer/wwwroot/js/StoreAddDerivationScheme.js
Normal file
@ -0,0 +1,77 @@
|
||||
$(function () {
|
||||
var ledgerDetected = false;
|
||||
var recommendedPubKey = "";
|
||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
|
||||
|
||||
function WriteAlert(type, message) {
|
||||
|
||||
}
|
||||
|
||||
function Write(prefix, type, message) {
|
||||
if (type === "error") {
|
||||
$("#no-ledger-info").css("display", "block");
|
||||
$("#ledger-in fo").css("display", "none");
|
||||
}
|
||||
}
|
||||
|
||||
$("#ledger-info-recommended").on("click", function (elem) {
|
||||
elem.preventDefault();
|
||||
$("#DerivationScheme").val(recommendedPubKey);
|
||||
$("#DerivationSchemeFormat").val("BTCPay");
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#CryptoCurrency").on("change", function (elem) {
|
||||
$("#no-ledger-info").css("display", "none");
|
||||
$("#ledger-info").css("display", "none");
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
var updateInfo = function () {
|
||||
if (!ledgerDetected)
|
||||
return false;
|
||||
var cryptoCode = $("#CryptoCurrency").val();
|
||||
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
|
||||
.catch(function (reason) { Write('check', 'error', reason); })
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (result.error) {
|
||||
Write('check', 'error', result.error);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Write('check', 'success', 'This store is configured to use your ledger');
|
||||
recommendedPubKey = result.extPubKey;
|
||||
$("#no-ledger-info").css("display", "none");
|
||||
$("#ledger-info").css("display", "block");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
bridge.isSupported()
|
||||
.then(function (supported) {
|
||||
if (!supported) {
|
||||
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
|
||||
}
|
||||
else {
|
||||
bridge.sendCommand('test', null, 5)
|
||||
.catch(function (reason) {
|
||||
if (reason.message === "Sign failed")
|
||||
reason = "Have you forgot to activate browser support in your ledger app?";
|
||||
Write('hw', 'error', reason);
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (result.error) {
|
||||
Write('hw', 'error', result.error);
|
||||
} else {
|
||||
Write('hw', 'success', 'Ledger detected');
|
||||
ledgerDetected = true;
|
||||
updateInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
145
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
145
BTCPayServer/wwwroot/js/StoreWallet.js
Normal file
@ -0,0 +1,145 @@
|
||||
$(function () {
|
||||
var ledgerDetected = false;
|
||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
|
||||
var recommendedFees = "";
|
||||
var recommendedBalance = "";
|
||||
|
||||
function WriteAlert(type, message) {
|
||||
$(".alert").removeClass("alert-danger");
|
||||
$(".alert").removeClass("alert-warning");
|
||||
$(".alert").removeClass("alert-success");
|
||||
$(".alert").addClass("alert-" + type);
|
||||
$(".alert").css("display", "block");
|
||||
$("#alertMessage").text(message);
|
||||
}
|
||||
|
||||
function Write(prefix, type, message) {
|
||||
|
||||
$("#" + prefix + "-loading").css("display", "none");
|
||||
$("#" + prefix + "-error").css("display", "none");
|
||||
$("#" + prefix + "-success").css("display", "none");
|
||||
|
||||
$("#" + prefix+"-" + type).css("display", "block");
|
||||
|
||||
$("." + prefix +"-label").text(message);
|
||||
}
|
||||
|
||||
$("#sendform").on("submit", function (elem) {
|
||||
elem.preventDefault();
|
||||
|
||||
if ($("#amount-textbox").val() === "") {
|
||||
$("#amount-textbox").val(recommendedBalance);
|
||||
$("#substract-checkbox").prop("checked", true);
|
||||
}
|
||||
|
||||
if ($("#fee-textbox").val() === "") {
|
||||
$("#fee-textbox").val(recommendedFees);
|
||||
}
|
||||
|
||||
var args = "";
|
||||
args += "cryptoCode=" + $("#cryptoCurrencies").val();
|
||||
args += "&destination=" + $("#destination-textbox").val();
|
||||
args += "&amount=" + $("#amount-textbox").val();
|
||||
args += "&feeRate=" + $("#fee-textbox").val();
|
||||
args += "&substractFees=" + $("#substract-checkbox").prop("checked");
|
||||
|
||||
WriteAlert("warning", 'Please validate the transaction on your ledger');
|
||||
|
||||
var confirmButton = $("#confirm-button");
|
||||
confirmButton.prop("disabled", true);
|
||||
confirmButton.addClass("disabled");
|
||||
|
||||
bridge.sendCommand('sendtoaddress', args, 60 * 5 /* timeout */)
|
||||
.catch(function (reason) {
|
||||
WriteAlert("danger", reason);
|
||||
confirmButton.prop("disabled", false);
|
||||
confirmButton.removeClass("disabled");
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
confirmButton.prop("disabled", false);
|
||||
confirmButton.removeClass("disabled");
|
||||
if (result.error) {
|
||||
WriteAlert("danger", result.error);
|
||||
} else {
|
||||
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
|
||||
updateInfo();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#crypto-balance-link").on("click", function (elem) {
|
||||
elem.preventDefault();
|
||||
var val = $("#crypto-balance-link").text();
|
||||
$("#amount-textbox").val(val);
|
||||
$("#substract-checkbox").prop('checked', true);
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#crypto-fee-link").on("click", function (elem) {
|
||||
elem.preventDefault();
|
||||
var val = $("#crypto-fee-link").text();
|
||||
$("#fee-textbox").val(val);
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#cryptoCurrencies").on("change", function (elem) {
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
var updateInfo = function () {
|
||||
if (!ledgerDetected)
|
||||
return false;
|
||||
$(".crypto-info").css("display", "none");
|
||||
var cryptoCode = $("#cryptoCurrencies").val();
|
||||
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
|
||||
.catch(function (reason) { Write('check', 'error', reason); })
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (result.error) {
|
||||
Write('check', 'error', result.error);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
Write('check', 'success', 'This store is configured to use your ledger');
|
||||
$(".crypto-info").css("display", "block");
|
||||
recommendedFees = result.recommendedSatoshiPerByte;
|
||||
recommendedBalance = result.balance;
|
||||
$("#crypto-fee").text(result.recommendedSatoshiPerByte);
|
||||
$("#crypto-balance").text(result.balance);
|
||||
$("#crypto-code").text(cryptoCode);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
bridge.isSupported()
|
||||
.then(function (supported) {
|
||||
if (!supported) {
|
||||
Write('hw', 'error', 'U2F or Websocket are not supported by this browser');
|
||||
}
|
||||
else {
|
||||
bridge.sendCommand('test', null, 5)
|
||||
.catch(function (reason)
|
||||
{
|
||||
if (reason.message === "Sign failed")
|
||||
reason = "Have you forgot to activate browser support in your ledger app?";
|
||||
Write('hw', 'error', reason);
|
||||
})
|
||||
.then(function (result) {
|
||||
if (!result)
|
||||
return;
|
||||
if (result.error) {
|
||||
Write('hw', 'error', result.error);
|
||||
} else {
|
||||
Write('hw', 'success', 'Ledger detected');
|
||||
$("#sendform").css("display", "block");
|
||||
ledgerDetected = true;
|
||||
updateInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user