mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 22:46:49 +01:00
wip
This commit is contained in:
parent
a9dd25e045
commit
e0c57b1691
10 changed files with 495 additions and 243 deletions
|
@ -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<string>? Roles { get; set; }
|
||||
public IEnumerable<AppUserStoreInfo>? 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
|
||||
|
|
79
BTCPayApp.CommonServer/IBTCPayAppHubClient.cs
Normal file
79
BTCPayApp.CommonServer/IBTCPayAppHubClient.cs
Normal file
|
@ -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<Dictionary<string,string>> Pair(PairRequest request);
|
||||
Task<AppHandshakeResponse> Handshake(AppHandshake request);
|
||||
Task<bool> BroadcastTransaction(string tx);
|
||||
Task<decimal> GetFeeRate(int blockTarget);
|
||||
Task<BestBlockResponse> GetBestBlock();
|
||||
Task<string> GetBlockHeader(string hash);
|
||||
|
||||
Task<TxInfoResponse> FetchTxsAndTheirBlockHeads(string[] txIds);
|
||||
Task<string> DeriveScript(string identifier);
|
||||
Task TrackScripts(string identifier, string[] scripts);
|
||||
Task<string> UpdatePsbt(string[] identifiers, string psbt);
|
||||
Task<CoinResponse[]> 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<string,TransactionResponse> Txs { get; set; }
|
||||
public Dictionary<string,string> Blocks { get; set; }
|
||||
public Dictionary<string,int> 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<string, string?> Derivations { get; set; } = new();
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -7,7 +7,6 @@ public static class BTCPayAppExtensions
|
|||
{
|
||||
public static IServiceCollection AddBTCPayApp(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<BtcPayAppService>();
|
||||
serviceCollection.AddSingleton<BTCPayAppState>();
|
||||
serviceCollection.AddHostedService(serviceProvider => serviceProvider.GetRequiredService<BTCPayAppState>());
|
||||
return serviceCollection;
|
||||
|
|
|
@ -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<BTCPayAppHub, IBTCPayAppServerClient> _hubContext;
|
||||
private readonly ILogger<BTCPayAppState> _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<string, TrackedSource> _connectionScheme = new();
|
||||
|
||||
public BTCPayAppState(
|
||||
IHubContext<BTCPayAppHub, IBTCPayAppServerClient> hubContext,
|
||||
ILogger<BTCPayAppState> 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<NewBlockEvent>(OnNewBlock));
|
||||
_compositeDisposable.Add(
|
||||
_eventAggregator.Subscribe<NewTransactionEvent>(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<RPCBlockHeader>
|
||||
{
|
||||
public readonly Dictionary<uint256, RPCBlockHeader> ByHashes;
|
||||
public readonly Dictionary<int, RPCBlockHeader> ByHeight;
|
||||
public BlockHeaders(IList<RPCBlockHeader> 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<uint256, RPCBlockHeader>(headers.Count);
|
||||
ByHeight = new Dictionary<int, RPCBlockHeader>(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<RPCBlockHeader> GetEnumerator()
|
||||
{
|
||||
_connectionScheme.TryRemove(contextConnectionId, out _);
|
||||
return ByHeight.Values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public class BTCPayAppHub : Hub<IBTCPayAppServerClient>, IBTCPayAppServerHub
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)]
|
||||
public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, 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<bool> 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<decimal> GetFeeRate(int blockTarget)
|
||||
{
|
||||
|
||||
var feeProvider = _feeProviderFactory.CreateFeeProvider( _btcPayNetworkProvider.BTC);
|
||||
return (await feeProvider.GetFeeRateAsync(blockTarget)).SatoshiPerByte;
|
||||
}
|
||||
|
||||
public async Task<BestBlockResponse> 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<string> GetBlockHeader(string hash)
|
||||
{
|
||||
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var bh = await explorerClient.RPCClient.GetBlockHeaderAsync(uint256.Parse(hash));
|
||||
return bh.ToString();
|
||||
}
|
||||
|
||||
public async Task<TxInfoResponse> 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<string> 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<string> 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<CoinResponse[]> GetUTXOs(string[] identifiers)
|
||||
{
|
||||
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
||||
var result = new List<CoinResponse>();
|
||||
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<Dictionary<string, string>> Pair(PairRequest request)
|
||||
{
|
||||
return await _appState.Pair(Context.ConnectionId, request);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
|
||||
{
|
||||
|
||||
return await _appState.Handshake(Context.ConnectionId, request);
|
||||
}
|
||||
}
|
||||
|
|
132
BTCPayServer/App/BTCPayAppState.cs
Normal file
132
BTCPayServer/App/BTCPayAppState.cs
Normal file
|
@ -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<BTCPayAppHub, IBTCPayAppHubClient> _hubContext;
|
||||
private readonly ILogger<BTCPayAppState> _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<string, TrackedSource> _connectionScheme = new();
|
||||
|
||||
public BTCPayAppState(
|
||||
IHubContext<BTCPayAppHub, IBTCPayAppHubClient> hubContext,
|
||||
ILogger<BTCPayAppState> 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<NewBlockEvent>(OnNewBlock));
|
||||
_compositeDisposable.Add(
|
||||
_eventAggregator.Subscribe<NewOnChainTransactionEvent>(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<AppHandshakeResponse> 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<Dictionary<string, string>> Pair(string contextConnectionId, PairRequest request)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
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;
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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<IActionResult> 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<BTCPayNetwork>("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<string?> GetSeed(ExplorerClient client, DerivationSchemeSettings derivation)
|
||||
{
|
||||
return derivation.IsHotWallet &&
|
||||
await client.GetMetadataAsync<string>(derivation.AccountDerivation, WellknownMetadataKeys.Mnemonic) is
|
||||
{ } seed &&
|
||||
!string.IsNullOrEmpty(seed)
|
||||
? seed
|
||||
: 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<string> 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; }
|
||||
}
|
||||
}
|
57
BTCPayServer/App/Exts.cs
Normal file
57
BTCPayServer/App/Exts.cs
Normal file
|
@ -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<GetBlockchainInfoResponse> GetBlockchainInfoAsyncEx(this RPCClient client, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await client.SendCommandAsync("getblockchaininfo", cancellationToken).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<GetBlockchainInfoResponse>(result.ResultString);
|
||||
}
|
||||
|
||||
public static async Task<BlockHeaders> GetBlockHeadersAsync(this RPCClient rpc, IList<int> 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<RPCBlockHeader> 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<long>();
|
||||
if (confs == -1)
|
||||
return null;
|
||||
|
||||
var prev = response["previousblockhash"]?.Value<string>();
|
||||
return new RPCBlockHeader(
|
||||
blk,
|
||||
prev is null ? null : new uint256(prev),
|
||||
response["height"].Value<int>(),
|
||||
NBitcoin.Utils.UnixTimeToDateTime(response["time"].Value<long>()),
|
||||
new uint256(response["merkleroot"]?.Value<string>()));
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue