BTCWallet is single currency, introduce BTCWalletProvider

This commit is contained in:
nicolas.dorier
2018-01-11 14:36:12 +09:00
parent 3ff293ab7f
commit 55d50af39d
8 changed files with 146 additions and 68 deletions

View File

@@ -66,13 +66,17 @@ 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(network))
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);
var unused = wallet.BroadcastTransactionsAsync(payment.Transactions);
await _InvoiceRepository.AddRefundsAsync(invoiceId, payment.RefundTo.Select(p => new TxOut(p.Amount, p.Script)).ToArray());
return new PaymentAckActionResult(payment.CreateACK(invoiceId + " is currently processing, thanks for your purchase..."));
}

View File

@@ -165,7 +165,7 @@ namespace BTCPayServer.Controllers
}).ToList()
};
var isMultiCurrency = invoice.Payments.Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
var isMultiCurrency = invoice.GetPayments().Select(p=>p.GetCryptoCode()).Concat(new[] { network.CryptoCode }).Distinct().Count() > 1;
if (isMultiCurrency)
model.NetworkFeeDescription = $"{accounting.NetworkFee} {network.CryptoCode}";

View File

@@ -45,7 +45,7 @@ namespace BTCPayServer.Controllers
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository;
BTCPayWallet _Wallet;
BTCPayWalletProvider _WalletProvider;
IRateProviderFactory _RateProviders;
StoreRepository _StoreRepository;
UserManager<ApplicationUser> _UserManager;
@@ -57,7 +57,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 +69,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));
@@ -116,14 +116,20 @@ 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),
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 = _.RateProvider.GetRateAsync(invoice.Currency),
getAddress = _.Wallet.ReserveAddressAsync(_.DerivationStrategy)
};
});

View File

@@ -32,7 +32,7 @@ namespace BTCPayServer.Controllers
TokenRepository tokenRepo,
UserManager<ApplicationUser> userManager,
AccessTokenController tokenController,
BTCPayWallet wallet,
BTCPayWalletProvider walletProvider,
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IHostingEnvironment env)
@@ -41,14 +41,14 @@ namespace BTCPayServer.Controllers
_TokenRepository = tokenRepo;
_UserManager = userManager;
_TokenController = tokenController;
_Wallet = wallet;
_WalletProvider = walletProvider;
_Env = env;
_NetworkProvider = networkProvider;
_ExplorerProvider = explorerProvider;
}
BTCPayNetworkProvider _NetworkProvider;
private ExplorerClientProvider _ExplorerProvider;
BTCPayWallet _Wallet;
BTCPayWalletProvider _WalletProvider;
AccessTokenController _TokenController;
StoreRepository _Repo;
TokenRepository _TokenRepository;
@@ -95,7 +95,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 _.Wallet.GetBalance(_.DerivationStrategy)).ToString() + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
@@ -210,6 +213,12 @@ 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")
{
@@ -218,7 +227,7 @@ namespace BTCPayServer.Controllers
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
await _Wallet.TrackAsync(strategy);
await wallet.TrackAsync(strategy);
vm.DerivationScheme = strategy.ToString();
}
store.SetDerivationStrategy(network, vm.DerivationScheme);
@@ -240,12 +249,12 @@ namespace BTCPayServer.Controllers
try
{
var scheme = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
var line = scheme.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
var line = scheme.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()));
vm.AddressSamples.Add((line.Path.Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
catch
@@ -315,7 +324,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")
{
@@ -349,7 +358,7 @@ namespace BTCPayServer.Controllers
}
}
return DerivationStrategy.Parse(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme).ToString(), network);
return new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme);
}
[HttpGet]

View File

