#nullable enable using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using BTCPayApp.CommonServer; using BTCPayServer.Abstractions.Constants; using BTCPayServer.HostedServices; using BTCPayServer.Services; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using NBitcoin; using NBXplorer.DerivationStrategy; using NBXplorer.Models; using Newtonsoft.Json; namespace BTCPayServer.Controllers; public class GetBlockchainInfoResponse { [JsonProperty("headers")] public int Headers { get; set; } [JsonProperty("blocks")] public int Blocks { get; set; } [JsonProperty("verificationprogress")] public double VerificationProgress { get; set; } [JsonProperty("mediantime")] public long? MedianTime { get; set; } [JsonProperty("initialblockdownload")] public bool? InitialBlockDownload { get; set; } [JsonProperty("bestblockhash")] [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] public uint256 BestBlockHash { get; set; } } public record RPCBlockHeader(uint256 Hash, uint256? Previous, int Height, DateTimeOffset Time, uint256 MerkleRoot) { public SlimChainedBlock ToSlimChainedBlock() => new(Hash, Previous, Height); } public class BlockHeaders : IEnumerable { public readonly Dictionary ByHashes; public readonly Dictionary ByHeight; public BlockHeaders(IList headers) { ByHashes = new Dictionary(headers.Count); ByHeight = new Dictionary(headers.Count); foreach (var header in headers) { ByHashes.TryAdd(header.Hash, header); ByHeight.TryAdd(header.Height, header); } } public IEnumerator GetEnumerator() { return ByHeight.Values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } [Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)] public class BTCPayAppHub : Hub, IBTCPayAppHubServer { private readonly BTCPayNetworkProvider _btcPayNetworkProvider; private readonly NBXplorerDashboard _nbXplorerDashboard; private readonly BTCPayAppState _appState; private readonly ExplorerClientProvider _explorerClientProvider; private readonly IFeeProviderFactory _feeProviderFactory; public BTCPayAppHub(BTCPayNetworkProvider btcPayNetworkProvider, NBXplorerDashboard nbXplorerDashboard, BTCPayAppState appState, ExplorerClientProvider explorerClientProvider, IFeeProviderFactory feeProviderFactory) { _btcPayNetworkProvider = btcPayNetworkProvider; _nbXplorerDashboard = nbXplorerDashboard; _appState = appState; _explorerClientProvider = explorerClientProvider; _feeProviderFactory = feeProviderFactory; } public override async Task OnConnectedAsync() { await _appState.Connected(Context.ConnectionId); //TODO: this needs to happen BEFORE connection is established if (!_nbXplorerDashboard.IsFullySynched(_btcPayNetworkProvider.BTC.CryptoCode, out _)) { Context.Abort(); return; } await Clients.Client(Context.ConnectionId).NotifyNetwork(_btcPayNetworkProvider.BTC.NBitcoinNetwork.ToString()); } public override async Task OnDisconnectedAsync(Exception? exception) { await _appState.Disconnected(Context.ConnectionId); await base.OnDisconnectedAsync(exception); } public async Task BroadcastTransaction(string tx) { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); Transaction txObj = Transaction.Parse(tx, explorerClient.Network.NBitcoinNetwork); var result = await explorerClient.BroadcastAsync(txObj); return result.Success; } public async Task GetFeeRate(int blockTarget) { var feeProvider = _feeProviderFactory.CreateFeeProvider( _btcPayNetworkProvider.BTC); return (await feeProvider.GetFeeRateAsync(blockTarget)).SatoshiPerByte; } public async Task GetBestBlock() { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var bcInfo = await explorerClient.RPCClient.GetBlockchainInfoAsyncEx(); var bh = await GetBlockHeader(bcInfo.BestBlockHash.ToString()); return new BestBlockResponse() { BlockHash = bcInfo.BestBlockHash.ToString(), BlockHeight = bcInfo.Blocks, BlockHeader = bh }; } public async Task GetBlockHeader(string hash) { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var bh = await explorerClient.RPCClient.GetBlockHeaderAsync(uint256.Parse(hash)); return Convert.ToHexString(bh.ToBytes()); } public async Task FetchTxsAndTheirBlockHeads(string[] txIds) { var cancellationToken = Context.ConnectionAborted; var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var uints = txIds.Select(uint256.Parse).ToArray(); var txsFetch = await Task.WhenAll(uints.Select( uint256 => explorerClient.GetTransactionAsync(uint256, cancellationToken))); var batch = explorerClient.RPCClient.PrepareBatch(); var headersTask = txsFetch.Where(result => result.BlockId is not null && result.BlockId != uint256.Zero) .Distinct().ToDictionary(result => result.BlockId, result => batch.GetBlockHeaderAsync(result.BlockId, cancellationToken)); await batch.SendBatchAsync(cancellationToken); var headerToHeight = (await Task.WhenAll(headersTask.Values)).ToDictionary(header => header.GetHash(), header => txsFetch.First(result => result.BlockId == header.GetHash()).Height!); return new TxInfoResponse() { Txs = txsFetch.ToDictionary(tx => tx.TransactionHash.ToString(), tx => new TransactionResponse() { BlockHash = tx.BlockId?.ToString(), BlockHeight = (int?) tx.Height, Transaction = tx.Transaction.ToString() }), Blocks = headersTask.ToDictionary(kv => kv.Key.ToString(), kv => Convert.ToHexString(kv.Value.Result.ToBytes())), BlockHeghts = headerToHeight.ToDictionary(kv => kv.Key.ToString(), kv =>(int) kv.Value!) }; } public async Task DeriveScript(string identifier) { var cancellationToken = Context.ConnectionAborted; var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var ts = TrackedSource.Parse(identifier,explorerClient.Network ) as DerivationSchemeTrackedSource; var kpi = await explorerClient.GetUnusedAsync(ts.DerivationStrategy, DerivationFeature.Deposit, 0, true, cancellationToken); return kpi.ScriptPubKey.ToHex(); } public async Task TrackScripts(string identifier, string[] scripts) { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var ts = TrackedSource.Parse(identifier,explorerClient.Network ) as GroupTrackedSource; var s = scripts.Select(Script.FromHex).Select(script => script.GetDestinationAddress(explorerClient.Network.NBitcoinNetwork)).Select(address => address.ToString()).ToArray(); await explorerClient.AddGroupAddressAsync(explorerClient.CryptoCode,ts.GroupId, s); } public async Task UpdatePsbt(string[] identifiers, string psbt) { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var resultPsbt = PSBT.Parse(psbt, explorerClient.Network.NBitcoinNetwork); foreach (string identifier in identifiers) { var ts = TrackedSource.Parse(identifier,explorerClient.Network); if (ts is not DerivationSchemeTrackedSource derivationSchemeTrackedSource) continue; var res = await explorerClient.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = resultPsbt, DerivationScheme = derivationSchemeTrackedSource.DerivationStrategy, }); resultPsbt = resultPsbt.Combine(res.PSBT); } return resultPsbt.ToHex(); } public async Task GetUTXOs(string[] identifiers) { var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); var result = new List(); foreach (string identifier in identifiers) { var ts = TrackedSource.Parse(identifier,explorerClient.Network); if (ts is not DerivationSchemeTrackedSource derivationSchemeTrackedSource) continue; var utxos = await explorerClient.GetUTXOsAsync(derivationSchemeTrackedSource.DerivationStrategy); result.AddRange(utxos.GetUnspentUTXOs(0).Select(utxo => new CoinResponse() { Identifier = identifier, Confirmed = utxo.Confirmations >0, Script = utxo.ScriptPubKey.ToHex(), Outpoint = utxo.Outpoint.ToString(), Value = utxo.Value.GetValue(_btcPayNetworkProvider.BTC), Path = utxo.KeyPath.ToString() })); } return result.ToArray(); } public async Task SendPaymentUpdate(string identifier, LightningPayment lightningPayment) { await _appState.PaymentUpdate(identifier, lightningPayment); } public async Task IdentifierActive(string group, bool active) { await _appState.IdentifierActive(group, Context.ConnectionId, active); } public async Task> Pair(PairRequest request) { return await _appState.Pair(Context.ConnectionId, request); } public async Task Handshake(AppHandshake request) { return await _appState.Handshake(Context.ConnectionId, request); } }