2023-08-21 14:48:16 +02:00
|
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
using System.Linq;
|
2023-07-06 15:07:28 +02:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using BTCPayApp.CommonServer;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
using BTCPayServer.Abstractions.Constants;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
using BTCPayServer.HostedServices;
|
|
|
|
|
using BTCPayServer.Services;
|
2023-08-21 14:48:16 +02:00
|
|
|
|
using BTCPayServer.Services.Wallets;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2023-07-03 09:56:00 +02:00
|
|
|
|
using Microsoft.AspNetCore.SignalR;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
using NBitcoin;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
using NBXplorer.DerivationStrategy;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
using NBXplorer.Models;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
using Newtonsoft.Json;
|
2023-07-03 09:56:00 +02:00
|
|
|
|
|
|
|
|
|
namespace BTCPayServer.Controllers;
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
|
|
|
|
|
public class GetBlockchainInfoResponse
|
2023-07-03 09:56:00 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("headers")]
|
|
|
|
|
public int Headers
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
get; set;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("blocks")]
|
|
|
|
|
public int Blocks
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
get; set;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("verificationprogress")]
|
|
|
|
|
public double VerificationProgress
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
get; set;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("mediantime")]
|
|
|
|
|
public long? MedianTime
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
get; set;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("initialblockdownload")]
|
|
|
|
|
public bool? InitialBlockDownload
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
get; set;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[JsonProperty("bestblockhash")]
|
|
|
|
|
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
|
|
|
|
public uint256 BestBlockHash { get; set; }
|
|
|
|
|
}
|
2023-08-17 15:03:37 +02:00
|
|
|
|
|
2023-08-21 14:48:16 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
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)
|
2023-07-06 15:07:28 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
ByHashes = new Dictionary<uint256, RPCBlockHeader>(headers.Count);
|
|
|
|
|
ByHeight = new Dictionary<int, RPCBlockHeader>(headers.Count);
|
|
|
|
|
foreach (var header in headers)
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
ByHashes.TryAdd(header.Hash, header);
|
|
|
|
|
ByHeight.TryAdd(header.Height, header);
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
2023-07-06 15:07:28 +02:00
|
|
|
|
}
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public IEnumerator<RPCBlockHeader> GetEnumerator()
|
|
|
|
|
{
|
|
|
|
|
return ByHeight.Values.GetEnumerator();
|
|
|
|
|
}
|
2023-07-06 15:07:28 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
2023-07-06 15:07:28 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
return GetEnumerator();
|
2023-07-06 15:07:28 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Bearer)]
|
|
|
|
|
public class BTCPayAppHub : Hub<IBTCPayAppHubClient>, IBTCPayAppHubServer
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
|
|
|
|
private readonly NBXplorerDashboard _nbXplorerDashboard;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
private readonly BTCPayAppState _appState;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
|
|
|
|
private readonly IFeeProviderFactory _feeProviderFactory;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public BTCPayAppHub(BTCPayNetworkProvider btcPayNetworkProvider,
|
|
|
|
|
NBXplorerDashboard nbXplorerDashboard,
|
|
|
|
|
BTCPayAppState appState,
|
|
|
|
|
ExplorerClientProvider explorerClientProvider,
|
|
|
|
|
IFeeProviderFactory feeProviderFactory)
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
_btcPayNetworkProvider = btcPayNetworkProvider;
|
|
|
|
|
_nbXplorerDashboard = nbXplorerDashboard;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
_appState = appState;
|
2024-05-02 15:00:09 +02:00
|
|
|
|
_explorerClientProvider = explorerClientProvider;
|
|
|
|
|
_feeProviderFactory = feeProviderFactory;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public override async Task OnConnectedAsync()
|
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
//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());
|
|
|
|
|
|
|
|
|
|
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public async Task<bool> BroadcastTransaction(string tx)
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
|
|
|
|
Transaction txObj = Transaction.Parse(tx, explorerClient.Network.NBitcoinNetwork);
|
|
|
|
|
var result = await explorerClient.BroadcastAsync(txObj);
|
|
|
|
|
return result.Success;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public async Task<decimal> GetFeeRate(int blockTarget)
|
2023-08-17 15:03:37 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
|
|
|
|
|
var feeProvider = _feeProviderFactory.CreateFeeProvider( _btcPayNetworkProvider.BTC);
|
|
|
|
|
return (await feeProvider.GetFeeRateAsync(blockTarget)).SatoshiPerByte;
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|
2023-08-21 14:48:16 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public async Task<BestBlockResponse> GetBestBlock()
|
2023-08-21 14:48:16 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
|
|
|
|
|
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
|
|
|
|
var bcInfo = await explorerClient.RPCClient.GetBlockchainInfoAsyncEx();
|
2024-05-03 16:19:10 +02:00
|
|
|
|
var bh = await GetBlockHeader(bcInfo.BestBlockHash.ToString());
|
2024-05-02 15:00:09 +02:00
|
|
|
|
|
|
|
|
|
return new BestBlockResponse()
|
2023-08-21 14:48:16 +02:00
|
|
|
|
{
|
2024-05-02 15:00:09 +02:00
|
|
|
|
BlockHash = bcInfo.BestBlockHash.ToString(),
|
|
|
|
|
BlockHeight = bcInfo.Blocks,
|
2024-05-03 16:19:10 +02:00
|
|
|
|
BlockHeader = bh
|
2024-05-02 15:00:09 +02:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<string> GetBlockHeader(string hash)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
var explorerClient = _explorerClientProvider.GetExplorerClient( _btcPayNetworkProvider.BTC);
|
|
|
|
|
var bh = await explorerClient.RPCClient.GetBlockHeaderAsync(uint256.Parse(hash));
|
2024-05-03 16:19:10 +02:00
|
|
|
|
return Convert.ToHexString(bh.ToBytes());
|
2024-05-02 15:00:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
}),
|
2024-05-03 16:19:10 +02:00
|
|
|
|
Blocks = headersTask.ToDictionary(kv => kv.Key.ToString(), kv => Convert.ToHexString(kv.Value.Result.ToBytes())),
|
2024-05-02 15:00:09 +02:00
|
|
|
|
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);
|
2023-08-21 14:48:16 +02:00
|
|
|
|
}
|
2024-05-02 15:00:09 +02:00
|
|
|
|
return resultPsbt.ToHex();
|
|
|
|
|
}
|
2023-08-21 14:48:16 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
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);
|
2023-08-21 14:48:16 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
|
|
|
|
|
}
|
2023-08-21 14:48:16 +02:00
|
|
|
|
|
2024-05-02 15:00:09 +02:00
|
|
|
|
public async Task<AppHandshakeResponse> Handshake(AppHandshake request)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
return await _appState.Handshake(Context.ConnectionId, request);
|
2023-08-21 14:48:16 +02:00
|
|
|
|
}
|
2023-08-17 15:03:37 +02:00
|
|
|
|
}
|