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 ;
2017-10-06 10:37:38 +09:00
using BTCPayServer.Data ;
2025-01-29 19:26:22 +09:00
using BTCPayServer.HostedServices ;
2018-02-15 13:33:29 +09:00
using BTCPayServer.Logging ;
2023-01-06 14:18:07 +01:00
using Dapper ;
2025-01-29 19:26:22 +09:00
using Microsoft.AspNetCore.Components.Web.Virtualization ;
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 ;
2025-01-29 19:26:22 +09:00
using NBitcoin.RPC ;
2020-06-28 17:55:27 +09:00
using NBXplorer ;
using NBXplorer.DerivationStrategy ;
using NBXplorer.Models ;
2022-12-08 13:16:18 +09:00
using Newtonsoft.Json.Linq ;
2025-01-29 19:26:22 +09:00
using static BTCPayServer . Services . TransactionLinkProviders ;
using static NBitcoin . Protocol . Behaviors . ChainBehavior ;
2017-09-13 15:47:34 +09:00
2017-09-15 16:06:57 +09:00
namespace BTCPayServer.Services.Wallets
2017-09-13 15:47:34 +09:00
{
2018-02-17 01:34:40 +09:00
public class ReceivedCoin
{
2019-12-24 08:20:44 +01:00
public Script ScriptPubKey { get ; set ; }
public OutPoint OutPoint { get ; set ; }
2018-02-17 01:34:40 +09:00
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 ; }
2022-04-05 14:46:42 +09:00
public long Confirmations { get ; set ; }
2022-12-13 09:09:25 +09:00
public BitcoinAddress Address { get ; set ; }
2018-02-17 01:34:40 +09:00
}
2018-01-10 15:43:07 +09:00
public class NetworkCoins
2018-01-07 02:16:42 +09:00
{
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 ; }
2018-01-11 14:36:12 +09:00
public DerivationStrategyBase Strategy { get ; set ; }
public BTCPayWallet Wallet { get ; set ; }
2018-01-07 02:16:42 +09:00
}
2025-01-29 19:26:22 +09:00
#nullable enable
public record BumpableInfo ( bool RBF , bool CPFP , ReplacementInfo ? ReplacementInfo ) ;
#nullable restore
public enum BumpableSupport
{
NotConfigured ,
NotCompatible ,
NotSynched ,
Ok
}
public class BumpableTransactions : Dictionary < uint256 , BumpableInfo >
{
public BumpableTransactions ( )
{
}
public BumpableSupport Support { get ; internal set ; }
}
2017-10-27 17:53:04 +09:00
public class BTCPayWallet
{
2022-12-08 13:16:18 +09:00
public WalletRepository WalletRepository { get ; }
2025-01-29 19:26:22 +09:00
public NBXplorerDashboard Dashboard { get ; }
2022-07-04 15:50:56 +09:00
public NBXplorerConnectionFactory NbxplorerConnectionFactory { get ; }
2021-11-22 17:16:08 +09:00
public Logs Logs { get ; }
2020-06-28 22:07:48 -05:00
private readonly ExplorerClient _Client ;
private readonly IMemoryCache _MemoryCache ;
2020-06-28 17:55:27 +09:00
public BTCPayWallet ( ExplorerClient client , IMemoryCache memoryCache , BTCPayNetwork network ,
2022-12-08 13:16:18 +09:00
WalletRepository walletRepository ,
2025-01-29 19:26:22 +09:00
NBXplorerDashboard dashboard ,
2022-07-04 15:50:56 +09:00
ApplicationDbContextFactory dbContextFactory , NBXplorerConnectionFactory nbxplorerConnectionFactory , Logs logs )
2017-10-27 17:53:04 +09:00
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( client ) ;
ArgumentNullException . ThrowIfNull ( memoryCache ) ;
2021-11-22 17:16:08 +09:00
Logs = logs ;
2017-10-27 17:53:04 +09:00
_Client = client ;
2018-01-11 14:36:12 +09:00
_Network = network ;
2022-12-08 13:16:18 +09:00
WalletRepository = walletRepository ;
2025-01-29 19:26:22 +09:00
Dashboard = dashboard ;
2020-04-16 14:25:52 +09:00
_dbContextFactory = dbContextFactory ;
2022-07-04 15:50:56 +09:00
NbxplorerConnectionFactory = nbxplorerConnectionFactory ;
2018-02-15 13:02:12 +09:00
_MemoryCache = memoryCache ;
2017-10-27 17:53:04 +09:00
}
2018-01-11 14:36:12 +09:00
private readonly BTCPayNetwork _Network ;
2020-04-16 14:25:52 +09:00
private readonly ApplicationDbContextFactory _dbContextFactory ;
2018-01-11 14:36:12 +09:00
public BTCPayNetwork Network
2017-10-27 17:53:04 +09:00
{
2018-01-11 14:36:12 +09:00
get
{
return _Network ;
}
2017-10-27 17:53:04 +09:00
}
2018-02-17 01:34:40 +09:00
public TimeSpan CacheSpan { get ; private set ; } = TimeSpan . FromMinutes ( 5 ) ;
2018-01-11 17:29:48 +09:00
2022-12-08 13:16:18 +09:00
public async Task < KeyPathInformation > ReserveAddressAsync ( string storeId , DerivationStrategyBase derivationStrategy , string generatedBy )
2017-10-27 17:53:04 +09:00
{
2022-12-08 13:16:18 +09:00
if ( storeId ! = null )
ArgumentNullException . ThrowIfNull ( generatedBy ) ;
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( derivationStrategy ) ;
2018-01-11 14:36:12 +09:00
var pathInfo = await _Client . GetUnusedAsync ( derivationStrategy , DerivationFeature . Deposit , 0 , true ) . ConfigureAwait ( false ) ;
2018-01-13 02:28:23 +09:00
// 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 ) ;
}
2022-12-08 13:16:18 +09:00
if ( storeId ! = null )
{
await WalletRepository . EnsureWalletObject (
2022-12-13 09:09:25 +09:00
new WalletObjectId ( new WalletId ( storeId , Network . CryptoCode ) , WalletObjectData . Types . Address , pathInfo . Address . ToString ( ) ) ,
2022-12-08 13:16:18 +09:00
new JObject ( ) { [ "generatedBy" ] = generatedBy } ) ;
}
2020-01-18 06:12:27 +01:00
return pathInfo ;
2018-01-11 14:36:12 +09:00
}
2018-02-13 03:27:36 +09:00
public async Task < ( BitcoinAddress , KeyPath ) > GetChangeAddressAsync ( DerivationStrategyBase derivationStrategy )
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( derivationStrategy ) ;
2018-02-13 03:27:36 +09:00
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 ) ;
}
2018-01-11 14:36:12 +09:00
public async Task TrackAsync ( DerivationStrategyBase derivationStrategy )
{
2020-04-05 18:34:46 +09:00
await _Client . TrackAsync ( derivationStrategy , new TrackWalletRequest ( )
{
Wait = false
} ) ;
2017-10-27 17:53:04 +09:00
}
2020-03-30 00:28:22 +09:00
public async Task < TransactionResult > GetTransactionAsync ( uint256 txId , bool includeOffchain = false , CancellationToken cancellation = default ( CancellationToken ) )
2018-01-07 02:16:42 +09:00
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( 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 ;
2018-01-07 02:16:42 +09:00
}
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-01-07 02:16:42 +09:00
{
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 ( ) ) ;
2018-02-15 15:17:12 +09:00
_FetchingUTXOs . TryRemove ( strategy . ToString ( ) , out var unused ) ;
2018-02-15 13:02:12 +09:00
}
2020-06-28 22:07:48 -05:00
readonly ConcurrentDictionary < string , TaskCompletionSource < UTXOChanges > > _FetchingUTXOs = new ConcurrentDictionary < string , TaskCompletionSource < UTXOChanges > > ( ) ;
2018-02-15 14:44:08 +09:00
2018-02-15 13:33:29 +09:00
private async Task < UTXOChanges > GetUTXOChanges ( DerivationStrategyBase strategy , CancellationToken cancellation )
{
2018-02-15 14:44:08 +09:00
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
{
2018-02-15 14:44:08 +09:00
var utxos = await _MemoryCache . GetOrCreateAsync ( "CACHEDCOINS_" + strategy . ToString ( ) , async entry = >
2018-02-15 13:02:12 +09:00
{
2018-02-15 14:44:08 +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 ) ;
2018-02-15 14:44:08 +09:00
}
catch
{
2018-03-17 19:26:30 +09:00
Logs . PayServer . LogError ( $"{Network.CryptoCode}: Call to NBXplorer GetUTXOsAsync timed out, this should never happen, please report this issue to NBXplorer developers" ) ;
2018-02-15 14:44:08 +09:00
throw ;
}
var spentTime = DateTimeOffset . UtcNow - now ;
if ( spentTime . TotalSeconds > 30 )
{
2018-03-17 19:26:30 +09:00
Logs . PayServer . LogWarning ( $"{Network.CryptoCode}: NBXplorer took {(int)spentTime.TotalSeconds} seconds to reply, there is something wrong, please report this issue to NBXplorer developers" ) ;
2018-02-15 14:44:08 +09:00
}
entry . AbsoluteExpiration = DateTimeOffset . UtcNow + CacheSpan ;
return result ;
} ) ;
2018-03-13 15:39:52 +09:00
_FetchingUTXOs . TryRemove ( strategy . ToString ( ) , out var unused ) ;
2018-02-15 14:44:08 +09:00
completionSource . TrySetResult ( utxos ) ;
}
2018-02-17 01:34:40 +09:00
catch ( Exception ex )
2018-02-15 14:44:08 +09:00
{
completionSource . TrySetException ( ex ) ;
}
finally
{
_FetchingUTXOs . TryRemove ( strategy . ToString ( ) , out var unused ) ;
}
return await completionSource . Task ;
2018-01-07 02:16:42 +09:00
}
2022-07-04 15:50:56 +09:00
List < TransactionInformation > dummy = new List < TransactionInformation > ( ) ;
2023-06-22 16:09:53 +09:00
public async Task < IList < TransactionHistoryLine > > FetchTransactionHistory ( DerivationStrategyBase derivationStrategyBase , int? skip = null , int? count = null , TimeSpan ? interval = null , CancellationToken cancellationToken = default )
2018-07-27 00:08:07 +09:00
{
2022-07-04 15:50:56 +09:00
// This is two paths:
// * Sometimes we can ask the DB to do the filtering of rows: If that's the case, we should try to filter at the DB level directly as it is the most efficient.
// * Sometimes we can't query the DB or the given network need to do additional filtering. In such case, we can't really filter at the DB level, and we need to fetch all transactions in memory.
var needAdditionalFiltering = _Network . FilterValidTransactions ( dummy ) ! = dummy ;
if ( ! NbxplorerConnectionFactory . Available | | needAdditionalFiltering )
{
var txs = await FetchTransactions ( derivationStrategyBase ) ;
var txinfos = txs . UnconfirmedTransactions . Transactions . Concat ( txs . ConfirmedTransactions . Transactions )
. OrderByDescending ( t = > t . Timestamp )
. Skip ( skip is null ? 0 : skip . Value )
. Take ( count is null ? int . MaxValue : count . Value ) ;
var lines = new List < TransactionHistoryLine > ( Math . Min ( ( count is int v ? v : int . MaxValue ) , txs . UnconfirmedTransactions . Transactions . Count + txs . ConfirmedTransactions . Transactions . Count ) ) ;
DateTimeOffset ? timestampLimit = interval is TimeSpan i ? DateTimeOffset . UtcNow - i : null ;
foreach ( var t in txinfos )
{
if ( timestampLimit is DateTimeOffset l & &
t . Timestamp < = l )
break ;
lines . Add ( FromTransactionInformation ( t ) ) ;
}
return lines ;
}
// This call is more efficient for big wallets, as it doesn't need to load all transactions from the history
else
{
await using var ctx = await NbxplorerConnectionFactory . OpenConnection ( ) ;
2023-06-22 16:09:53 +09:00
var cmd = new CommandDefinition (
commandText : "SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, r.asset_id, COALESCE((SELECT height FROM get_tip('BTC')) - t.blk_height + 1, 0) AS confs " +
2022-07-04 15:50:56 +09:00
"FROM get_wallets_recent(@wallet_id, @code, @interval, @count, @skip) r " +
"JOIN txs t USING (code, tx_id) " +
2023-06-22 16:09:53 +09:00
"ORDER BY r.seen_at DESC" ,
parameters : new
2022-07-04 15:50:56 +09:00
{
wallet_id = NBXplorer . Client . DBUtils . nbxv1_get_wallet_id ( Network . CryptoCode , derivationStrategyBase . ToString ( ) ) ,
code = Network . CryptoCode ,
2023-08-23 16:11:25 +09:00
count = count = = int . MaxValue ? null : count ,
2023-03-23 13:42:06 +09:00
skip = skip ,
2022-07-04 15:50:56 +09:00
interval = interval is TimeSpan t ? t : TimeSpan . FromDays ( 365 * 1000 )
2023-06-22 16:09:53 +09:00
} ,
cancellationToken : cancellationToken ) ;
var rows = await ctx . QueryAsync < ( string tx_id , DateTimeOffset seen_at , string blk_id , long? blk_height , long balance_change , string asset_id , long confs ) > ( cmd ) ;
2022-07-04 15:50:56 +09:00
rows . TryGetNonEnumeratedCount ( out int c ) ;
var lines = new List < TransactionHistoryLine > ( c ) ;
foreach ( var row in rows )
{
lines . Add ( new TransactionHistoryLine ( )
{
BalanceChange = string . IsNullOrEmpty ( row . asset_id ) ? Money . Satoshis ( row . balance_change ) : new AssetMoney ( uint256 . Parse ( row . asset_id ) , row . balance_change ) ,
Height = row . blk_height ,
SeenAt = row . seen_at ,
TransactionId = uint256 . Parse ( row . tx_id ) ,
Confirmations = row . confs ,
2025-01-28 14:42:06 +09:00
BlockHash = string . IsNullOrEmpty ( row . blk_id ) ? null : uint256 . Parse ( row . blk_id )
2022-07-04 15:50:56 +09:00
} ) ;
}
return lines ;
}
}
2025-01-29 19:26:22 +09:00
public async Task < BumpableTransactions > GetBumpableTransactions ( DerivationStrategyBase derivationStrategyBase , CancellationToken cancellationToken )
{
var result = new BumpableTransactions ( ) ;
result . Support = BumpableSupport . NotConfigured ;
if ( ! NbxplorerConnectionFactory . Available )
return result ;
result . Support = BumpableSupport . NotCompatible ;
var state = this . Dashboard . Get ( Network . CryptoCode ) ;
if ( AsVersion ( state ? . Status ? . Version ? ? "" ) < new Version ( "2.5.22" ) )
return result ;
result . Support = BumpableSupport . NotSynched ;
if ( state ? . Status . IsFullySynched is not true )
return result ;
result . Support = BumpableSupport . Ok ;
await using var ctx = await NbxplorerConnectionFactory . OpenConnection ( ) ;
var cmd = new CommandDefinition (
commandText : "" "
WITH unconfs AS (
SELECT code , tx_id , raw
FROM txs
WHERE code = @code AND raw IS NOT NULL AND mempool IS TRUE AND replaced_by IS NULL AND blk_id IS NULL ) ,
tracked_txs AS (
SELECT code , tx_id ,
COUNT ( * ) FILTER ( WHERE is_out IS FALSE ) input_count ,
COUNT ( * ) FILTER ( WHERE is_out IS TRUE AND feature = ' Change ' ) change_count
FROM nbxv1_tracked_txs
WHERE code = @code AND wallet_id = @walletId AND mempool IS TRUE AND replaced_by IS NULL AND blk_id IS NULL
GROUP BY code , tx_id
) ,
unspent_utxos AS (
SELECT code , tx_id , COUNT ( * ) FILTER ( WHERE input_tx_id IS NULL ) unspent_count
FROM wallets_utxos
WHERE code = @code AND wallet_id = @walletId AND mempool IS TRUE AND replaced_by IS NULL AND blk_id IS NULL
GROUP BY code , tx_id
)
SELECT tt . tx_id , u . raw , tt . input_count , tt . change_count , uu . unspent_count FROM unconfs u
JOIN tracked_txs tt USING ( code , tx_id )
JOIN unspent_utxos uu USING ( code , tx_id ) ;
"" ",
parameters : new
{
code = Network . CryptoCode ,
walletId = NBXplorer . Client . DBUtils . nbxv1_get_wallet_id ( Network . CryptoCode , derivationStrategyBase . ToString ( ) )
} ,
cancellationToken : cancellationToken ) ;
// We can only replace mempool transaction where all inputs belong to us. (output_count and input_count count those belonging to us)
var rows = ( await ctx . QueryAsync < ( string tx_id , byte [ ] raw , int input_count , int change_count , int unspent_count ) > ( cmd ) ) ;
if ( Enumerable . TryGetNonEnumeratedCount ( rows , out int c ) & & c = = 0 )
return result ;
HashSet < uint256 > canRBF = new ( ) ;
HashSet < uint256 > canCPFP = new ( ) ;
foreach ( var r in rows )
{
Transaction tx ;
try
{
tx = Transaction . Load ( r . raw , Network . NBitcoinNetwork ) ;
}
catch
{
continue ;
}
if ( ( state . MempoolInfo ? . FullRBF is true | | tx . RBF ) & & tx . Inputs . Count = = r . input_count & &
r . change_count > 0 )
{
canRBF . Add ( uint256 . Parse ( r . tx_id ) ) ;
}
if ( r . unspent_count > 0 )
{
canCPFP . Add ( uint256 . Parse ( r . tx_id ) ) ;
}
}
// Then only transactions that doesn't have any descendant (outside itself)
var entries = await _Client . RPCClient . FetchMempoolEntries ( canRBF . Concat ( canCPFP ) . ToHashSet ( ) , cancellationToken ) ;
foreach ( var entry in entries )
{
if ( entry . Value . DescendantCount ! = 1 )
{
canRBF . Remove ( entry . Key ) ;
}
}
if ( state is not
{
MempoolInfo :
{
IncrementalRelayFeeRate : { } incRelayFeeRate ,
MempoolMinfeeRate : { } minFeeRate
}
} )
{
incRelayFeeRate = new FeeRate ( 1.0 m ) ;
minFeeRate = new FeeRate ( 1.0 m ) ;
}
foreach ( var r in rows )
{
var id = uint256 . Parse ( r . tx_id ) ;
if ( ! entries . TryGetValue ( id , out var mempoolEntry ) )
{
canCPFP . Remove ( id ) ;
canRBF . Remove ( id ) ;
}
result . Add ( id , new ( canRBF . Contains ( id ) , canCPFP . Contains ( id ) , new ReplacementInfo ( mempoolEntry , incRelayFeeRate , minFeeRate ) ) ) ;
}
return result ;
}
private Version AsVersion ( string version )
{
if ( Version . TryParse ( version . Split ( '-' ) . FirstOrDefault ( ) , out var v ) )
return v ;
return new Version ( "0.0.0.0" ) ;
}
2022-07-04 15:50:56 +09:00
private static TransactionHistoryLine FromTransactionInformation ( TransactionInformation t )
{
return new TransactionHistoryLine ( )
{
BalanceChange = t . BalanceChange ,
Confirmations = t . Confirmations ,
Height = t . Height ,
SeenAt = t . Timestamp ,
TransactionId = t . TransactionId
} ;
}
private async Task < GetTransactionsResponse > FetchTransactions ( DerivationStrategyBase derivationStrategyBase )
{
var transactions = await _Client . GetTransactionsAsync ( derivationStrategyBase ) ;
return FilterValidTransactions ( transactions ) ;
2021-03-11 13:34:52 +01:00
}
private GetTransactionsResponse FilterValidTransactions ( GetTransactionsResponse response )
{
return new GetTransactionsResponse ( )
{
Height = response . Height ,
UnconfirmedTransactions =
new TransactionInformationSet ( )
{
Transactions = _Network . FilterValidTransactions ( response . UnconfirmedTransactions . Transactions )
} ,
ConfirmedTransactions =
new TransactionInformationSet ( )
{
Transactions = _Network . FilterValidTransactions ( response . ConfirmedTransactions . Transactions )
} ,
ReplacedTransactions = new TransactionInformationSet ( )
{
Transactions = _Network . FilterValidTransactions ( response . ReplacedTransactions . Transactions )
}
} ;
}
2022-07-04 15:50:56 +09:00
public async Task < TransactionHistoryLine > FetchTransaction ( DerivationStrategyBase derivationStrategyBase , uint256 transactionId )
2021-03-11 13:34:52 +01:00
{
var tx = await _Client . GetTransactionAsync ( derivationStrategyBase , transactionId ) ;
2021-12-31 16:59:02 +09:00
if ( tx is null | | ! _Network . FilterValidTransactions ( new List < TransactionInformation > ( ) { tx } ) . Any ( ) )
2021-03-11 13:34:52 +01:00
{
return null ;
}
2022-07-04 15:50:56 +09:00
return FromTransactionInformation ( tx ) ;
2018-07-27 00:08:07 +09:00
}
2018-02-13 03:27:36 +09:00
public Task < BroadcastResult [ ] > BroadcastTransactionsAsync ( List < Transaction > transactions )
2017-10-27 17:53:04 +09:00
{
2018-01-11 14:36:12 +09:00
var tasks = transactions . Select ( t = > _Client . BroadcastAsync ( t ) ) . ToArray ( ) ;
2017-10-27 17:53:04 +09:00
return Task . WhenAll ( tasks ) ;
}
2022-06-09 20:58:51 -07:00
public async Task < ReceivedCoin [ ] > GetUnspentCoins (
DerivationStrategyBase derivationStrategy ,
bool excludeUnconfirmed = false ,
CancellationToken cancellation = default ( CancellationToken )
)
2018-02-13 03:27:36 +09:00
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( derivationStrategy ) ;
2018-02-17 01:34:40 +09:00
return ( await GetUTXOChanges ( derivationStrategy , cancellation ) )
2022-06-09 20:58:51 -07:00
. GetUnspentUTXOs ( excludeUnconfirmed )
2018-02-17 01:34:40 +09:00
. 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 ,
2021-04-20 04:02:06 +02:00
Coin = c . AsCoin ( derivationStrategy ) ,
2022-12-13 09:09:25 +09:00
Confirmations = c . Confirmations ,
// Some old version of NBX doesn't have Address in this call
Address = c . Address ? ? c . ScriptPubKey . GetDestinationAddress ( Network . NBitcoinNetwork )
2018-02-17 01:34:40 +09:00
} ) . ToArray ( ) ;
2018-02-13 03:27:36 +09:00
}
2018-01-10 15:43:07 +09:00
2021-04-08 05:43:51 +02:00
public Task < GetBalanceResponse > GetBalance ( DerivationStrategyBase derivationStrategy , CancellationToken cancellation = default ( CancellationToken ) )
2017-10-27 17:53:04 +09:00
{
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 ;
2021-04-08 05:43:51 +02:00
return result ;
2019-12-10 22:17:59 +09:00
} ) ;
2017-10-27 17:53:04 +09:00
}
}
2025-01-29 19:26:22 +09:00
public record ReplacementInfo ( MempoolEntry Entry , FeeRate IncrementalRelayFee , FeeRate MinMempoolFeeRate )
{
public record BumpResult ( Money NewTxFee , Money BumpTxFee , FeeRate NewTxFeeRate , FeeRate NewEffectiveFeeRate ) ;
public BumpResult CalculateBumpResult ( FeeRate newEffectiveFeeRate )
{
var packageFeeRate = GetEffectiveFeeRate ( ) ;
var newTotalFee = GetFeeRoundUp ( newEffectiveFeeRate , GetPackageVirtualSize ( ) ) ;
var oldTotalFee = GetPackageFee ( ) ;
var bump = newTotalFee - oldTotalFee ;
var newTxFee = Entry . BaseFee + bump ;
var newTxFeeRate = new FeeRate ( newTxFee , Entry . VirtualSizeBytes ) ;
var totalFeeRate = new FeeRate ( newTotalFee , GetPackageVirtualSize ( ) ) ;
return new BumpResult ( newTxFee , bump , newTxFeeRate , totalFeeRate ) ;
}
static Money GetFeeRoundUp ( FeeRate feeRate , int vsize ) = > ( Money ) ( ( feeRate . FeePerK . Satoshi * vsize + 999 ) / 1000 ) ;
public FeeRate CalculateNewMinFeeRate ( )
{
var packageFeeRate = GetEffectiveFeeRate ( ) ;
var newMinFeeRate = new FeeRate ( packageFeeRate . SatoshiPerByte + IncrementalRelayFee . SatoshiPerByte ) ;
var bump = CalculateBumpResult ( newMinFeeRate ) ;
2022-07-04 15:50:56 +09:00
2025-01-29 19:26:22 +09:00
if ( bump . NewTxFeeRate < MinMempoolFeeRate )
{
// We need to pay a bit more fee for the transaction to be relayed
var newTxFee = GetFeeRoundUp ( MinMempoolFeeRate , Entry . VirtualSizeBytes ) ;
newMinFeeRate = new FeeRate ( GetPackageFee ( ) - Entry . BaseFee + newTxFee , GetPackageVirtualSize ( ) ) ;
}
return newMinFeeRate ;
}
public int GetPackageVirtualSize ( ) = >
Entry . DescendantVirtualSizeBytes + Entry . AncestorVirtualSizeBytes - Entry . VirtualSizeBytes ;
public Money GetPackageFee ( ) = >
Entry . DescendantFees + Entry . AncestorFees - Entry . BaseFee ;
// Note: This isn't a correct way to calculate the package fee rate, but it is good enough for our purpose.
// It is only accounting the fee from direct ancestors/descendants. (not of uncles/cousins/brothers)
// Another more precise fee rate is documented https://x.com/ajtowns/status/1886025911562309967
// But it is more complex to calculate, as we need to recursively fetch the mempool for all the descendants
public FeeRate GetEffectiveFeeRate ( ) = > new FeeRate ( GetPackageFee ( ) , GetPackageVirtualSize ( ) ) ;
}
2022-07-04 15:50:56 +09:00
public class TransactionHistoryLine
{
public DateTimeOffset SeenAt { get ; set ; }
public long? Height { get ; set ; }
public long Confirmations { get ; set ; }
public uint256 TransactionId { get ; set ; }
public uint256 BlockHash { get ; set ; }
public IMoney BalanceChange { get ; set ; }
}
2017-09-13 15:47:34 +09:00
}