From e0c57b169102a0787cc6c412d8427cd0399426f2 Mon Sep 17 00:00:00 2001 From: Kukks Date: Thu, 2 May 2024 15:00:09 +0200 Subject: [PATCH] wip --- BTCPayApp.CommonServer/AppUserInfo.cs | 10 + BTCPayApp.CommonServer/IBTCPayAppHubClient.cs | 79 +++++ .../IBTCPayAppServerClient.cs | 20 -- BTCPayApp.CommonServer/PairSuccessResult.cs | 12 - BTCPayServer/App/BTCPayAppExtensions.cs | 1 - BTCPayServer/App/BTCPayAppHub.cs | 327 ++++++++++++------ BTCPayServer/App/BTCPayAppState.cs | 132 +++++++ BTCPayServer/App/BtcPayAppController.cs | 61 +--- BTCPayServer/App/BtcPayAppService.cs | 39 --- BTCPayServer/App/Exts.cs | 57 +++ 10 files changed, 495 insertions(+), 243 deletions(-) create mode 100644 BTCPayApp.CommonServer/IBTCPayAppHubClient.cs delete mode 100644 BTCPayApp.CommonServer/IBTCPayAppServerClient.cs delete mode 100644 BTCPayApp.CommonServer/PairSuccessResult.cs create mode 100644 BTCPayServer/App/BTCPayAppState.cs delete mode 100644 BTCPayServer/App/BtcPayAppService.cs create mode 100644 BTCPayServer/App/Exts.cs diff --git a/BTCPayApp.CommonServer/AppUserInfo.cs b/BTCPayApp.CommonServer/AppUserInfo.cs index 0cc0e38e9..aa3aa3915 100644 --- a/BTCPayApp.CommonServer/AppUserInfo.cs +++ b/BTCPayApp.CommonServer/AppUserInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; namespace BTCPayApp.CommonServer; @@ -8,6 +9,15 @@ public class AppUserInfo public string? Email { get; set; } public IEnumerable? Roles { get; set; } public IEnumerable? Stores { get; set; } + + public static bool Equals(AppUserInfo? x, AppUserInfo? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.UserId == y.UserId && x.Email == y.Email && Equals(x.Roles, y.Roles) && Equals(x.Stores, y.Stores); + } } public class AppUserStoreInfo diff --git a/BTCPayApp.CommonServer/IBTCPayAppHubClient.cs b/BTCPayApp.CommonServer/IBTCPayAppHubClient.cs new file mode 100644 index 000000000..597a03794 --- /dev/null +++ b/BTCPayApp.CommonServer/IBTCPayAppHubClient.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace BTCPayApp.CommonServer; + +//methods available on the hub in the client +public interface IBTCPayAppHubClient +{ + Task NotifyNetwork(string network); + Task TransactionDetected(string identifier, string txId); + Task NewBlock(string block); +} +//methods available on the hub in the server +public interface IBTCPayAppHubServer +{ + Task> Pair(PairRequest request); + Task Handshake(AppHandshake request); + Task BroadcastTransaction(string tx); + Task GetFeeRate(int blockTarget); + Task GetBestBlock(); + Task GetBlockHeader(string hash); + + Task FetchTxsAndTheirBlockHeads(string[] txIds); + Task DeriveScript(string identifier); + Task TrackScripts(string identifier, string[] scripts); + Task UpdatePsbt(string[] identifiers, string psbt); + Task GetUTXOs(string[] identifiers); +} + +public class CoinResponse +{ + public string Identifier{ get; set; } + public bool Confirmed { get; set; } + public string Script { get; set; } + public string Outpoint { get; set; } + public decimal Value { get; set; } + public string Path { get; set; } +} + +public class TxInfoResponse +{ + public Dictionary Txs { get; set; } + public Dictionary Blocks { get; set; } + public Dictionary BlockHeghts { get; set; } +} + +public class TransactionResponse +{ + public string? BlockHash { get; set; } + public int? BlockHeight { get; set; } + public string Transaction { get; set; } + +} + + +public class BestBlockResponse +{ + public required string BlockHash { get; set; } + public required int BlockHeight { get; set; } + + public string BlockHeader { get; set; } +} + +public class AppHandshake +{ + public string[] Identifiers { get; set; } +} + +public class AppHandshakeResponse +{ + //response about identifiers being tracked successfully + public string[] IdentifiersAcknowledged { get; set; } +} + + +public class PairRequest +{ + public Dictionary Derivations { get; set; } = new(); +} diff --git a/BTCPayApp.CommonServer/IBTCPayAppServerClient.cs b/BTCPayApp.CommonServer/IBTCPayAppServerClient.cs deleted file mode 100644 index 29187b011..000000000 --- a/BTCPayApp.CommonServer/IBTCPayAppServerClient.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Threading.Tasks; - -namespace BTCPayApp.CommonServer; - -public interface IBTCPayAppServerClient -{ - Task TransactionDetected(string txid); - Task NewBlock(string block); -} - -public interface IBTCPayAppServerHub -{ - Task Handshake(AppHandshake handshake); - Task GetTransactions(); -} - -public class AppHandshake -{ - public string? DerivationScheme { get; set; } -} diff --git a/BTCPayApp.CommonServer/PairSuccessResult.cs b/BTCPayApp.CommonServer/PairSuccessResult.cs deleted file mode 100644 index 9d92e663b..000000000 --- a/BTCPayApp.CommonServer/PairSuccessResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace BTCPayApp.CommonServer; - -public class PairSuccessResult -{ - public string? Key { get; set; } - public string? StoreId { get; set; } - public string? UserId { get; set; } - - public string? ExistingWallet { get; set; } - public string? ExistingWalletSeed { get; set; } - public string? Network { get; set; } -} diff --git a/BTCPayServer/App/BTCPayAppExtensions.cs b/BTCPayServer/App/BTCPayAppExtensions.cs index fe5566911..d4763dec6 100644 --- a/BTCPayServer/App/BTCPayAppExtensions.cs +++ b/BTCPayServer/App/BTCPayAppExtensions.cs @@ -7,7 +7,6 @@ public static class BTCPayAppExtensions { public static IServiceCollection AddBTCPayApp(this IServiceCollection serviceCollection) { - serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddHostedService(serviceProvider => serviceProvider.GetRequiredService()); return serviceCollection; diff --git a/BTCPayServer/App/BTCPayAppHub.cs b/BTCPayServer/App/BTCPayAppHub.cs index 1f430b787..d2a5b64b2 100644 --- a/BTCPayServer/App/BTCPayAppHub.cs +++ b/BTCPayServer/App/BTCPayAppHub.cs @@ -1,160 +1,265 @@ #nullable enable using System; -using System.Collections.Concurrent; +using System.Collections; +using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using BTCPayApp.CommonServer; using BTCPayServer.Abstractions.Constants; -using BTCPayServer.Data; +using BTCPayServer.HostedServices; +using BTCPayServer.Services; using BTCPayServer.Services.Wallets; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using NBitcoin; -using NBXplorer; +using NBXplorer.DerivationStrategy; using NBXplorer.Models; -using NewBlockEvent = BTCPayServer.Events.NewBlockEvent; +using Newtonsoft.Json; namespace BTCPayServer.Controllers; -public class BTCPayAppState : IHostedService + +public class GetBlockchainInfoResponse { - private readonly IHubContext _hubContext; - private readonly ILogger _logger; - private readonly ExplorerClientProvider _explorerClientProvider; - private readonly BTCPayNetworkProvider _networkProvider; - private readonly EventAggregator _eventAggregator; - private CompositeDisposable? _compositeDisposable; - public ExplorerClient ExplorerClient { get; private set; } - private DerivationSchemeParser _derivationSchemeParser; - private readonly ConcurrentDictionary _connectionScheme = new(); - - public BTCPayAppState( - IHubContext hubContext, - ILogger logger, - ExplorerClientProvider explorerClientProvider, - BTCPayNetworkProvider networkProvider, - EventAggregator eventAggregator) + [JsonProperty("headers")] + public int Headers { - _hubContext = hubContext; - _logger = logger; - _explorerClientProvider = explorerClientProvider; - _networkProvider = networkProvider; - _eventAggregator = eventAggregator; + get; set; + } + [JsonProperty("blocks")] + public int Blocks + { + get; set; + } + [JsonProperty("verificationprogress")] + public double VerificationProgress + { + get; set; } - public Task StartAsync(CancellationToken cancellationToken) + [JsonProperty("mediantime")] + public long? MedianTime { - ExplorerClient = _explorerClientProvider.GetExplorerClient("BTC"); - _derivationSchemeParser = new DerivationSchemeParser(_networkProvider.BTC); - _compositeDisposable = new(); - _compositeDisposable.Add( - _eventAggregator.Subscribe(OnNewBlock)); - _compositeDisposable.Add( - _eventAggregator.Subscribe(OnNewTransaction)); - return Task.CompletedTask; + get; set; } - private void OnNewTransaction(NewTransactionEvent obj) + [JsonProperty("initialblockdownload")] + public bool? InitialBlockDownload { - if (obj.CryptoCode != "BTC") - return; - - _connectionScheme.Where(pair => pair.Value == obj.TrackedSource) - .Select(pair => pair.Key) - .ToList() - .ForEach(connectionId => - { - _hubContext.Clients.Client(connectionId) - .TransactionDetected(obj.TransactionData.TransactionHash.ToString()); - }); + get; set; } + [JsonProperty("bestblockhash")] + [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))] + public uint256 BestBlockHash { get; set; } +} - private void OnNewBlock(NewBlockEvent obj) - { - if (obj.CryptoCode != "BTC") - return; - _hubContext.Clients.All.NewBlock(obj.Hash.ToString()); - } - public Task StopAsync(CancellationToken cancellationToken) +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) { - _compositeDisposable?.Dispose(); - return Task.CompletedTask; - } - - public TrackedSource? GetConnectionState(string connectionId) - { - _connectionScheme.TryGetValue(connectionId, out var res); - return res; - } - - public async Task Handshake(string contextConnectionId, AppHandshake handshake) - { - try + ByHashes = new Dictionary(headers.Count); + ByHeight = new Dictionary(headers.Count); + foreach (var header in headers) { - var ts = - TrackedSource.Create(_derivationSchemeParser.Parse(handshake.DerivationScheme)); - await ExplorerClient.TrackAsync(ts); - _connectionScheme.AddOrReplace(contextConnectionId, ts); - } - catch (Exception e) - { - _logger.LogError(e, "Error during handshake"); - throw; + ByHashes.TryAdd(header.Hash, header); + ByHeight.TryAdd(header.Height, header); } } - - public void RemoveConnection(string contextConnectionId) + public IEnumerator GetEnumerator() { - _connectionScheme.TryRemove(contextConnectionId, out _); + return ByHeight.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } -[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)] -public class BTCPayAppHub : Hub, IBTCPayAppServerHub +[Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)] +public class BTCPayAppHub : Hub, IBTCPayAppHubServer { + private readonly BTCPayNetworkProvider _btcPayNetworkProvider; + private readonly NBXplorerDashboard _nbXplorerDashboard; private readonly BTCPayAppState _appState; - private readonly BTCPayWallet _wallet; + private readonly ExplorerClientProvider _explorerClientProvider; + private readonly IFeeProviderFactory _feeProviderFactory; - - public BTCPayAppHub(BTCPayAppState appState, BTCPayWalletProvider walletProvider) + public BTCPayAppHub(BTCPayNetworkProvider btcPayNetworkProvider, + NBXplorerDashboard nbXplorerDashboard, + BTCPayAppState appState, + ExplorerClientProvider explorerClientProvider, + IFeeProviderFactory feeProviderFactory) { + _btcPayNetworkProvider = btcPayNetworkProvider; + _nbXplorerDashboard = nbXplorerDashboard; _appState = appState; - _wallet = walletProvider.GetWallet("BTC"); + _explorerClientProvider = explorerClientProvider; + _feeProviderFactory = feeProviderFactory; } public override async Task OnConnectedAsync() { - } - - public override Task OnDisconnectedAsync(Exception? exception) - { - _appState.RemoveConnection(Context.ConnectionId); - return base.OnDisconnectedAsync(exception); - } - - public Task Handshake(AppHandshake handshake) - { - return _appState.Handshake(Context.ConnectionId, handshake); - } - - public async Task GetTransactions() - { - var deriv = _appState.GetConnectionState(Context.ConnectionId); - if(deriv is null) - throw new InvalidOperationException("Handshake not done"); - var txs = await _appState.ExplorerClient.GetTransactionsAsync(deriv); - if (txs is null) + //TODO: this needs to happen BEFORE connection is established + if (!_nbXplorerDashboard.IsFullySynched(_btcPayNetworkProvider.BTC.CryptoCode, out _)) { - throw new InvalidOperationException("NBXplorer failed to get transactions"); + Context.Abort(); + return; } - + await Clients.Client(Context.ConnectionId).NotifyNetwork(_btcPayNetworkProvider.BTC.NBitcoinNetwork.ToString()); } + + 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 explorerClient.RPCClient.GetBlockHeaderAsync(bcInfo.BestBlockHash); + + return new BestBlockResponse() + { + BlockHash = bcInfo.BestBlockHash.ToString(), + BlockHeight = bcInfo.Blocks, + BlockHeader = bh.ToString() + }; + } + + public async Task GetBlockHeader(string hash) + { + + var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC); + var bh = await explorerClient.RPCClient.GetBlockHeaderAsync(uint256.Parse(hash)); + return bh.ToString(); + } + + 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 = txsFetch.Where(tx => tx.BlockId is not null).ToDictionary(tx => tx.BlockId.ToString(), tx => tx.BlockId.ToString()), + 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> Pair(PairRequest request) + { + return await _appState.Pair(Context.ConnectionId, request); + + + } + + public async Task Handshake(AppHandshake request) + { + + return await _appState.Handshake(Context.ConnectionId, request); + } } diff --git a/BTCPayServer/App/BTCPayAppState.cs b/BTCPayServer/App/BTCPayAppState.cs new file mode 100644 index 000000000..19ab8ffa8 --- /dev/null +++ b/BTCPayServer/App/BTCPayAppState.cs @@ -0,0 +1,132 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using BTCPayApp.CommonServer; +using BTCPayServer.Events; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using NBitcoin; +using NBXplorer; +using NBXplorer.Models; +using NewBlockEvent = BTCPayServer.Events.NewBlockEvent; + +namespace BTCPayServer.Controllers; + +public class BTCPayAppState : IHostedService +{ + private readonly IHubContext _hubContext; + private readonly ILogger _logger; + private readonly ExplorerClientProvider _explorerClientProvider; + private readonly BTCPayNetworkProvider _networkProvider; + private readonly EventAggregator _eventAggregator; + private CompositeDisposable? _compositeDisposable; + public ExplorerClient ExplorerClient { get; private set; } + private DerivationSchemeParser _derivationSchemeParser; + // private readonly ConcurrentDictionary _connectionScheme = new(); + + public BTCPayAppState( + IHubContext hubContext, + ILogger logger, + ExplorerClientProvider explorerClientProvider, + BTCPayNetworkProvider networkProvider, + EventAggregator eventAggregator) + { + _hubContext = hubContext; + _logger = logger; + _explorerClientProvider = explorerClientProvider; + _networkProvider = networkProvider; + _eventAggregator = eventAggregator; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + ExplorerClient = _explorerClientProvider.GetExplorerClient("BTC"); + _derivationSchemeParser = new DerivationSchemeParser(_networkProvider.BTC); + _compositeDisposable = new(); + _compositeDisposable.Add( + _eventAggregator.Subscribe(OnNewBlock)); + _compositeDisposable.Add( + _eventAggregator.Subscribe(OnNewTransaction)); + return Task.CompletedTask; + } + + private void OnNewTransaction(NewOnChainTransactionEvent obj) + { + if (obj.CryptoCode != "BTC") + return; + + var identifier = obj.NewTransactionEvent.TrackedSource.ToString()!; + _hubContext.Clients + .Group(identifier) + .TransactionDetected(identifier, obj.NewTransactionEvent.TransactionData.TransactionHash.ToString()); + } + + private void OnNewBlock(NewBlockEvent obj) + { + if (obj.CryptoCode != "BTC") + return; + _hubContext.Clients.All.NewBlock(obj.Hash.ToString()); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _compositeDisposable?.Dispose(); + return Task.CompletedTask; + } + + public async Task Handshake(string contextConnectionId, AppHandshake handshake) + { + + foreach (var ts in handshake.Identifiers) + { + try + { + await _hubContext.Groups.AddToGroupAsync(contextConnectionId, ts); + } + catch (Exception e) + { + _logger.LogError(e, "Error during handshake"); + throw; + } + } + + //TODO: Check if the provided identifiers are already tracked on the server + //TODO: Maybe also introduce a checkpoint to make sure nothing is missed, but this may be somethign to handle alongside VSS + return new AppHandshakeResponse() + { + IdentifiersAcknowledged = handshake.Identifiers + }; + } + + public async Task> Pair(string contextConnectionId, PairRequest request) + { + var result = new Dictionary(); + foreach (var derivation in request.Derivations) + { + + if(derivation.Value is null) + { + var id =await ExplorerClient.CreateGroupAsync(); + + result.Add(derivation.Key, id.TrackedSource); + } + else + { + var strategy = _derivationSchemeParser.ParseOutputDescriptor(derivation.Value); + result.Add(derivation.Key, TrackedSource.Create(strategy.Item1).ToString()); + } + } + await Handshake(contextConnectionId, new AppHandshake() + { + Identifiers = result.Values.ToArray() + }); + return result; + + + } +} diff --git a/BTCPayServer/App/BtcPayAppController.cs b/BTCPayServer/App/BtcPayAppController.cs index 04d12301b..9969fa0ec 100644 --- a/BTCPayServer/App/BtcPayAppController.cs +++ b/BTCPayServer/App/BtcPayAppController.cs @@ -34,7 +34,6 @@ namespace BTCPayServer.App; [Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)] [Route("btcpayapp")] public class BtcPayAppController( - BtcPayAppService appService, APIKeyRepository apiKeyRepository, StoreRepository storeRepository, BTCPayNetworkProvider btcPayNetworkProvider, @@ -254,63 +253,5 @@ public class BtcPayAppController( } return result.Succeeded ? TypedResults.Ok() : TypedResults.Problem(result.ToString().Split(": ").Last(), statusCode: 401); } - - [HttpGet("pair/{code}")] - public async Task StartPair(string code) - { - var res = appService.ConsumePairingCode(code); - if (res is null) - { - return Unauthorized(); - } - - StoreData? store = null; - if (res.StoreId is not null) - { - store = await storeRepository.FindStore(res.StoreId, res.UserId); - if (store is null) - { - return NotFound(); - } - } - - var key = new APIKeyData - { - Id = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20)), - Type = APIKeyType.Permanent, - UserId = res.UserId, - Label = "BTCPay App Pairing" - }; - key.SetBlob(new APIKeyBlob {Permissions = [Policies.Unrestricted] }); - await apiKeyRepository.CreateKey(key); - - var onchain = store?.GetDerivationSchemeSettings(handlers, "BTC"); - string? onchainSeed = null; - if (onchain is not null) - { - var explorerClient = explorerClientProvider.GetExplorerClient("BTC"); - onchainSeed = await GetSeed(explorerClient, onchain); - } - - var nBitcoinNetwork = btcPayNetworkProvider.GetNetwork("BTC").NBitcoinNetwork; - return Ok(new PairSuccessResult - { - Key = key.Id, - StoreId = store?.Id, - UserId = res.UserId, - ExistingWallet = onchain?.AccountDerivation?.GetExtPubKeys()?.FirstOrDefault()?.ToString(nBitcoinNetwork), - ExistingWalletSeed = onchainSeed, - Network = nBitcoinNetwork.Name - }); - } - - private async Task GetSeed(ExplorerClient client, DerivationSchemeSettings derivation) - { - return derivation.IsHotWallet && - await client.GetMetadataAsync(derivation.AccountDerivation, WellknownMetadataKeys.Mnemonic) is - { } seed && - !string.IsNullOrEmpty(seed) - ? seed - : null; - } + } diff --git a/BTCPayServer/App/BtcPayAppService.cs b/BTCPayServer/App/BtcPayAppService.cs deleted file mode 100644 index aaa4d63e0..000000000 --- a/BTCPayServer/App/BtcPayAppService.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; - -namespace BTCPayServer.Controllers; - -public class BtcPayAppService -{ - private readonly IMemoryCache _memoryCache; - - public BtcPayAppService(IMemoryCache memoryCache) - { - _memoryCache = memoryCache; - } - - private string CacheKey(string k) => $"BtcPayAppService_{k}"; - - public async Task GeneratePairingCode(string storeId, string userId) - { - var code = Guid.NewGuid().ToString(); - _memoryCache.Set(CacheKey(code), new PairingRequest() {Key = code, StoreId = storeId, UserId = userId}, - TimeSpan.FromMinutes(5)); - return code; - } - - public PairingRequest? ConsumePairingCode(string code) - { - return _memoryCache.TryGetValue(CacheKey(code), out var pairingRequest) - ? (PairingRequest?)pairingRequest - : null; - } - - public class PairingRequest - { - public string Key { get; set; } - public string StoreId { get; set; } - public string UserId { get; set; } - } -} \ No newline at end of file diff --git a/BTCPayServer/App/Exts.cs b/BTCPayServer/App/Exts.cs new file mode 100644 index 000000000..2411c9547 --- /dev/null +++ b/BTCPayServer/App/Exts.cs @@ -0,0 +1,57 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NBitcoin; +using NBitcoin.RPC; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace BTCPayServer.Controllers; + +//TODO: this currently requires NBX to be enabled with RPCPROXY enabled, we need to fix the whitelisted rpc commands to remove this dependency +public static class Exts +{ + public static async Task GetBlockchainInfoAsyncEx(this RPCClient client, CancellationToken cancellationToken = default) + { + var result = await client.SendCommandAsync("getblockchaininfo", cancellationToken).ConfigureAwait(false); + return JsonConvert.DeserializeObject(result.ResultString); + } + + public static async Task GetBlockHeadersAsync(this RPCClient rpc, IList blockHeights, CancellationToken cancellationToken) + { + var batch = rpc.PrepareBatch(); + var hashes = blockHeights.Select(h => batch.GetBlockHashAsync(h)).ToArray(); + await batch.SendBatchAsync(cancellationToken); + + batch = rpc.PrepareBatch(); + var headers = hashes.Select(async h => await batch.GetBlockHeaderAsyncEx(await h, cancellationToken)).ToArray(); + await batch.SendBatchAsync(cancellationToken); + + return new BlockHeaders(headers.Select(h => h.GetAwaiter().GetResult()).Where(h => h is not null).ToList()); + } + + public static async Task GetBlockHeaderAsyncEx(this RPCClient rpc, uint256 blk, CancellationToken cancellationToken) + { + var header = await rpc.SendCommandAsync(new NBitcoin.RPC.RPCRequest("getblockheader", new[] { blk.ToString() }) + { + ThrowIfRPCError = false + }, cancellationToken); + if (header.Result is null || header.Error is not null) + return null; + var response = header.Result; + var confs = response["confirmations"].Value(); + if (confs == -1) + return null; + + var prev = response["previousblockhash"]?.Value(); + return new RPCBlockHeader( + blk, + prev is null ? null : new uint256(prev), + response["height"].Value(), + NBitcoin.Utils.UnixTimeToDateTime(response["time"].Value()), + new uint256(response["merkleroot"]?.Value())); + } + +}