btcpayserver/BTCPayServer/Services/Wallets/BTCPayWallet.cs

244 lines
10 KiB
C#
Raw Normal View History

2020-06-28 21:44:35 -05:00
using System;
2020-06-28 17:55:27 +09:00
using System.Collections.Concurrent;
2017-09-13 15:47:34 +09:00
using System.Collections.Generic;
using System.Linq;
2020-06-28 17:55:27 +09:00
using System.Threading;
2017-09-13 15:47:34 +09:00
using System.Threading.Tasks;
using BTCPayServer.Data;
2018-02-15 13:33:29 +09:00
using BTCPayServer.Logging;
2020-04-16 14:25:52 +09:00
using Microsoft.EntityFrameworkCore;
2020-06-28 17:55:27 +09:00
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBXplorer;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
2017-09-13 15:47:34 +09:00
namespace BTCPayServer.Services.Wallets
2017-09-13 15:47:34 +09:00
{
public class ReceivedCoin
{
2019-12-24 08:20:44 +01:00
public Script ScriptPubKey { get; set; }
public OutPoint OutPoint { get; set; }
public DateTimeOffset Timestamp { get; set; }
public KeyPath KeyPath { get; set; }
2019-12-24 08:20:44 +01:00
public IMoney Value { get; set; }
2020-01-06 13:57:32 +01:00
public Coin Coin { get; set; }
}
public class NetworkCoins
{
2018-01-10 18:30:45 +09:00
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 readonly ExplorerClient _Client;
private readonly IMemoryCache _MemoryCache;
2020-06-28 17:55:27 +09:00
public BTCPayWallet(ExplorerClient client, IMemoryCache memoryCache, BTCPayNetwork network,
2020-04-16 14:25:52 +09:00
ApplicationDbContextFactory dbContextFactory)
{
if (client == null)
throw new ArgumentNullException(nameof(client));
2018-02-15 13:02:12 +09:00
if (memoryCache == null)
throw new ArgumentNullException(nameof(memoryCache));
_Client = client;
_Network = network;
2020-04-16 14:25:52 +09:00
_dbContextFactory = dbContextFactory;
2018-02-15 13:02:12 +09:00
_MemoryCache = memoryCache;
}
private readonly BTCPayNetwork _Network;
2020-04-16 14:25:52 +09:00
private readonly ApplicationDbContextFactory _dbContextFactory;
public BTCPayNetwork Network
{
get
{
return _Network;
}
}
public TimeSpan CacheSpan { get; private set; } = TimeSpan.FromMinutes(5);
public async Task<KeyPathInformation> ReserveAddressAsync(DerivationStrategyBase derivationStrategy)
{
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;
}
2018-02-13 03:27:36 +09:00
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, new TrackWalletRequest()
{
Wait = false
});
}
2020-03-30 00:28:22 +09:00
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, bool includeOffchain = false, CancellationToken cancellation = default(CancellationToken))
{
2018-01-10 18:30:45 +09:00
if (txId == null)
throw new ArgumentNullException(nameof(txId));
2018-02-15 12:42:48 +09:00
var tx = await _Client.GetTransactionAsync(txId, cancellation);
2020-03-30 00:28:22 +09:00
if (tx is null && includeOffchain)
{
var offchainTx = await GetOffchainTransactionAsync(txId);
if (offchainTx != null)
tx = new TransactionResult()
{
Confirmations = -1,
2020-06-28 17:55:27 +09:00
TransactionHash = offchainTx.GetHash(),
Transaction = offchainTx
2020-03-30 00:28:22 +09:00
};
}
2018-01-11 21:01:00 +09:00
return tx;
}
2020-04-16 14:25:52 +09:00
public async Task<Transaction> GetOffchainTransactionAsync(uint256 txid)
2020-03-30 00:28:22 +09:00
{
2020-04-16 14:25:52 +09:00
using var ctx = this._dbContextFactory.CreateContext();
var txData = await ctx.OffchainTransactions.FindAsync(txid.ToString());
if (txData is null)
return null;
return Transaction.Load(txData.Blob, this._Network.NBitcoinNetwork);
2020-03-30 00:28:22 +09:00
}
2020-04-16 14:25:52 +09:00
public async Task SaveOffchainTransactionAsync(Transaction tx)
2020-03-30 00:28:22 +09:00
{
2020-04-16 14:25:52 +09:00
using var ctx = this._dbContextFactory.CreateContext();
ctx.OffchainTransactions.Add(new OffchainTransactionData()
{
Id = tx.GetHash().ToString(),
Blob = tx.ToBytes()
});
try
{
await ctx.SaveChangesAsync();
}
// Already in db
catch (DbUpdateException)
2020-03-30 00:28:22 +09:00
{
}
}
2018-02-15 13:02:12 +09:00
public void InvalidateCache(DerivationStrategyBase strategy)
{
2018-02-15 13:02:12 +09:00
_MemoryCache.Remove("CACHEDCOINS_" + strategy.ToString());
2019-12-10 22:17:59 +09:00
_MemoryCache.Remove("CACHEDBALANCE_" + strategy.ToString());
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
2018-02-15 13:02:12 +09:00
}
readonly ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>> _FetchingUTXOs = new ConcurrentDictionary<string, TaskCompletionSource<UTXOChanges>>();
2018-02-15 13:33:29 +09:00
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
2018-02-15 13:33:29 +09:00
{
var utxos = await _MemoryCache.GetOrCreateAsync("CACHEDCOINS_" + strategy.ToString(), async entry =>
2018-02-15 13:02:12 +09:00
{
var now = DateTimeOffset.UtcNow;
UTXOChanges result = null;
try
{
2018-11-21 20:41:51 +09:00
result = await _Client.GetUTXOsAsync(strategy, cancellation).ConfigureAwait(false);
}
catch
{
Logs.PayServer.LogError($"{Network.CryptoCode}: 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($"{Network.CryptoCode}: 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;
});
2018-03-13 15:39:52 +09:00
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
completionSource.TrySetResult(utxos);
}
catch (Exception ex)
{
completionSource.TrySetException(ex);
}
finally
{
_FetchingUTXOs.TryRemove(strategy.ToString(), out var unused);
}
return await completionSource.Task;
}
public async Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Network.FilterValidTransactions(await _Client.GetTransactionsAsync(derivationStrategyBase));
}
2018-02-13 03:27:36 +09:00
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)
{
var tasks = transactions.Select(t => _Client.BroadcastAsync(t)).ToArray();
return Task.WhenAll(tasks);
}
2018-02-15 13:02:12 +09:00
public async Task<ReceivedCoin[]> GetUnspentCoins(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
2018-02-13 03:27:36 +09:00
{
if (derivationStrategy == null)
throw new ArgumentNullException(nameof(derivationStrategy));
return (await GetUTXOChanges(derivationStrategy, cancellation))
.GetUnspentUTXOs()
.Select(c => new ReceivedCoin()
{
KeyPath = c.KeyPath,
2019-12-24 08:20:44 +01:00
Value = c.Value,
Timestamp = c.Timestamp,
OutPoint = c.Outpoint,
2020-01-06 13:57:32 +01:00
ScriptPubKey = c.ScriptPubKey,
Coin = c.AsCoin(derivationStrategy)
}).ToArray();
2018-02-13 03:27:36 +09:00
}
2019-12-10 22:17:59 +09:00
public Task<decimal> GetBalance(DerivationStrategyBase derivationStrategy, CancellationToken cancellation = default(CancellationToken))
{
2019-12-10 22:17:59 +09:00
return _MemoryCache.GetOrCreateAsync("CACHEDBALANCE_" + derivationStrategy.ToString(), async (entry) =>
{
var result = await _Client.GetBalanceAsync(derivationStrategy, cancellation);
entry.AbsoluteExpiration = DateTimeOffset.UtcNow + CacheSpan;
return result.Total.GetValue(_Network);
});
}
}
2017-09-13 15:47:34 +09:00
}