@@ -45,7 +45,7 @@ namespace BTCPayServer.HostedServices
InvoiceRepository _InvoiceRepository;
EventAggregator _EventAggregator;
BTCPayWallet _Wallet;
BTCPayWalletProvider _WalletProvider;
BTCPayNetworkProvider _NetworkProvider;
public InvoiceWatcher(
@@ -53,10 +53,10 @@ namespace BTCPayServer.HostedServices
BTCPayNetworkProvider networkProvider,
InvoiceRepository invoiceRepository,
EventAggregator eventAggregator,
BTCPayWallet wallet)
BTCPayWalletProvider walletProvider)
{
PollInterval = TimeSpan.FromMinutes(1.0);
_Wallet = wallet ?? throw new ArgumentNullException(nameof(wallet));
_WalletProvider = walletProvider ?? throw new ArgumentNullException(nameof(walletProvider));
_InvoiceRepository = invoiceRepository ?? throw new ArgumentNullException(nameof(invoiceRepository));
_EventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_NetworkProvider = networkProvider;
@@ -150,29 +150,39 @@ namespace BTCPayServer.HostedServices
}
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
foreach (NetworkCoins coins in await GetCoinsPerNetwork(context, invoice, derivationStrategies))
var payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
foreach (Task<NetworkCoins> coinsAsync in GetCoinsPerNetwork(context, invoice, derivationStrategies))
{
var coins = await coinsAsync;
if (coins.TimestampedCoins.Length == 0)
continue;
bool dirtyAddress = false;
if (coins.State != null)
context.ModifiedKnownStates.AddOrReplace(coins.Strategy.Network, coins.State);
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Strategy.Network).Select(p => p.Outpoint));
context.ModifiedKnownStates.AddOrReplace(coins.Wallet.Network, coins.State);
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Wallet.Network).Select(p => p.Outpoint));
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
{
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Wallet.Network.CryptoCode).ConfigureAwait(false);
#pragma warning disable CS0618
invoice.Payments.Add(payment);
#pragma warning restore CS0618
alreadyAccounted.Add(coin.Coin.Outpoint);
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
dirtyAddress = true;
}
var network = coins.Strategy.Network;
if (dirtyAddress)
{
payments = await GetPaymentsWithTransaction(derivationStrategies, invoice);
}
var network = coins.Wallet.Network;
var cryptoData = invoice.GetCryptoData(network);
var cryptoDataAll = invoice.GetCryptoData();
var accounting = cryptoData.Calculate();
if (invoice.Status == "new" || invoice.Status == "expired")
{
var totalPaid = (await GetPaymentsWithTransaction(derivationStrategies, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
var totalPaid = payments.Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalPaid >= accounting.TotalDue)
{
if (invoice.Status == "new")
@@ -203,7 +213,7 @@ namespace BTCPayServer.HostedServices
context.MarkDirty();
if (dirtyAddress)
{
var address = await _Wallet.ReserveAddressAsync(coins.Strategy);
var address = await coins.Wallet.ReserveAddressAsync(coins.Strategy);
Logs.PayServer.LogInformation("Generate new " + address);
await _InvoiceRepository.NewAddress(invoice.Id, address, network);
}
@@ -212,7 +222,7 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == "paid")
{
var transactions = await GetPaymentsWithTransaction(derivationStrategies, invoice);
var transactions = payments;
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
{
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
@@ -250,7 +260,7 @@ namespace BTCPayServer.HostedServices
if (invoice.Status == "confirmed")
{
var transactions = await GetPaymentsWithTransaction(derivationStrategies, invoice);
var transactions = payments;
transactions = transactions.Where(t => t.Confirmations >= 6);
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
if (totalConfirmed >= accounting.TotalDue)
@@ -263,18 +273,23 @@ namespace BTCPayServer.HostedServices
}
}
private async Task<IEnumerable<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
private IEnumerable<Task<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
{
var getCoinsResponsesAsync = strategies
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
return strategies
.Select(d => (Wallet: _WalletProvider.GetWallet(d.Network),
Network: d.Network,
Strategy: d.DerivationStrategyBase))
.Where(d => d.Wallet != null)
.Select(d => (Network: d.Network,
Coins: d.Wallet.GetCoins(d.Strategy, context.KnownStates.TryGet(d.Network))))
.Select(async d =>
{
var coins = await d.Coins;
// Keep only coins from the invoice
coins.TimestampedCoins = coins.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + d.Network.CryptoCode)).ToArray();
return coins;
})
.ToArray();
await Task.WhenAll(getCoinsResponsesAsync);
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
foreach (var response in getCoinsResponses)
{
response.TimestampedCoins = response.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
}
return getCoinsResponses.Where(s => s.TimestampedCoins.Length != 0).ToArray();
}
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice)
@@ -283,7 +298,10 @@ namespace BTCPayServer.HostedServices
List<AccountedPaymentEntity> accountedPayments = new List<AccountedPaymentEntity>();
foreach (var network in derivations.Select(d => d.Network))
{
var transactions = await _Wallet.GetTransactions(network, invoice.GetPayments(network).Select(t => t.Outpoint.Hash).ToArray());
var wallet = _WalletProvider.GetWallet(network);
if (wallet == null)
continue;
var transactions = await wallet.GetTransactions(network, invoice.GetPayments(network).Select(t => t.Outpoint.Hash).ToArray());
var conflicts = GetConflicts(transactions.Select(t => t.Value));
foreach (var payment in invoice.GetPayments(network))
{

View File

@@ -134,7 +134,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWallet>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
{

View File

@@ -26,31 +26,40 @@ namespace BTCPayServer.Services.Wallets
}
public TimestampedCoin[] TimestampedCoins { get; set; }
public KnownState State { get; set; }
public DerivationStrategy Strategy { get; set; }
public DerivationStrategyBase Strategy { get; set; }
public BTCPayWallet Wallet { get; set; }
}
public class BTCPayWallet
{
private ExplorerClientProvider _Client;
private ExplorerClient _Client;
public BTCPayWallet(ExplorerClientProvider client)
public BTCPayWallet(ExplorerClient client, BTCPayNetwork network)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
_Network = network;
}
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);
get
{
return _Network;
}
}
public async Task TrackAsync(DerivationStrategy derivationStrategy)
public async Task<BitcoinAddress> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);
await client.TrackAsync(derivationStrategy.DerivationStrategyBase);
var pathInfo = await _Client.GetUnusedAsync(derivationStrategy, DerivationFeature.Deposit, 0, true).ConfigureAwait(false);
return pathInfo.ScriptPubKey.GetDestinationAddress(_Client.Network);
}
public async Task TrackAsync(DerivationStrategyBase derivationStrategy)
{
await _Client.TrackAsync(derivationStrategy);
}
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
@@ -59,36 +68,31 @@ namespace BTCPayServer.Services.Wallets
throw new ArgumentNullException(nameof(network));
if (txId == null)
throw new ArgumentNullException(nameof(txId));
var client = _Client.GetExplorerClient(network);
return client.GetTransactionAsync(txId, cancellation);
return _Client.GetTransactionAsync(txId, cancellation);
}
public async Task<NetworkCoins> GetCoins(DerivationStrategy strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
public async Task<NetworkCoins> GetCoins(DerivationStrategyBase strategy, KnownState state, CancellationToken cancellation = default(CancellationToken))
{
var client = _Client.GetExplorerClient(strategy.Network);
if (client == null)
return new NetworkCoins() { TimestampedCoins = new NetworkCoins.TimestampedCoin[0], State = null, Strategy = strategy };
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
var changes = await _Client.SyncAsync(strategy, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
return new NetworkCoins()
{
TimestampedCoins = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => new NetworkCoins.TimestampedCoin() { Coin = c.AsCoin(), DateTime = c.Timestamp }).ToArray(),
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
Strategy = strategy,
Wallet = this
};
}
public Task BroadcastTransactionsAsync(BTCPayNetwork network, List<Transaction> transactions)
public Task BroadcastTransactionsAsync(List<Transaction> transactions)
{
var client = _Client.GetExplorerClient(network);
var tasks = transactions.Select(t => client.BroadcastAsync(t)).ToArray();
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
return Task.WhenAll(tasks);
}
public async Task<Money> GetBalance(DerivationStrategy derivationStrategy)
public async Task<Money> GetBalance(DerivationStrategyBase derivationStrategy)
{
var client = _Client.GetExplorerClient(derivationStrategy.Network);
var result = await client.SyncAsync(derivationStrategy.DerivationStrategyBase, null, true);
var result = await _Client.SyncAsync(derivationStrategy, null, true);
return result.Confirmed.UTXOs.Select(u => u.Value)
.Concat(result.Unconfirmed.UTXOs.Select(u => u.Value))
.Sum();

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services.Wallets
{
public class BTCPayWalletProvider
{
private ExplorerClientProvider _Client;
BTCPayNetworkProvider _NetworkProvider;
public BTCPayWalletProvider(ExplorerClientProvider client, BTCPayNetworkProvider networkProvider)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
_Client = client;
_NetworkProvider = networkProvider;
}
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));
var network = _NetworkProvider.GetNetwork(cryptoCode);
var client = _Client.GetExplorerClient(cryptoCode);
if (network == null && client == null)
return null;
return new BTCPayWallet(client, network);
}
}
}