Compare commits
1 Commits
master
...
cleanupdbt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f265a4fad6 |
@ -1,8 +0,0 @@
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public enum Backend
|
||||
{
|
||||
Postgres,
|
||||
DBTrie
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -16,8 +16,8 @@ using Microsoft.Extensions.Configuration;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Client;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
ARG SupportDBTrie=true
|
||||
ENV SupportDBTrie $SupportDBTrie
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
RUN cd NBXplorer.Tests && dotnet build
|
||||
|
||||
@ -6,7 +6,7 @@ using System.Linq;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBitcoin.RPC;
|
||||
using System.Threading;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
@ -61,7 +61,7 @@ namespace NBXplorer.Tests
|
||||
}
|
||||
|
||||
static BitcoinAddress Dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.Main);
|
||||
public static KeyPathInformation GetKeyInformation(this IRepository repo, Script script)
|
||||
public static KeyPathInformation GetKeyInformation(this Repository repo, Script script)
|
||||
{
|
||||
return repo.GetKeyInformations(new Script[] { script }).GetAwaiter().GetResult()[script].SingleOrDefault();
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ namespace NBXplorer.Tests
|
||||
[Trait("Maintenance", "Maintenance")]
|
||||
public async Task GenerateFullSchema()
|
||||
{
|
||||
using var t = ServerTester.Create(Backend.Postgres);
|
||||
using var t = ServerTester.Create();
|
||||
var script = await GenerateDbScript(t);
|
||||
File.WriteAllText(GetFullSchemaFile(), script);
|
||||
}
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net6.0</TargetFramework>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
|
||||
<SupportDBTrie Condition="'$(SupportDBTrie)' == ''">true</SupportDBTrie>
|
||||
<DefineConstants Condition="'$(SupportDBTrie)' == 'true'">$(DefineConstants);SUPPORT_DBTRIE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Scripts\generate-whale.sql" />
|
||||
@ -31,9 +29,4 @@
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Data\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,11 +1,7 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Backends;
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBXplorer.Backends.DBTrie;
|
||||
#endif
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Backend;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
@ -14,15 +10,15 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
public class RepositoryTester : IDisposable
|
||||
{
|
||||
public static RepositoryTester Create(Backend backend, bool caching, [CallerMemberName]string name = null)
|
||||
public static RepositoryTester Create(bool caching, [CallerMemberName]string name = null)
|
||||
{
|
||||
return new RepositoryTester(backend, name, caching);
|
||||
return new RepositoryTester(name, caching);
|
||||
}
|
||||
|
||||
string _Name;
|
||||
private IRepositoryProvider _Provider;
|
||||
private RepositoryProvider _Provider;
|
||||
|
||||
RepositoryTester(Backend backend, string name, bool caching)
|
||||
RepositoryTester(string name, bool caching)
|
||||
{
|
||||
_Name = name;
|
||||
var conf = new Configuration.ExplorerConfiguration()
|
||||
@ -43,33 +39,18 @@ namespace NBXplorer.Tests
|
||||
services.AddSingleton(KeyPathTemplates.Default);
|
||||
services.AddSingleton(new NBXplorerNetworkProvider(ChainName.Regtest));
|
||||
|
||||
if (backend == Backend.DBTrie)
|
||||
{
|
||||
#if SUPPORT_DBTRIE
|
||||
ServerTester.DeleteFolderRecursive(name);
|
||||
services.AddSingleton<IRepositoryProvider, RepositoryProvider>();
|
||||
services.AddSingleton<ChainProvider>();
|
||||
#else
|
||||
throw new NotSupportedException("DBTrie not supported");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddLogging();
|
||||
services.AddSingleton<DbConnectionFactory>();
|
||||
ConfigurationBuilder builder = new ConfigurationBuilder();
|
||||
builder.AddInMemoryCollection(new[] { new KeyValuePair<string,string>("POSTGRES", ServerTester.GetTestPostgres(null, name)) });
|
||||
services.AddSingleton<IConfiguration>(builder.Build());
|
||||
services.AddSingleton<IRepositoryProvider, PostgresRepositoryProvider>();
|
||||
services.AddSingleton<HostedServices.DatabaseSetupHostedService>();
|
||||
|
||||
}
|
||||
services.AddLogging();
|
||||
services.AddSingleton<DbConnectionFactory>();
|
||||
ConfigurationBuilder builder = new ConfigurationBuilder();
|
||||
builder.AddInMemoryCollection(new[] { new KeyValuePair<string, string>("POSTGRES", ServerTester.GetTestPostgres(null, name)) });
|
||||
services.AddSingleton<IConfiguration>(builder.Build());
|
||||
services.AddSingleton<RepositoryProvider>();
|
||||
services.AddSingleton<HostedServices.DatabaseSetupHostedService>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
_Provider = provider.GetService<IRepositoryProvider>();
|
||||
if (backend == Backend.Postgres)
|
||||
{
|
||||
provider.GetRequiredService<HostedServices.DatabaseSetupHostedService>().StartAsync(default).GetAwaiter().GetResult();
|
||||
}
|
||||
_Provider = provider.GetService<RepositoryProvider>();
|
||||
|
||||
provider.GetRequiredService<HostedServices.DatabaseSetupHostedService>().StartAsync(default).GetAwaiter().GetResult();
|
||||
_Provider.StartAsync(default).GetAwaiter().GetResult();
|
||||
_Repository = _Provider.GetRepository(new NBXplorerNetworkProvider(ChainName.Regtest).GetFromCryptoCode("BTC"));
|
||||
}
|
||||
@ -80,8 +61,8 @@ namespace NBXplorer.Tests
|
||||
ServerTester.DeleteFolderRecursive(_Name);
|
||||
}
|
||||
|
||||
private IRepository _Repository;
|
||||
public IRepository Repository
|
||||
private Repository _Repository;
|
||||
public Repository Repository
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@ -26,22 +26,13 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
private readonly string _Directory;
|
||||
|
||||
public static ServerTester Create(Backend backend, [CallerMemberNameAttribute] string caller = null)
|
||||
public static ServerTester Create([CallerMemberNameAttribute] string caller = null)
|
||||
{
|
||||
return new ServerTester(backend, caller);
|
||||
}
|
||||
|
||||
public static ServerTester Create([CallerMemberNameAttribute]string caller = null)
|
||||
{
|
||||
return Create(Backend.DBTrie, caller);
|
||||
return new ServerTester(caller);
|
||||
}
|
||||
public static ServerTester CreateNoAutoStart([CallerMemberNameAttribute]string caller = null)
|
||||
{
|
||||
return new ServerTester(Backend.DBTrie, caller, false);
|
||||
}
|
||||
public static ServerTester CreateNoAutoStart(Backend backend, [CallerMemberNameAttribute] string caller = null)
|
||||
{
|
||||
return new ServerTester(backend, caller, false);
|
||||
return new ServerTester(caller, false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -66,10 +57,9 @@ namespace NBXplorer.Tests
|
||||
}
|
||||
|
||||
public string Caller { get; }
|
||||
public ServerTester(Backend backend, string directory, bool autoStart = true)
|
||||
public ServerTester(string directory, bool autoStart = true)
|
||||
{
|
||||
_Name = directory;
|
||||
Backend = backend;
|
||||
SetEnvironment();
|
||||
Caller = directory;
|
||||
var rootTestData = "TestData";
|
||||
@ -80,28 +70,6 @@ namespace NBXplorer.Tests
|
||||
if (autoStart)
|
||||
Start();
|
||||
}
|
||||
|
||||
#if SUPPORT_DBTRIE
|
||||
public async Task Load(string dataName)
|
||||
{
|
||||
datadir = Path.Combine(_Directory, "explorer");
|
||||
if (Directory.Exists(datadir))
|
||||
DeleteFolderRecursive(datadir);
|
||||
Directory.CreateDirectory(_Directory);
|
||||
Directory.CreateDirectory(datadir);
|
||||
datadir = Path.Combine(datadir, "RegTest", "db");
|
||||
Directory.CreateDirectory(datadir);
|
||||
foreach (var file in Directory.GetFiles(Path.Combine("Data", dataName)))
|
||||
{
|
||||
File.Copy(file, Path.Combine(datadir, Path.GetFileName(file)));
|
||||
}
|
||||
LoadedData = true;
|
||||
await using var db = await DBTrie.DBTrieEngine.OpenFromFolder(datadir);
|
||||
using var tx = await db.OpenTransaction();
|
||||
await tx.GetTable("IndexProgress").Delete();
|
||||
}
|
||||
#endif
|
||||
|
||||
public RPCWalletType? RPCWalletType
|
||||
{
|
||||
get;
|
||||
@ -151,16 +119,8 @@ namespace NBXplorer.Tests
|
||||
var port = CustomServer.FreeTcpPort();
|
||||
List<(string key, string value)> keyValues = new List<(string key, string value)>();
|
||||
keyValues.Add(("conf", Path.Combine(datadir, "settings.config")));
|
||||
if (Backend == Backend.Postgres)
|
||||
{
|
||||
PostgresConnectionString ??= GetTestPostgres(null, _Name);
|
||||
keyValues.Add(("postgres", PostgresConnectionString));
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalFlags.Add("--dbtrie");
|
||||
keyValues.Add(("cachechain", "0"));
|
||||
}
|
||||
PostgresConnectionString ??= GetTestPostgres(null, _Name);
|
||||
keyValues.Add(("postgres", PostgresConnectionString));
|
||||
keyValues.AddRange(AdditionalConfiguration);
|
||||
keyValues.Add(("datadir", datadir));
|
||||
keyValues.Add(("port", port.ToString()));
|
||||
@ -491,8 +451,6 @@ namespace NBXplorer.Tests
|
||||
|
||||
private readonly string _Name;
|
||||
|
||||
public Backend Backend { get; set; }
|
||||
|
||||
public uint256 SendToAddress(BitcoinAddress address, Money amount)
|
||||
{
|
||||
return SendToAddressAsync(address, amount).GetAwaiter().GetResult();
|
||||
|
||||
@ -11,4 +11,12 @@ namespace NBXplorer.Tests
|
||||
Timeout = 60_000;
|
||||
}
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class FactWithTimeoutAttribute : FactAttribute
|
||||
{
|
||||
public FactWithTimeoutAttribute()
|
||||
{
|
||||
Timeout = 60_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,6 @@ services:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: NBXplorer.Tests/Dockerfile
|
||||
args:
|
||||
- SupportDBTrie=false
|
||||
environment:
|
||||
TESTS_POSTGRES: User ID=postgres;Include Error Detail=true;Host=postgres;Port=5432
|
||||
depends_on:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Logging;
|
||||
using System;
|
||||
@ -23,12 +23,12 @@ namespace NBXplorer
|
||||
}
|
||||
class AddressPool
|
||||
{
|
||||
IRepository _Repository;
|
||||
Repository _Repository;
|
||||
Task _Task;
|
||||
CancellationTokenSource _Cts;
|
||||
internal Channel<RefillPoolRequest> _Channel = Channel.CreateUnbounded<RefillPoolRequest>();
|
||||
|
||||
public AddressPool(IRepository repository)
|
||||
public AddressPool(Repository repository)
|
||||
{
|
||||
_Repository = repository;
|
||||
}
|
||||
@ -68,7 +68,7 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
public AddressPoolService(NBXplorerNetworkProvider networks, IRepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
|
||||
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
|
||||
{
|
||||
this.networks = networks;
|
||||
this.repositoryProvider = repositoryProvider;
|
||||
@ -76,7 +76,7 @@ namespace NBXplorer
|
||||
}
|
||||
Dictionary<NBXplorerNetwork, AddressPool> _AddressPoolByNetwork;
|
||||
private readonly NBXplorerNetworkProvider networks;
|
||||
private readonly IRepositoryProvider repositoryProvider;
|
||||
private readonly RepositoryProvider repositoryProvider;
|
||||
private readonly KeyPathTemplates keyPathTemplates;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -16,18 +16,18 @@ namespace NBXplorer.Analytics
|
||||
const int BlockWindow = 5;
|
||||
class NetworkFingerprintData
|
||||
{
|
||||
internal IIndexer indexer;
|
||||
internal Indexer indexer;
|
||||
internal FingerprintDistribution Distribution;
|
||||
internal FingerprintDistribution DefaultDistribution;
|
||||
internal Queue<FingerprintDistribution> BlockDistributions = new Queue<FingerprintDistribution>();
|
||||
}
|
||||
|
||||
private readonly EventAggregator eventAggregator;
|
||||
private readonly IIndexers indexers;
|
||||
private readonly Indexers indexers;
|
||||
private readonly Dictionary<NBXplorerNetwork, NetworkFingerprintData> data = new Dictionary<NBXplorerNetwork, NetworkFingerprintData>();
|
||||
IDisposable subscription;
|
||||
public FingerprintHostedService(EventAggregator eventAggregator,
|
||||
IIndexers indexers)
|
||||
Indexers indexers)
|
||||
{
|
||||
this.eventAggregator = eventAggregator;
|
||||
this.indexers = indexers;
|
||||
|
||||
@ -271,10 +271,6 @@ namespace NBXplorer
|
||||
}
|
||||
|
||||
Dictionary<Script, KeyPath> _KeyPaths = new Dictionary<Script, KeyPath>();
|
||||
public KeyPath GetKeyPath(Script scriptPubkey)
|
||||
{
|
||||
return _KeyPaths.TryGet(scriptPubkey);
|
||||
}
|
||||
|
||||
Dictionary<uint256, AnnotatedTransaction> _TxById = new Dictionary<uint256, AnnotatedTransaction>();
|
||||
public AnnotatedTransaction GetByTxId(uint256 txId)
|
||||
|
||||
@ -9,7 +9,6 @@ namespace NBXplorer
|
||||
{
|
||||
this.youngToOld = youngToOld;
|
||||
}
|
||||
private static readonly AnnotatedTransactionComparer _Youngness = new AnnotatedTransactionComparer(true);
|
||||
private static readonly AnnotatedTransactionComparer _Oldness = new AnnotatedTransactionComparer(false);
|
||||
public static AnnotatedTransactionComparer OldToYoung
|
||||
{
|
||||
|
||||
@ -6,7 +6,7 @@ using System;
|
||||
using System.Data.Common;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class DbConnectionFactory
|
||||
{
|
||||
@ -30,7 +30,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
return CreateConnectionHelper(network, null);
|
||||
}
|
||||
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network, Action<Npgsql.NpgsqlConnectionStringBuilder> action)
|
||||
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network, Action<NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
return new DbConnectionHelper(network, await CreateConnection(action), KeyPathTemplates)
|
||||
{
|
||||
@ -42,12 +42,12 @@ namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
return CreateConnection(null);
|
||||
}
|
||||
public async Task<DbConnection> CreateConnection(Action<Npgsql.NpgsqlConnectionStringBuilder> action)
|
||||
public async Task<DbConnection> CreateConnection(Action<NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
int maxRetries = 10;
|
||||
int retries = maxRetries;
|
||||
retry:
|
||||
var conn = new Npgsql.NpgsqlConnection(GetConnectionString(action));
|
||||
retry:
|
||||
var conn = new NpgsqlConnection(GetConnectionString(action));
|
||||
try
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
@ -9,7 +9,7 @@ using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class DbConnectionHelper : IDisposable, IAsyncDisposable
|
||||
{
|
||||
@ -51,15 +51,15 @@ namespace NBXplorer.Backends.Postgres
|
||||
typeMapper.MapComposite<NewOutRaw>("new_out");
|
||||
typeMapper.MapComposite<NewInRaw>("new_in");
|
||||
typeMapper.MapComposite<OutpointRaw>("outpoint");
|
||||
typeMapper.MapComposite<PostgresRepository.DescriptorScriptInsert>("nbxv1_ds");
|
||||
typeMapper.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
|
||||
}
|
||||
|
||||
public Task<bool> FetchMatches(IEnumerable<Transaction> txs, SlimChainedBlock slimBlock, Money? minUtxoValue)
|
||||
{
|
||||
var outCount = txs.Select(t => t.Outputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewOut> outs = new List<DbConnectionHelper.NewOut>(outCount);
|
||||
List<NewOut> outs = new List<NewOut>(outCount);
|
||||
var inCount = txs.Select(t => t.Inputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewIn> ins = new List<DbConnectionHelper.NewIn>(inCount);
|
||||
List<NewIn> ins = new List<NewIn>(inCount);
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
if (!tx.IsCoinBase)
|
||||
@ -67,7 +67,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
int i = 0;
|
||||
foreach (var input in tx.Inputs)
|
||||
{
|
||||
ins.Add(new DbConnectionHelper.NewIn(tx.GetHash(), i, input.PrevOut.Hash, (int)input.PrevOut.N));
|
||||
ins.Add(new NewIn(tx.GetHash(), i, input.PrevOut.Hash, (int)input.PrevOut.N));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@ -77,7 +77,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
io++;
|
||||
if (minUtxoValue != null && output.Value < minUtxoValue)
|
||||
continue;
|
||||
outs.Add(new DbConnectionHelper.NewOut(tx.GetHash(), io, output.ScriptPubKey, output.Value));
|
||||
outs.Add(new NewOut(tx.GetHash(), io, output.ScriptPubKey, output.Value));
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +117,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
ins.Add(new NewInRaw(ni.txId.ToString(), ni.idx, ni.spentTxId.ToString(), ni.spentIdx));
|
||||
}
|
||||
return await Connection.ExecuteScalarAsync<bool>("CALL fetch_matches(@code, @outs, @ins, 'f');", new { code = Network.CryptoCode, outs = outs, ins = ins });
|
||||
return await Connection.ExecuteScalarAsync<bool>("CALL fetch_matches(@code, @outs, @ins, 'f');", new { code = Network.CryptoCode, outs, ins });
|
||||
}
|
||||
public record SaveTransactionRecord(Transaction? Transaction, uint256? Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset? SeenAt)
|
||||
{
|
||||
@ -150,7 +150,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
|
||||
public async Task MakeOrphanFrom(int height)
|
||||
{
|
||||
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height = height });
|
||||
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height });
|
||||
}
|
||||
|
||||
public async Task<Dictionary<OutPoint, TxOut>> GetOutputs(IEnumerable<OutPoint> outPoints)
|
||||
@ -159,7 +159,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
List<OutpointRaw> rawOutpoints = new List<OutpointRaw>(outpointCount);
|
||||
foreach (var o in outPoints)
|
||||
rawOutpoints.Add(new OutpointRaw(o.Hash.ToString(), o.N));
|
||||
Dictionary <OutPoint, TxOut> result = new Dictionary<OutPoint, TxOut>();
|
||||
Dictionary<OutPoint, TxOut> result = new Dictionary<OutPoint, TxOut>();
|
||||
foreach (var r in await Connection.QueryAsync<(string tx_id, long idx, string script, long value, string asset_id)>(
|
||||
"SELECT o.tx_id, o.idx, o.script, o.value, o.asset_id FROM unnest(@outpoints) outpoints " +
|
||||
"JOIN outs o ON code=@code AND o.tx_id=outpoints.tx_id AND o.idx=outpoints.idx",
|
||||
@ -169,7 +169,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
outpoints = rawOutpoints
|
||||
}))
|
||||
{
|
||||
var txout = this.Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
|
||||
var txout = Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
|
||||
txout.Value = Money.Satoshis(r.value);
|
||||
txout.ScriptPubKey = Script.FromHex(r.script);
|
||||
result.TryAdd(new OutPoint(uint256.Parse(r.tx_id), (uint)r.idx), txout);
|
||||
@ -1,4 +1,4 @@
|
||||
namespace NBXplorer.Backends
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class GenerateAddressQuery
|
||||
{
|
||||
686
NBXplorer/Backend/Indexers.cs
Normal file
686
NBXplorer/Backend/Indexers.cs
Normal file
@ -0,0 +1,686 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Protocol;
|
||||
using NBitcoin.Protocol.Behaviors;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.Events;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public enum BitcoinDWaiterState
|
||||
{
|
||||
NotStarted,
|
||||
CoreSynching,
|
||||
NBXplorerSynching,
|
||||
Ready
|
||||
}
|
||||
public class Indexer
|
||||
{
|
||||
public Indexer(
|
||||
AddressPoolService addressPoolService,
|
||||
ILogger logger,
|
||||
NBXplorerNetwork network,
|
||||
RPCClient rpcClient,
|
||||
Repository repository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ExplorerConfiguration explorerConfiguration,
|
||||
ChainConfiguration chainConfiguration,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
AddressPoolService = addressPoolService;
|
||||
Logger = logger;
|
||||
this.network = network;
|
||||
RPCClient = rpcClient;
|
||||
Repository = repository;
|
||||
ConnectionFactory = connectionFactory;
|
||||
ExplorerConfiguration = explorerConfiguration;
|
||||
ChainConfiguration = chainConfiguration;
|
||||
EventAggregator = eventAggregator;
|
||||
}
|
||||
CancellationTokenSource cts;
|
||||
Task _indexerLoop;
|
||||
Node _Node;
|
||||
Channel<object> _Channel = Channel.CreateUnbounded<object>();
|
||||
Channel<Block> _DownloadedBlocks = Channel.CreateUnbounded<Block>();
|
||||
async Task IndexerLoop()
|
||||
{
|
||||
TimeSpan retryDelay = TimeSpan.FromSeconds(0);
|
||||
retry:
|
||||
try
|
||||
{
|
||||
await IndexerLoopCore(cts.Token);
|
||||
}
|
||||
catch when (cts.Token.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, $"Unhandled exception in the indexer, retrying in {retryDelay.TotalSeconds} seconds");
|
||||
try
|
||||
{
|
||||
await Task.Delay(retryDelay, cts.Token);
|
||||
}
|
||||
catch { }
|
||||
retryDelay += TimeSpan.FromSeconds(5.0);
|
||||
retryDelay = TimeSpan.FromTicks(Math.Min(retryDelay.Ticks, TimeSpan.FromMinutes(1.0).Ticks));
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task IndexerLoopCore(CancellationToken token)
|
||||
{
|
||||
await ConnectNode(token, true);
|
||||
await foreach (var item in _Channel.Reader.ReadAllAsync(token))
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network, b =>
|
||||
{
|
||||
b.NoResetOnClose = true;
|
||||
// It seems that when running a big rescan, the postgres connection process
|
||||
// is taking more and more RAM.
|
||||
// While I didn't find the source of the issue, disabling connection pooling
|
||||
// will force postgres to create a new connection process, freeing the memory.
|
||||
// Note that since PullBlocks are consolidated during rescans, it will only create
|
||||
// 1 connection every ~2000 blocks.
|
||||
b.Pooling = !(item is PullBlocks && State == BitcoinDWaiterState.NBXplorerSynching);
|
||||
});
|
||||
if (item is PullBlocks pb)
|
||||
{
|
||||
var headers = ConsolidatePullBlocks(_Channel.Reader, pb);
|
||||
using var pullBlockTimeout = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
pullBlockTimeout.CancelAfter(PullBlockTimeout);
|
||||
foreach (var batch in headers.Batch(maxinflight))
|
||||
{
|
||||
_ = _Node.SendMessageAsync(
|
||||
new GetDataPayload(
|
||||
batch.Select(b => new InventoryVector(_Node.AddSupportedOptions(InventoryType.MSG_BLOCK), b.GetHash())
|
||||
).ToArray()));
|
||||
var remaining = batch.Select(b => b.GetHash()).ToHashSet();
|
||||
List<Block> unorderedBlocks = new List<Block>();
|
||||
await foreach (var block in _DownloadedBlocks.Reader.ReadAllAsync(pullBlockTimeout.Token))
|
||||
{
|
||||
pullBlockTimeout.CancelAfter(PullBlockTimeout);
|
||||
if (!remaining.Remove(block.Header.GetHash()))
|
||||
continue;
|
||||
if (lastIndexedBlock is null || block.Header.HashPrevBlock == lastIndexedBlock.Hash)
|
||||
{
|
||||
SlimChainedBlock slimChainedBlock = lastIndexedBlock is null ?
|
||||
await RPCClient.GetBlockHeaderAsyncEx(block.Header.GetHash()) :
|
||||
new SlimChainedBlock(block.Header.GetHash(), lastIndexedBlock.Hash, lastIndexedBlock.Height + 1);
|
||||
await SaveMatches(conn, block, slimChainedBlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
unorderedBlocks.Add(block);
|
||||
}
|
||||
if (remaining.Count == 0)
|
||||
{
|
||||
// There are two reasons to receive unordered blocks:
|
||||
// 1. There is a fork.
|
||||
// 2. Node decides to send headers without asking.
|
||||
if (unorderedBlocks.Count > 0)
|
||||
{
|
||||
Task<SlimChainedBlock>[] slimChainedBlocks = new Task<SlimChainedBlock>[unorderedBlocks.Count];
|
||||
var rpcBatch = RPCClient.PrepareBatch();
|
||||
for (int i = 0; i < unorderedBlocks.Count; i++)
|
||||
{
|
||||
slimChainedBlocks[i] = rpcBatch.GetBlockHeaderAsyncEx(unorderedBlocks[i].GetHash());
|
||||
}
|
||||
await rpcBatch.SendBatchAsync();
|
||||
// If there is a fork, we should index the unordered blocks
|
||||
bool unconfedBlocks = false;
|
||||
bool fork = await RPCClient.GetBlockHeaderAsyncEx(lastIndexedBlock.Hash) == null;
|
||||
foreach (var b in unorderedBlocks.Zip(slimChainedBlocks)
|
||||
.Where(b => fork || b.Second.Result.Height > lastIndexedBlock.Height)
|
||||
.OrderBy(b => b.Second.Result.Height)
|
||||
.ToList())
|
||||
{
|
||||
var slimBlock = await b.Second;
|
||||
if (fork && !unconfedBlocks)
|
||||
{
|
||||
await conn.MakeOrphanFrom(slimBlock.Height);
|
||||
unconfedBlocks = true;
|
||||
}
|
||||
await SaveMatches(conn, b.First, slimBlock);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
await SaveProgress(conn);
|
||||
await UpdateState();
|
||||
}
|
||||
await AskNextHeaders();
|
||||
}
|
||||
if (item is NodeDisconnected)
|
||||
{
|
||||
await ConnectNode(token, false);
|
||||
}
|
||||
if (item is Transaction tx)
|
||||
{
|
||||
var txs = PullTransactions(_Channel.Reader, tx);
|
||||
await SaveMatches(conn, txs, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to pull as much non-conflicting transactions as possible in one batch
|
||||
private List<Transaction> PullTransactions(ChannelReader<object> reader, Transaction tx)
|
||||
{
|
||||
List<Transaction> txs = new List<Transaction>();
|
||||
HashSet<OutPoint> spent = new HashSet<OutPoint>(tx.Inputs.Capacity);
|
||||
bool EnsureNoConflict(Transaction tx)
|
||||
{
|
||||
foreach (var i in tx.Inputs.Select(i => i.PrevOut))
|
||||
if (!spent.Add(i))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
EnsureNoConflict(tx);
|
||||
txs.Add(tx);
|
||||
|
||||
while (reader.TryPeek(out var p) && p is Transaction tx2)
|
||||
{
|
||||
if (!EnsureNoConflict(tx2))
|
||||
break;
|
||||
txs.Add(tx2);
|
||||
reader.TryRead(out _);
|
||||
}
|
||||
return txs;
|
||||
}
|
||||
|
||||
// We sometimes receive burst of blocks, with some dups.
|
||||
// This method will pump as much headers from the channel as possible, removing the dups
|
||||
// along the way.
|
||||
private IList<BlockHeader> ConsolidatePullBlocks(ChannelReader<object> reader, PullBlocks pb)
|
||||
{
|
||||
List<PullBlocks> requests = new List<PullBlocks>();
|
||||
requests.Add(pb);
|
||||
while (reader.TryPeek(out var p) && p is PullBlocks pb2)
|
||||
{
|
||||
reader.TryRead(out _);
|
||||
requests.Add(pb2);
|
||||
}
|
||||
|
||||
var headerCount = requests.Select(r => r.headers.Count).Sum();
|
||||
HashSet<uint256> blocks = new HashSet<uint256>(headerCount);
|
||||
List<BlockHeader> result = new List<BlockHeader>(headerCount);
|
||||
foreach (var h in requests.SelectMany(r => r.headers))
|
||||
{
|
||||
h.PrecomputeHash(false, true);
|
||||
if (blocks.Add(h.GetHash()))
|
||||
result.Add(h);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static TimeSpan PullBlockTimeout = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
private async Task ConnectNode(CancellationToken token, bool forceRestart)
|
||||
{
|
||||
if (_Node is not null)
|
||||
{
|
||||
if (!forceRestart && _Node.State == NodeState.HandShaked)
|
||||
return;
|
||||
_Node.DisconnectAsync("Restarting");
|
||||
_Node = null;
|
||||
}
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
using (var handshakeTimeout = CancellationTokenSource.CreateLinkedTokenSource(token))
|
||||
{
|
||||
var userAgent = "NBXplorer-" + RandomUtils.GetInt64();
|
||||
var nodeParams = new NodeConnectionParameters()
|
||||
{
|
||||
UserAgent = userAgent,
|
||||
ConnectCancellation = handshakeTimeout.Token,
|
||||
IsRelay = true
|
||||
};
|
||||
if (ExplorerConfiguration.SocksEndpoint != null)
|
||||
{
|
||||
var socks = new SocksSettingsBehavior()
|
||||
{
|
||||
OnlyForOnionHosts = false,
|
||||
SocksEndpoint = ExplorerConfiguration.SocksEndpoint
|
||||
};
|
||||
if (ExplorerConfiguration.SocksCredentials != null)
|
||||
socks.NetworkCredential = ExplorerConfiguration.SocksCredentials;
|
||||
nodeParams.TemplateBehaviors.Add(socks);
|
||||
}
|
||||
var node = await Node.ConnectAsync(network.NBitcoinNetwork, ChainConfiguration.NodeEndpoint, nodeParams);
|
||||
Logger.LogInformation($"TCP Connection succeed, handshaking...");
|
||||
node.VersionHandshake(handshakeTimeout.Token);
|
||||
Logger.LogInformation($"Handshaked");
|
||||
await node.SendMessageAsync(new SendHeadersPayload());
|
||||
|
||||
await RPCArgs.TestRPCAsync(Network, RPCClient, token, Logger);
|
||||
if (await RPCClient.SupportTxIndex() is bool txIndex)
|
||||
{
|
||||
ChainConfiguration.HasTxIndex = txIndex;
|
||||
}
|
||||
if (ChainConfiguration.HasTxIndex)
|
||||
{
|
||||
Logger.LogInformation($"Has txindex support");
|
||||
}
|
||||
var peer = (await RPCClient.GetPeersInfoAsync())
|
||||
.FirstOrDefault(p => p.SubVersion == userAgent);
|
||||
if (peer.IsWhitelisted())
|
||||
{
|
||||
if (firstConnect)
|
||||
{
|
||||
firstConnect = false;
|
||||
}
|
||||
Logger.LogInformation($"NBXplorer is correctly whitelisted by the node");
|
||||
}
|
||||
else if (peer is null)
|
||||
{
|
||||
Logger.LogWarning($"{Network.CryptoCode}: The RPC server you are connecting to, doesn't seem to be the same server as the one providing the P2P connection. This is an untested setup and may have non-obvious side effects.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var addressStr = peer.Address is IPEndPoint end ? end.Address.ToString() : peer.Address?.ToString();
|
||||
Logger.LogWarning($"{Network.CryptoCode}: Your NBXplorer server is not whitelisted by your node," +
|
||||
$" you should add \"whitelist={addressStr}\" to the configuration file of your node. (Or use whitebind)");
|
||||
}
|
||||
|
||||
int waitTime = 10;
|
||||
|
||||
// Need NetworkInfo for the get status
|
||||
NetworkInfo = await RPCClient.GetNetworkInfoAsync();
|
||||
retry:
|
||||
BlockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
if (BlockchainInfo.IsSynching(Network))
|
||||
{
|
||||
State = BitcoinDWaiterState.CoreSynching;
|
||||
await Task.Delay(waitTime * 2, token);
|
||||
waitTime = Math.Min(5_000, waitTime * 2);
|
||||
goto retry;
|
||||
}
|
||||
await RPCClient.EnsureWalletCreated(Logger);
|
||||
if (Network.NBitcoinNetwork.ChainName == ChainName.Regtest && !ChainConfiguration.NoWarmup)
|
||||
{
|
||||
if (await RPCClient.WarmupBlockchain(Logger))
|
||||
BlockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
}
|
||||
_NodeTip = await RPCClient.GetBlockHeaderAsyncEx(BlockchainInfo.BestBlockHash);
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
// Refresh the NetworkInfo that may have become different while it was synching.
|
||||
NetworkInfo = await RPCClient.GetNetworkInfoAsync();
|
||||
_Node = node;
|
||||
EmptyChannel(_Channel);
|
||||
EmptyChannel(_DownloadedBlocks);
|
||||
node.MessageReceived += Node_MessageReceived;
|
||||
node.Disconnected += Node_Disconnected;
|
||||
|
||||
var locator = await AskNextHeaders();
|
||||
lastIndexedBlock = await Repository.GetLastIndexedSlimChainedBlock(locator);
|
||||
if (lastIndexedBlock is null)
|
||||
{
|
||||
var locatorTip = await RPCClient.GetBlockHeaderAsyncEx(locator.Blocks[0]);
|
||||
lastIndexedBlock = locatorTip;
|
||||
}
|
||||
await UpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
private void EmptyChannel<T>(Channel<T> channel)
|
||||
{
|
||||
while (channel.Reader.TryRead(out _)) { }
|
||||
}
|
||||
|
||||
bool firstConnect = true;
|
||||
private async Task<BlockLocator> AskNextHeaders()
|
||||
{
|
||||
var indexProgress = await Repository.GetIndexProgress();
|
||||
if (indexProgress is null)
|
||||
{
|
||||
indexProgress = await GetDefaultCurrentLocation();
|
||||
}
|
||||
await _Node.SendMessageAsync(new GetHeadersPayload(indexProgress));
|
||||
return indexProgress;
|
||||
}
|
||||
|
||||
static int[] BlockLocatorComposition = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960 };
|
||||
private async Task SaveProgress(DbConnectionHelper conn)
|
||||
{
|
||||
// We pick blocks spaced exponentially from the the tip to build our block locator
|
||||
var heights = BlockLocatorComposition.Select(l => lastIndexedBlock.Height - l).ToArray();
|
||||
var blks = await conn.Connection.QueryAsync<string>(
|
||||
"SELECT blk_id FROM blks " +
|
||||
"WHERE code=@code AND height=ANY(@heights) AND confirmed IS TRUE " +
|
||||
"ORDER BY height DESC", new { code = Network.CryptoCode, heights });
|
||||
var locator = new BlockLocator();
|
||||
foreach (var b in blks)
|
||||
locator.Blocks.Add(uint256.Parse(b));
|
||||
await Repository.SetIndexProgress(conn.Connection, locator);
|
||||
}
|
||||
|
||||
private async Task UpdateState()
|
||||
{
|
||||
var blockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
if (blockchainInfo.IsSynching(Network))
|
||||
{
|
||||
State = BitcoinDWaiterState.CoreSynching;
|
||||
}
|
||||
else if (lastIndexedBlock != null)
|
||||
{
|
||||
int minBlock = 6;
|
||||
// Prevent some corner cases in tests, if we suddenly mine 200 blocks, we should still be synched on regtest
|
||||
if (Network.NBitcoinNetwork.ChainName == ChainName.Regtest)
|
||||
minBlock = 200;
|
||||
State = blockchainInfo.Headers - lastIndexedBlock.Height < minBlock ? BitcoinDWaiterState.Ready : BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BlockLocator> GetDefaultCurrentLocation()
|
||||
{
|
||||
if (ChainConfiguration.StartHeight > BlockchainInfo.Headers)
|
||||
throw new InvalidOperationException($"{Network.CryptoCode}: StartHeight should not be above the current tip");
|
||||
BlockLocator blockLocator = null;
|
||||
if (ChainConfiguration.StartHeight == -1)
|
||||
{
|
||||
var bestBlock = await RPCClient.GetBestBlockHashAsync();
|
||||
var bh = await RPCClient.GetBlockHeaderAsyncEx(bestBlock);
|
||||
blockLocator = new BlockLocator();
|
||||
blockLocator.Blocks.Add(bh.Previous ?? bh.Hash);
|
||||
Logger.LogInformation($"Current Index Progress not found, start syncing from the header's chain tip (At height: {BlockchainInfo.Headers})");
|
||||
}
|
||||
else
|
||||
{
|
||||
var header = await RPCClient.GetBlockHeaderAsync(ChainConfiguration.StartHeight);
|
||||
var header2 = await RPCClient.GetBlockHeaderAsyncEx(header.GetHash());
|
||||
blockLocator = new BlockLocator();
|
||||
blockLocator.Blocks.Add(header2.Previous ?? header2.Hash);
|
||||
Logger.LogInformation($"Current Index Progress not found, start syncing at height {ChainConfiguration.StartHeight}");
|
||||
}
|
||||
return blockLocator;
|
||||
}
|
||||
|
||||
private async Task SaveMatches(DbConnectionHelper conn, Block block, SlimChainedBlock slimChainedBlock)
|
||||
{
|
||||
block.Header.PrecomputeHash(false, false);
|
||||
await SaveMatches(conn, block.Transactions, slimChainedBlock, true);
|
||||
EventAggregator.Publish(new RawBlockEvent(block, Network), true);
|
||||
lastIndexedBlock = slimChainedBlock;
|
||||
}
|
||||
|
||||
SlimChainedBlock _NodeTip;
|
||||
|
||||
private async Task SaveMatches(DbConnectionHelper conn, List<Transaction> transactions, SlimChainedBlock slimChainedBlock, bool fireEvents)
|
||||
{
|
||||
foreach (var tx in transactions)
|
||||
tx.PrecomputeHash(false, true);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (slimChainedBlock != null)
|
||||
{
|
||||
await conn.NewBlock(slimChainedBlock);
|
||||
}
|
||||
var matches = await Repository.GetMatchesAndSave(conn, transactions, slimChainedBlock, now, true);
|
||||
_ = AddressPoolService.GenerateAddresses(Network, matches);
|
||||
|
||||
long confirmations = 0;
|
||||
if (slimChainedBlock != null)
|
||||
{
|
||||
if (slimChainedBlock.Height >= _NodeTip.Height)
|
||||
_NodeTip = slimChainedBlock;
|
||||
confirmations = _NodeTip.Height - slimChainedBlock.Height + 1;
|
||||
await conn.NewBlockCommit(slimChainedBlock.Hash);
|
||||
var blockEvent = new NewBlockEvent()
|
||||
{
|
||||
CryptoCode = Network.CryptoCode,
|
||||
Hash = slimChainedBlock.Hash,
|
||||
Height = slimChainedBlock.Height,
|
||||
PreviousBlockHash = slimChainedBlock.Previous,
|
||||
Confirmations = confirmations
|
||||
};
|
||||
await Repository.SaveEvent(conn, blockEvent);
|
||||
EventAggregator.Publish(blockEvent);
|
||||
}
|
||||
if (fireEvents)
|
||||
{
|
||||
NewTransactionEvent[] evts = new NewTransactionEvent[matches.Length];
|
||||
for (int i = 0; i < matches.Length; i++)
|
||||
{
|
||||
var txEvt = new NewTransactionEvent()
|
||||
{
|
||||
TrackedSource = matches[i].TrackedSource,
|
||||
DerivationStrategy = matches[i].TrackedSource is DerivationSchemeTrackedSource dsts ? dsts.DerivationStrategy : null,
|
||||
CryptoCode = Network.CryptoCode,
|
||||
BlockId = slimChainedBlock?.Hash,
|
||||
TransactionData = new TransactionResult()
|
||||
{
|
||||
BlockId = slimChainedBlock?.Hash,
|
||||
Height = slimChainedBlock?.Height,
|
||||
Confirmations = confirmations,
|
||||
Timestamp = now,
|
||||
Transaction = matches[i].Transaction,
|
||||
TransactionHash = matches[i].TransactionHash
|
||||
},
|
||||
Outputs = matches[i].GetReceivedOutputs().ToList(),
|
||||
Replacing = matches[i].Replacing.ToList()
|
||||
};
|
||||
|
||||
evts[i] = txEvt;
|
||||
}
|
||||
await Repository.SaveEvents(conn, evts);
|
||||
foreach (var ev in evts)
|
||||
{
|
||||
EventAggregator.Publish(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SlimChainedBlock lastIndexedBlock;
|
||||
record PullBlocks(IList<BlockHeader> headers);
|
||||
record NodeDisconnected();
|
||||
private void Node_MessageReceived(Node node, IncomingMessage message)
|
||||
{
|
||||
if (message.Message.Payload is HeadersPayload h && h.Headers.Count != 0)
|
||||
{
|
||||
_Channel.Writer.TryWrite(new PullBlocks(h.Headers));
|
||||
}
|
||||
else if (message.Message.Payload is BlockPayload b)
|
||||
{
|
||||
_DownloadedBlocks.Writer.TryWrite(b.Object);
|
||||
}
|
||||
else if (message.Message.Payload is InvPayload invs)
|
||||
{
|
||||
if (State != BitcoinDWaiterState.Ready)
|
||||
return;
|
||||
var data = new GetDataPayload();
|
||||
foreach (var inv in invs.Inventory.Where(t => t.Type.HasFlag(InventoryType.MSG_TX)))
|
||||
{
|
||||
inv.Type = node.AddSupportedOptions(inv.Type);
|
||||
data.Inventory.Add(inv);
|
||||
}
|
||||
if (data.Inventory.Count != 0)
|
||||
{
|
||||
node.SendMessageAsync(data);
|
||||
}
|
||||
// DOGE coin doing doge things forget we want header first sync... reboot the connection
|
||||
else
|
||||
{
|
||||
if (invs.Inventory.Where(t => t.Type.HasFlag(InventoryType.MSG_BLOCK)).Any())
|
||||
{
|
||||
node.DisconnectAsync("Not sending headers first anymore");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.Message.Payload is TxPayload tx)
|
||||
{
|
||||
_Channel.Writer.TryWrite(tx.Object);
|
||||
}
|
||||
}
|
||||
|
||||
private void Node_Disconnected(Node node)
|
||||
{
|
||||
if (node.DisconnectReason.Reason != "Restarting")
|
||||
{
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
var exception = node.DisconnectReason.Exception?.Message;
|
||||
if (!string.IsNullOrEmpty(exception))
|
||||
exception = $" ({exception})";
|
||||
else
|
||||
exception = string.Empty;
|
||||
Logger.LogWarning($"Node disconnected for reason: {node.DisconnectReason.Reason}{exception}");
|
||||
}
|
||||
_Channel.Writer.TryWrite(new NodeDisconnected());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation($"Restarting node connection...");
|
||||
}
|
||||
node.MessageReceived -= Node_MessageReceived;
|
||||
node.Disconnected -= Node_Disconnected;
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
}
|
||||
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return Task.CompletedTask;
|
||||
cts = new CancellationTokenSource();
|
||||
_indexerLoop = IndexerLoop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cts?.Cancel();
|
||||
_Channel.Writer.Complete();
|
||||
if (_indexerLoop is not null)
|
||||
await _indexerLoop;
|
||||
_Node?.DisconnectAsync();
|
||||
}
|
||||
public NBXplorerNetwork Network => network;
|
||||
|
||||
BitcoinDWaiterState _State = BitcoinDWaiterState.NotStarted;
|
||||
public BitcoinDWaiterState State
|
||||
{
|
||||
get
|
||||
{
|
||||
return _State;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_State != value)
|
||||
{
|
||||
var old = _State;
|
||||
_State = value;
|
||||
EventAggregator.Publish(new BitcoinDStateChangedEvent(Network, old, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long? SyncHeight => lastIndexedBlock?.Height;
|
||||
|
||||
public GetNetworkInfoResponse NetworkInfo { get; internal set; }
|
||||
public AddressPoolService AddressPoolService { get; }
|
||||
public ILogger Logger { get; }
|
||||
public RPCClient RPCClient { get; }
|
||||
public Repository Repository { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
public ChainConfiguration ChainConfiguration { get; }
|
||||
public EventAggregator EventAggregator { get; }
|
||||
public GetBlockchainInfoResponse BlockchainInfo { get; private set; }
|
||||
|
||||
NBXplorerNetwork network;
|
||||
private int maxinflight = 10;
|
||||
|
||||
public Task SaveMatches(Transaction transaction)
|
||||
{
|
||||
return SaveMatches(transaction, false);
|
||||
}
|
||||
public async Task SaveMatches(Transaction transaction, bool fireEvents)
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network);
|
||||
await SaveMatches(conn, new List<Transaction>(1) { transaction }, null, fireEvents);
|
||||
}
|
||||
|
||||
public RPCClient GetConnectedClient()
|
||||
{
|
||||
if (State == BitcoinDWaiterState.CoreSynching || State == BitcoinDWaiterState.NBXplorerSynching || State == BitcoinDWaiterState.Ready)
|
||||
return RPCClient;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public class Indexers : IHostedService
|
||||
{
|
||||
|
||||
Dictionary<string, Indexer> _Indexers = new Dictionary<string, Indexer>();
|
||||
|
||||
public AddressPoolService AddressPoolService { get; }
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
public IRPCClients RpcClients { get; }
|
||||
public ExplorerConfiguration Configuration { get; }
|
||||
public NBXplorerNetworkProvider NetworkProvider { get; }
|
||||
public RepositoryProvider RepositoryProvider { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public EventAggregator EventAggregator { get; }
|
||||
|
||||
public Indexers(
|
||||
AddressPoolService addressPoolService,
|
||||
ILoggerFactory loggerFactory,
|
||||
IRPCClients rpcClients,
|
||||
ExplorerConfiguration configuration,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
RepositoryProvider repositoryProvider,
|
||||
DbConnectionFactory connectionFactory,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
AddressPoolService = addressPoolService;
|
||||
LoggerFactory = loggerFactory;
|
||||
RpcClients = rpcClients;
|
||||
Configuration = configuration;
|
||||
NetworkProvider = networkProvider;
|
||||
RepositoryProvider = repositoryProvider;
|
||||
ConnectionFactory = connectionFactory;
|
||||
EventAggregator = eventAggregator;
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var config in Configuration.ChainConfigurations)
|
||||
{
|
||||
var network = NetworkProvider.GetFromCryptoCode(config.CryptoCode);
|
||||
_Indexers.Add(config.CryptoCode, new Indexer(
|
||||
AddressPoolService,
|
||||
LoggerFactory.CreateLogger($"NBXplorer.Indexer.{config.CryptoCode}"),
|
||||
network,
|
||||
RpcClients.Get(network),
|
||||
RepositoryProvider.GetRepository(network),
|
||||
ConnectionFactory,
|
||||
Configuration,
|
||||
config,
|
||||
EventAggregator));
|
||||
}
|
||||
await Task.WhenAll(_Indexers.Values.Select(v => ((Indexer)v).StartAsync(cancellationToken)));
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.WhenAll(_Indexers.Values.Select(v => ((Indexer)v).StopAsync(cancellationToken)));
|
||||
}
|
||||
|
||||
public Indexer GetIndexer(NBXplorerNetwork network)
|
||||
{
|
||||
_Indexers.TryGetValue(network.CryptoCode, out var r);
|
||||
return r;
|
||||
}
|
||||
|
||||
public IEnumerable<Indexer> All()
|
||||
{
|
||||
return _Indexers.Values;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,13 +20,14 @@ using NBXplorer.Altcoins.Liquid;
|
||||
using NBXplorer.Client;
|
||||
using NBitcoin.Scripting;
|
||||
using System.Text.RegularExpressions;
|
||||
using static NBXplorer.Backends.Postgres.DbConnectionHelper;
|
||||
using static NBXplorer.Backend.DbConnectionHelper;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class PostgresRepositoryProvider : IRepositoryProvider
|
||||
public class RepositoryProvider : IHostedService
|
||||
{
|
||||
Dictionary<string, PostgresRepository> _Repositories = new Dictionary<string, PostgresRepository>();
|
||||
Dictionary<string, Repository> _Repositories = new Dictionary<string, Repository>();
|
||||
ExplorerConfiguration _Configuration;
|
||||
|
||||
public Task StartCompletion => Task.CompletedTask;
|
||||
@ -35,7 +36,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
|
||||
public PostgresRepositoryProvider(NBXplorerNetworkProvider networks,
|
||||
public RepositoryProvider(NBXplorerNetworkProvider networks,
|
||||
ExplorerConfiguration configuration,
|
||||
DbConnectionFactory connectionFactory,
|
||||
KeyPathTemplates keyPathTemplates)
|
||||
@ -46,12 +47,12 @@ namespace NBXplorer.Backends.Postgres
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
|
||||
public IRepository GetRepository(string cryptoCode)
|
||||
public Repository GetRepository(string cryptoCode)
|
||||
{
|
||||
_Repositories.TryGetValue(cryptoCode.ToUpperInvariant(), out PostgresRepository repository);
|
||||
_Repositories.TryGetValue(cryptoCode.ToUpperInvariant(), out Repository repository);
|
||||
return repository;
|
||||
}
|
||||
public IRepository GetRepository(NBXplorerNetwork network)
|
||||
public Repository GetRepository(NBXplorerNetwork network)
|
||||
{
|
||||
return GetRepository(network.CryptoCode);
|
||||
}
|
||||
@ -63,7 +64,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
var settings = GetChainSetting(net);
|
||||
if (settings != null)
|
||||
{
|
||||
var repo = new PostgresRepository(ConnectionFactory, net, KeyPathTemplates, settings.RPC, _Configuration);
|
||||
var repo = new Repository(ConnectionFactory, net, KeyPathTemplates, settings.RPC, _Configuration);
|
||||
repo.MaxPoolSize = _Configuration.MaxGapSize;
|
||||
repo.MinPoolSize = _Configuration.MinGapSize;
|
||||
repo.MinUtxoValue = settings.MinUtxoValue;
|
||||
@ -120,13 +121,13 @@ namespace NBXplorer.Backends.Postgres
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
public class PostgresRepository : IRepository
|
||||
public class Repository
|
||||
{
|
||||
private DbConnectionFactory connectionFactory;
|
||||
private readonly RPCClient rpc;
|
||||
|
||||
public DbConnectionFactory ConnectionFactory => connectionFactory;
|
||||
public PostgresRepository(DbConnectionFactory connectionFactory, NBXplorerNetwork network, KeyPathTemplates keyPathTemplates, RPCClient rpc, ExplorerConfiguration conf)
|
||||
public Repository(DbConnectionFactory connectionFactory, NBXplorerNetwork network, KeyPathTemplates keyPathTemplates, RPCClient rpc, ExplorerConfiguration conf)
|
||||
{
|
||||
this.connectionFactory = connectionFactory;
|
||||
Network = network;
|
||||
@ -182,11 +183,6 @@ namespace NBXplorer.Backends.Postgres
|
||||
return new TrackedTransaction(transactionKey, trackedSource, tx, knownScriptMapping);
|
||||
}
|
||||
|
||||
public ValueTask<int> DefragmentTables(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public record DescriptorKey(string code, string descriptor);
|
||||
internal DescriptorKey GetDescriptorKey(DerivationStrategyBase strategy, DerivationFeature derivationFeature)
|
||||
{
|
||||
@ -345,7 +341,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
|
||||
private long ToGenerateCount(GenerateAddressQuery query, long? gap)
|
||||
{
|
||||
var toGenerate = (gap is null || gap >= MinPoolSize) ? 0 : Math.Max(0, MaxPoolSize - gap.Value);
|
||||
var toGenerate = gap is null || gap >= MinPoolSize ? 0 : Math.Max(0, MaxPoolSize - gap.Value);
|
||||
if (query?.MaxAddresses is int max)
|
||||
toGenerate = Math.Min(max, toGenerate);
|
||||
if (query?.MinAddresses is int min)
|
||||
@ -601,11 +597,11 @@ namespace NBXplorer.Backends.Postgres
|
||||
var outpointCount = inputCount + outputCount;
|
||||
|
||||
var scripts = new List<Script>(outpointCount);
|
||||
var transactionsPerScript = new MultiValueDictionary<Script, NBitcoin.Transaction>(outpointCount);
|
||||
var transactionsPerScript = new MultiValueDictionary<Script, Transaction>(outpointCount);
|
||||
|
||||
var matches = new Dictionary<string, TrackedTransaction>();
|
||||
var noMatchTransactions = slimBlock?.Hash is null ? new HashSet<uint256>(txs.Count) : null;
|
||||
var transactions = new Dictionary<uint256, NBitcoin.Transaction>(txs.Count);
|
||||
var transactions = new Dictionary<uint256, Transaction>(txs.Count);
|
||||
var outpoints = new List<OutPoint>(inputCount);
|
||||
|
||||
foreach (var tx in txs)
|
||||
@ -631,7 +627,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
var matchedOuts = await result.ReadAsync();
|
||||
var matchedIns = await result.ReadAsync();
|
||||
var matchedConflicts = await result.ReadAsync();
|
||||
List<SaveTransactionRecord> txRecords = new ();
|
||||
List<SaveTransactionRecord> txRecords = new();
|
||||
var elementContext = Network.IsElement ? new ElementMatchContext() : null;
|
||||
foreach (var r in matchedConflicts)
|
||||
{
|
||||
@ -712,7 +708,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
await connection.Connection.ExecuteAsync("CALL save_matches(@code)", new { code = Network.CryptoCode });
|
||||
}
|
||||
}
|
||||
end:
|
||||
end:
|
||||
if (noMatchTransactions != null)
|
||||
{
|
||||
foreach (var txId in noMatchTransactions)
|
||||
@ -836,7 +832,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
var connection = helper.Connection;
|
||||
var key = GetDescriptorKey(strategy, derivationFeature);
|
||||
string additionalColumn = Network.IsElement ? ", ds_metadata->>'blindedAddress' blinded_addr" : string.Empty;
|
||||
retry:
|
||||
retry:
|
||||
var unused = await connection.QueryFirstOrDefaultAsync(
|
||||
$"SELECT script, addr, nbxv1_get_keypath(d_metadata, idx) keypath, ds_metadata->>'redeem' redeem {additionalColumn} FROM descriptors_scripts_unused " +
|
||||
"WHERE code=@code AND descriptor=@descriptor " +
|
||||
@ -907,7 +903,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
ki.KeyPath,
|
||||
derivation,
|
||||
addr);
|
||||
}
|
||||
}
|
||||
|
||||
var wid = GetWalletKey(ki.TrackedSource).wid;
|
||||
if (descriptorKey is not null)
|
||||
@ -944,7 +940,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
private async Task ImportAddressToRPC(DbConnectionHelper connection, TrackedSource trackedSource, BitcoinAddress address, KeyPath keyPath)
|
||||
{
|
||||
var k = GetWalletKey(trackedSource);
|
||||
var shouldImportRPC = ImportRPCMode.Parse((await connection.GetMetadata<string>(k.wid, WellknownMetadataKeys.ImportAddressToRPC)));
|
||||
var shouldImportRPC = ImportRPCMode.Parse(await connection.GetMetadata<string>(k.wid, WellknownMetadataKeys.ImportAddressToRPC));
|
||||
if (shouldImportRPC != ImportRPCMode.Legacy)
|
||||
return;
|
||||
var accountKey = await connection.GetMetadata<BitcoinExtKey>(k.wid, WellknownMetadataKeys.AccountHDKey);
|
||||
@ -967,17 +963,6 @@ namespace NBXplorer.Backends.Postgres
|
||||
}
|
||||
}
|
||||
}
|
||||
#if SUPPORT_DBTRIE
|
||||
public ValueTask<bool> MigrateOutPoints(string directory, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
public ValueTask<int> MigrateSavedTransactions(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
#endif
|
||||
public Task Ping()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@ -1062,15 +1047,15 @@ namespace NBXplorer.Backends.Postgres
|
||||
|
||||
var outCount = transactions.Select(t => t.ReceivedCoins.Count).Sum();
|
||||
var inCount = transactions.Select(t => t.SpentOutpoints.Count).Sum();
|
||||
List<DbConnectionHelper.NewOut> outs = new List<DbConnectionHelper.NewOut>(outCount);
|
||||
List<DbConnectionHelper.NewIn> ins = new List<DbConnectionHelper.NewIn>(inCount);
|
||||
List<NewOut> outs = new List<NewOut>(outCount);
|
||||
List<NewIn> ins = new List<NewIn>(inCount);
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
if (!tx.IsCoinBase)
|
||||
{
|
||||
foreach (var input in tx.SpentOutpoints)
|
||||
{
|
||||
ins.Add(new DbConnectionHelper.NewIn(
|
||||
ins.Add(new NewIn(
|
||||
tx.TransactionHash,
|
||||
tx.IndexOfInput(input),
|
||||
input.Hash,
|
||||
@ -1081,7 +1066,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
|
||||
foreach (var output in tx.GetReceivedOutputs())
|
||||
{
|
||||
outs.Add(new DbConnectionHelper.NewOut(
|
||||
outs.Add(new NewOut(
|
||||
tx.TransactionHash,
|
||||
output.Index,
|
||||
output.ScriptPubKey,
|
||||
@ -1115,7 +1100,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
public async Task<List<SavedTransaction>> SaveTransactions(DateTimeOffset now, Transaction[] transactions, SlimChainedBlock slimBlock)
|
||||
{
|
||||
await using var helper = await connectionFactory.CreateConnectionHelper(Network);
|
||||
await helper.SaveTransactions(transactions.Select(t => new SaveTransactionRecord(t, null as uint256, slimBlock?.Hash, null as int?, (long?)slimBlock?.Height, false, new DateTimeOffset?(now))));
|
||||
await helper.SaveTransactions(transactions.Select(t => new SaveTransactionRecord(t, null as uint256, slimBlock?.Hash, null as int?, slimBlock?.Height, false, new DateTimeOffset?(now))));
|
||||
return transactions.Select(t => new SavedTransaction()
|
||||
{
|
||||
BlockHash = slimBlock?.Hash,
|
||||
@ -1199,7 +1184,7 @@ namespace NBXplorer.Backends.Postgres
|
||||
.Select(p => new
|
||||
{
|
||||
code = Network.CryptoCode,
|
||||
descriptor = this.GetDescriptorKey(trackedSource.DerivationStrategy, p.DerivationFeature).descriptor,
|
||||
GetDescriptorKey(trackedSource.DerivationStrategy, p.DerivationFeature).descriptor,
|
||||
next_index = p.HighestKeyIndexFound + 1
|
||||
})
|
||||
.ToArray();
|
||||
@ -1,7 +1,7 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer.Backends
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class SavedTransaction
|
||||
{
|
||||
@ -1,654 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBitcoin.RPC;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Configuration;
|
||||
using NBitcoin.Protocol;
|
||||
using System.Threading;
|
||||
using System.IO;
|
||||
using NBitcoin;
|
||||
using System.Net;
|
||||
using NBitcoin.Protocol.Behaviors;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer.Events;
|
||||
|
||||
namespace NBXplorer.Backends.DBTrie
|
||||
{
|
||||
public class BitcoinDWaiters : IHostedService, IRPCClients, IIndexers
|
||||
{
|
||||
Dictionary<string, BitcoinDWaiter> _Waiters;
|
||||
private readonly AddressPoolService addressPool;
|
||||
private readonly NBXplorerNetworkProvider networkProvider;
|
||||
private readonly ChainProvider chains;
|
||||
private readonly IRepositoryProvider repositoryProvider;
|
||||
private readonly ExplorerConfiguration config;
|
||||
private readonly IRPCClients rpcProvider;
|
||||
private readonly EventAggregator eventAggregator;
|
||||
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
|
||||
public BitcoinDWaiters(
|
||||
ILoggerFactory loggerFactory,
|
||||
AddressPoolService addressPool,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
ChainProvider chains,
|
||||
IRepositoryProvider repositoryProvider,
|
||||
ExplorerConfiguration config,
|
||||
IRPCClients rpcProvider,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
LoggerFactory = loggerFactory;
|
||||
this.addressPool = addressPool;
|
||||
this.networkProvider = networkProvider;
|
||||
this.chains = chains;
|
||||
this.repositoryProvider = repositoryProvider;
|
||||
this.config = config;
|
||||
this.rpcProvider = rpcProvider;
|
||||
this.eventAggregator = eventAggregator;
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await repositoryProvider.StartCompletion;
|
||||
_Waiters = networkProvider
|
||||
.GetAll()
|
||||
.Select(s => (Repository: (Repository)repositoryProvider.GetRepository(s),
|
||||
RPCClient: rpcProvider.Get(s),
|
||||
Chain: chains.GetChain(s),
|
||||
Network: s))
|
||||
.Where(s => s.Repository != null && s.RPCClient != null && s.Chain != null)
|
||||
.Select(s => new BitcoinDWaiter(
|
||||
LoggerFactory.CreateLogger($"NBXplorer.BitcoinDWaiters.{s.Network.CryptoCode}"),
|
||||
s.RPCClient,
|
||||
config,
|
||||
networkProvider.GetFromCryptoCode(s.Network.CryptoCode),
|
||||
s.Chain,
|
||||
s.Repository,
|
||||
addressPool,
|
||||
eventAggregator))
|
||||
.ToDictionary(s => s.Network.CryptoCode, s => s);
|
||||
await Task.WhenAll(_Waiters.Select(s => s.Value.StartAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.WhenAll(_Waiters.Select(s => s.Value.StopAsync(cancellationToken)).ToArray());
|
||||
}
|
||||
|
||||
public BitcoinDWaiter GetWaiter(NBXplorerNetwork network)
|
||||
{
|
||||
return GetWaiter(network.CryptoCode);
|
||||
}
|
||||
public BitcoinDWaiter GetWaiter(string cryptoCode)
|
||||
{
|
||||
_Waiters.TryGetValue(cryptoCode.ToUpperInvariant(), out BitcoinDWaiter waiter);
|
||||
return waiter;
|
||||
}
|
||||
|
||||
public IEnumerable<BitcoinDWaiter> All()
|
||||
{
|
||||
return _Waiters.Values;
|
||||
}
|
||||
|
||||
public RPCClient Get(NBXplorerNetwork network)
|
||||
{
|
||||
return GetWaiter(network)?.RPC;
|
||||
}
|
||||
|
||||
public IIndexer GetIndexer(NBXplorerNetwork network)
|
||||
{
|
||||
return GetWaiter(network);
|
||||
}
|
||||
|
||||
IEnumerable<IIndexer> IIndexers.All()
|
||||
{
|
||||
return All();
|
||||
}
|
||||
}
|
||||
|
||||
public class BitcoinDWaiter : IHostedService, IIndexer
|
||||
{
|
||||
RPCClient _OriginalRPC;
|
||||
NBXplorerNetwork _Network;
|
||||
ExplorerConfiguration _Configuration;
|
||||
private ExplorerBehavior _ExplorerPrototype;
|
||||
SlimChain _Chain;
|
||||
EventAggregator _EventAggregator;
|
||||
private readonly ChainConfiguration _ChainConfiguration;
|
||||
|
||||
public BitcoinDWaiter(
|
||||
ILogger logger,
|
||||
RPCClient rpc,
|
||||
ExplorerConfiguration configuration,
|
||||
NBXplorerNetwork network,
|
||||
SlimChain chain,
|
||||
Repository repository,
|
||||
AddressPoolService addressPoolService,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
if (addressPoolService == null)
|
||||
throw new ArgumentNullException(nameof(addressPoolService));
|
||||
Logger = logger;
|
||||
_OriginalRPC = rpc;
|
||||
_Configuration = configuration;
|
||||
_Network = network;
|
||||
_Chain = chain;
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
_EventAggregator = eventAggregator;
|
||||
_ChainConfiguration = _Configuration.ChainConfigurations.First(c => c.CryptoCode == _Network.CryptoCode);
|
||||
_ExplorerPrototype = new ExplorerBehavior(repository, chain, addressPoolService, eventAggregator) { StartHeight = _ChainConfiguration.StartHeight };
|
||||
}
|
||||
private Node _Node;
|
||||
|
||||
|
||||
public NBXplorerNetwork Network
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Network;
|
||||
}
|
||||
}
|
||||
|
||||
public RPCClient RPC
|
||||
{
|
||||
get
|
||||
{
|
||||
return _OriginalRPC;
|
||||
}
|
||||
}
|
||||
|
||||
public BitcoinDWaiterState State
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool RPCAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
return State == BitcoinDWaiterState.Ready ||
|
||||
State == BitcoinDWaiterState.CoreSynching ||
|
||||
State == BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
}
|
||||
IDisposable _Subscription;
|
||||
Task _Loop;
|
||||
CancellationTokenSource _Cts;
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Disposed)
|
||||
throw new ObjectDisposedException(nameof(BitcoinDWaiter));
|
||||
|
||||
_Cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_Loop = StartLoop(_Cts.Token, _Tick);
|
||||
_Subscription = _EventAggregator.Subscribe<FullySynchedEvent>(s =>
|
||||
{
|
||||
if (s.Network == Network)
|
||||
_Tick.Set();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Signaler _Tick = new Signaler();
|
||||
|
||||
private async Task StartLoop(CancellationToken token, Signaler tick)
|
||||
{
|
||||
try
|
||||
{
|
||||
int errors = 0;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
errors = Math.Min(11, errors);
|
||||
try
|
||||
{
|
||||
while (await StepAsync(token))
|
||||
{
|
||||
}
|
||||
await tick.Wait(PollingInterval, token);
|
||||
errors = 0;
|
||||
}
|
||||
catch (ConfigException) when (!token.IsCancellationRequested)
|
||||
{
|
||||
// Probably RPC errors, don't spam
|
||||
await Wait(errors, tick, token);
|
||||
errors++;
|
||||
}
|
||||
catch (Exception ex) when (!token.IsCancellationRequested)
|
||||
{
|
||||
Logs.Configuration.LogError(ex, $"{_Network.CryptoCode}: Unhandled in Waiter loop");
|
||||
await Wait(errors, tick, token);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch when (token.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
EnsureNodeDisposed();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Wait(int errors, Signaler tick, CancellationToken token)
|
||||
{
|
||||
var timeToWait = TimeSpan.FromSeconds(5.0) * (errors + 1);
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Testing again in {(int)timeToWait.TotalSeconds} seconds");
|
||||
await tick.Wait(timeToWait, token);
|
||||
}
|
||||
|
||||
public BlockLocator GetLocation()
|
||||
{
|
||||
return GetExplorerBehavior()?.CurrentLocation;
|
||||
}
|
||||
|
||||
public TimeSpan PollingInterval
|
||||
{
|
||||
get; set;
|
||||
} = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_Disposed = true;
|
||||
_Cts.Cancel();
|
||||
_Subscription.Dispose();
|
||||
EnsureNodeDisposed();
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
_Chain = null;
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
async Task<bool> StepAsync(CancellationToken token)
|
||||
{
|
||||
var oldState = State;
|
||||
switch (State)
|
||||
{
|
||||
case BitcoinDWaiterState.NotStarted:
|
||||
await RPCArgs.TestRPCAsync(_Network, _OriginalRPC, token, Logger);
|
||||
_OriginalRPC.Capabilities = _OriginalRPC.Capabilities;
|
||||
GetBlockchainInfoResponse blockchainInfo = null;
|
||||
try
|
||||
{
|
||||
blockchainInfo = await _OriginalRPC.GetBlockchainInfoAsyncEx();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Configuration.LogError(ex, $"{_Network.CryptoCode}: Failed to connect to RPC");
|
||||
break;
|
||||
}
|
||||
if (blockchainInfo.IsSynching(_Network))
|
||||
{
|
||||
State = BitcoinDWaiterState.CoreSynching;
|
||||
}
|
||||
else
|
||||
{
|
||||
blockchainInfo = await Warmup(blockchainInfo);
|
||||
await ConnectToBitcoinD(token, blockchainInfo);
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
break;
|
||||
case BitcoinDWaiterState.CoreSynching:
|
||||
GetBlockchainInfoResponse blockchainInfo2;
|
||||
try
|
||||
{
|
||||
blockchainInfo2 = await _OriginalRPC.GetBlockchainInfoAsyncEx();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Configuration.LogError(ex, $"{_Network.CryptoCode}: Failed to connect to RPC");
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
break;
|
||||
}
|
||||
if (!blockchainInfo2.IsSynching(_Network))
|
||||
{
|
||||
blockchainInfo2 = await Warmup(blockchainInfo2);
|
||||
await ConnectToBitcoinD(token, blockchainInfo2);
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
break;
|
||||
case BitcoinDWaiterState.NBXplorerSynching:
|
||||
var explorer = GetExplorerBehavior();
|
||||
if (explorer == null)
|
||||
{
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
}
|
||||
else if (!explorer.IsSynching())
|
||||
{
|
||||
State = BitcoinDWaiterState.Ready;
|
||||
}
|
||||
break;
|
||||
case BitcoinDWaiterState.Ready:
|
||||
var explorer2 = GetExplorerBehavior();
|
||||
if (explorer2 == null)
|
||||
{
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
}
|
||||
else if (explorer2.IsSynching())
|
||||
{
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
var changed = oldState != State;
|
||||
|
||||
if (changed)
|
||||
{
|
||||
if (oldState == BitcoinDWaiterState.NotStarted)
|
||||
NetworkInfo = await _OriginalRPC.GetNetworkInfoAsync();
|
||||
_EventAggregator.Publish(new BitcoinDStateChangedEvent(_Network, oldState, State));
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private async Task<GetBlockchainInfoResponse> Warmup(GetBlockchainInfoResponse blockchainInfo2)
|
||||
{
|
||||
await _OriginalRPC.EnsureWalletCreated(Logger);
|
||||
if (Network.NBitcoinNetwork.ChainName == ChainName.Regtest && !_ChainConfiguration.NoWarmup)
|
||||
{
|
||||
if (await _OriginalRPC.WarmupBlockchain(Logger))
|
||||
blockchainInfo2 = await _OriginalRPC.GetBlockchainInfoAsyncEx();
|
||||
}
|
||||
|
||||
return blockchainInfo2;
|
||||
}
|
||||
|
||||
private Node GetHandshakedNode()
|
||||
{
|
||||
return _Node?.State == NodeState.HandShaked ? _Node : null;
|
||||
}
|
||||
|
||||
internal ExplorerBehavior GetExplorerBehavior()
|
||||
{
|
||||
return GetHandshakedNode()?.Behaviors?.Find<ExplorerBehavior>();
|
||||
}
|
||||
|
||||
private async Task ConnectToBitcoinD(CancellationToken cancellation, GetBlockchainInfoResponse blockchainInfo)
|
||||
{
|
||||
var node = GetHandshakedNode();
|
||||
if (node != null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
EnsureNodeDisposed();
|
||||
_Chain.ResetToGenesis();
|
||||
_Chain.SetCapacity((int)(blockchainInfo.Headers * 1.1));
|
||||
if (_Configuration.CacheChain)
|
||||
{
|
||||
LoadChainFromCache();
|
||||
if (!await HasBlock(_OriginalRPC, _Chain.Tip))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: The cached chain contains a tip unknown to the node, dropping the cache...");
|
||||
_Chain.ResetToGenesis();
|
||||
}
|
||||
}
|
||||
var heightBefore = _Chain.Height;
|
||||
using (var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellation))
|
||||
{
|
||||
timeout.CancelAfter(_Network.ChainLoadingTimeout);
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Trying to connect via the P2P protocol to trusted node ({_ChainConfiguration.NodeEndpoint.ToEndpointString()})...");
|
||||
var userAgent = "NBXplorer-" + RandomUtils.GetInt64();
|
||||
bool handshaked = false;
|
||||
bool connected = false;
|
||||
bool chainLoaded = false;
|
||||
using (var handshakeTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellation))
|
||||
{
|
||||
try
|
||||
{
|
||||
handshakeTimeout.CancelAfter(TimeSpan.FromSeconds(10));
|
||||
node = await Node.ConnectAsync(_Network.NBitcoinNetwork, _ChainConfiguration.NodeEndpoint, new NodeConnectionParameters()
|
||||
{
|
||||
UserAgent = userAgent,
|
||||
ConnectCancellation = handshakeTimeout.Token,
|
||||
IsRelay = true
|
||||
});
|
||||
connected = true;
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: TCP Connection succeed, handshaking...");
|
||||
node.VersionHandshake(handshakeTimeout.Token);
|
||||
handshaked = true;
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Handshaked");
|
||||
var loadChainTimeout = _Network.NBitcoinNetwork.ChainName == ChainName.Regtest ? TimeSpan.FromSeconds(5) : _Network.ChainCacheLoadingTimeout;
|
||||
if (_Chain.Height < 5)
|
||||
loadChainTimeout = TimeSpan.FromDays(7); // unlimited
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Loading chain from node");
|
||||
try
|
||||
{
|
||||
using (var cts1 = CancellationTokenSource.CreateLinkedTokenSource(cancellation))
|
||||
{
|
||||
cts1.CancelAfter(loadChainTimeout);
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Loading chain...");
|
||||
node.SynchronizeSlimChain(_Chain, cancellationToken: cts1.Token);
|
||||
}
|
||||
}
|
||||
catch when (!cancellation.IsCancellationRequested) // Timeout happens with SynchronizeChain, if so, throw away the cached chain
|
||||
{
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Failed to load chain before timeout, let's try again without the chain cache...");
|
||||
_Chain.ResetToGenesis();
|
||||
node.SynchronizeSlimChain(_Chain, cancellationToken: cancellation);
|
||||
}
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Chain loaded");
|
||||
chainLoaded = true;
|
||||
var peer = (await _OriginalRPC.GetPeersInfoAsync())
|
||||
.FirstOrDefault(p => p.SubVersion == userAgent);
|
||||
if (peer.IsWhitelisted())
|
||||
{
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: NBXplorer is correctly whitelisted by the node");
|
||||
}
|
||||
else
|
||||
{
|
||||
var addressStr = peer.Address is IPEndPoint end ? end.Address.ToString() : peer.Address?.ToString();
|
||||
Logs.Explorer.LogWarning($"{Network.CryptoCode}: Your NBXplorer server is not whitelisted by your node," +
|
||||
$" you should add \"whitelist={addressStr}\" to the configuration file of your node. (Or use whitebind)");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (!connected)
|
||||
{
|
||||
Logs.Explorer.LogWarning($"{Network.CryptoCode}: NBXplorer failed to connect to the node via P2P ({_ChainConfiguration.NodeEndpoint.ToEndpointString()}).{Environment.NewLine}" +
|
||||
$"It may come from: A firewall blocking the traffic, incorrect IP or port, or your node may not have an available connection slot. {Environment.NewLine}" +
|
||||
$"To make sure your node have an available connection slot, use \"whitebind\" or \"whitelist\" in your node configuration. (typically whitelist=127.0.0.1 if NBXplorer and the node are on the same machine.){Environment.NewLine}");
|
||||
}
|
||||
else if (!handshaked)
|
||||
{
|
||||
Logs.Explorer.LogWarning($"{Network.CryptoCode}: NBXplorer connected to the remote node but failed to handhsake via P2P.{Environment.NewLine}" +
|
||||
$"Your node may not have an available connection slot, or you may try to connect to the wrong node. (ie, trying to connect to a LTC node on the BTC configuration).{Environment.NewLine}" +
|
||||
$"To make sure your node have an available connection slot, use \"whitebind\" or \"whitelist\" in your node configuration. (typically whitelist=127.0.0.1 if NBXplorer and the node are on the same machine.){Environment.NewLine}");
|
||||
}
|
||||
else if (!chainLoaded)
|
||||
{
|
||||
Logs.Explorer.LogWarning($"{Network.CryptoCode}: NBXplorer connected and handshaked the remote node but failed to load the chain of header.{Environment.NewLine}" +
|
||||
$"Your connection may be throttled, or you may try to connect to the wrong node. (ie, trying to connect to a LTC node on the BTC configuration).{Environment.NewLine}");
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Height: " + _Chain.Height);
|
||||
if (_Configuration.CacheChain && heightBefore != _Chain.Height)
|
||||
{
|
||||
SaveChainInCache();
|
||||
}
|
||||
GC.Collect();
|
||||
node.Behaviors.Add(new SlimChainBehavior(_Chain));
|
||||
var explorer = (ExplorerBehavior)_ExplorerPrototype.Clone();
|
||||
node.Behaviors.Add(explorer);
|
||||
node.StateChanged += Node_StateChanged;
|
||||
_Node = node;
|
||||
}
|
||||
catch
|
||||
{
|
||||
EnsureNodeDisposed(node ?? _Node);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Node_StateChanged(Node node, NodeState oldState)
|
||||
{
|
||||
_Tick.Set();
|
||||
}
|
||||
|
||||
private void EnsureNodeDisposed(Node node = null)
|
||||
{
|
||||
node = node ?? _Node;
|
||||
if (node != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
node.StateChanged -= Node_StateChanged;
|
||||
node.DisconnectAsync();
|
||||
}
|
||||
catch { }
|
||||
node = null;
|
||||
_Node = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> HasBlock(RPCClient rpc, uint256 tip)
|
||||
{
|
||||
try
|
||||
{
|
||||
await rpc.GetBlockHeaderAsync(tip);
|
||||
return true;
|
||||
}
|
||||
catch (RPCException r) when (r.RPCCode == RPCErrorCode.RPC_METHOD_NOT_FOUND)
|
||||
{
|
||||
try
|
||||
{
|
||||
await rpc.GetBlockAsync(tip);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (RPCException r) when (r.RPCCode == RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY || r.RPCCode == RPCErrorCode.RPC_INVALID_PARAMETER)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveChainInCache()
|
||||
{
|
||||
var suffix = _Network.CryptoCode == "BTC" ? "" : _Network.CryptoCode;
|
||||
var cachePath = Path.Combine(_Configuration.DataDir, $"{suffix}chain-slim.dat");
|
||||
var cachePathTemp = Path.Combine(_Configuration.DataDir, $"{suffix}chain-slim.dat.temp");
|
||||
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Saving chain to cache...");
|
||||
using (var fs = new FileStream(cachePathTemp, FileMode.Create, FileAccess.Write, FileShare.None, 1024 * 1024))
|
||||
{
|
||||
_Chain.Save(fs);
|
||||
fs.Flush();
|
||||
}
|
||||
|
||||
if (File.Exists(cachePath))
|
||||
File.Delete(cachePath);
|
||||
File.Move(cachePathTemp, cachePath);
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Chain cached");
|
||||
}
|
||||
|
||||
private void LoadChainFromCache()
|
||||
{
|
||||
var suffix = _Network.CryptoCode == "BTC" ? "" : _Network.CryptoCode;
|
||||
{
|
||||
var legacyCachePath = Path.Combine(_Configuration.DataDir, $"{suffix}chain.dat");
|
||||
if (_Configuration.CacheChain && File.Exists(legacyCachePath))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Loading chain from cache...");
|
||||
var chain = new ConcurrentChain(_Network.NBitcoinNetwork);
|
||||
chain.Load(File.ReadAllBytes(legacyCachePath), _Network.NBitcoinNetwork);
|
||||
LoadSlimAndSaveToSlimFormat(chain);
|
||||
File.Delete(legacyCachePath);
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Height: " + _Chain.Height);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var cachePath = Path.Combine(_Configuration.DataDir, $"{suffix}chain-stripped.dat");
|
||||
if (_Configuration.CacheChain && File.Exists(cachePath))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Loading chain from cache...");
|
||||
var chain = new ConcurrentChain(_Network.NBitcoinNetwork);
|
||||
chain.Load(File.ReadAllBytes(cachePath), _Network.NBitcoinNetwork, new ConcurrentChain.ChainSerializationFormat()
|
||||
{
|
||||
SerializeBlockHeader = false,
|
||||
SerializePrecomputedBlockHash = true,
|
||||
});
|
||||
LoadSlimAndSaveToSlimFormat(chain);
|
||||
File.Delete(cachePath);
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Height: " + _Chain.Height);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var slimCachePath = Path.Combine(_Configuration.DataDir, $"{suffix}chain-slim.dat");
|
||||
if (_Configuration.CacheChain && File.Exists(slimCachePath))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Loading chain from cache...");
|
||||
using (var file = new FileStream(slimCachePath, FileMode.Open, FileAccess.Read, FileShare.None, 1024 * 1024))
|
||||
{
|
||||
_Chain.Load(file);
|
||||
}
|
||||
Logs.Configuration.LogInformation($"{_Network.CryptoCode}: Height: " + _Chain.Height);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSlimAndSaveToSlimFormat(ConcurrentChain chain)
|
||||
{
|
||||
foreach (var block in chain.ToEnumerable(false))
|
||||
{
|
||||
_Chain.TrySetTip(block.HashBlock, block.Previous?.HashBlock);
|
||||
}
|
||||
SaveChainInCache();
|
||||
}
|
||||
|
||||
public async Task SaveMatches(Transaction transaction)
|
||||
{
|
||||
var explorerBehavior = GetExplorerBehavior();
|
||||
if (explorerBehavior is null)
|
||||
return;
|
||||
await explorerBehavior.SaveMatches(transaction, false);
|
||||
}
|
||||
|
||||
public RPCClient GetConnectedClient()
|
||||
{
|
||||
if (!RPCAvailable)
|
||||
return null;
|
||||
return RPC;
|
||||
}
|
||||
|
||||
bool _Disposed = false;
|
||||
|
||||
|
||||
public GetNetworkInfoResponse NetworkInfo { get; internal set; }
|
||||
|
||||
public long? SyncHeight
|
||||
{
|
||||
get
|
||||
{
|
||||
var loc = GetLocation();
|
||||
if (loc is null)
|
||||
return null;
|
||||
return _Chain.FindFork(loc)?.Height;
|
||||
}
|
||||
}
|
||||
|
||||
public ILogger Logger { get; }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,106 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBitcoin;
|
||||
using NBitcoin.Protocol;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace NBXplorer.Backends.DBTrie
|
||||
{
|
||||
public class BlockDownloader : IDisposable
|
||||
{
|
||||
private SlimChain chain;
|
||||
private Node node;
|
||||
|
||||
public BlockDownloader(SlimChain chain, Node node)
|
||||
{
|
||||
this.chain = chain;
|
||||
this.node = node;
|
||||
node.StateChanged += Node_StateChanged;
|
||||
node.MessageReceived += Node_MessageReceived;
|
||||
}
|
||||
Channel<Block> blocks = Channel.CreateUnbounded<Block>();
|
||||
private void Node_MessageReceived(Node node, IncomingMessage message)
|
||||
{
|
||||
if (message.Message.Payload is BlockPayload b)
|
||||
blocks.Writer.TryWrite(b.Object);
|
||||
}
|
||||
|
||||
private void Node_StateChanged(Node node, NodeState oldState)
|
||||
{
|
||||
if (node.State != NodeState.HandShaked)
|
||||
blocks.Writer.TryComplete();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
node.StateChanged -= Node_StateChanged;
|
||||
node.MessageReceived -= Node_MessageReceived;
|
||||
blocks.Writer.TryComplete();
|
||||
}
|
||||
|
||||
const int maxinflight = 5;
|
||||
internal async IAsyncEnumerable<Block> DownloadBlocks(BlockLocator fork, [EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var hashes in EnumerateToTip(fork, chain).Select(c => c.Hash).Batch(maxinflight))
|
||||
{
|
||||
Dictionary<uint256, Block> outoforder = new Dictionary<uint256, Block>();
|
||||
var hashesEnum = hashes.GetEnumerator();
|
||||
if (!hashesEnum.MoveNext())
|
||||
yield break;
|
||||
await node.SendMessageAsync(new GetDataPayload(hashes.Select(h => new InventoryVector(node.AddSupportedOptions(InventoryType.MSG_BLOCK), h)).ToArray()));
|
||||
|
||||
|
||||
await foreach (var block in blocks.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
var blockHash = block.Header.GetHash();
|
||||
if (blockHash == hashesEnum.Current)
|
||||
{
|
||||
yield return block;
|
||||
if (!hashesEnum.MoveNext())
|
||||
break;
|
||||
while (outoforder.TryGetValue(hashesEnum.Current, out var block2))
|
||||
{
|
||||
yield return block2;
|
||||
outoforder.Remove(hashesEnum.Current);
|
||||
if (!hashesEnum.MoveNext())
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outoforder.TryAdd(blockHash, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private IEnumerable<SlimChainedBlock> EnumerateToTip(BlockLocator fork, SlimChain chain)
|
||||
{
|
||||
var bh = chain.FindFork(fork);
|
||||
if (bh is null)
|
||||
throw new InvalidOperationException("No fork found with the chain");
|
||||
int height = bh.Height + 1;
|
||||
var prev = bh.Hash;
|
||||
while (true)
|
||||
{
|
||||
bh = chain.GetBlock(height);
|
||||
if (bh is null)
|
||||
yield break;
|
||||
if (bh.Previous != prev)
|
||||
yield break;
|
||||
|
||||
yield return bh;
|
||||
|
||||
height = bh.Height + 1;
|
||||
prev = bh.Hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,31 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBitcoin;
|
||||
using NBXplorer.Configuration;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer.Backends.DBTrie
|
||||
{
|
||||
public class ChainProvider
|
||||
{
|
||||
Dictionary<string, SlimChain> _Chains = new Dictionary<string, SlimChain>();
|
||||
public ChainProvider(ExplorerConfiguration configuration)
|
||||
{
|
||||
foreach(var net in configuration.NetworkProvider.GetAll().Where(n => configuration.Supports(n)))
|
||||
{
|
||||
_Chains.Add(net.CryptoCode, new SlimChain(net.NBitcoinNetwork.GenesisHash));
|
||||
}
|
||||
}
|
||||
|
||||
public SlimChain GetChain(NBXplorerNetwork network)
|
||||
{
|
||||
return GetChain(network.CryptoCode);
|
||||
}
|
||||
public SlimChain GetChain(string network)
|
||||
{
|
||||
_Chains.TryGetValue(network, out SlimChain concurrent);
|
||||
return concurrent;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,340 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBXplorer.Logging;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Protocol;
|
||||
using NBitcoin.Protocol.Behaviors;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace NBXplorer.Backends.DBTrie
|
||||
{
|
||||
public class FullySynchedEvent
|
||||
{
|
||||
public FullySynchedEvent(NBXplorerNetwork network)
|
||||
{
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public NBXplorerNetwork Network { get; }
|
||||
}
|
||||
public class ExplorerBehavior : NodeBehavior
|
||||
{
|
||||
public ExplorerBehavior(Repository repo, SlimChain chain, AddressPoolService addressPoolService, EventAggregator eventAggregator)
|
||||
{
|
||||
if (repo == null)
|
||||
throw new ArgumentNullException(nameof(repo));
|
||||
if (chain == null)
|
||||
throw new ArgumentNullException(nameof(chain));
|
||||
if (addressPoolService == null)
|
||||
throw new ArgumentNullException(nameof(addressPoolService));
|
||||
_Chain = chain;
|
||||
AddressPoolService = addressPoolService;
|
||||
_Repository = repo;
|
||||
_EventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
EventAggregator _EventAggregator;
|
||||
|
||||
Repository Repository
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Repository;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly SlimChain _Chain;
|
||||
private readonly Repository _Repository;
|
||||
|
||||
public SlimChain Chain
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Chain;
|
||||
}
|
||||
}
|
||||
public int StartHeight
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public override object Clone()
|
||||
{
|
||||
return new ExplorerBehavior(_Repository, _Chain, AddressPoolService, _EventAggregator) { StartHeight = StartHeight };
|
||||
}
|
||||
|
||||
public BlockLocator CurrentLocation { get; private set; }
|
||||
|
||||
protected override void AttachCore()
|
||||
{
|
||||
AttachedNode.StateChanged += AttachedNode_StateChanged;
|
||||
AttachedNode.MessageReceived += AttachedNode_MessageReceived;
|
||||
if (AttachedNode.State == NodeState.HandShaked)
|
||||
NodeHandshaked(AttachedNode);
|
||||
}
|
||||
|
||||
private BlockLocator GetDefaultCurrentLocation()
|
||||
{
|
||||
if (StartHeight > Chain.Height)
|
||||
throw new InvalidOperationException($"{Network.CryptoCode}: StartHeight should not be above the current tip");
|
||||
|
||||
BlockLocator blockLocator = null;
|
||||
if (StartHeight == -1)
|
||||
{
|
||||
blockLocator = Chain.GetTipLocator();
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Current Index Progress not found, start syncing from the header's chain tip (At height: {Chain.Height})");
|
||||
}
|
||||
else
|
||||
{
|
||||
blockLocator = Chain.GetLocator(StartHeight);
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Current Index Progress not found, start syncing at height {Chain.Height}");
|
||||
}
|
||||
return blockLocator;
|
||||
}
|
||||
|
||||
public NBXplorerNetwork Network
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Repository.Network;
|
||||
}
|
||||
}
|
||||
|
||||
public AddressPoolService AddressPoolService
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
protected override void DetachCore()
|
||||
{
|
||||
AttachedNode.StateChanged -= AttachedNode_StateChanged;
|
||||
AttachedNode.MessageReceived -= AttachedNode_MessageReceived;
|
||||
_Cts.Cancel();
|
||||
}
|
||||
private void AttachedNode_MessageReceived(Node node, IncomingMessage message)
|
||||
{
|
||||
if (message.Message.Payload is InvPayload invs)
|
||||
{
|
||||
// Do not asks transactions if we are synching so that we can process blocks faster
|
||||
if (IsSynching())
|
||||
return;
|
||||
var data = new GetDataPayload();
|
||||
foreach (var inv in invs.Inventory.Where(t => t.Type.HasFlag(InventoryType.MSG_TX)))
|
||||
{
|
||||
inv.Type = node.AddSupportedOptions(inv.Type);
|
||||
data.Inventory.Add(inv);
|
||||
}
|
||||
if (data.Inventory.Count != 0)
|
||||
node.SendMessageAsync(data);
|
||||
}
|
||||
else if (message.Message.Payload is HeadersPayload headers)
|
||||
{
|
||||
if (headers.Headers.Count == 0)
|
||||
return;
|
||||
_NewBlock.Set();
|
||||
}
|
||||
else if (message.Message.Payload is TxPayload txPayload)
|
||||
{
|
||||
Run(() => SaveMatches(txPayload.Object, true));
|
||||
}
|
||||
}
|
||||
Task Run(Func<Task> act)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await act();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Explorer.LogError($"{Network.CryptoCode}: Unhandled error while treating a message");
|
||||
Logs.Explorer.LogError(ex.ToString());
|
||||
this.AttachedNode?.DisconnectAsync($"{Network.CryptoCode}: Unhandled error while treating a message", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
private async Task SaveMatches(Block block)
|
||||
{
|
||||
block.Header.PrecomputeHash(false, false);
|
||||
foreach (var tx in block.Transactions)
|
||||
tx.PrecomputeHash(false, true);
|
||||
|
||||
var blockHash = block.GetHash();
|
||||
var delay = TimeSpan.FromSeconds(1);
|
||||
retry:
|
||||
try
|
||||
{
|
||||
var slimBlockHeader = Chain.GetBlock(blockHash);
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
var matches =
|
||||
(await Repository.GetMatches(block, slimBlockHeader, now, true))
|
||||
.ToArray();
|
||||
await SaveMatches(matches, slimBlockHeader, now, true);
|
||||
if (slimBlockHeader != null)
|
||||
{
|
||||
var blockEvent = new Models.NewBlockEvent()
|
||||
{
|
||||
CryptoCode = _Repository.Network.CryptoCode,
|
||||
Hash = blockHash,
|
||||
Height = slimBlockHeader.Height,
|
||||
PreviousBlockHash = slimBlockHeader.Previous
|
||||
};
|
||||
await Repository.SaveEvent(blockEvent);
|
||||
_EventAggregator.Publish(blockEvent);
|
||||
_EventAggregator.Publish(new RawBlockEvent(block, this.Network), true);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Explorer.LogWarning(ex, $"{Network.CryptoCode}: Error while saving block in database, retrying in {delay.TotalSeconds} seconds ({ex.Message})");
|
||||
await Task.Delay(delay, _Cts.Token);
|
||||
delay = delay * 2;
|
||||
var maxDelay = TimeSpan.FromSeconds(60);
|
||||
if (delay > maxDelay)
|
||||
delay = maxDelay;
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
internal async Task SaveMatches(Transaction transaction, bool fireEvents)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var matches = (await Repository.GetMatches(transaction, null, now, false)).ToArray();
|
||||
await SaveMatches(matches, null, now, fireEvents);
|
||||
}
|
||||
private async Task SaveMatches(TrackedTransaction[] matches, SlimChainedBlock slimBlock, DateTimeOffset now, bool fireEvents)
|
||||
{
|
||||
await Repository.SaveMatches(matches);
|
||||
_ = AddressPoolService.GenerateAddresses(Network, matches);
|
||||
var saved = await Repository.SaveTransactions(now, matches.Select(m => m.Transaction).Distinct().ToArray(), slimBlock);
|
||||
var savedTransactions = saved.ToDictionary(s => s.Transaction.GetHash());
|
||||
|
||||
int? maybeHeight = null;
|
||||
var chainHeight = Chain.Height;
|
||||
if (fireEvents)
|
||||
{
|
||||
Task[] saving = new Task[matches.Length];
|
||||
for (int i = 0; i < matches.Length; i++)
|
||||
{
|
||||
var txEvt = new Models.NewTransactionEvent()
|
||||
{
|
||||
TrackedSource = matches[i].TrackedSource,
|
||||
DerivationStrategy = (matches[i].TrackedSource is DerivationSchemeTrackedSource dsts) ? dsts.DerivationStrategy : null,
|
||||
CryptoCode = Network.CryptoCode,
|
||||
BlockId = slimBlock?.Hash,
|
||||
TransactionData = new TransactionResult()
|
||||
{
|
||||
BlockId = slimBlock?.Hash,
|
||||
Height = maybeHeight,
|
||||
Confirmations = maybeHeight == null ? 0 : chainHeight - maybeHeight.Value + 1,
|
||||
Timestamp = now,
|
||||
Transaction = matches[i].Transaction,
|
||||
TransactionHash = matches[i].TransactionHash
|
||||
},
|
||||
Outputs = matches[i].GetReceivedOutputs().ToList()
|
||||
};
|
||||
|
||||
saving[i] = Repository.SaveEvent(txEvt);
|
||||
_EventAggregator.Publish(txEvt);
|
||||
}
|
||||
await Task.WhenAll(saving);
|
||||
}
|
||||
}
|
||||
public bool IsSynching()
|
||||
{
|
||||
var location = CurrentLocation;
|
||||
if (location == null)
|
||||
return true;
|
||||
var fork = Chain.FindFork(location);
|
||||
return Chain.Height - fork.Height > 10;
|
||||
}
|
||||
|
||||
private void AttachedNode_StateChanged(Node node, NodeState oldState)
|
||||
{
|
||||
if (node.State == NodeState.HandShaked)
|
||||
{
|
||||
NodeHandshaked(node);
|
||||
}
|
||||
if (node.State == NodeState.Offline)
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Closed connection with node");
|
||||
if (node.State == NodeState.Failed)
|
||||
Logs.Explorer.LogError($"{Network.CryptoCode}: Connection unexpectedly failed: {node.DisconnectReason.Reason}");
|
||||
}
|
||||
|
||||
Task _BlockLoop;
|
||||
private void NodeHandshaked(Node node)
|
||||
{
|
||||
if (_BlockLoop != null)
|
||||
return;
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Handshaked node");
|
||||
_BlockLoop = IndexBlockLoop(node, _Cts.Token);
|
||||
}
|
||||
|
||||
Signaler _NewBlock = new Signaler();
|
||||
private async Task IndexBlockLoop(Node node, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
CurrentLocation = await Repository.GetIndexProgress();
|
||||
if (CurrentLocation is null)
|
||||
{
|
||||
CurrentLocation = GetDefaultCurrentLocation();
|
||||
}
|
||||
var fork = Chain.FindFork(CurrentLocation);
|
||||
if (fork == null)
|
||||
{
|
||||
CurrentLocation = GetDefaultCurrentLocation();
|
||||
fork = Chain.FindFork(CurrentLocation);
|
||||
}
|
||||
Logs.Explorer.LogInformation($"{Network.CryptoCode}: Starting scan at block " + fork.Height);
|
||||
|
||||
var downloader = new BlockDownloader(Chain, node);
|
||||
|
||||
while (true)
|
||||
{
|
||||
int downloaded = 0;
|
||||
Block lastBlock = null;
|
||||
await foreach (var block in downloader.DownloadBlocks(CurrentLocation, cancellationToken))
|
||||
{
|
||||
await SaveMatches(block);
|
||||
downloaded++;
|
||||
if (downloaded % 5 == 0)
|
||||
{
|
||||
CurrentLocation = Chain.GetLocator(block.Header.GetHash()) ?? CurrentLocation;
|
||||
await Repository.SetIndexProgress(CurrentLocation);
|
||||
}
|
||||
lastBlock = block;
|
||||
}
|
||||
if (lastBlock != null)
|
||||
{
|
||||
CurrentLocation = Chain.GetLocator(lastBlock.Header.GetHash()) ?? CurrentLocation;
|
||||
await Repository.SetIndexProgress(CurrentLocation);
|
||||
}
|
||||
if (CurrentLocation.Blocks.Count > 0 && CurrentLocation.Blocks[0] == Chain.TipBlock.Hash)
|
||||
_EventAggregator.Publish(new FullySynchedEvent(Network), true);
|
||||
await _NewBlock.Wait(cancellationToken);
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logs.Explorer.LogError($"{Network.CryptoCode}: Unhandled error in IndexBlockLoop");
|
||||
Logs.Explorer.LogError(ex.ToString());
|
||||
node.DisconnectAsync($"{Network.CryptoCode}: Unhandled error in IndexBlockLoop", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,210 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
extern alias DBTrieLib;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using NBXplorer.Altcoins.Liquid;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace NBXplorer.Backends.DBTrie
|
||||
{
|
||||
public class LiquidRepository : Repository
|
||||
{
|
||||
private readonly RPCClient _rpcClient;
|
||||
|
||||
internal LiquidRepository(DBTrieLib.DBTrie.DBTrieEngine engine, NBXplorerNetwork network, KeyPathTemplates keyPathTemplates,
|
||||
RPCClient rpcClient, SlimChain headerChain) : base(engine, network, keyPathTemplates, rpcClient, headerChain)
|
||||
{
|
||||
_rpcClient = rpcClient;
|
||||
}
|
||||
|
||||
class ElementsTrackedTransaction : TrackedTransaction
|
||||
{
|
||||
public ElementsTrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, IEnumerable<Coin> receivedCoins, Dictionary<Script, KeyPath> knownScriptMapping) :
|
||||
base(key, trackedSource, receivedCoins, knownScriptMapping)
|
||||
{
|
||||
ClearCoinValues();
|
||||
Unblind(receivedCoins, false);
|
||||
}
|
||||
public ElementsTrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, Transaction transaction, Dictionary<Script, KeyPath> knownScriptMapping) :
|
||||
base(key, trackedSource, transaction, knownScriptMapping)
|
||||
{
|
||||
ClearCoinValues();
|
||||
Unblind(transaction.Outputs.AsCoins(), false);
|
||||
}
|
||||
|
||||
private void ClearCoinValues()
|
||||
{
|
||||
ReceivedCoins = ReceivedCoins.Select(coin => (ICoin) new Coin(coin.Outpoint.Clone(), coin.TxOut.Clone())).ToHashSet();
|
||||
foreach (var coin in ReceivedCoins.OfType<Coin>())
|
||||
{
|
||||
coin.Amount = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override ITrackedTransactionSerializable CreateBitcoinSerializable()
|
||||
{
|
||||
return new ElementsTransactionMatchData(this);
|
||||
}
|
||||
|
||||
public void Unblind(ElementsTransaction unblindedTransaction, bool saveUnblindData)
|
||||
{
|
||||
Unblind(unblindedTransaction.Outputs.AsCoins(), saveUnblindData);
|
||||
}
|
||||
|
||||
public void Unblind(IEnumerable<ICoin> unblindedCoins, bool saveUnblindData)
|
||||
{
|
||||
foreach (var coin in unblindedCoins)
|
||||
{
|
||||
AssetMoney assetMoney = null;
|
||||
if (coin is AssetCoin assetCoin)
|
||||
{
|
||||
assetMoney = assetCoin.Money;
|
||||
}
|
||||
if (coin.TxOut is ElementsTxOut elementsTxOut &&
|
||||
elementsTxOut.Asset.AssetId != null &&
|
||||
elementsTxOut.Value != null)
|
||||
{
|
||||
assetMoney = new AssetMoney(elementsTxOut.Asset.AssetId, elementsTxOut.Value.Satoshi);
|
||||
}
|
||||
if (assetMoney != null &&
|
||||
TryGetReceivedCoinByIndex((int)coin.Outpoint.N) is Coin existingCoin)
|
||||
{
|
||||
if (saveUnblindData)
|
||||
Unblinded.TryAdd((int)existingCoin.Outpoint.N, assetMoney);
|
||||
this.ReceivedCoins.Remove(existingCoin);
|
||||
this.ReceivedCoins.Add(new AssetCoin(assetMoney, existingCoin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ICoin TryGetReceivedCoinByIndex(int index)
|
||||
{
|
||||
return this.ReceivedCoins.FirstOrDefault(r => r.Outpoint.N == index);
|
||||
}
|
||||
public void Unblind(IEnumerable<ElementsTransactionMatchData.UnblindData> unblindData)
|
||||
{
|
||||
foreach (var unblind in unblindData)
|
||||
{
|
||||
if (TryGetReceivedCoinByIndex(unblind.Index) is Coin existingCoin)
|
||||
{
|
||||
this.ReceivedCoins.Remove(existingCoin);
|
||||
var money = new AssetMoney(unblind.AssetId, unblind.Value);
|
||||
this.ReceivedCoins.Add(new AssetCoin(money, existingCoin));
|
||||
this.Unblinded.Add(unblind.Index, money);
|
||||
}
|
||||
}
|
||||
}
|
||||
public Dictionary<int, AssetMoney> Unblinded = new Dictionary<int, AssetMoney>();
|
||||
}
|
||||
class ElementsTransactionMatchData : TrackedTransaction.TransactionMatchData
|
||||
{
|
||||
internal class UnblindData : IBitcoinSerializable
|
||||
{
|
||||
|
||||
long _Value;
|
||||
public long Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
uint256 _AssetId;
|
||||
public uint256 AssetId
|
||||
{
|
||||
get
|
||||
{
|
||||
return _AssetId;
|
||||
}
|
||||
set
|
||||
{
|
||||
_AssetId = value;
|
||||
}
|
||||
}
|
||||
|
||||
int _Index;
|
||||
public int Index
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Index;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Index = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadWrite(BitcoinStream stream)
|
||||
{
|
||||
stream.ReadWrite(ref _Index);
|
||||
stream.ReadWrite(ref _AssetId);
|
||||
stream.ReadWrite(ref _Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<UnblindData> _UnblindData = new List<UnblindData>();
|
||||
internal List<UnblindData> Unblind => _UnblindData;
|
||||
|
||||
public ElementsTransactionMatchData(TrackedTransactionKey key) : base(key)
|
||||
{
|
||||
|
||||
}
|
||||
public ElementsTransactionMatchData(ElementsTrackedTransaction trackedTransaction) : base(trackedTransaction)
|
||||
{
|
||||
foreach (var unblind in trackedTransaction.Unblinded)
|
||||
_UnblindData.Add(new UnblindData() { Index = unblind.Key, AssetId = unblind.Value.AssetId, Value = unblind.Value.Quantity });
|
||||
}
|
||||
|
||||
public override void ReadWrite(BitcoinStream stream)
|
||||
{
|
||||
base.ReadWrite(stream);
|
||||
stream.ReadWrite(ref _UnblindData);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task AfterMatch(TrackedTransaction tx, IReadOnlyCollection<KeyPathInformation> keyInfos)
|
||||
{
|
||||
await base.AfterMatch(tx, keyInfos);
|
||||
|
||||
if (tx is ElementsTrackedTransaction etx)
|
||||
{
|
||||
var unblinded = await _rpcClient.UnblindTransaction(tx, keyInfos);
|
||||
if (unblinded != null)
|
||||
{
|
||||
etx.Unblind(unblinded, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, Transaction tx, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new ElementsTrackedTransaction(transactionKey, trackedSource, tx, knownScriptMapping);
|
||||
}
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, IEnumerable<Coin> coins, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new ElementsTrackedTransaction(transactionKey, trackedSource, coins, knownScriptMapping);
|
||||
}
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, ITrackedTransactionSerializable tx)
|
||||
{
|
||||
var trackedTransaction = (ElementsTrackedTransaction)base.CreateTrackedTransaction(trackedSource, tx);
|
||||
trackedTransaction.Unblind(((ElementsTransactionMatchData)tx).Unblind);
|
||||
return trackedTransaction;
|
||||
}
|
||||
internal override ITrackedTransactionSerializable CreateBitcoinSerializableTrackedTransaction(TrackedTransactionKey trackedTransactionKey)
|
||||
{
|
||||
return new ElementsTransactionMatchData(trackedTransactionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends
|
||||
{
|
||||
public interface IIndexers
|
||||
{
|
||||
IIndexer GetIndexer(NBXplorerNetwork network);
|
||||
IEnumerable<IIndexer> All();
|
||||
}
|
||||
public enum BitcoinDWaiterState
|
||||
{
|
||||
NotStarted,
|
||||
CoreSynching,
|
||||
NBXplorerSynching,
|
||||
Ready
|
||||
}
|
||||
public interface IIndexer
|
||||
{
|
||||
RPCClient GetConnectedClient();
|
||||
NBXplorerNetwork Network { get; }
|
||||
BitcoinDWaiterState State { get; }
|
||||
long? SyncHeight { get; }
|
||||
GetNetworkInfoResponse NetworkInfo { get; }
|
||||
Task SaveMatches(Transaction transaction);
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends
|
||||
{
|
||||
public interface IRepository
|
||||
{
|
||||
int BatchSize { get; set; }
|
||||
int MaxPoolSize { get; set; }
|
||||
int MinPoolSize { get; set; }
|
||||
Money MinUtxoValue { get; set; }
|
||||
NBXplorerNetwork Network { get; }
|
||||
Serializer Serializer { get; }
|
||||
Task Prune(TrackedSource trackedSource, IEnumerable<TrackedTransaction> prunable);
|
||||
Task UpdateAddressPool(DerivationSchemeTrackedSource trackedSource, Dictionary<DerivationFeature, int?> highestKeyIndexFound);
|
||||
Task CancelReservation(DerivationStrategyBase strategy, KeyPath[] keyPaths);
|
||||
TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, IEnumerable<Coin> coins, Dictionary<Script, KeyPath> knownScriptMapping);
|
||||
TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, Transaction tx, Dictionary<Script, KeyPath> knownScriptMapping);
|
||||
ValueTask<int> DefragmentTables(CancellationToken cancellationToken = default);
|
||||
Task<int> GenerateAddresses(DerivationStrategyBase strategy, DerivationFeature derivationFeature, GenerateAddressQuery query = null);
|
||||
Task<int> GenerateAddresses(DerivationStrategyBase strategy, DerivationFeature derivationFeature, int maxAddresses);
|
||||
Task<IList<NewEventBase>> GetEvents(long lastEventId, int? limit = null);
|
||||
Task<BlockLocator> GetIndexProgress();
|
||||
Task<MultiValueDictionary<Script, KeyPathInformation>> GetKeyInformations(IList<Script> scripts);
|
||||
Task<IList<NewEventBase>> GetLatestEvents(int limit = 10);
|
||||
Task<TrackedTransaction[]> GetMatches(Block block, SlimChainedBlock slimBlock, DateTimeOffset now, bool useCache);
|
||||
Task<TrackedTransaction[]> GetMatches(IList<Transaction> txs, SlimChainedBlock slimBlock, DateTimeOffset now, bool useCache);
|
||||
Task<TrackedTransaction[]> GetMatches(Transaction tx, SlimChainedBlock slimBlock, DateTimeOffset now, bool useCache);
|
||||
Task<TMetadata> GetMetadata<TMetadata>(TrackedSource source, string key) where TMetadata : class;
|
||||
Task<Dictionary<OutPoint, TxOut>> GetOutPointToTxOut(IList<OutPoint> outPoints);
|
||||
Task<SavedTransaction[]> GetSavedTransactions(uint256 txid);
|
||||
Task<TrackedTransaction[]> GetTransactions(TrackedSource trackedSource, uint256 txId = null, bool needTx = true, CancellationToken cancellation = default);
|
||||
Task<KeyPathInformation> GetUnused(DerivationStrategyBase strategy, DerivationFeature derivationFeature, int n, bool reserve);
|
||||
#if SUPPORT_DBTRIE
|
||||
ValueTask<bool> MigrateOutPoints(string directory, CancellationToken cancellationToken = default);
|
||||
ValueTask<int> MigrateSavedTransactions(CancellationToken cancellationToken = default);
|
||||
#endif
|
||||
Task Ping();
|
||||
Task<long> SaveEvent(NewEventBase evt);
|
||||
Task SaveKeyInformations(KeyPathInformation[] keyPathInformations);
|
||||
Task SaveMatches(TrackedTransaction[] transactions);
|
||||
Task SaveMetadata<TMetadata>(TrackedSource source, string key, TMetadata value) where TMetadata : class;
|
||||
Task<List<SavedTransaction>> SaveTransactions(DateTimeOffset now, Transaction[] transactions, SlimChainedBlock slimBlock);
|
||||
Task SetIndexProgress(BlockLocator locator);
|
||||
Task Track(IDestination address);
|
||||
ValueTask<int> TrimmingEvents(int maxEvents, CancellationToken cancellationToken = default);
|
||||
Task<SlimChainedBlock> GetTip();
|
||||
Task SaveBlocks(IList<SlimChainedBlock> slimBlocks);
|
||||
Task EnsureWalletCreated(DerivationStrategyBase derivation);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends
|
||||
{
|
||||
public interface IRepositoryProvider : IHostedService
|
||||
{
|
||||
Task StartCompletion { get; }
|
||||
|
||||
IRepository GetRepository(NBXplorerNetwork network);
|
||||
IRepository GetRepository(string cryptoCode);
|
||||
}
|
||||
}
|
||||
@ -1,679 +0,0 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Protocol;
|
||||
using NBitcoin.Protocol.Behaviors;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.Events;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
public class PostgresIndexers : IHostedService, IIndexers
|
||||
{
|
||||
class PostgresIndexer : IIndexer
|
||||
{
|
||||
public PostgresIndexer(
|
||||
AddressPoolService addressPoolService,
|
||||
ILogger logger,
|
||||
NBXplorerNetwork network,
|
||||
RPCClient rpcClient,
|
||||
PostgresRepository repository,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ExplorerConfiguration explorerConfiguration,
|
||||
ChainConfiguration chainConfiguration,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
AddressPoolService = addressPoolService;
|
||||
Logger = logger;
|
||||
this.network = network;
|
||||
RPCClient = rpcClient;
|
||||
Repository = repository;
|
||||
ConnectionFactory = connectionFactory;
|
||||
ExplorerConfiguration = explorerConfiguration;
|
||||
ChainConfiguration = chainConfiguration;
|
||||
EventAggregator = eventAggregator;
|
||||
}
|
||||
CancellationTokenSource cts;
|
||||
Task _indexerLoop;
|
||||
Node _Node;
|
||||
Channel<object> _Channel = Channel.CreateUnbounded<object>();
|
||||
Channel<Block> _DownloadedBlocks = Channel.CreateUnbounded<Block>();
|
||||
async Task IndexerLoop()
|
||||
{
|
||||
TimeSpan retryDelay = TimeSpan.FromSeconds(0);
|
||||
retry:
|
||||
try
|
||||
{
|
||||
await IndexerLoopCore(cts.Token);
|
||||
}
|
||||
catch when (cts.Token.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, $"Unhandled exception in the indexer, retrying in {retryDelay.TotalSeconds} seconds");
|
||||
try
|
||||
{
|
||||
await Task.Delay(retryDelay, cts.Token);
|
||||
}
|
||||
catch { }
|
||||
retryDelay += TimeSpan.FromSeconds(5.0);
|
||||
retryDelay = TimeSpan.FromTicks(Math.Min(retryDelay.Ticks, TimeSpan.FromMinutes(1.0).Ticks));
|
||||
goto retry;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task IndexerLoopCore(CancellationToken token)
|
||||
{
|
||||
await ConnectNode(token, true);
|
||||
await foreach (var item in _Channel.Reader.ReadAllAsync(token))
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network, b =>
|
||||
{
|
||||
b.NoResetOnClose = true;
|
||||
// It seems that when running a big rescan, the postgres connection process
|
||||
// is taking more and more RAM.
|
||||
// While I didn't find the source of the issue, disabling connection pooling
|
||||
// will force postgres to create a new connection process, freeing the memory.
|
||||
// Note that since PullBlocks are consolidated during rescans, it will only create
|
||||
// 1 connection every ~2000 blocks.
|
||||
b.Pooling = !(item is PullBlocks && State == BitcoinDWaiterState.NBXplorerSynching);
|
||||
});
|
||||
if (item is PullBlocks pb)
|
||||
{
|
||||
var headers = ConsolidatePullBlocks(_Channel.Reader, pb);
|
||||
using var pullBlockTimeout = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
pullBlockTimeout.CancelAfter(PullBlockTimeout);
|
||||
foreach (var batch in headers.Batch(maxinflight))
|
||||
{
|
||||
_ = _Node.SendMessageAsync(
|
||||
new GetDataPayload(
|
||||
batch.Select(b => new InventoryVector(_Node.AddSupportedOptions(InventoryType.MSG_BLOCK), b.GetHash())
|
||||
).ToArray()));
|
||||
var remaining = batch.Select(b => b.GetHash()).ToHashSet();
|
||||
List<Block> unorderedBlocks = new List<Block>();
|
||||
await foreach (var block in _DownloadedBlocks.Reader.ReadAllAsync(pullBlockTimeout.Token))
|
||||
{
|
||||
pullBlockTimeout.CancelAfter(PullBlockTimeout);
|
||||
if (!remaining.Remove(block.Header.GetHash()))
|
||||
continue;
|
||||
if (lastIndexedBlock is null || block.Header.HashPrevBlock == lastIndexedBlock.Hash)
|
||||
{
|
||||
SlimChainedBlock slimChainedBlock = lastIndexedBlock is null ?
|
||||
await RPCClient.GetBlockHeaderAsyncEx(block.Header.GetHash()) :
|
||||
new SlimChainedBlock(block.Header.GetHash(), lastIndexedBlock.Hash, lastIndexedBlock.Height + 1);
|
||||
await SaveMatches(conn, block, slimChainedBlock);
|
||||
}
|
||||
else
|
||||
{
|
||||
unorderedBlocks.Add(block);
|
||||
}
|
||||
if (remaining.Count == 0)
|
||||
{
|
||||
// There are two reasons to receive unordered blocks:
|
||||
// 1. There is a fork.
|
||||
// 2. Node decides to send headers without asking.
|
||||
if (unorderedBlocks.Count > 0)
|
||||
{
|
||||
Task<SlimChainedBlock>[] slimChainedBlocks = new Task<SlimChainedBlock>[unorderedBlocks.Count];
|
||||
var rpcBatch = RPCClient.PrepareBatch();
|
||||
for (int i = 0; i < unorderedBlocks.Count; i++)
|
||||
{
|
||||
slimChainedBlocks[i] = rpcBatch.GetBlockHeaderAsyncEx(unorderedBlocks[i].GetHash());
|
||||
}
|
||||
await rpcBatch.SendBatchAsync();
|
||||
// If there is a fork, we should index the unordered blocks
|
||||
bool unconfedBlocks = false;
|
||||
bool fork = await RPCClient.GetBlockHeaderAsyncEx(lastIndexedBlock.Hash) == null;
|
||||
foreach (var b in Enumerable.Zip(unorderedBlocks, slimChainedBlocks)
|
||||
.Where(b => fork || b.Second.Result.Height > lastIndexedBlock.Height)
|
||||
.OrderBy(b => b.Second.Result.Height)
|
||||
.ToList())
|
||||
{
|
||||
var slimBlock = await b.Second;
|
||||
if (fork && !unconfedBlocks)
|
||||
{
|
||||
await conn.MakeOrphanFrom(slimBlock.Height);
|
||||
unconfedBlocks = true;
|
||||
}
|
||||
await SaveMatches(conn, b.First, slimBlock);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
await SaveProgress(conn);
|
||||
await UpdateState();
|
||||
}
|
||||
await AskNextHeaders();
|
||||
}
|
||||
if (item is NodeDisconnected)
|
||||
{
|
||||
await ConnectNode(token, false);
|
||||
}
|
||||
if (item is Transaction tx)
|
||||
{
|
||||
var txs = PullTransactions(_Channel.Reader, tx);
|
||||
await SaveMatches(conn, txs, null, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to pull as much non-conflicting transactions as possible in one batch
|
||||
private List<Transaction> PullTransactions(ChannelReader<object> reader, Transaction tx)
|
||||
{
|
||||
List<Transaction> txs = new List<Transaction>();
|
||||
HashSet<OutPoint> spent = new HashSet<OutPoint>(tx.Inputs.Capacity);
|
||||
bool EnsureNoConflict(Transaction tx)
|
||||
{
|
||||
foreach (var i in tx.Inputs.Select(i => i.PrevOut))
|
||||
if (!spent.Add(i))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
EnsureNoConflict(tx);
|
||||
txs.Add(tx);
|
||||
|
||||
while (reader.TryPeek(out var p) && p is Transaction tx2)
|
||||
{
|
||||
if (!EnsureNoConflict(tx2))
|
||||
break;
|
||||
txs.Add(tx2);
|
||||
reader.TryRead(out _);
|
||||
}
|
||||
return txs;
|
||||
}
|
||||
|
||||
// We sometimes receive burst of blocks, with some dups.
|
||||
// This method will pump as much headers from the channel as possible, removing the dups
|
||||
// along the way.
|
||||
private IList<BlockHeader> ConsolidatePullBlocks(ChannelReader<object> reader, PullBlocks pb)
|
||||
{
|
||||
List<PullBlocks> requests = new List<PullBlocks>();
|
||||
requests.Add(pb);
|
||||
while (reader.TryPeek(out var p) && p is PullBlocks pb2)
|
||||
{
|
||||
reader.TryRead(out _);
|
||||
requests.Add(pb2);
|
||||
}
|
||||
|
||||
var headerCount = requests.Select(r => r.headers.Count).Sum();
|
||||
HashSet<uint256> blocks = new HashSet<uint256>(headerCount);
|
||||
List<BlockHeader> result = new List<BlockHeader>(headerCount);
|
||||
foreach (var h in requests.SelectMany(r => r.headers))
|
||||
{
|
||||
h.PrecomputeHash(false, true);
|
||||
if (blocks.Add(h.GetHash()))
|
||||
result.Add(h);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static TimeSpan PullBlockTimeout = TimeSpan.FromMinutes(1.0);
|
||||
|
||||
private async Task ConnectNode(CancellationToken token, bool forceRestart)
|
||||
{
|
||||
if (_Node is not null)
|
||||
{
|
||||
if (!forceRestart && _Node.State == NodeState.HandShaked)
|
||||
return;
|
||||
_Node.DisconnectAsync("Restarting");
|
||||
_Node = null;
|
||||
}
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
using (var handshakeTimeout = CancellationTokenSource.CreateLinkedTokenSource(token))
|
||||
{
|
||||
var userAgent = "NBXplorer-" + RandomUtils.GetInt64();
|
||||
var nodeParams = new NodeConnectionParameters()
|
||||
{
|
||||
UserAgent = userAgent,
|
||||
ConnectCancellation = handshakeTimeout.Token,
|
||||
IsRelay = true
|
||||
};
|
||||
if (ExplorerConfiguration.SocksEndpoint != null)
|
||||
{
|
||||
var socks = new SocksSettingsBehavior()
|
||||
{
|
||||
OnlyForOnionHosts = false,
|
||||
SocksEndpoint = ExplorerConfiguration.SocksEndpoint
|
||||
};
|
||||
if (ExplorerConfiguration.SocksCredentials != null)
|
||||
socks.NetworkCredential = ExplorerConfiguration.SocksCredentials;
|
||||
nodeParams.TemplateBehaviors.Add(socks);
|
||||
}
|
||||
var node = await Node.ConnectAsync(network.NBitcoinNetwork, ChainConfiguration.NodeEndpoint, nodeParams);
|
||||
Logger.LogInformation($"TCP Connection succeed, handshaking...");
|
||||
node.VersionHandshake(handshakeTimeout.Token);
|
||||
Logger.LogInformation($"Handshaked");
|
||||
await node.SendMessageAsync(new SendHeadersPayload());
|
||||
|
||||
await RPCArgs.TestRPCAsync(Network, RPCClient, token, Logger);
|
||||
if (await RPCClient.SupportTxIndex() is bool txIndex)
|
||||
{
|
||||
ChainConfiguration.HasTxIndex = txIndex;
|
||||
}
|
||||
if (ChainConfiguration.HasTxIndex)
|
||||
{
|
||||
Logger.LogInformation($"Has txindex support");
|
||||
}
|
||||
var peer = (await RPCClient.GetPeersInfoAsync())
|
||||
.FirstOrDefault(p => p.SubVersion == userAgent);
|
||||
if (peer.IsWhitelisted())
|
||||
{
|
||||
if (firstConnect)
|
||||
{
|
||||
firstConnect = false;
|
||||
}
|
||||
Logger.LogInformation($"NBXplorer is correctly whitelisted by the node");
|
||||
}
|
||||
else if (peer is null)
|
||||
{
|
||||
Logger.LogWarning($"{Network.CryptoCode}: The RPC server you are connecting to, doesn't seem to be the same server as the one providing the P2P connection. This is an untested setup and may have non-obvious side effects.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var addressStr = peer.Address is IPEndPoint end ? end.Address.ToString() : peer.Address?.ToString();
|
||||
Logger.LogWarning($"{Network.CryptoCode}: Your NBXplorer server is not whitelisted by your node," +
|
||||
$" you should add \"whitelist={addressStr}\" to the configuration file of your node. (Or use whitebind)");
|
||||
}
|
||||
|
||||
int waitTime = 10;
|
||||
|
||||
// Need NetworkInfo for the get status
|
||||
NetworkInfo = await RPCClient.GetNetworkInfoAsync();
|
||||
retry:
|
||||
BlockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
if (BlockchainInfo.IsSynching(Network))
|
||||
{
|
||||
State = BitcoinDWaiterState.CoreSynching;
|
||||
await Task.Delay(waitTime * 2, token);
|
||||
waitTime = Math.Min(5_000, waitTime * 2);
|
||||
goto retry;
|
||||
}
|
||||
await RPCClient.EnsureWalletCreated(Logger);
|
||||
if (Network.NBitcoinNetwork.ChainName == ChainName.Regtest && !ChainConfiguration.NoWarmup)
|
||||
{
|
||||
if (await RPCClient.WarmupBlockchain(Logger))
|
||||
BlockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
}
|
||||
_NodeTip = await RPCClient.GetBlockHeaderAsyncEx(BlockchainInfo.BestBlockHash);
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
// Refresh the NetworkInfo that may have become different while it was synching.
|
||||
NetworkInfo = await RPCClient.GetNetworkInfoAsync();
|
||||
_Node = node;
|
||||
EmptyChannel(_Channel);
|
||||
EmptyChannel(_DownloadedBlocks);
|
||||
node.MessageReceived += Node_MessageReceived;
|
||||
node.Disconnected += Node_Disconnected;
|
||||
|
||||
var locator = await AskNextHeaders();
|
||||
lastIndexedBlock = await Repository.GetLastIndexedSlimChainedBlock(locator);
|
||||
if (lastIndexedBlock is null)
|
||||
{
|
||||
var locatorTip = await RPCClient.GetBlockHeaderAsyncEx(locator.Blocks[0]);
|
||||
lastIndexedBlock = locatorTip;
|
||||
}
|
||||
await UpdateState();
|
||||
}
|
||||
}
|
||||
|
||||
private void EmptyChannel<T>(Channel<T> channel)
|
||||
{
|
||||
while (channel.Reader.TryRead(out _)) { }
|
||||
}
|
||||
|
||||
bool firstConnect = true;
|
||||
private async Task<BlockLocator> AskNextHeaders()
|
||||
{
|
||||
var indexProgress = await Repository.GetIndexProgress();
|
||||
if (indexProgress is null)
|
||||
{
|
||||
indexProgress = await GetDefaultCurrentLocation();
|
||||
}
|
||||
await _Node.SendMessageAsync(new GetHeadersPayload(indexProgress));
|
||||
return indexProgress;
|
||||
}
|
||||
|
||||
static int[] BlockLocatorComposition = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 40, 80, 160, 320, 640, 1280, 2560, 5120, 10240, 20480, 40960 };
|
||||
private async Task SaveProgress(DbConnectionHelper conn)
|
||||
{
|
||||
// We pick blocks spaced exponentially from the the tip to build our block locator
|
||||
var heights = BlockLocatorComposition.Select(l => lastIndexedBlock.Height - l).ToArray();
|
||||
var blks = await conn.Connection.QueryAsync<string>(
|
||||
"SELECT blk_id FROM blks " +
|
||||
"WHERE code=@code AND height=ANY(@heights) AND confirmed IS TRUE " +
|
||||
"ORDER BY height DESC", new { code = Network.CryptoCode, heights });
|
||||
var locator = new BlockLocator();
|
||||
foreach (var b in blks)
|
||||
locator.Blocks.Add(uint256.Parse(b));
|
||||
await Repository.SetIndexProgress(conn.Connection, locator);
|
||||
}
|
||||
|
||||
private async Task UpdateState()
|
||||
{
|
||||
var blockchainInfo = await RPCClient.GetBlockchainInfoAsyncEx();
|
||||
if (blockchainInfo.IsSynching(Network))
|
||||
{
|
||||
State = BitcoinDWaiterState.CoreSynching;
|
||||
}
|
||||
else if (lastIndexedBlock != null)
|
||||
{
|
||||
int minBlock = 6;
|
||||
// Prevent some corner cases in tests, if we suddenly mine 200 blocks, we should still be synched on regtest
|
||||
if (Network.NBitcoinNetwork.ChainName == ChainName.Regtest)
|
||||
minBlock = 200;
|
||||
State = blockchainInfo.Headers - lastIndexedBlock.Height < minBlock ? BitcoinDWaiterState.Ready : BitcoinDWaiterState.NBXplorerSynching;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<BlockLocator> GetDefaultCurrentLocation()
|
||||
{
|
||||
if (ChainConfiguration.StartHeight > BlockchainInfo.Headers)
|
||||
throw new InvalidOperationException($"{Network.CryptoCode}: StartHeight should not be above the current tip");
|
||||
BlockLocator blockLocator = null;
|
||||
if (ChainConfiguration.StartHeight == -1)
|
||||
{
|
||||
var bestBlock = await RPCClient.GetBestBlockHashAsync();
|
||||
var bh = await RPCClient.GetBlockHeaderAsyncEx(bestBlock);
|
||||
blockLocator = new BlockLocator();
|
||||
blockLocator.Blocks.Add(bh.Previous ?? bh.Hash);
|
||||
Logger.LogInformation($"Current Index Progress not found, start syncing from the header's chain tip (At height: {BlockchainInfo.Headers})");
|
||||
}
|
||||
else
|
||||
{
|
||||
var header = await RPCClient.GetBlockHeaderAsync(ChainConfiguration.StartHeight);
|
||||
var header2 = await RPCClient.GetBlockHeaderAsyncEx(header.GetHash());
|
||||
blockLocator = new BlockLocator();
|
||||
blockLocator.Blocks.Add(header2.Previous ?? header2.Hash);
|
||||
Logger.LogInformation($"Current Index Progress not found, start syncing at height {ChainConfiguration.StartHeight}");
|
||||
}
|
||||
return blockLocator;
|
||||
}
|
||||
|
||||
private async Task SaveMatches(DbConnectionHelper conn, Block block, SlimChainedBlock slimChainedBlock)
|
||||
{
|
||||
block.Header.PrecomputeHash(false, false);
|
||||
await SaveMatches(conn, block.Transactions, slimChainedBlock, true);
|
||||
EventAggregator.Publish(new RawBlockEvent(block, this.Network), true);
|
||||
lastIndexedBlock = slimChainedBlock;
|
||||
}
|
||||
|
||||
SlimChainedBlock _NodeTip;
|
||||
|
||||
private async Task SaveMatches(DbConnectionHelper conn, List<Transaction> transactions, SlimChainedBlock slimChainedBlock, bool fireEvents)
|
||||
{
|
||||
foreach (var tx in transactions)
|
||||
tx.PrecomputeHash(false, true);
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
if (slimChainedBlock != null)
|
||||
{
|
||||
await conn.NewBlock(slimChainedBlock);
|
||||
}
|
||||
var matches = await Repository.GetMatchesAndSave(conn, transactions, slimChainedBlock, now, true);
|
||||
_ = AddressPoolService.GenerateAddresses(Network, matches);
|
||||
|
||||
long confirmations = 0;
|
||||
if (slimChainedBlock != null)
|
||||
{
|
||||
if (slimChainedBlock.Height >= _NodeTip.Height)
|
||||
_NodeTip = slimChainedBlock;
|
||||
confirmations = _NodeTip.Height - slimChainedBlock.Height + 1;
|
||||
await conn.NewBlockCommit(slimChainedBlock.Hash);
|
||||
var blockEvent = new Models.NewBlockEvent()
|
||||
{
|
||||
CryptoCode = Network.CryptoCode,
|
||||
Hash = slimChainedBlock.Hash,
|
||||
Height = slimChainedBlock.Height,
|
||||
PreviousBlockHash = slimChainedBlock.Previous,
|
||||
Confirmations = confirmations
|
||||
};
|
||||
await Repository.SaveEvent(conn, blockEvent);
|
||||
EventAggregator.Publish(blockEvent);
|
||||
}
|
||||
if (fireEvents)
|
||||
{
|
||||
NewTransactionEvent[] evts = new NewTransactionEvent[matches.Length];
|
||||
for (int i = 0; i < matches.Length; i++)
|
||||
{
|
||||
var txEvt = new Models.NewTransactionEvent()
|
||||
{
|
||||
TrackedSource = matches[i].TrackedSource,
|
||||
DerivationStrategy = (matches[i].TrackedSource is DerivationSchemeTrackedSource dsts) ? dsts.DerivationStrategy : null,
|
||||
CryptoCode = Network.CryptoCode,
|
||||
BlockId = slimChainedBlock?.Hash,
|
||||
TransactionData = new TransactionResult()
|
||||
{
|
||||
BlockId = slimChainedBlock?.Hash,
|
||||
Height = slimChainedBlock?.Height,
|
||||
Confirmations = confirmations,
|
||||
Timestamp = now,
|
||||
Transaction = matches[i].Transaction,
|
||||
TransactionHash = matches[i].TransactionHash
|
||||
},
|
||||
Outputs = matches[i].GetReceivedOutputs().ToList(),
|
||||
Replacing = matches[i].Replacing.ToList()
|
||||
};
|
||||
|
||||
evts[i] = txEvt;
|
||||
}
|
||||
await Repository.SaveEvents(conn, evts);
|
||||
foreach (var ev in evts)
|
||||
{
|
||||
EventAggregator.Publish(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SlimChainedBlock lastIndexedBlock;
|
||||
record PullBlocks(IList<BlockHeader> headers);
|
||||
record NodeDisconnected();
|
||||
private void Node_MessageReceived(Node node, IncomingMessage message)
|
||||
{
|
||||
if (message.Message.Payload is HeadersPayload h && h.Headers.Count != 0)
|
||||
{
|
||||
_Channel.Writer.TryWrite(new PullBlocks(h.Headers));
|
||||
}
|
||||
else if (message.Message.Payload is BlockPayload b)
|
||||
{
|
||||
_DownloadedBlocks.Writer.TryWrite(b.Object);
|
||||
}
|
||||
else if (message.Message.Payload is InvPayload invs)
|
||||
{
|
||||
if (State != BitcoinDWaiterState.Ready)
|
||||
return;
|
||||
var data = new GetDataPayload();
|
||||
foreach (var inv in invs.Inventory.Where(t => t.Type.HasFlag(InventoryType.MSG_TX)))
|
||||
{
|
||||
inv.Type = node.AddSupportedOptions(inv.Type);
|
||||
data.Inventory.Add(inv);
|
||||
}
|
||||
if (data.Inventory.Count != 0)
|
||||
{
|
||||
node.SendMessageAsync(data);
|
||||
}
|
||||
// DOGE coin doing doge things forget we want header first sync... reboot the connection
|
||||
else
|
||||
{
|
||||
if (invs.Inventory.Where(t => t.Type.HasFlag(InventoryType.MSG_BLOCK)).Any())
|
||||
{
|
||||
node.DisconnectAsync("Not sending headers first anymore");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (message.Message.Payload is TxPayload tx)
|
||||
{
|
||||
_Channel.Writer.TryWrite(tx.Object);
|
||||
}
|
||||
}
|
||||
|
||||
private void Node_Disconnected(Node node)
|
||||
{
|
||||
if (node.DisconnectReason.Reason != "Restarting")
|
||||
{
|
||||
if (!cts.IsCancellationRequested)
|
||||
{
|
||||
var exception = node.DisconnectReason.Exception?.Message;
|
||||
if (!string.IsNullOrEmpty(exception))
|
||||
exception = $" ({exception})";
|
||||
else
|
||||
exception = String.Empty;
|
||||
Logger.LogWarning($"Node disconnected for reason: {node.DisconnectReason.Reason}{exception}");
|
||||
}
|
||||
_Channel.Writer.TryWrite(new NodeDisconnected());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation($"Restarting node connection...");
|
||||
}
|
||||
node.MessageReceived -= Node_MessageReceived;
|
||||
node.Disconnected -= Node_Disconnected;
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
}
|
||||
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return Task.CompletedTask;
|
||||
cts = new CancellationTokenSource();
|
||||
_indexerLoop = IndexerLoop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cts?.Cancel();
|
||||
_Channel.Writer.Complete();
|
||||
if (_indexerLoop is not null)
|
||||
await _indexerLoop;
|
||||
_Node?.DisconnectAsync();
|
||||
}
|
||||
public NBXplorerNetwork Network => network;
|
||||
|
||||
BitcoinDWaiterState _State = BitcoinDWaiterState.NotStarted;
|
||||
public BitcoinDWaiterState State
|
||||
{
|
||||
get
|
||||
{
|
||||
return _State;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_State != value)
|
||||
{
|
||||
var old = _State;
|
||||
_State = value;
|
||||
EventAggregator.Publish(new BitcoinDStateChangedEvent(Network, old, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long? SyncHeight => lastIndexedBlock?.Height;
|
||||
|
||||
public GetNetworkInfoResponse NetworkInfo { get; internal set; }
|
||||
public AddressPoolService AddressPoolService { get; }
|
||||
public ILogger Logger { get; }
|
||||
public RPCClient RPCClient { get; }
|
||||
public PostgresRepository Repository { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
public ChainConfiguration ChainConfiguration { get; }
|
||||
public EventAggregator EventAggregator { get; }
|
||||
public GetBlockchainInfoResponse BlockchainInfo { get; private set; }
|
||||
|
||||
NBXplorerNetwork network;
|
||||
private int maxinflight = 10;
|
||||
|
||||
public Task SaveMatches(Transaction transaction)
|
||||
{
|
||||
return SaveMatches(transaction, false);
|
||||
}
|
||||
public async Task SaveMatches(Transaction transaction, bool fireEvents)
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network);
|
||||
await SaveMatches(conn, new List<Transaction>(1) { transaction }, null, fireEvents);
|
||||
}
|
||||
|
||||
public RPCClient GetConnectedClient()
|
||||
{
|
||||
if (State == BitcoinDWaiterState.CoreSynching || State == BitcoinDWaiterState.NBXplorerSynching || State == BitcoinDWaiterState.Ready)
|
||||
return RPCClient;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary<string, IIndexer> _Indexers = new Dictionary<string, IIndexer>();
|
||||
|
||||
public AddressPoolService AddressPoolService { get; }
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
public IRPCClients RpcClients { get; }
|
||||
public ExplorerConfiguration Configuration { get; }
|
||||
public NBXplorerNetworkProvider NetworkProvider { get; }
|
||||
public IRepositoryProvider RepositoryProvider { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public EventAggregator EventAggregator { get; }
|
||||
|
||||
public PostgresIndexers(
|
||||
AddressPoolService addressPoolService,
|
||||
ILoggerFactory loggerFactory,
|
||||
IRPCClients rpcClients,
|
||||
ExplorerConfiguration configuration,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
IRepositoryProvider repositoryProvider,
|
||||
DbConnectionFactory connectionFactory,
|
||||
EventAggregator eventAggregator)
|
||||
{
|
||||
AddressPoolService = addressPoolService;
|
||||
LoggerFactory = loggerFactory;
|
||||
RpcClients = rpcClients;
|
||||
Configuration = configuration;
|
||||
NetworkProvider = networkProvider;
|
||||
RepositoryProvider = repositoryProvider;
|
||||
ConnectionFactory = connectionFactory;
|
||||
EventAggregator = eventAggregator;
|
||||
}
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var config in Configuration.ChainConfigurations)
|
||||
{
|
||||
var network = NetworkProvider.GetFromCryptoCode(config.CryptoCode);
|
||||
_Indexers.Add(config.CryptoCode, new PostgresIndexer(
|
||||
AddressPoolService,
|
||||
LoggerFactory.CreateLogger($"NBXplorer.Indexer.{config.CryptoCode}"),
|
||||
network,
|
||||
RpcClients.Get(network),
|
||||
(PostgresRepository)RepositoryProvider.GetRepository(network),
|
||||
ConnectionFactory,
|
||||
Configuration,
|
||||
config,
|
||||
EventAggregator));
|
||||
}
|
||||
await Task.WhenAll(_Indexers.Values.Select(v => ((PostgresIndexer)v).StartAsync(cancellationToken)));
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.WhenAll(_Indexers.Values.Select(v => ((PostgresIndexer)v).StopAsync(cancellationToken)));
|
||||
}
|
||||
|
||||
public IIndexer GetIndexer(NBXplorerNetwork network)
|
||||
{
|
||||
_Indexers.TryGetValue(network.CryptoCode, out var r);
|
||||
return r;
|
||||
}
|
||||
|
||||
public IEnumerable<IIndexer> All()
|
||||
{
|
||||
return _Indexers.Values;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -18,13 +18,13 @@ namespace NBXplorer
|
||||
}
|
||||
public class Broadcaster
|
||||
{
|
||||
public Broadcaster(IIndexers indexers, ILoggerFactory loggerFactory)
|
||||
public Broadcaster(Indexers indexers, ILoggerFactory loggerFactory)
|
||||
{
|
||||
Indexers = indexers;
|
||||
LoggerFactory = loggerFactory;
|
||||
}
|
||||
|
||||
public IIndexers Indexers { get; }
|
||||
public Indexers Indexers { get; }
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
record Reject
|
||||
{
|
||||
|
||||
@ -7,10 +7,6 @@ namespace NBXplorer.Configuration
|
||||
{
|
||||
public static class ConfigurationExtensions
|
||||
{
|
||||
public static bool IsPostgres(this IConfiguration configuration)
|
||||
{
|
||||
return configuration.GetOrDefault<string>("POSTGRES", null) is string;
|
||||
}
|
||||
public static T GetOrDefault<T>(this IConfiguration configuration, string key, T defaultValue)
|
||||
{
|
||||
var str = configuration[key] ?? configuration[key.Replace(".", string.Empty)];
|
||||
|
||||
@ -26,9 +26,6 @@ namespace NBXplorer.Configuration
|
||||
app.Option("--signet | -signet", $"Use signet", CommandOptionType.BoolValue);
|
||||
app.Option("--chains", $"Chains to support comma separated (default: btc, available: {chains})", CommandOptionType.SingleValue);
|
||||
app.Option($"--nowarmup", $"If true and on regtest, no block will be generated by NBXplorer when the tip got stalled or if no block has been mined (default: false)", CommandOptionType.BoolValue);
|
||||
#if SUPPORT_DBTRIE
|
||||
app.Option($"--dbcache", $"If more than 0, the size of the cache for the database, in MB. Else, no limit on the size of the cache. (default: 50)", CommandOptionType.SingleValue);
|
||||
#endif
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
@ -67,23 +64,22 @@ namespace NBXplorer.Configuration
|
||||
app.Option("--signalfilesdir", $"The directory where files signaling if a chain is ready is created (default: the network specific datadir)", CommandOptionType.SingleValue);
|
||||
app.Option("--noauth", $"Disable cookie authentication", CommandOptionType.BoolValue);
|
||||
app.Option("--instancename", $"Define an instance name for this server that, if not null, will show in status response and in HTTP response headers (default: empty)", CommandOptionType.SingleValue);
|
||||
#if SUPPORT_DBTRIE
|
||||
app.Option("--cachechain", $"Whether the chain of header is locally cached for faster startup (default: true)", CommandOptionType.SingleValue);
|
||||
#endif
|
||||
app.Option("--rpcnotest", $"Faster start because RPC connection testing skipped (default: false)", CommandOptionType.SingleValue);
|
||||
app.Option("--exposerpc", $"Expose the node RPC through the REST API (default: false)", CommandOptionType.SingleValue);
|
||||
app.Option("--postgres", $"Use PostgresSQL backend. Set the connection string of the postgres backend (For example: \"User ID=postgres;Host=postgres;Port=5432;Application Name=nbxplorer;Database=nbxplorer\", more options on https://www.npgsql.org/doc/connection-string-parameters.html)", CommandOptionType.SingleValue);
|
||||
#if SUPPORT_DBTRIE
|
||||
app.Option("--dbtrie", $"Use DBTrie backend. This backend is deprecated, only use if you haven't yet migrated. For more information about how to migrate, see https://github.com/dgarage/NBXplorer/tree/master/docs/Postgres-Migration.md", CommandOptionType.BoolValue);
|
||||
app.Option("--automigrate", $"If legacy installation detected, migrate it to postgres (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option("--deleteaftermigration", $"If automigrate is used, and this flag is true, the old DBTrie database will be automatically deleted after migration (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option("--nomigrateevts", $"Do not migrate the events table (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option("--nomigraterawtxs", $"Do not migrate the raw bytes of transactions (default: false)", CommandOptionType.BoolValue);
|
||||
#endif
|
||||
app.Option("--socksendpoint", "Configure a SocksV5 endpoint as proxy to connect to P2P", CommandOptionType.SingleValue);
|
||||
app.Option("--socksuser", "SocksV5 username credential", CommandOptionType.SingleValue);
|
||||
app.Option("--sockspassword", "SocksV5 password credential", CommandOptionType.SingleValue);
|
||||
app.Option("-v | --verbose", $"Verbose logs (default: true)", CommandOptionType.SingleValue);
|
||||
|
||||
app.Option("--dbtrie", "Do not use", CommandOptionType.BoolValue);
|
||||
app.Option("--automigrate", "Do not use", CommandOptionType.BoolValue);
|
||||
app.Option("--nomigrateevts", "Do not use", CommandOptionType.BoolValue);
|
||||
app.Option("--nomigraterawtxs", "Do not use", CommandOptionType.BoolValue);
|
||||
app.Option("--cachechain", "Do not use", CommandOptionType.SingleValue);
|
||||
app.Option($"--deleteaftermigration", "Do not use", CommandOptionType.SingleValue);
|
||||
app.Option($"--dbcache", "Do not use", CommandOptionType.SingleValue);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
@ -77,15 +77,6 @@ namespace NBXplorer.Configuration
|
||||
{
|
||||
get; set;
|
||||
} = 30;
|
||||
#if SUPPORT_DBTRIE
|
||||
public bool IsPostgres { get; set; }
|
||||
public bool IsDbTrie { get; set; }
|
||||
public bool NoMigrateEvents { get; private set; }
|
||||
public bool NoMigrateRawTxs { get; private set; }
|
||||
public int DBCache { get; set; }
|
||||
#else
|
||||
public bool IsPostgres => true;
|
||||
#endif
|
||||
public List<ChainConfiguration> ChainConfigurations
|
||||
{
|
||||
get; set;
|
||||
@ -173,11 +164,6 @@ namespace NBXplorer.Configuration
|
||||
Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray()));
|
||||
MinGapSize = config.GetOrDefault<int>("mingapsize", 20);
|
||||
MaxGapSize = config.GetOrDefault<int>("maxgapsize", 30);
|
||||
#if SUPPORT_DBTRIE
|
||||
DBCache = config.GetOrDefault<int>("dbcache", 50);
|
||||
if (DBCache > 0)
|
||||
Logs.Configuration.LogInformation($"DBCache: {DBCache} MB");
|
||||
#endif
|
||||
if (MinGapSize >= MaxGapSize)
|
||||
throw new ConfigException("mingapsize should be equal or lower than maxgapsize");
|
||||
if(!Directory.Exists(BaseDataDir))
|
||||
@ -189,9 +175,6 @@ namespace NBXplorer.Configuration
|
||||
SignalFilesDir = SignalFilesDir ?? DataDir;
|
||||
if (!Directory.Exists(SignalFilesDir))
|
||||
Directory.CreateDirectory(SignalFilesDir);
|
||||
#if SUPPORT_DBTRIE
|
||||
CacheChain = config.GetOrDefault<bool>("cachechain", true);
|
||||
#endif
|
||||
NoAuthentication = config.GetOrDefault<bool>("noauth", false);
|
||||
InstanceName = config.GetOrDefault<string>("instancename", "");
|
||||
TrimEvents = config.GetOrDefault<int>("trimevents", -1);
|
||||
@ -218,26 +201,13 @@ namespace NBXplorer.Configuration
|
||||
RabbitMqPassword = config.GetOrDefault<string>("rmqpass", "");
|
||||
RabbitMqTransactionExchange = config.GetOrDefault<string>("rmqtranex", "");
|
||||
RabbitMqBlockExchange = config.GetOrDefault<string>("rmqblockex", "");
|
||||
#if SUPPORT_DBTRIE
|
||||
IsPostgres = config.IsPostgres();
|
||||
IsDbTrie = config.GetOrDefault<bool>("dbtrie", false); ;
|
||||
NoMigrateEvents = config.GetOrDefault<bool>("nomigrateevts", false);
|
||||
NoMigrateRawTxs = config.GetOrDefault<bool>("nomigraterawtxs", false);
|
||||
if (!IsPostgres && !IsDbTrie)
|
||||
{
|
||||
throw new ConfigException("You need to select your backend implementation. There is two choices, PostgresSQL and DBTrie." + Environment.NewLine +
|
||||
" * To use postgres, please use --postgres \"...\" (or NBXPLORER_POSTGRES=\"...\") with a postgres connection string (see https://www.connectionstrings.com/postgresql/)" + Environment.NewLine +
|
||||
" * To use DBTrie, use --dbtrie (or NBXPLORER_DBTRIE=1). This backend is deprecated, only use if you haven't yet migrated. For more information about how to migrate, see https://github.com/dgarage/NBXplorer/tree/master/docs/Postgres-Migration.md");
|
||||
}
|
||||
if (IsDbTrie)
|
||||
{
|
||||
Logs.Configuration.LogWarning("Warning: A DBTrie backend has been selected, but this backend is deprecated, only use if you haven't yet migrated to postgres. For more information about how to migrate, see https://github.com/dgarage/NBXplorer/tree/master/docs/Postgres-Migration.md");
|
||||
}
|
||||
if (IsDbTrie && IsPostgres)
|
||||
{
|
||||
throw new ConfigException("You need to select your backend implementation. But --dbtrie and --postgres are both specified.");
|
||||
}
|
||||
#endif
|
||||
|
||||
var obsolete = string.Join(", ",
|
||||
new[] { "dbtrie", "automigrate", "nomigrateevts", "nomigraterawtxs", "cachechain", "deleteaftermigration", "dbcache" }
|
||||
.Where(o => config.GetOrDefault<bool>(o, false)));
|
||||
if (obsolete != string.Empty)
|
||||
throw new ConfigException($"Options '{obsolete}' are not supported anymore, if you need to migrate an old instance to the new postgres backend, please use NBXplorer v2.3.67 and follow https://github.com/dgarage/NBXplorer/blob/master/docs/Postgres-Migration.md.");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -254,14 +224,6 @@ namespace NBXplorer.Configuration
|
||||
{
|
||||
return ChainConfigurations.Any(c => network.CryptoCode == c.CryptoCode);
|
||||
}
|
||||
|
||||
#if SUPPORT_DBTRIE
|
||||
public bool CacheChain
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
#endif
|
||||
public bool NoAuthentication
|
||||
{
|
||||
get;
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
public partial class ControllerBase : Controller
|
||||
{
|
||||
public ControllerBase(
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
IRPCClients rpcClients,
|
||||
IRepositoryProvider repositoryProvider,
|
||||
IIndexers indexers)
|
||||
{
|
||||
NetworkProvider = networkProvider;
|
||||
RPCClients = rpcClients;
|
||||
RepositoryProvider = repositoryProvider;
|
||||
Indexers = indexers;
|
||||
}
|
||||
|
||||
public NBXplorerNetworkProvider NetworkProvider { get; }
|
||||
public IRPCClients RPCClients { get; }
|
||||
public IRepositoryProvider RepositoryProvider { get; }
|
||||
public IIndexers Indexers { get; }
|
||||
|
||||
internal static TrackedSource GetTrackedSource(DerivationStrategyBase derivationScheme, BitcoinAddress address)
|
||||
{
|
||||
TrackedSource trackedSource = null;
|
||||
if (address != null)
|
||||
trackedSource = new AddressTrackedSource(address);
|
||||
if (derivationScheme != null)
|
||||
trackedSource = new DerivationSchemeTrackedSource(derivationScheme);
|
||||
return trackedSource;
|
||||
}
|
||||
internal NBXplorerNetwork GetNetwork(string cryptoCode, bool checkRPC)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||
var network = NetworkProvider.GetFromCryptoCode(cryptoCode);
|
||||
if (network == null || Indexers.GetIndexer(network) is null)
|
||||
throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported"));
|
||||
|
||||
if (checkRPC)
|
||||
{
|
||||
var rpc = GetAvailableRPC(network);
|
||||
if (rpc is null || rpc.Capabilities == null)
|
||||
throw new NBXplorerError(400, "rpc-unavailable", $"The RPC interface is currently not available.").AsException();
|
||||
}
|
||||
return network;
|
||||
}
|
||||
protected RPCClient GetAvailableRPC(NBXplorerNetwork network)
|
||||
{
|
||||
return Indexers.GetIndexer(network)?.GetConnectedClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Analytics;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
@ -24,9 +24,7 @@ namespace NBXplorer.Controllers
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase strategy,
|
||||
[FromBody]
|
||||
JObject body,
|
||||
[FromServices]
|
||||
IUTXOService utxoService)
|
||||
JObject body)
|
||||
{
|
||||
if (body == null)
|
||||
throw new ArgumentNullException(nameof(body));
|
||||
@ -153,7 +151,7 @@ namespace NBXplorer.Controllers
|
||||
txBuilder.SetLockTime(new LockTime(0));
|
||||
}
|
||||
}
|
||||
var utxos = (await utxoService.GetUTXOs(network.CryptoCode, strategy)).As<UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations);
|
||||
var utxos = (await GetUTXOs(network.CryptoCode, strategy, null)).As<UTXOChanges>().GetUnspentUTXOs(request.MinConfirmations);
|
||||
var availableCoinsByOutpoint = utxos.ToDictionary(o => o.Outpoint);
|
||||
if (request.IncludeOnlyOutpoints != null)
|
||||
{
|
||||
@ -432,7 +430,7 @@ namespace NBXplorer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task UpdateHDKeyPathsWitnessAndRedeem(UpdatePSBTRequest update, IRepository repo)
|
||||
private static async Task UpdateHDKeyPathsWitnessAndRedeem(UpdatePSBTRequest update, Repository repo)
|
||||
{
|
||||
var strategy = update.DerivationScheme;
|
||||
var pubkeys = strategy.GetExtPubKeys().Select(p => p.AsHDKeyCache()).ToArray();
|
||||
@ -498,7 +496,7 @@ namespace NBXplorer.Controllers
|
||||
!((input.GetSignableCoin() ?? input.GetCoin())?.IsMalleable is false));
|
||||
}
|
||||
|
||||
private async Task UpdateUTXO(UpdatePSBTRequest update, IRepository repo, RPCClient rpc)
|
||||
private async Task UpdateUTXO(UpdatePSBTRequest update, Repository repo, RPCClient rpc)
|
||||
{
|
||||
if (rpc is not null)
|
||||
{
|
||||
|
||||
@ -22,46 +22,57 @@ using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using NBXplorer.Analytics;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using NBitcoin.Scripting;
|
||||
using System.Globalization;
|
||||
using RabbitMQ.Client;
|
||||
using Dapper;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
[Route("v1")]
|
||||
[Authorize]
|
||||
public partial class MainController : ControllerBase, IUTXOService
|
||||
public partial class MainController : Controller
|
||||
{
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public NBXplorerNetworkProvider NetworkProvider { get; }
|
||||
public IRPCClients RPCClients { get; }
|
||||
public RepositoryProvider RepositoryProvider { get; }
|
||||
public Indexers Indexers { get; }
|
||||
|
||||
JsonSerializerSettings _SerializerSettings;
|
||||
public MainController(
|
||||
ExplorerConfiguration explorerConfiguration,
|
||||
IRepositoryProvider repositoryProvider,
|
||||
RepositoryProvider repositoryProvider,
|
||||
EventAggregator eventAggregator,
|
||||
IRPCClients rpcClients,
|
||||
AddressPoolService addressPoolService,
|
||||
ScanUTXOSetServiceAccessor scanUTXOSetService,
|
||||
RebroadcasterHostedService rebroadcaster,
|
||||
KeyPathTemplates keyPathTemplates,
|
||||
MvcNewtonsoftJsonOptions jsonOptions,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
Analytics.FingerprintHostedService fingerprintService,
|
||||
IIndexers indexers
|
||||
) : base(networkProvider, rpcClients, repositoryProvider, indexers)
|
||||
Indexers indexers,
|
||||
DbConnectionFactory connectionFactory
|
||||
)
|
||||
{
|
||||
RPCClients = rpcClients;
|
||||
ConnectionFactory = connectionFactory;
|
||||
ExplorerConfiguration = explorerConfiguration;
|
||||
RepositoryProvider = repositoryProvider;
|
||||
_SerializerSettings = jsonOptions.SerializerSettings;
|
||||
_EventAggregator = eventAggregator;
|
||||
ScanUTXOSetService = scanUTXOSetService.Instance;
|
||||
Rebroadcaster = rebroadcaster;
|
||||
this.keyPathTemplates = keyPathTemplates;
|
||||
NetworkProvider = networkProvider;
|
||||
this.fingerprintService = fingerprintService;
|
||||
Indexers = indexers;
|
||||
AddressPoolService = addressPoolService;
|
||||
}
|
||||
EventAggregator _EventAggregator;
|
||||
private readonly KeyPathTemplates keyPathTemplates;
|
||||
private readonly FingerprintHostedService fingerprintService;
|
||||
|
||||
public RebroadcasterHostedService Rebroadcaster { get; }
|
||||
public AddressPoolService AddressPoolService
|
||||
{
|
||||
get;
|
||||
@ -82,6 +93,37 @@ namespace NBXplorer.Controllers
|
||||
return new NBXplorerError(401, "json-rpc-not-exposed", $"JSON-RPC is not configured to be exposed. Only the following methods are available: {string.Join(", ", WhitelistedRPCMethods)}").AsException();
|
||||
}
|
||||
|
||||
internal static TrackedSource GetTrackedSource(DerivationStrategyBase derivationScheme, BitcoinAddress address)
|
||||
{
|
||||
TrackedSource trackedSource = null;
|
||||
if (address != null)
|
||||
trackedSource = new AddressTrackedSource(address);
|
||||
if (derivationScheme != null)
|
||||
trackedSource = new DerivationSchemeTrackedSource(derivationScheme);
|
||||
return trackedSource;
|
||||
}
|
||||
internal NBXplorerNetwork GetNetwork(string cryptoCode, bool checkRPC)
|
||||
{
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
cryptoCode = cryptoCode.ToUpperInvariant();
|
||||
var network = NetworkProvider.GetFromCryptoCode(cryptoCode);
|
||||
if (network == null || Indexers.GetIndexer(network) is null)
|
||||
throw new NBXplorerException(new NBXplorerError(404, "cryptoCode-not-supported", $"{cryptoCode} is not supported"));
|
||||
|
||||
if (checkRPC)
|
||||
{
|
||||
var rpc = GetAvailableRPC(network);
|
||||
if (rpc is null || rpc.Capabilities == null)
|
||||
throw new NBXplorerError(400, "rpc-unavailable", $"The RPC interface is currently not available.").AsException();
|
||||
}
|
||||
return network;
|
||||
}
|
||||
protected RPCClient GetAvailableRPC(NBXplorerNetwork network)
|
||||
{
|
||||
return Indexers.GetIndexer(network)?.GetConnectedClient();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("cryptos/{cryptoCode}/rpc")]
|
||||
[Consumes("application/json", "application/json-rpc")]
|
||||
@ -243,7 +285,7 @@ namespace NBXplorer.Controllers
|
||||
var status = new StatusResult()
|
||||
{
|
||||
NetworkType = network.NBitcoinNetwork.ChainName,
|
||||
Backend = ExplorerConfiguration.IsPostgres ? "Postgres" : "DBTrie",
|
||||
Backend = "Postgres",
|
||||
CryptoCode = network.CryptoCode,
|
||||
Version = typeof(MainController).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>().Version,
|
||||
SupportedCryptoCodes = Indexers.All().Select(w => w.Network.CryptoCode).ToArray(),
|
||||
@ -848,110 +890,8 @@ namespace NBXplorer.Controllers
|
||||
throw new NBXplorerError(404, "scanutxoset-info-not-found", "ScanUTXOSet has not been called with this derivationScheme of the result has expired").AsException();
|
||||
return Json(info, network.Serializer.Settings);
|
||||
}
|
||||
#if SUPPORT_DBTRIE
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/balance")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/balance")]
|
||||
[PostgresImplementationActionConstraint(false)]
|
||||
public async Task<IActionResult> GetBalance(string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
var getTransactionsResult = await GetTransactions(cryptoCode, derivationScheme, address, includeTransaction: false);
|
||||
var jsonResult = getTransactionsResult as JsonResult;
|
||||
var transactions = jsonResult?.Value as GetTransactionsResponse;
|
||||
if (transactions == null)
|
||||
return getTransactionsResult;
|
||||
|
||||
var network = this.GetNetwork(cryptoCode, false);
|
||||
var balance = new GetBalanceResponse()
|
||||
{
|
||||
Confirmed = CalculateBalance(network, transactions.ConfirmedTransactions),
|
||||
Unconfirmed = CalculateBalance(network, transactions.UnconfirmedTransactions),
|
||||
Immature = CalculateBalance(network, transactions.ImmatureTransactions)
|
||||
};
|
||||
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
|
||||
balance.Available = balance.Total.Sub(balance.Immature);
|
||||
return Json(balance, jsonResult.SerializerSettings);
|
||||
}
|
||||
|
||||
private IMoney CalculateBalance(NBXplorerNetwork network, TransactionInformationSet transactions)
|
||||
{
|
||||
if (network.NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance)
|
||||
{
|
||||
return new MoneyBag(transactions.Transactions.Select(t => t.BalanceChange).ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
return transactions.Transactions.Select(t => t.BalanceChange).OfType<Money>().Sum();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/utxos")]
|
||||
[PostgresImplementationActionConstraint(false)]
|
||||
public async Task<IActionResult> GetUTXOs(
|
||||
string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
UTXOChanges changes = null;
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
|
||||
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = RepositoryProvider.GetRepository(network);
|
||||
|
||||
changes = new UTXOChanges();
|
||||
changes.CurrentHeight = (await repo.GetTip()).Height;
|
||||
var transactions = await GetAnnotatedTransactions(repo, trackedSource, false);
|
||||
|
||||
changes.Confirmed = ToUTXOChange(transactions.ConfirmedState);
|
||||
changes.Confirmed.SpentOutpoints.Clear();
|
||||
changes.Unconfirmed = ToUTXOChange(transactions.UnconfirmedState - transactions.ConfirmedState);
|
||||
|
||||
FillUTXOsInformation(changes.Confirmed.UTXOs, transactions, changes.CurrentHeight);
|
||||
FillUTXOsInformation(changes.Unconfirmed.UTXOs, transactions, changes.CurrentHeight);
|
||||
|
||||
changes.TrackedSource = trackedSource;
|
||||
changes.DerivationStrategy = (trackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy;
|
||||
|
||||
return Json(changes, repo.Serializer.Settings);
|
||||
}
|
||||
|
||||
private UTXOChange ToUTXOChange(UTXOState state)
|
||||
{
|
||||
UTXOChange change = new UTXOChange();
|
||||
change.SpentOutpoints.AddRange(state.SpentUTXOs);
|
||||
change.UTXOs.AddRange(state.UTXOByOutpoint.Select(u => new UTXO(u.Value)));
|
||||
return change;
|
||||
}
|
||||
|
||||
int MaxHeight = int.MaxValue;
|
||||
|
||||
private void FillUTXOsInformation(List<UTXO> utxos, AnnotatedTransactionCollection transactions, int currentHeight)
|
||||
{
|
||||
for (int i = 0; i < utxos.Count; i++)
|
||||
{
|
||||
var utxo = utxos[i];
|
||||
utxo.KeyPath = transactions.GetKeyPath(utxo.ScriptPubKey);
|
||||
if (utxo.KeyPath != null)
|
||||
utxo.Feature = keyPathTemplates.GetDerivationFeature(utxo.KeyPath);
|
||||
var txHeight = transactions.GetByTxId(utxo.Outpoint.Hash).Height is long h ? h : MaxHeight;
|
||||
var isUnconf = txHeight == MaxHeight;
|
||||
utxo.Confirmations = isUnconf ? 0 : currentHeight - txHeight + 1;
|
||||
utxo.Timestamp = transactions.GetByTxId(utxo.Outpoint.Hash).Record.FirstSeen;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
private async Task<AnnotatedTransactionCollection> GetAnnotatedTransactions(IRepository repo, TrackedSource trackedSource, bool includeTransaction, uint256 txId = null)
|
||||
private async Task<AnnotatedTransactionCollection> GetAnnotatedTransactions(Repository repo, TrackedSource trackedSource, bool includeTransaction, uint256 txId = null)
|
||||
{
|
||||
var transactions = await repo.GetTransactions(trackedSource, txId, includeTransaction, this.HttpContext?.RequestAborted ?? default);
|
||||
|
||||
@ -966,8 +906,6 @@ namespace NBXplorer.Controllers
|
||||
|
||||
var annotatedTransactions = new AnnotatedTransactionCollection(transactions, trackedSource, repo.Network.NBitcoinNetwork);
|
||||
|
||||
Rebroadcaster.RebroadcastPeriodically(repo.Network, trackedSource, annotatedTransactions.UnconfirmedTransactions
|
||||
.Concat(annotatedTransactions.CleanupTransactions).Select(c => c.Record.Key).ToArray());
|
||||
return annotatedTransactions;
|
||||
}
|
||||
|
||||
@ -1286,16 +1224,167 @@ namespace NBXplorer.Controllers
|
||||
}
|
||||
return new PruneResponse() { TotalPruned = prunableIds.Count };
|
||||
}
|
||||
#if SUPPORT_DBTRIE
|
||||
public Task<IActionResult> GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/balance")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/balance")]
|
||||
public async Task<IActionResult> GetBalance(string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
return this.GetUTXOs(cryptoCode, derivationStrategy, null);
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (Repository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
var b = await conn.QueryAsync("SELECT * FROM wallets_balances WHERE code=@code AND wallet_id=@walletId", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid });
|
||||
MoneyBag
|
||||
available = new MoneyBag(),
|
||||
confirmed = new MoneyBag(),
|
||||
immature = new MoneyBag(),
|
||||
total = new MoneyBag(),
|
||||
unconfirmed = new MoneyBag();
|
||||
foreach (var r in b)
|
||||
{
|
||||
if (r.asset_id == string.Empty)
|
||||
{
|
||||
confirmed += Money.Satoshis((long)r.confirmed_balance);
|
||||
unconfirmed += Money.Satoshis((long)r.unconfirmed_balance - (long)r.confirmed_balance);
|
||||
available += Money.Satoshis((long)r.available_balance);
|
||||
total += Money.Satoshis((long)r.available_balance + (long)r.immature_balance);
|
||||
immature += Money.Satoshis((long)r.immature_balance);
|
||||
}
|
||||
else
|
||||
{
|
||||
var assetId = uint256.Parse(r.asset_id);
|
||||
confirmed += new AssetMoney(assetId, (long)r.confirmed_balance);
|
||||
unconfirmed += new AssetMoney(assetId, (long)r.unconfirmed_balance - (long)r.confirmed_balance);
|
||||
available += new AssetMoney(assetId, (long)r.available_balance);
|
||||
total += new AssetMoney(assetId, (long)r.available_balance + (long)r.immature_balance);
|
||||
immature += new AssetMoney(assetId, (long)r.immature_balance);
|
||||
}
|
||||
}
|
||||
|
||||
var balance = new GetBalanceResponse()
|
||||
{
|
||||
Confirmed = Format(network, confirmed),
|
||||
Unconfirmed = Format(network, unconfirmed),
|
||||
Available = Format(network, available),
|
||||
Total = Format(network, total),
|
||||
Immature = Format(network, immature)
|
||||
};
|
||||
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
|
||||
return Json(balance, network.JsonSerializerSettings);
|
||||
}
|
||||
#else
|
||||
public Task<IActionResult> GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
|
||||
private IMoney Format(NBXplorerNetwork network, MoneyBag bag)
|
||||
{
|
||||
throw new NotSupportedException("This should never be called");
|
||||
if (network.IsElement)
|
||||
return RemoveZeros(bag);
|
||||
var c = bag.Count();
|
||||
if (c == 0)
|
||||
return Money.Zero;
|
||||
if (c == 1 && bag.First() is Money m)
|
||||
return m;
|
||||
return RemoveZeros(bag);
|
||||
}
|
||||
|
||||
private static MoneyBag RemoveZeros(MoneyBag bag)
|
||||
{
|
||||
// Super hack to know if we deal with zero
|
||||
return new MoneyBag(bag.Where(a => !a.Negate().Equals(a)).ToArray());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/utxos")]
|
||||
public async Task<IActionResult> GetUTXOs(
|
||||
string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (Repository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
var height = await conn.ExecuteScalarAsync<long>("SELECT height FROM get_tip(@code)", new { code = network.CryptoCode });
|
||||
|
||||
|
||||
// On elements, we can't get blinded address from the scriptPubKey, so we need to fetch it rather than compute it
|
||||
string addrColumns = "NULL as address";
|
||||
if (network.IsElement && !derivationScheme.Unblinded())
|
||||
{
|
||||
addrColumns = "ds.metadata->>'blindedAddress' as address";
|
||||
}
|
||||
|
||||
string descriptorJoin = string.Empty;
|
||||
string descriptorColumns = "NULL as redeem, NULL as keypath, NULL as feature";
|
||||
if (derivationScheme is not null)
|
||||
{
|
||||
descriptorJoin = " JOIN descriptors_scripts ds USING (code, script) JOIN descriptors d USING (code, descriptor)";
|
||||
descriptorColumns = "ds.metadata->>'redeem' redeem, nbxv1_get_keypath(d.metadata, ds.idx) AS keypath, d.metadata->>'feature' feature";
|
||||
}
|
||||
|
||||
var utxos = (await conn.QueryAsync<(
|
||||
long? blk_height,
|
||||
string tx_id,
|
||||
int idx,
|
||||
long value,
|
||||
string script,
|
||||
string address,
|
||||
string redeem,
|
||||
string keypath,
|
||||
string feature,
|
||||
bool mempool,
|
||||
bool input_mempool,
|
||||
DateTime tx_seen_at)>(
|
||||
$"SELECT blk_height, tx_id, wu.idx, value, script, {addrColumns}, {descriptorColumns}, mempool, input_mempool, seen_at " +
|
||||
$"FROM wallets_utxos wu{descriptorJoin} WHERE code=@code AND wallet_id=@walletId AND immature IS FALSE", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid }));
|
||||
UTXOChanges changes = new UTXOChanges()
|
||||
{
|
||||
CurrentHeight = (int)height,
|
||||
TrackedSource = trackedSource,
|
||||
DerivationStrategy = derivationScheme
|
||||
};
|
||||
foreach (var utxo in utxos.OrderBy(u => u.tx_seen_at))
|
||||
{
|
||||
var u = new UTXO()
|
||||
{
|
||||
Index = utxo.idx,
|
||||
Timestamp = new DateTimeOffset(utxo.tx_seen_at),
|
||||
Value = Money.Satoshis(utxo.value),
|
||||
ScriptPubKey = Script.FromHex(utxo.script),
|
||||
Redeem = utxo.redeem is null ? null : Script.FromHex(utxo.redeem),
|
||||
TransactionHash = uint256.Parse(utxo.tx_id)
|
||||
};
|
||||
u.Outpoint = new OutPoint(u.TransactionHash, u.Index);
|
||||
if (utxo.blk_height is long)
|
||||
{
|
||||
u.Confirmations = (int)(height - utxo.blk_height + 1);
|
||||
}
|
||||
|
||||
if (utxo.keypath is not null)
|
||||
{
|
||||
u.KeyPath = KeyPath.Parse(utxo.keypath);
|
||||
u.Feature = Enum.Parse<DerivationFeature>(utxo.feature);
|
||||
}
|
||||
u.Address = utxo.address is null ? u.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : BitcoinAddress.Create(utxo.address, network.NBitcoinNetwork);
|
||||
if (!utxo.mempool)
|
||||
changes.Confirmed.UTXOs.Add(u);
|
||||
else if (!utxo.input_mempool)
|
||||
changes.Unconfirmed.UTXOs.Add(u);
|
||||
if (utxo.input_mempool && !utxo.mempool)
|
||||
changes.Unconfirmed.SpentOutpoints.Add(u.Outpoint);
|
||||
}
|
||||
return Json(changes, network.JsonSerializerSettings);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,204 +0,0 @@
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.ModelBinders;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
[Route("v1")]
|
||||
[Authorize]
|
||||
public class PostgresMainController : ControllerBase, IUTXOService
|
||||
{
|
||||
public PostgresMainController(
|
||||
DbConnectionFactory connectionFactory,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
IRPCClients rpcClients,
|
||||
IIndexers indexers,
|
||||
KeyPathTemplates keyPathTemplates,
|
||||
IRepositoryProvider repositoryProvider) : base(networkProvider, rpcClients, repositoryProvider, indexers)
|
||||
{
|
||||
ConnectionFactory = connectionFactory;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/balance")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/balance")]
|
||||
[PostgresImplementationActionConstraint(true)]
|
||||
public async Task<IActionResult> GetBalance(string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
var b = await conn.QueryAsync("SELECT * FROM wallets_balances WHERE code=@code AND wallet_id=@walletId", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid });
|
||||
MoneyBag
|
||||
available = new MoneyBag(),
|
||||
confirmed = new MoneyBag(),
|
||||
immature = new MoneyBag(),
|
||||
total = new MoneyBag(),
|
||||
unconfirmed = new MoneyBag();
|
||||
foreach (var r in b)
|
||||
{
|
||||
if (r.asset_id == string.Empty)
|
||||
{
|
||||
confirmed += Money.Satoshis((long)r.confirmed_balance);
|
||||
unconfirmed += Money.Satoshis((long)r.unconfirmed_balance - (long)r.confirmed_balance);
|
||||
available += Money.Satoshis((long)r.available_balance);
|
||||
total += Money.Satoshis((long)r.available_balance + (long)r.immature_balance);
|
||||
immature += Money.Satoshis((long)r.immature_balance);
|
||||
}
|
||||
else
|
||||
{
|
||||
var assetId = uint256.Parse(r.asset_id);
|
||||
confirmed += new AssetMoney(assetId, (long)r.confirmed_balance);
|
||||
unconfirmed += new AssetMoney(assetId, (long)r.unconfirmed_balance - (long)r.confirmed_balance);
|
||||
available += new AssetMoney(assetId, (long)r.available_balance);
|
||||
total += new AssetMoney(assetId, (long)r.available_balance + (long)r.immature_balance);
|
||||
immature += new AssetMoney(assetId, (long)r.immature_balance);
|
||||
}
|
||||
}
|
||||
|
||||
var balance = new GetBalanceResponse()
|
||||
{
|
||||
Confirmed = Format(network, confirmed),
|
||||
Unconfirmed = Format(network, unconfirmed),
|
||||
Available = Format(network, available),
|
||||
Total = Format(network, total),
|
||||
Immature = Format(network, immature)
|
||||
};
|
||||
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
|
||||
return Json(balance, network.JsonSerializerSettings);
|
||||
}
|
||||
|
||||
private IMoney Format(NBXplorerNetwork network, MoneyBag bag)
|
||||
{
|
||||
if (network.IsElement)
|
||||
return RemoveZeros(bag);
|
||||
var c = bag.Count();
|
||||
if (c == 0)
|
||||
return Money.Zero;
|
||||
if (c == 1 && bag.First() is Money m)
|
||||
return m;
|
||||
return RemoveZeros(bag);
|
||||
}
|
||||
|
||||
private static MoneyBag RemoveZeros(MoneyBag bag)
|
||||
{
|
||||
// Super hack to know if we deal with zero
|
||||
return new MoneyBag(bag.Where(a => !a.Negate().Equals(a)).ToArray());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos")]
|
||||
[Route("cryptos/{cryptoCode}/addresses/{address}/utxos")]
|
||||
[PostgresImplementationActionConstraint(true)]
|
||||
public async Task<IActionResult> GetUTXOs(
|
||||
string cryptoCode,
|
||||
[ModelBinder(BinderType = typeof(DerivationStrategyModelBinder))]
|
||||
DerivationStrategyBase derivationScheme,
|
||||
[ModelBinder(BinderType = typeof(BitcoinAddressModelBinder))]
|
||||
BitcoinAddress address)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
var height = await conn.ExecuteScalarAsync<long>("SELECT height FROM get_tip(@code)", new { code = network.CryptoCode });
|
||||
|
||||
|
||||
// On elements, we can't get blinded address from the scriptPubKey, so we need to fetch it rather than compute it
|
||||
string addrColumns = "NULL as address";
|
||||
if (network.IsElement && !derivationScheme.Unblinded())
|
||||
{
|
||||
addrColumns = "ds.metadata->>'blindedAddress' as address";
|
||||
}
|
||||
|
||||
string descriptorJoin = string.Empty;
|
||||
string descriptorColumns = "NULL as redeem, NULL as keypath, NULL as feature";
|
||||
if (derivationScheme is not null)
|
||||
{
|
||||
descriptorJoin = " JOIN descriptors_scripts ds USING (code, script) JOIN descriptors d USING (code, descriptor)";
|
||||
descriptorColumns = "ds.metadata->>'redeem' redeem, nbxv1_get_keypath(d.metadata, ds.idx) AS keypath, d.metadata->>'feature' feature";
|
||||
}
|
||||
|
||||
var utxos = (await conn.QueryAsync<(
|
||||
long? blk_height,
|
||||
string tx_id,
|
||||
int idx,
|
||||
long value,
|
||||
string script,
|
||||
string address,
|
||||
string redeem,
|
||||
string keypath,
|
||||
string feature,
|
||||
bool mempool,
|
||||
bool input_mempool,
|
||||
DateTime tx_seen_at)>(
|
||||
$"SELECT blk_height, tx_id, wu.idx, value, script, {addrColumns}, {descriptorColumns}, mempool, input_mempool, seen_at " +
|
||||
$"FROM wallets_utxos wu{descriptorJoin} WHERE code=@code AND wallet_id=@walletId AND immature IS FALSE", new { code = network.CryptoCode, walletId = repo.GetWalletKey(trackedSource).wid }));
|
||||
UTXOChanges changes = new UTXOChanges()
|
||||
{
|
||||
CurrentHeight = (int)height,
|
||||
TrackedSource = trackedSource,
|
||||
DerivationStrategy = derivationScheme
|
||||
};
|
||||
foreach (var utxo in utxos.OrderBy(u => u.tx_seen_at))
|
||||
{
|
||||
var u = new UTXO()
|
||||
{
|
||||
Index = utxo.idx,
|
||||
Timestamp = new DateTimeOffset(utxo.tx_seen_at),
|
||||
Value = Money.Satoshis(utxo.value),
|
||||
ScriptPubKey = Script.FromHex(utxo.script),
|
||||
Redeem = utxo.redeem is null ? null : Script.FromHex(utxo.redeem),
|
||||
TransactionHash = uint256.Parse(utxo.tx_id)
|
||||
};
|
||||
u.Outpoint = new OutPoint(u.TransactionHash, u.Index);
|
||||
if (utxo.blk_height is long)
|
||||
{
|
||||
u.Confirmations = (int)(height - utxo.blk_height + 1);
|
||||
}
|
||||
|
||||
if (utxo.keypath is not null)
|
||||
{
|
||||
u.KeyPath = KeyPath.Parse(utxo.keypath);
|
||||
u.Feature = Enum.Parse<DerivationFeature>(utxo.feature);
|
||||
}
|
||||
u.Address = utxo.address is null ? u.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork) : BitcoinAddress.Create(utxo.address, network.NBitcoinNetwork);
|
||||
if (!utxo.mempool)
|
||||
changes.Confirmed.UTXOs.Add(u);
|
||||
else if (!utxo.input_mempool)
|
||||
changes.Unconfirmed.UTXOs.Add(u);
|
||||
if (utxo.input_mempool && !utxo.mempool)
|
||||
changes.Unconfirmed.SpentOutpoints.Add(u.Outpoint);
|
||||
}
|
||||
return Json(changes, network.JsonSerializerSettings);
|
||||
}
|
||||
|
||||
public Task<IActionResult> GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
return this.GetUTXOs(cryptoCode, derivationStrategy, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -49,11 +49,6 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Unsubscribe()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish<T>(T evt, bool internalEvent = false) where T : class
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.Events
|
||||
{
|
||||
|
||||
@ -21,12 +21,7 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using NBXplorer.Authentication;
|
||||
using NBXplorer.MessageBrokers;
|
||||
using NBXplorer.HostedServices;
|
||||
using NBXplorer.Controllers;
|
||||
#if SUPPORT_DBTRIE
|
||||
using NBXplorer.Backends.DBTrie;
|
||||
#endif
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
|
||||
namespace NBXplorer
|
||||
@ -157,46 +152,19 @@ namespace NBXplorer
|
||||
services.TryAddSingleton<CookieRepository>();
|
||||
services.TryAddSingleton<Broadcaster>();
|
||||
|
||||
// MainController wants to resolve this, even if unused by postgres backend
|
||||
services.TryAddSingleton<RebroadcasterHostedService>();
|
||||
if (configuration.IsPostgres())
|
||||
{
|
||||
services.AddHostedService<HostedServices.DatabaseSetupHostedService>();
|
||||
services.AddSingleton<IHostedService, IRepositoryProvider>(o => o.GetRequiredService<IRepositoryProvider>());
|
||||
services.TryAddSingleton<IRepositoryProvider, PostgresRepositoryProvider>();
|
||||
services.AddSingleton<DbConnectionFactory>();
|
||||
#if SUPPORT_DBTRIE
|
||||
if (configuration.GetOrDefault("AUTOMIGRATE", false))
|
||||
{
|
||||
services.AddHostedService<HostedServices.DBTrieToPostgresMigratorHostedService>();
|
||||
services.TryAddSingleton<ChainProvider>();
|
||||
services.TryAddSingleton<RepositoryProvider>();
|
||||
}
|
||||
#endif
|
||||
services.TryAddTransient<IUTXOService, PostgresMainController>();
|
||||
services.TryAddSingleton<PostgresIndexers>();
|
||||
services.TryAddSingleton<IIndexers>(o => o.GetRequiredService<PostgresIndexers>());
|
||||
services.AddSingleton<IHostedService, PostgresIndexers>(o => o.GetRequiredService<PostgresIndexers>());
|
||||
services.AddHostedService<HostedServices.DatabaseSetupHostedService>();
|
||||
services.AddSingleton<IHostedService, RepositoryProvider>(o => o.GetRequiredService<RepositoryProvider>());
|
||||
services.TryAddSingleton<RepositoryProvider>();
|
||||
services.AddSingleton<DbConnectionFactory>();
|
||||
services.TryAddSingleton<Indexers>();
|
||||
services.TryAddSingleton<Indexers>(o => o.GetRequiredService<Indexers>());
|
||||
services.AddSingleton<IHostedService, Indexers>(o => o.GetRequiredService<Indexers>());
|
||||
|
||||
services.AddSingleton<CheckMempoolTransactionsPeriodicTask>();
|
||||
services.AddSingleton<RefreshWalletHistoryPeriodicTask>();
|
||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(RefreshWalletHistoryPeriodicTask), TimeSpan.FromMinutes(30.0)));
|
||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(CheckMempoolTransactionsPeriodicTask), TimeSpan.FromMinutes(5.0)));
|
||||
services.AddHostedService<PeriodicTaskLauncherHostedService>();
|
||||
}
|
||||
#if SUPPORT_DBTRIE
|
||||
else
|
||||
{
|
||||
services.TryAddTransient<IUTXOService, MainController>();
|
||||
services.TryAddSingleton<ChainProvider>();
|
||||
services.AddSingleton<IHostedService, IRepositoryProvider>(o => o.GetRequiredService<IRepositoryProvider>());
|
||||
services.TryAddSingleton<IRepositoryProvider, RepositoryProvider>();
|
||||
services.TryAddSingleton<BitcoinDWaiters>();
|
||||
services.TryAddSingleton<IIndexers>(o => o.GetRequiredService<BitcoinDWaiters>());
|
||||
services.AddSingleton<IHostedService, BitcoinDWaiters>(o => o.GetRequiredService<BitcoinDWaiters>());
|
||||
services.AddSingleton<IHostedService, RebroadcasterHostedService>(o => o.GetRequiredService<RebroadcasterHostedService>());
|
||||
}
|
||||
#endif
|
||||
services.AddSingleton<CheckMempoolTransactionsPeriodicTask>();
|
||||
services.AddSingleton<RefreshWalletHistoryPeriodicTask>();
|
||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(RefreshWalletHistoryPeriodicTask), TimeSpan.FromMinutes(30.0)));
|
||||
services.AddTransient<ScheduledTask>(o => new ScheduledTask(typeof(CheckMempoolTransactionsPeriodicTask), TimeSpan.FromMinutes(5.0)));
|
||||
services.AddHostedService<PeriodicTaskLauncherHostedService>();
|
||||
|
||||
services.TryAddSingleton<EventAggregator>();
|
||||
services.TryAddSingleton<AddressPoolService>();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -10,14 +10,14 @@ namespace NBXplorer.HealthChecks
|
||||
{
|
||||
public NodesHealthCheck(
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
IIndexers indexers)
|
||||
Indexers indexers)
|
||||
{
|
||||
NetworkProvider = networkProvider;
|
||||
Indexers = indexers;
|
||||
}
|
||||
|
||||
public NBXplorerNetworkProvider NetworkProvider { get; }
|
||||
public IIndexers Indexers { get; }
|
||||
public Indexers Indexers { get; }
|
||||
|
||||
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Dapper;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Backend;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -12,7 +11,7 @@ namespace NBXplorer.HostedServices
|
||||
{
|
||||
public CheckMempoolTransactionsPeriodicTask(
|
||||
DbConnectionFactory dbConnectionFactory,
|
||||
IIndexers indexers,
|
||||
Indexers indexers,
|
||||
Broadcaster broadcaster)
|
||||
{
|
||||
DbConnectionFactory = dbConnectionFactory;
|
||||
@ -21,7 +20,7 @@ namespace NBXplorer.HostedServices
|
||||
}
|
||||
|
||||
public DbConnectionFactory DbConnectionFactory { get; }
|
||||
public IIndexers Indexers { get; }
|
||||
public Indexers Indexers { get; }
|
||||
public Broadcaster Broadcaster { get; }
|
||||
|
||||
public async Task Do(CancellationToken cancellationToken)
|
||||
|
||||
@ -1,974 +0,0 @@
|
||||
#if SUPPORT_DBTRIE
|
||||
extern alias DBTrieLib;
|
||||
|
||||
using Dapper;
|
||||
using DBTrieLib::DBTrie;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backends.DBTrie;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using static NBXplorer.Backends.Postgres.DbConnectionHelper;
|
||||
|
||||
namespace NBXplorer.HostedServices
|
||||
{
|
||||
public class DBTrieToPostgresMigratorHostedService : IHostedService
|
||||
{
|
||||
public class MigrationProgress
|
||||
{
|
||||
public bool EventsMigrated { get; set; }
|
||||
public bool KeyPathInformationMigrated { get; set; }
|
||||
public bool MetadataMigrated { get; set; }
|
||||
public bool HighestPathMigrated { get; set; }
|
||||
public bool AvailableKeysMigrated { get; set; }
|
||||
public bool SavedTransactionsMigrated { get; set; }
|
||||
public bool TrackedTransactionsMigrated { get; set; }
|
||||
public bool TrackedTransactionsInputsMigrated { get; set; }
|
||||
public bool BlocksMigrated { get; set; }
|
||||
public bool FullyUpdated => TrackedTransactionsInputsMigrated;
|
||||
}
|
||||
public DBTrieToPostgresMigratorHostedService(
|
||||
RepositoryProvider repositoryProvider,
|
||||
IRepositoryProvider postgresRepositoryProvider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IConfiguration configuration,
|
||||
KeyPathTemplates keyPathTemplates,
|
||||
IRPCClients rpcClients,
|
||||
DbConnectionFactory connectionFactory,
|
||||
ExplorerConfiguration explorerConfiguration)
|
||||
{
|
||||
LegacyRepositoryProvider = repositoryProvider;
|
||||
this.LoggerFactory = loggerFactory;
|
||||
Configuration = configuration;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
RpcClients = rpcClients;
|
||||
ConnectionFactory = connectionFactory;
|
||||
ExplorerConfiguration = explorerConfiguration;
|
||||
PostgresRepositoryProvider = (PostgresRepositoryProvider)postgresRepositoryProvider;
|
||||
}
|
||||
|
||||
public RepositoryProvider LegacyRepositoryProvider { get; }
|
||||
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
public IConfiguration Configuration { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
public IRPCClients RpcClients { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
public PostgresRepositoryProvider PostgresRepositoryProvider { get; }
|
||||
|
||||
bool started;
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!LegacyRepositoryProvider.Exists())
|
||||
return;
|
||||
|
||||
var migrationId = await PostgresRepositoryProvider.GetMigrationId();
|
||||
|
||||
var migrationState = LegacyRepositoryProvider.GetMigrationState();
|
||||
|
||||
if (migrationId != null && (migrationState.State != RepositoryProvider.MigrationState.NotStarted && migrationState.MigrationId != migrationId))
|
||||
{
|
||||
throw new ConfigException("The database has started migration of a different DBTrie backend.");
|
||||
}
|
||||
|
||||
if (migrationState.MigrationId is not null && migrationId != migrationState.MigrationId)
|
||||
{
|
||||
if (migrationState.State != RepositoryProvider.MigrationState.Done ||
|
||||
!Configuration.GetOrDefault<bool>("deleteaftermigration", false))
|
||||
{
|
||||
if (migrationState.State == RepositoryProvider.MigrationState.Done)
|
||||
{
|
||||
var error = "The DBTrie database has been been migrated to a different postgres database. Please do one of the following alternative: " + Environment.NewLine +
|
||||
"1. If this is a configuration mistake, switch the postgres database to the one you originally migrated to" + Environment.NewLine +
|
||||
"2. If you want to start NBXplorer on the postgres database, turn off --automigrate" + Environment.NewLine +
|
||||
$"3. If you want to migrate DBTrie on a new database, delete '{LegacyRepositoryProvider.GetMigrationLockPath()}'";
|
||||
throw new ConfigException(error);
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = "The DBTrie database is beeing migrated to a different postgres database. Please do one of the following alternative: " + Environment.NewLine +
|
||||
"1. If this is a configuration mistake, switch the postgres database you were originally migrating to" + Environment.NewLine +
|
||||
$"2. If you want to migrate DBTrie on a new database, delete '{LegacyRepositoryProvider.GetMigrationLockPath()}'";
|
||||
throw new ConfigException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (migrationState.State == RepositoryProvider.MigrationState.Done)
|
||||
{
|
||||
DeleteAfterMigrationOrWarning();
|
||||
return;
|
||||
}
|
||||
if (migrationState.State == RepositoryProvider.MigrationState.NotStarted)
|
||||
{
|
||||
if (migrationId != null)
|
||||
{
|
||||
throw new ConfigException("This postgres database is migrating (or migrated) a different DBTrie database.");
|
||||
}
|
||||
var id = RandomUtils.GetUInt256();
|
||||
migrationId = id.ToString();
|
||||
File.WriteAllText(LegacyRepositoryProvider.GetMigrationLockPath(), $"InProgress {migrationId}");
|
||||
await PostgresRepositoryProvider.SetMigrationId(id);
|
||||
}
|
||||
LegacyRepositoryProvider.MigrationMode = true;
|
||||
await LegacyRepositoryProvider.StartAsync(cancellationToken);
|
||||
started = true;
|
||||
Stopwatch w = new Stopwatch();
|
||||
w.Start();
|
||||
try
|
||||
{
|
||||
foreach (var legacyRepo in LegacyRepositoryProvider.GetRepositories())
|
||||
{
|
||||
var postgresRepo = PostgresRepositoryProvider.GetRepository(legacyRepo.Network);
|
||||
await Migrate(legacyRepo.Network, legacyRepo, (PostgresRepository)postgresRepo,
|
||||
LoggerFactory.CreateLogger($"NBXplorer.PostgresMigration.{legacyRepo.Network.CryptoCode}"), cancellationToken);
|
||||
}
|
||||
}
|
||||
catch when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var logger = LoggerFactory.CreateLogger($"NBXplorer.PostgresMigration");
|
||||
await using (var conn = await ConnectionFactory.CreateConnection(builder =>
|
||||
{
|
||||
builder.CommandTimeout = Constants.FifteenMinutes;
|
||||
}))
|
||||
{
|
||||
logger.LogInformation($"Running ANALYZE and VACUUM FULL...");
|
||||
try
|
||||
{
|
||||
await conn.ExecuteAsync("VACUUM FULL;");
|
||||
await conn.ExecuteAsync("ANALYZE;");
|
||||
}
|
||||
// Don't care if it fails
|
||||
catch { }
|
||||
}
|
||||
w.Stop();
|
||||
logger.LogInformation($"The migration completed in {(int)w.Elapsed.TotalMinutes} minutes.");
|
||||
File.WriteAllText(LegacyRepositoryProvider.GetMigrationLockPath(), $"Done {migrationId}");
|
||||
DeleteAfterMigrationOrWarning();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
private void DeleteAfterMigrationOrWarning()
|
||||
{
|
||||
var logger = LoggerFactory.CreateLogger("NBXplorer.PostgresMigration");
|
||||
if (!Configuration.GetOrDefault<bool>("deleteaftermigration", false))
|
||||
logger.LogWarning($"A legacy DBTrie database has been previously migrated to postgres and is still present. You can safely delete it if you do not expect using it in the future. To delete the old DBTrie database, start NBXplorer with --deleteaftermigration (or environment variable: NBXPLORER_DELETEAFTERMIGRATION=1)");
|
||||
else
|
||||
{
|
||||
Directory.Delete(LegacyRepositoryProvider.GetDatabasePath(), true);
|
||||
logger.LogInformation($"Old migrated legacy DBTrie database has been deleted");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RegisterTypes(System.Data.Common.DbConnection conn)
|
||||
{
|
||||
var pconn = (Npgsql.NpgsqlConnection)conn;
|
||||
try
|
||||
{
|
||||
await pconn.ExecuteAsync(
|
||||
"CREATE TYPE m_txs AS (tx_id TEXT, raw BYTEA, seen_at TIMESTAMPTZ);" +
|
||||
"CREATE TYPE m_blks_txs AS (tx_id TEXT, blk_id TEXT);" +
|
||||
"CREATE TYPE m_evt AS (id BIGINT, type TEXT, data JSONB);");
|
||||
}
|
||||
// They may already exists
|
||||
catch { }
|
||||
pconn.ReloadTypes();
|
||||
pconn.TypeMapper.MapComposite<UpdateTransaction>("m_txs");
|
||||
pconn.TypeMapper.MapComposite<UpdateBlockTransaction>("m_blks_txs");
|
||||
pconn.TypeMapper.MapComposite<InsertEvents>("m_evt");
|
||||
}
|
||||
private async Task UnregisterTypes(System.Data.Common.DbConnection conn)
|
||||
{
|
||||
var pconn = (Npgsql.NpgsqlConnection)conn;
|
||||
pconn.TypeMapper.UnmapComposite<UpdateTransaction>("m_txs");
|
||||
pconn.TypeMapper.UnmapComposite<UpdateBlockTransaction>("m_blks_txs");
|
||||
pconn.TypeMapper.UnmapComposite<InsertEvents>("m_evt");
|
||||
await pconn.ExecuteAsync(
|
||||
"DROP TYPE m_txs;" +
|
||||
"DROP TYPE m_blks_txs;" +
|
||||
"DROP TYPE m_evt;");
|
||||
pconn.ReloadTypes();
|
||||
}
|
||||
|
||||
|
||||
record InsertEvents(long id, string type, string data);
|
||||
record InsertDescriptor(string code, string descriptor, string metadata, string wallet_id);
|
||||
record InsertMetadata(string wallet_id, string key, string value);
|
||||
record UpdateNextIndex(string code, string descriptor, long next_idx);
|
||||
record UpdateUsedScript(string code, string descriptor, long idx);
|
||||
record UpdateBlock(string code, string blk_id, string prev_id, long height);
|
||||
record UpdateTransaction(string tx_id, byte[] raw, DateTime seen_at);
|
||||
record UpdateBlockTransaction(string tx_id, string blk_id);
|
||||
private async Task Migrate(NBXplorerNetwork network, Repository legacyRepo, PostgresRepository postgresRepo, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
using var conn = await postgresRepo.ConnectionFactory.CreateConnection(builder =>
|
||||
{
|
||||
builder.CommandTimeout = 120;
|
||||
});
|
||||
var data = await conn.QueryFirstOrDefaultAsync<string>("SELECT data_json FROM nbxv1_settings WHERE code=@code AND key='MigrationProgress'", new { code = network.CryptoCode });
|
||||
var progress = data is null ? new MigrationProgress() : JsonConvert.DeserializeObject<MigrationProgress>(data);
|
||||
if (progress.FullyUpdated)
|
||||
return;
|
||||
await RegisterTypes(conn);
|
||||
if (!progress.EventsMigrated)
|
||||
{
|
||||
if (!ExplorerConfiguration.NoMigrateEvents)
|
||||
{
|
||||
using (var tx = await conn.BeginTransactionAsync())
|
||||
{
|
||||
logger.LogInformation($"Migrating events to postgres...");
|
||||
long lastEventId = -1;
|
||||
nextbatch:
|
||||
var batch = await legacyRepo.GetEvents(lastEventId, 1000);
|
||||
if (batch.Count > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var parameters = batch.Select(e => new InsertEvents(
|
||||
e.EventId,
|
||||
e.EventType,
|
||||
e.ToJson(network.JsonSerializerSettings)
|
||||
)).ToArray();
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO nbxv1_evts " +
|
||||
"SELECT @code, id, type, data " +
|
||||
"FROM unnest(@records)",
|
||||
new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = parameters
|
||||
});
|
||||
lastEventId = parameters.Select(p => p.id).Max();
|
||||
goto nextbatch;
|
||||
}
|
||||
await conn.ExecuteAsync("INSERT INTO nbxv1_evts_ids AS ei VALUES (@code, @curr_id) ON CONFLICT (code) DO UPDATE SET curr_id=@curr_id WHERE ei.curr_id < @curr_id", new { code = network.CryptoCode, curr_id = lastEventId });
|
||||
progress.EventsMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Events migrated.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Events migration skipped");
|
||||
}
|
||||
}
|
||||
Dictionary<string, TrackedSource> hashToTrackedSource = new Dictionary<string, TrackedSource>();
|
||||
if (!progress.KeyPathInformationMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating scripts to postgres...");
|
||||
using (var tx = await conn.BeginTransactionAsync())
|
||||
{
|
||||
List<KeyPathInformation> batch = new List<KeyPathInformation>(10_000);
|
||||
HashSet<string> processedWalletKeys = new HashSet<string>();
|
||||
List<PostgresRepository.WalletKey> walletKeys = new List<PostgresRepository.WalletKey>();
|
||||
HashSet<(string code, string descriptor)> processedDescriptors = new HashSet<(string code, string descriptor)>();
|
||||
List<InsertDescriptor> descriptors = new List<InsertDescriptor>();
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var scriptsTable = legacyTx.GetTable($"{legacyRepo._Suffix}Scripts");
|
||||
var total = await scriptsTable.GetRecordCount();
|
||||
int migrated = 0;
|
||||
// The triggers update the next_idx, used and gap fields of descriptors.
|
||||
// Those make the insert very slow, and those are updated when reserverd and available scripts
|
||||
// are imported later.
|
||||
await conn.ExecuteAsync(
|
||||
"ALTER TABLE descriptors_scripts " +
|
||||
"DISABLE TRIGGER USER; " +
|
||||
"ALTER TABLE descriptors_scripts " +
|
||||
"ENABLE TRIGGER descriptors_scripts_wallets_scripts_trigger;");
|
||||
await foreach (var row in scriptsTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
migrated++;
|
||||
if (migrated % 10_000 == 0)
|
||||
logger.LogInformation($"Progress: " + (int)(((double)migrated / (double)total) * 100.0) + "%");
|
||||
using (row)
|
||||
{
|
||||
var keyInfo = legacyRepo.ToObject<KeyPathInformation>(await row.ReadValue())
|
||||
.AddAddress(network.NBitcoinNetwork);
|
||||
hashToTrackedSource.TryAdd(keyInfo.TrackedSource.GetHash().ToString(), keyInfo.TrackedSource);
|
||||
batch.Add(keyInfo);
|
||||
if (keyInfo.TrackedSource is DerivationSchemeTrackedSource ts)
|
||||
{
|
||||
var keyTemplate = KeyPathTemplates.GetKeyPathTemplate(keyInfo.Feature);
|
||||
var k = postgresRepo.GetDescriptorKey(ts.DerivationStrategy, keyInfo.Feature);
|
||||
if (processedDescriptors.Add((k.code, k.descriptor)))
|
||||
descriptors.Add(new InsertDescriptor(k.code, k.descriptor, network.Serializer.ToString(new LegacyDescriptorMetadata()
|
||||
{
|
||||
Derivation = ts.DerivationStrategy,
|
||||
Feature = keyInfo.Feature,
|
||||
KeyPathTemplate = keyTemplate,
|
||||
Type = LegacyDescriptorMetadata.TypeName
|
||||
}),
|
||||
postgresRepo.GetWalletKey(ts.DerivationStrategy).wid));
|
||||
}
|
||||
var wk = postgresRepo.GetWalletKey(keyInfo.TrackedSource);
|
||||
if (processedWalletKeys.Add(wk.wid))
|
||||
walletKeys.Add(wk);
|
||||
if (batch.Count >= 10_000)
|
||||
{
|
||||
await CreateWalletAndDescriptor(conn, walletKeys, descriptors);
|
||||
walletKeys.Clear();
|
||||
descriptors.Clear();
|
||||
await postgresRepo.SaveKeyInformations(conn, batch.ToArray());
|
||||
batch.Clear();
|
||||
walletKeys.Clear();
|
||||
descriptors.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await CreateWalletAndDescriptor(conn, walletKeys, descriptors);
|
||||
await postgresRepo.SaveKeyInformations(conn, batch.ToArray());
|
||||
|
||||
await conn.ExecuteAsync("UPDATE descriptors_scripts SET used='t'");
|
||||
await conn.ExecuteAsync(
|
||||
"ALTER TABLE descriptors_scripts " +
|
||||
"ENABLE TRIGGER USER;");
|
||||
|
||||
await conn.ExecuteAsync("CREATE TABLE tmp_mapping_tracked_sources AS SELECT * FROM unnest (@a, @b) AS r (hash, descriptor)", new
|
||||
{
|
||||
a = hashToTrackedSource.Select(k => k.Key).ToArray(),
|
||||
b = hashToTrackedSource.Select(k => k.Value.ToString()).ToArray()
|
||||
});
|
||||
logger.LogInformation($"Scripts migrated.");
|
||||
progress.KeyPathInformationMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (hashToTrackedSource.Count is 0)
|
||||
{
|
||||
// We didn't run keypath migration
|
||||
logger.LogInformation($"Scanning the tracked source...");
|
||||
foreach (var r in await conn.QueryAsync<(string hash, string derivation)>(
|
||||
"SELECT hash, descriptor " +
|
||||
"FROM tmp_mapping_tracked_sources"))
|
||||
{
|
||||
hashToTrackedSource.TryAdd(r.hash, TrackedSource.Parse(r.derivation, network));
|
||||
}
|
||||
}
|
||||
|
||||
if (!progress.MetadataMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating metadata to postgres...");
|
||||
using (var tx = await conn.BeginTransactionAsync())
|
||||
{
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var metadataTable = legacyTx.GetTable($"{legacyRepo._Suffix}Metadata");
|
||||
List<InsertMetadata> batch = new List<InsertMetadata>(1000);
|
||||
await foreach (var row in metadataTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (row)
|
||||
{
|
||||
var v = network.Serializer.ToObject<JToken>(legacyRepo.Unzip(await row.ReadValue()));
|
||||
var s = Encoding.UTF8.GetString(row.Key.Span).Split('-');
|
||||
var trackedSource = hashToTrackedSource[s[0]];
|
||||
var key = s[1];
|
||||
batch.Add(new InsertMetadata(postgresRepo.GetWalletKey(trackedSource).wid, key, v.ToString(Formatting.None)));
|
||||
if (batch.Count >= 1000)
|
||||
{
|
||||
await conn.ExecuteAsync("INSERT INTO nbxv1_metadata VALUES (@wallet_id, @key, @value::JSONB)", batch);
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await conn.ExecuteAsync("INSERT INTO nbxv1_metadata VALUES (@wallet_id, @key, @value::JSONB)", batch);
|
||||
logger.LogInformation($"metadata migrated.");
|
||||
progress.MetadataMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
};
|
||||
}
|
||||
|
||||
if (!progress.HighestPathMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating highest path to postgres...");
|
||||
using (var tx = await conn.BeginTransactionAsync())
|
||||
{
|
||||
var batch = new List<UpdateNextIndex>(100);
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var highestPath = legacyTx.GetTable($"{legacyRepo._Suffix}HighestPath");
|
||||
await foreach (var row in highestPath.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (row)
|
||||
{
|
||||
var s = Encoding.UTF8.GetString(row.Key.Span).Split('-');
|
||||
var feature = Enum.Parse<DerivationFeature>(s[1]);
|
||||
var scheme = ((DerivationSchemeTrackedSource)hashToTrackedSource[s[0]]).DerivationStrategy;
|
||||
var key = postgresRepo.GetDescriptorKey(scheme, feature);
|
||||
var v = await row.ReadValue();
|
||||
uint value = System.Buffers.Binary.BinaryPrimitives.ReadUInt32BigEndian(v.Span);
|
||||
value = value & ~0x8000_0000U;
|
||||
batch.Add(new UpdateNextIndex(key.code, key.descriptor, value + 1));
|
||||
if (batch.Count >= 100)
|
||||
{
|
||||
await conn.ExecuteAsync("UPDATE descriptors SET next_idx=@next_idx WHERE code=@code AND descriptor=@descriptor", batch);
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await conn.ExecuteAsync("UPDATE descriptors SET next_idx=@next_idx WHERE code=@code AND descriptor=@descriptor", batch);
|
||||
logger.LogInformation($"highest path migrated.");
|
||||
progress.HighestPathMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (!progress.AvailableKeysMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating available keys to postgres...");
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
var batch = new List<PostgresRepository.DescriptorScriptInsert>(10_000);
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var availableTable = legacyTx.GetTable($"{legacyRepo._Suffix}AvailableKeys");
|
||||
var total = await availableTable.GetRecordCount();
|
||||
int migrated = 0;
|
||||
await conn.ExecuteAsync(
|
||||
"ALTER TABLE descriptors_scripts " +
|
||||
"DISABLE TRIGGER USER;");
|
||||
await foreach (var row in availableTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
migrated++;
|
||||
if (migrated % 10_000 == 0)
|
||||
logger.LogInformation($"Progress: " + (int)(((double)migrated / (double)total) * 100.0) + "%");
|
||||
using (row)
|
||||
{
|
||||
var s = Encoding.UTF8.GetString(row.Key.Span).Split('-');
|
||||
if (!Enum.TryParse<DerivationFeature>(s[1], out var feature))
|
||||
continue; // This should never happen, but one user got a corruption on DBTrie before and this happen...
|
||||
var scheme = ((DerivationSchemeTrackedSource)hashToTrackedSource[s[0]]).DerivationStrategy;
|
||||
var key = postgresRepo.GetDescriptorKey(scheme, feature);
|
||||
var idx = int.Parse(s[^1]);
|
||||
batch.Add(new PostgresRepository.DescriptorScriptInsert(key.descriptor, idx, null, null, null, true));
|
||||
if (batch.Count >= 10_000)
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE descriptors_scripts ds SET used='f' " +
|
||||
"FROM unnest(@records) r " +
|
||||
"WHERE ds.code=@code AND ds.descriptor=r.descriptor AND ds.idx=r.idx AND ds.used IS TRUE",
|
||||
new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = batch
|
||||
});
|
||||
batch.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE descriptors_scripts ds SET used='f' " +
|
||||
"FROM unnest(@records) r " +
|
||||
"WHERE ds.code=@code AND ds.descriptor=r.descriptor AND ds.idx=r.idx AND ds.used IS TRUE",
|
||||
new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = batch
|
||||
});
|
||||
// Update the gap of descriptors
|
||||
await conn.ExecuteAsync(
|
||||
"WITH cte AS (SELECT descriptor, MAX(ds.idx) last_idx FROM descriptors_scripts ds WHERE ds.code=@code AND ds.used IS TRUE GROUP BY descriptor)" +
|
||||
"UPDATE descriptors d " +
|
||||
"SET gap = COALESCE(next_idx - (SELECT last_idx FROM cte WHERE descriptor=d.descriptor) - 1, next_idx) " +
|
||||
"WHERE code=@code", new { code = network.CryptoCode });
|
||||
await conn.ExecuteAsync(
|
||||
"ALTER TABLE descriptors_scripts " +
|
||||
"ENABLE TRIGGER USER;");
|
||||
progress.AvailableKeysMigrated = true;
|
||||
|
||||
// Somehow, it seems some descriptors need to generate a few addresses.
|
||||
foreach (var desc in await conn.QueryAsync("SELECT metadata FROM descriptors WHERE code=@code AND gap < @minGap", new { code = network.CryptoCode, minGap = ExplorerConfiguration.MinGapSize }))
|
||||
{
|
||||
LegacyDescriptorMetadata metadata = network.Serializer.ToObject<LegacyDescriptorMetadata>((string)desc.metadata);
|
||||
if (KeyPathTemplates.GetSupportedDerivationFeatures().Contains(metadata.Feature))
|
||||
{
|
||||
await postgresRepo.GenerateAddressesCore(conn, metadata.Derivation, metadata.Feature, null);
|
||||
}
|
||||
}
|
||||
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Available keys migrated.");
|
||||
}
|
||||
|
||||
if (!progress.BlocksMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating blocks to postgres...");
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
HashSet<uint256> blocksToFetch = new HashSet<uint256>();
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var savedTxsTable = legacyTx.GetTable($"{legacyRepo._Suffix}Txs");
|
||||
await foreach (var row in savedTxsTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (row)
|
||||
{
|
||||
if (row.Key.Length == 64)
|
||||
{
|
||||
blocksToFetch.Add(new uint256(row.Key.Span.Slice(32, 32)));
|
||||
}
|
||||
}
|
||||
}
|
||||
var trackedTxs = legacyTx.GetTable($"{legacyRepo._Suffix}Transactions");
|
||||
await foreach (var row in trackedTxs.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (row)
|
||||
{
|
||||
var key = TrackedTransactionKey.Parse(row.Key.Span);
|
||||
if (key.BlockHash is not null)
|
||||
blocksToFetch.Add(key.BlockHash);
|
||||
}
|
||||
}
|
||||
|
||||
var indexProgress = await legacyRepo.GetIndexProgress(legacyTx);
|
||||
if (indexProgress?.Blocks is not null)
|
||||
{
|
||||
foreach (var b in indexProgress.Blocks)
|
||||
blocksToFetch.Add(b);
|
||||
}
|
||||
logger.LogInformation($"Blocks to import: " + blocksToFetch.Count);
|
||||
IGetBlockHeaders getBlocks = await GetBlockProvider(network, logger);
|
||||
foreach (var batch in blocksToFetch.Batch(500))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var rpc = RpcClients.Get(network);
|
||||
var update = await getBlocks.GetUpdateBlocks(batch);
|
||||
await conn.ExecuteAsync("INSERT INTO blks VALUES (@code, @blk_id, @height, @prev_id, 't')", update);
|
||||
}
|
||||
|
||||
// Here, we just make sure the index progress we save only have confirmed blocks.
|
||||
// differences may happen if loading from slim-chain when the node crashed.
|
||||
if (indexProgress?.Blocks is not null)
|
||||
{
|
||||
var confirmedBlocks = await getBlocks.GetUpdateBlocks(indexProgress.Blocks);
|
||||
var locator = new BlockLocator();
|
||||
foreach (var b in confirmedBlocks)
|
||||
{
|
||||
locator.Blocks.Add(new uint256(b.blk_id));
|
||||
}
|
||||
await postgresRepo.SetIndexProgress(conn, locator);
|
||||
}
|
||||
|
||||
progress.BlocksMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Blocks migrated.");
|
||||
}
|
||||
|
||||
if (!progress.SavedTransactionsMigrated)
|
||||
{
|
||||
if (!ExplorerConfiguration.NoMigrateRawTxs)
|
||||
{
|
||||
logger.LogInformation($"Migrating raw transactions...");
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
var batchTxs = new List<UpdateTransaction>(1000);
|
||||
var batchBlocksTxs = new List<UpdateBlockTransaction>(1000);
|
||||
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var savedTxsTable = legacyTx.GetTable($"{legacyRepo._Suffix}Txs");
|
||||
var total = await savedTxsTable.GetRecordCount();
|
||||
HashSet<uint256> processedTxs = new HashSet<uint256>();
|
||||
long migrated = 0;
|
||||
await foreach (var row in savedTxsTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
migrated++;
|
||||
if (migrated % 10_000 == 0)
|
||||
logger.LogInformation($"Progress: " + (int)(((double)migrated / (double)total) * 100.0) + "%");
|
||||
using (row)
|
||||
{
|
||||
var txId = new uint256(row.Key.Span.Slice(0, 32));
|
||||
if (!processedTxs.Add(txId))
|
||||
continue;
|
||||
var savedTx = Repository.ToSavedTransaction(network.NBitcoinNetwork, row.Key, await row.ReadValue());
|
||||
if (savedTx.BlockHash is not null)
|
||||
{
|
||||
batchBlocksTxs.Add(new UpdateBlockTransaction(savedTx.Transaction.GetHash().ToString(), savedTx.BlockHash.ToString()));
|
||||
}
|
||||
batchTxs.Add(new UpdateTransaction(savedTx.Transaction.GetHash().ToString(), savedTx.Transaction.ToBytes(), savedTx.Timestamp.UtcDateTime));
|
||||
if (batchTxs.Count >= 1000)
|
||||
{
|
||||
await InsertTransactions(conn, network, batchTxs, batchBlocksTxs);
|
||||
batchBlocksTxs.Clear();
|
||||
batchTxs.Clear();
|
||||
processedTxs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await InsertTransactions(conn, network, batchTxs, batchBlocksTxs);
|
||||
batchTxs.Clear();
|
||||
batchBlocksTxs.Clear();
|
||||
progress.SavedTransactionsMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Raw transactions migrated.");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation("Raw transactions migration skipped");
|
||||
}
|
||||
}
|
||||
|
||||
if (!progress.TrackedTransactionsMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating tracked transactions and outputs...");
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var savedTxsTable = legacyTx.GetTable($"{legacyRepo._Suffix}Transactions");
|
||||
var total = await savedTxsTable.GetRecordCount();
|
||||
long migrated = 0;
|
||||
List<UpdateTransaction> batchTxs = new List<UpdateTransaction>(1000);
|
||||
List<NewOutRaw> outputs = new List<NewOutRaw>(1000);
|
||||
var batchBlocksTxs = new List<UpdateBlockTransaction>(1000);
|
||||
HashSet<(uint256, string)> processedTxs = new HashSet<(uint256, string)>();
|
||||
await foreach (var row in savedTxsTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
migrated++;
|
||||
if (migrated % 10_000 == 0)
|
||||
logger.LogInformation($"Progress: " + (int)(((double)migrated / (double)total) * 100.0) + "%");
|
||||
using (row)
|
||||
{
|
||||
var trackedSourceHash = Encoding.UTF8.GetString(row.Key.Span).Split('-')[0];
|
||||
TrackedTransaction tt = await ToTrackedTransaction(network, legacyRepo, hashToTrackedSource, row);
|
||||
if (tt.BlockHash is not null)
|
||||
{
|
||||
batchBlocksTxs.Add(new UpdateBlockTransaction(tt.TransactionHash.ToString(), tt.BlockHash.ToString()));
|
||||
}
|
||||
if (processedTxs.Add((tt.TransactionHash, trackedSourceHash)))
|
||||
{
|
||||
batchTxs.Add(new UpdateTransaction(tt.TransactionHash.ToString(), null, tt.FirstSeen.UtcDateTime));
|
||||
foreach (var o in tt.GetReceivedOutputs())
|
||||
{
|
||||
long value;
|
||||
string assetId;
|
||||
if (o.Value is Money m)
|
||||
{
|
||||
value = m.Satoshi;
|
||||
assetId = "";
|
||||
}
|
||||
else if (o.Value is AssetMoney am)
|
||||
{
|
||||
value = am.Quantity;
|
||||
assetId = am.AssetId.ToString();
|
||||
}
|
||||
else if (o.Value is null)
|
||||
{
|
||||
value = 1;
|
||||
assetId = NBXplorerNetwork.UnknownAssetId;
|
||||
}
|
||||
else
|
||||
continue;
|
||||
outputs.Add(new NewOutRaw(
|
||||
tt.TransactionHash.ToString(),
|
||||
o.Index,
|
||||
o.ScriptPubKey.ToHex(),
|
||||
value,
|
||||
assetId));
|
||||
}
|
||||
}
|
||||
if (batchTxs.Count >= 1000)
|
||||
{
|
||||
await InsertTransactions(conn, network, batchTxs, batchBlocksTxs);
|
||||
await InsertOutsMatches(conn, network, outputs);
|
||||
outputs.Clear();
|
||||
batchTxs.Clear();
|
||||
batchBlocksTxs.Clear();
|
||||
processedTxs.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
await InsertTransactions(conn, network, batchTxs, batchBlocksTxs);
|
||||
await InsertOutsMatches(conn, network, outputs);
|
||||
processedTxs.Clear();
|
||||
outputs.Clear();
|
||||
batchTxs.Clear();
|
||||
batchBlocksTxs.Clear();
|
||||
progress.TrackedTransactionsMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Tracked transactions migrated.");
|
||||
}
|
||||
|
||||
if (!progress.TrackedTransactionsInputsMigrated)
|
||||
{
|
||||
logger.LogInformation($"Migrating tracked transactions inputs...");
|
||||
using var tx = await conn.BeginTransactionAsync();
|
||||
using var legacyTx = await legacyRepo.engine.OpenTransaction();
|
||||
var savedTxsTable = legacyTx.GetTable($"{legacyRepo._Suffix}Transactions");
|
||||
var total = await savedTxsTable.GetRecordCount();
|
||||
long migrated = 0;
|
||||
List<NewInRaw> batch = new List<NewInRaw>(2000);
|
||||
List<NewInRaw> filteredBatch = new List<NewInRaw>(10_000);
|
||||
HashSet<uint256> processTxs = new HashSet<uint256>();
|
||||
await foreach (var row in savedTxsTable.Enumerate())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
migrated++;
|
||||
if (migrated % 5_000 == 0)
|
||||
logger.LogInformation($"Progress: " + (int)(((double)migrated / (double)total) * 100.0) + "%");
|
||||
using (row)
|
||||
{
|
||||
TrackedTransaction tt = await ToTrackedTransaction(network, legacyRepo, hashToTrackedSource, row);
|
||||
if (tt.Key.IsPruned)
|
||||
continue;
|
||||
if (!processTxs.Add(tt.Key.TxId))
|
||||
continue;
|
||||
foreach (var o in tt.SpentOutpoints)
|
||||
{
|
||||
batch.Add(new NewInRaw(
|
||||
tt.TransactionHash.ToString(),
|
||||
tt.IndexOfInput(o),
|
||||
o.Hash.ToString(),
|
||||
o.N));
|
||||
|
||||
if (batch.Count >= 2000)
|
||||
{
|
||||
await FindInsMatches(network, conn, batch, filteredBatch);
|
||||
processTxs.Clear();
|
||||
}
|
||||
if (filteredBatch.Count >= 10_000)
|
||||
{
|
||||
await InsertInsMatches(conn, network, filteredBatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await FindInsMatches(network, conn, batch, filteredBatch);
|
||||
await InsertInsMatches(conn, network, filteredBatch);
|
||||
await conn.ExecuteAsync("DROP TABLE tmp_mapping_tracked_sources;");
|
||||
await UnregisterTypes(conn);
|
||||
progress.TrackedTransactionsInputsMigrated = true;
|
||||
await SaveProgress(network, conn, progress);
|
||||
await tx.CommitAsync();
|
||||
logger.LogInformation($"Tracked transactions inputs migrated.");
|
||||
}
|
||||
|
||||
// Remove transactions which doesn't have any input or outputs
|
||||
await conn.ExecuteAsync(
|
||||
"DELETE FROM txs t " +
|
||||
"WHERE (t.code, t.tx_id) IN ( " +
|
||||
" SELECT t.code, t.tx_id FROM txs t " +
|
||||
" LEFT JOIN outs o ON t.code=o.code AND t.tx_id=o.tx_id " +
|
||||
" LEFT JOIN ins i ON t.code=i.code AND t.tx_id=i.tx_id " +
|
||||
" WHERE o.tx_id IS NULL AND i.tx_id IS NULL " +
|
||||
");");
|
||||
}
|
||||
|
||||
private static async Task InsertOutsMatches(System.Data.Common.DbConnection conn, NBXplorerNetwork network, List<NewOutRaw> outputs)
|
||||
{
|
||||
await conn.ExecuteAsync("INSERT INTO outs SELECT @code, tx_id, idx, script, value, asset_id FROM unnest(@records) ON CONFLICT DO NOTHING;", new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = outputs
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task InsertInsMatches(System.Data.Common.DbConnection conn, NBXplorerNetwork network, List<NewInRaw> filteredBatch)
|
||||
{
|
||||
await conn.ExecuteAsync("INSERT INTO ins SELECT @code, tx_id, idx, spent_tx_id, spent_idx FROM unnest(@records) ON CONFLICT DO NOTHING", new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = filteredBatch
|
||||
});
|
||||
filteredBatch.Clear();
|
||||
}
|
||||
|
||||
private static async Task FindInsMatches(NBXplorerNetwork network, System.Data.Common.DbConnection conn, List<NewInRaw> batch, List<NewInRaw> filteredBatch)
|
||||
{
|
||||
var matchedIns = new HashSet<(string tx_id, long idx)>();
|
||||
// We could do in one request, but it is too slow for big installs...
|
||||
foreach (var r in await conn.QueryAsync<(string tx_id, long idx)>(
|
||||
"SELECT o.tx_id, o.idx FROM outs o " +
|
||||
"JOIN unnest(@outpoints) p ON o.code=@code AND o.tx_id=p.tx_id AND o.idx=p.idx",
|
||||
new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
outpoints = batch.Select(i => new DbConnectionHelper.OutpointRaw(i.spent_tx_id, i.spent_idx)).ToArray()
|
||||
}))
|
||||
matchedIns.Add(r);
|
||||
foreach (var i in batch)
|
||||
{
|
||||
if (matchedIns.Contains((i.spent_tx_id, i.spent_idx)))
|
||||
filteredBatch.Add(i);
|
||||
}
|
||||
batch.Clear();
|
||||
}
|
||||
|
||||
private async Task InsertTransactions(System.Data.Common.DbConnection conn, NBXplorerNetwork network, List<UpdateTransaction> batchTxs, List<UpdateBlockTransaction> batchBlocksTxs)
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO txs AS t (code, tx_id, raw, seen_at) " +
|
||||
"SELECT @code, tx_id, raw, MIN(seen_at) FROM unnest(@records) " +
|
||||
"GROUP BY tx_id, raw " +
|
||||
"ON CONFLICT (code, tx_id) DO UPDATE SET raw=COALESCE(t.raw, EXCLUDED.raw), seen_at=LEAST(t.seen_at, EXCLUDED.seen_at) " +
|
||||
"WHERE (t.seen_at > EXCLUDED.seen_at) OR (t.raw IS NULL AND EXCLUDED.raw IS NOT NULL);", new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = batchTxs
|
||||
});
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO blks_txs (code, tx_id, blk_id) " +
|
||||
"SELECT @code code, tx_id, r.blk_id " +
|
||||
"FROM unnest(@records) r " +
|
||||
"JOIN blks b ON b.code=@code AND b.blk_id=r.blk_id " +
|
||||
"ON CONFLICT DO NOTHING;", new
|
||||
{
|
||||
code = network.CryptoCode,
|
||||
records = batchBlocksTxs
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IGetBlockHeaders> GetBlockProvider(NBXplorerNetwork network, ILogger logger)
|
||||
{
|
||||
IGetBlockHeaders getBlocks = null;
|
||||
using (var token = new CancellationTokenSource(20_000))
|
||||
{
|
||||
try
|
||||
{
|
||||
var rpc = RpcClients.Get(network);
|
||||
await RPCArgs.TestRPCAsync(network, rpc, token.Token, logger);
|
||||
getBlocks = new RPCGetBlockHeaders(rpc);
|
||||
logger.LogInformation($"Getting blocks from RPC...");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogInformation($"Unable to access the full node for block import, fall back the local chain-slim.dat. ({ex.Message})");
|
||||
var suffix = network.CryptoCode == "BTC" ? "" : network.CryptoCode;
|
||||
var slimCachePath = Path.Combine(ExplorerConfiguration.DataDir, $"{suffix}chain-slim.dat");
|
||||
if (!File.Exists(slimCachePath))
|
||||
throw new ConfigException($"Impossible to get the blocks from RPC, nor from {slimCachePath}");
|
||||
|
||||
logger.LogInformation($"Getting blocks from chain-slim.dat...");
|
||||
using (var file = new FileStream(slimCachePath, FileMode.Open, FileAccess.Read, FileShare.None, 1024 * 1024))
|
||||
{
|
||||
var chain = new SlimChain(network.NBitcoinNetwork.GenesisHash, (int)(((double)file.Length / 32.0) * 1.05));
|
||||
chain.Load(file);
|
||||
getBlocks = new SlimChainGetBlockHeaders(network, chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return getBlocks;
|
||||
}
|
||||
|
||||
private static async Task<TrackedTransaction> ToTrackedTransaction(NBXplorerNetwork network, Repository legacyRepo, Dictionary<string, TrackedSource> hashToTrackedSource, IRow row)
|
||||
{
|
||||
var seg = DBTrieLib.DBTrie.PublicExtensions.GetUnderlyingArraySegment(await row.ReadValue());
|
||||
MemoryStream ms = new MemoryStream(seg.Array, seg.Offset, seg.Count);
|
||||
BitcoinStream bs = new BitcoinStream(ms, false);
|
||||
bs.ConsensusFactory = network.NBitcoinNetwork.Consensus.ConsensusFactory;
|
||||
var trackedSerializable = legacyRepo.CreateBitcoinSerializableTrackedTransaction(TrackedTransactionKey.Parse(row.Key.Span));
|
||||
trackedSerializable.ReadWrite(bs);
|
||||
var trackedSource = hashToTrackedSource[Encoding.UTF8.GetString(row.Key.Span).Split('-')[0]];
|
||||
var tt = legacyRepo.ToTrackedTransaction(trackedSerializable, trackedSource);
|
||||
return tt;
|
||||
}
|
||||
|
||||
private static async Task CreateWalletAndDescriptor(System.Data.Common.DbConnection conn, List<PostgresRepository.WalletKey> walletKeys, List<InsertDescriptor> descriptors)
|
||||
{
|
||||
await conn.ExecuteAsync("INSERT INTO wallets VALUES (@wid, @metadata::JSONB) ON CONFLICT DO NOTHING;", walletKeys);
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO descriptors VALUES (@code, @descriptor, @metadata::JSONB) ON CONFLICT DO NOTHING;" +
|
||||
"INSERT INTO wallets_descriptors (code, descriptor, wallet_id) VALUES (@code, @descriptor, @wallet_id) ON CONFLICT DO NOTHING", descriptors);
|
||||
}
|
||||
|
||||
private static async Task SaveProgress(NBXplorerNetwork network, System.Data.Common.DbConnection conn, MigrationProgress progress)
|
||||
{
|
||||
await conn.ExecuteAsync(
|
||||
"INSERT INTO nbxv1_settings (code, key, data_json) VALUES (@code, 'MigrationProgress', @data::JSONB) " +
|
||||
"ON CONFLICT (code, key) DO " +
|
||||
"UPDATE SET data_json=EXCLUDED.data_json;", new { code = network.CryptoCode, data = JsonConvert.SerializeObject(progress) });
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (started)
|
||||
await LegacyRepositoryProvider.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
interface IGetBlockHeaders
|
||||
{
|
||||
Task<IList<UpdateBlock>> GetUpdateBlocks(IList<uint256> blockHashes);
|
||||
}
|
||||
class SlimChainGetBlockHeaders : IGetBlockHeaders
|
||||
{
|
||||
public SlimChainGetBlockHeaders(NBXplorerNetwork network, SlimChain slimChain)
|
||||
{
|
||||
Network = network;
|
||||
SlimChain = slimChain;
|
||||
}
|
||||
|
||||
public NBXplorerNetwork Network { get; }
|
||||
public SlimChain SlimChain { get; }
|
||||
|
||||
public Task<IList<UpdateBlock>> GetUpdateBlocks(IList<uint256> blockHashes)
|
||||
{
|
||||
List<UpdateBlock> update = new List<UpdateBlock>(blockHashes.Count);
|
||||
foreach (var hash in blockHashes)
|
||||
{
|
||||
var b = SlimChain.GetBlock(hash);
|
||||
if (b != null)
|
||||
{
|
||||
update.Add(new UpdateBlock(Network.CryptoCode, b.Hash.ToString(), b.Previous?.ToString(), b.Height));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IList<UpdateBlock>>(update);
|
||||
}
|
||||
}
|
||||
class RPCGetBlockHeaders : IGetBlockHeaders
|
||||
{
|
||||
public RPCGetBlockHeaders(RPCClient rpc)
|
||||
{
|
||||
Rpc = rpc;
|
||||
}
|
||||
|
||||
public RPCClient Rpc { get; }
|
||||
|
||||
public async Task<IList<UpdateBlock>> GetUpdateBlocks(IList<uint256> blockHashes)
|
||||
{
|
||||
var rpc = Rpc.PrepareBatch();
|
||||
List<Task<SlimChainedBlock>> gettingHeaders = new List<Task<SlimChainedBlock>>(blockHashes.Count);
|
||||
foreach (var blk in blockHashes)
|
||||
{
|
||||
var b = rpc.GetBlockHeaderAsyncEx(blk);
|
||||
gettingHeaders.Add(b);
|
||||
}
|
||||
await rpc.SendBatchAsync();
|
||||
|
||||
List<UpdateBlock> update = new List<UpdateBlock>(blockHashes.Count);
|
||||
foreach (var gh in gettingHeaders)
|
||||
{
|
||||
var blockHeader = await gh;
|
||||
if (blockHeader is not null)
|
||||
update.Add(new UpdateBlock(Rpc.Network.NetworkSet.CryptoCode, blockHeader.Hash.ToString(), blockHeader.Previous?.ToString(), blockHeader.Height));
|
||||
}
|
||||
return update;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -1,7 +1,7 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Backend;
|
||||
using Npgsql;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
@ -5,7 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.HostedServices
|
||||
{
|
||||
@ -14,7 +14,7 @@ namespace NBXplorer.HostedServices
|
||||
/// </summary>
|
||||
public class RPCReadyFileHostedService : IHostedService
|
||||
{
|
||||
public RPCReadyFileHostedService(EventAggregator eventAggregator, IIndexers indexers, ExplorerConfiguration explorerConfiguration)
|
||||
public RPCReadyFileHostedService(EventAggregator eventAggregator, Indexers indexers, ExplorerConfiguration explorerConfiguration)
|
||||
{
|
||||
EventAggregator = eventAggregator;
|
||||
Indexers = indexers;
|
||||
@ -22,7 +22,7 @@ namespace NBXplorer.HostedServices
|
||||
}
|
||||
|
||||
public EventAggregator EventAggregator { get; }
|
||||
public IIndexers Indexers { get; }
|
||||
public Indexers Indexers { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
|
||||
IDisposable disposable;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
using Dapper;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Backend;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
// Big hack to make CreatePSBT of MainController pick PostgresController as implementation for getting utxos.
|
||||
public interface IUTXOService
|
||||
{
|
||||
Task<IActionResult> GetUTXOs(string cryptoCode, DerivationStrategy.DerivationStrategyBase derivationStrategy);
|
||||
}
|
||||
}
|
||||
@ -79,34 +79,6 @@ namespace NBXplorer
|
||||
dictionary = new Dictionary<TKey, InnerCollectionView>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiValueDictionary{TKey, TValue}" /> class
|
||||
/// that is empty, has the default initial capacity, and uses the
|
||||
/// specified <see cref="IEqualityComparer{TKey}" />.
|
||||
/// </summary>
|
||||
/// <param name="comparer">Specified comparer to use for the <typeparamref name="TKey"/>s</param>
|
||||
/// <remarks>If <paramref name="comparer"/> is set to null, then the default <see cref="IEqualityComparer" /> for <typeparamref name="TKey"/> is used.</remarks>
|
||||
public MultiValueDictionary(IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
dictionary = new Dictionary<TKey, InnerCollectionView>(comparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MultiValueDictionary{TKey, TValue}" /> class
|
||||
/// that is empty, has the specified initial capacity, and uses the
|
||||
/// specified <see cref="IEqualityComparer{TKey}" />.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Initial number of keys that the <see cref="MultiValueDictionary{TKey, TValue}" /> will allocate space for</param>
|
||||
/// <param name="comparer">Specified comparer to use for the <typeparamref name="TKey"/>s</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Capacity must be >= 0</exception>
|
||||
/// <remarks>If <paramref name="comparer"/> is set to null, then the default <see cref="IEqualityComparer" /> for <typeparamref name="TKey"/> is used.</remarks>
|
||||
public MultiValueDictionary(int capacity, IEqualityComparer<TKey> comparer)
|
||||
{
|
||||
if(capacity < 0)
|
||||
throw new ArgumentOutOfRangeException("capacity", Properties.Resources.ArgumentOutOfRange_NeedNonNegNum);
|
||||
dictionary = new Dictionary<TKey, InnerCollectionView>(capacity, comparer);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Factories
|
||||
@ -114,72 +86,6 @@ namespace NBXplorer
|
||||
** Static Factories
|
||||
======================================================================*/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new new instance of the <see cref="MultiValueDictionary{TKey, TValue}" />
|
||||
/// class that is empty, has the default initial capacity, and uses the default
|
||||
/// <see cref="IEqualityComparer{TKey}" /> for <typeparamref name="TKey"/>. The
|
||||
/// internal dictionary will use instances of the <typeparamref name="TValueCollection"/>
|
||||
/// class as its collection type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValueCollection">
|
||||
/// The collection type that this <see cref="MultiValueDictionary{TKey, TValue}" />
|
||||
/// will contain in its internal dictionary.
|
||||
/// </typeparam>
|
||||
/// <returns>A new <see cref="MultiValueDictionary{TKey, TValue}" /> with the specified
|
||||
/// parameters.</returns>
|
||||
/// <exception cref="InvalidOperationException"><typeparamref name="TValueCollection"/> must not have
|
||||
/// IsReadOnly set to true by default.</exception>
|
||||
/// <remarks>
|
||||
/// Note that <typeparamref name="TValueCollection"/> must implement <see cref="ICollection{TValue}"/>
|
||||
/// in addition to being constructable through new(). The collection returned from the constructor
|
||||
/// must also not have IsReadOnly set to True by default.
|
||||
/// </remarks>
|
||||
public static MultiValueDictionary<TKey, TValue> Create<TValueCollection>()
|
||||
where TValueCollection : ICollection<TValue>, new()
|
||||
{
|
||||
if(new TValueCollection().IsReadOnly)
|
||||
throw new InvalidOperationException(Properties.Resources.Create_TValueCollectionReadOnly);
|
||||
|
||||
var multiValueDictionary = new MultiValueDictionary<TKey, TValue>();
|
||||
multiValueDictionary.NewCollectionFactory = () => new TValueCollection();
|
||||
return multiValueDictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new new instance of the <see cref="MultiValueDictionary{TKey, TValue}" />
|
||||
/// class that is empty, has the specified initial capacity, and uses the default
|
||||
/// <see cref="IEqualityComparer{TKey}" /> for <typeparamref name="TKey"/>. The
|
||||
/// internal dictionary will use instances of the <typeparamref name="TValueCollection"/>
|
||||
/// class as its collection type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValueCollection">
|
||||
/// The collection type that this <see cref="MultiValueDictionary{TKey, TValue}" />
|
||||
/// will contain in its internal dictionary.
|
||||
/// </typeparam>
|
||||
/// <param name="capacity">Initial number of keys that the <see cref="MultiValueDictionary{TKey, TValue}" /> will allocate space for</param>
|
||||
/// <returns>A new <see cref="MultiValueDictionary{TKey, TValue}" /> with the specified
|
||||
/// parameters.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Capacity must be >= 0</exception>
|
||||
/// <exception cref="InvalidOperationException"><typeparamref name="TValueCollection"/> must not have
|
||||
/// IsReadOnly set to true by default.</exception>
|
||||
/// <remarks>
|
||||
/// Note that <typeparamref name="TValueCollection"/> must implement <see cref="ICollection{TValue}"/>
|
||||
/// in addition to being constructable through new(). The collection returned from the constructor
|
||||
/// must also not have IsReadOnly set to True by default.
|
||||
/// </remarks>
|
||||
public static MultiValueDictionary<TKey, TValue> Create<TValueCollection>(int capacity)
|
||||
where TValueCollection : ICollection<TValue>, new()
|
||||
{
|
||||
if(capacity < 0)
|
||||
throw new ArgumentOutOfRangeException("capacity", Properties.Resources.ArgumentOutOfRange_NeedNonNegNum);
|
||||
if(new TValueCollection().IsReadOnly)
|
||||
throw new InvalidOperationException(Properties.Resources.Create_TValueCollectionReadOnly);
|
||||
|
||||
var multiValueDictionary = new MultiValueDictionary<TKey, TValue>(capacity);
|
||||
multiValueDictionary.NewCollectionFactory = () => new TValueCollection();
|
||||
return multiValueDictionary;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Static Factories with Func parameters
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\NBXplorer.xml</DocumentationFile>
|
||||
<NoWarn>1701;1702;1705;1591;CS1591</NoWarn>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<SupportDBTrie Condition="'$(SupportDBTrie)' == ''">true</SupportDBTrie>
|
||||
<DefineConstants Condition="'$(SupportDBTrie)' == 'true'">$(DefineConstants);SUPPORT_DBTRIE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="DBScripts\003.Legacy.sql" />
|
||||
@ -33,9 +31,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="DBTrie" Version="1.0.39" Condition="'$(SupportDBTrie)' == 'true'">
|
||||
<Aliases>DBTrieLib</Aliases>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="4.2.1" />
|
||||
<PackageReference Include="Npgsql" Version="6.0.7" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="5.1.2" />
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
/// <summary>
|
||||
/// Activate or deactivate a route if the postgres controller implementation should be used
|
||||
/// </summary>
|
||||
public class PostgresImplementationActionConstraint : Attribute, IActionConstraint
|
||||
{
|
||||
public PostgresImplementationActionConstraint(bool postgresImplementation)
|
||||
{
|
||||
PostgresImplementation = postgresImplementation;
|
||||
}
|
||||
public int Order => 100;
|
||||
|
||||
public bool PostgresImplementation { get; }
|
||||
|
||||
public bool Accept(ActionConstraintContext context)
|
||||
{
|
||||
var conf = context.RouteContext.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||
return string.IsNullOrEmpty(conf["POSTGRES"]) == !PostgresImplementation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"NBXPLORER_POSTGRES": "User ID=postgres;Application Name=test;Include Error Detail=true;Host=localhost;Port=39383",
|
||||
"NBXPLORER_AUTOMIGRATE": "1"
|
||||
},
|
||||
"applicationUrl": "http://localhost:4774",
|
||||
"launchUrl": "v1/cryptos/btc/status"
|
||||
|
||||
@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
||||
using NBitcoin.DataEncoders;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
|
||||
@ -1,245 +0,0 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.Events;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class RebroadcastResult
|
||||
{
|
||||
public List<Transaction> UnknownFailure { get; set; } = new List<Transaction>();
|
||||
public List<Transaction> MissingInputs { get; set; } = new List<Transaction>();
|
||||
public List<Transaction> Rebroadcasted { get; set; } = new List<Transaction>();
|
||||
public List<Transaction> AlreadyInMempool { get; set; } = new List<Transaction>();
|
||||
public List<TrackedTransaction> Cleaned { get; set; } = new List<TrackedTransaction>();
|
||||
|
||||
internal void Add(RebroadcastResult a)
|
||||
{
|
||||
UnknownFailure.AddRange(a.UnknownFailure);
|
||||
AlreadyInMempool.AddRange(a.AlreadyInMempool);
|
||||
Rebroadcasted.AddRange(a.Rebroadcasted);
|
||||
MissingInputs.AddRange(a.MissingInputs);
|
||||
Cleaned.AddRange(a.Cleaned);
|
||||
}
|
||||
}
|
||||
public class RebroadcasterHostedService : IHostedService
|
||||
{
|
||||
class RebroadcastedTransaction
|
||||
{
|
||||
public RebroadcastedTransaction(TrackedTransactionKey[] keys)
|
||||
{
|
||||
Keys = new HashSet<TrackedTransactionKey>();
|
||||
OrderedKeys = new List<TrackedTransactionKey>();
|
||||
AddKeys(keys);
|
||||
}
|
||||
|
||||
public void AddKeys(TrackedTransactionKey[] keys)
|
||||
{
|
||||
// We make sure there is no duplicates, while keeping the order of keys
|
||||
foreach (var k in keys)
|
||||
{
|
||||
if (Keys.Add(k))
|
||||
OrderedKeys.Add(k);
|
||||
}
|
||||
}
|
||||
|
||||
public TrackedSource TrackedSource;
|
||||
HashSet<TrackedTransactionKey> Keys;
|
||||
public List<TrackedTransactionKey> OrderedKeys;
|
||||
}
|
||||
class RebroadcastedTransactions
|
||||
{
|
||||
public NBXplorerNetwork Network;
|
||||
object CollectionLock = new object();
|
||||
Dictionary<TrackedSource, RebroadcastedTransaction> TransactionsHashSet = new Dictionary<TrackedSource, RebroadcastedTransaction>();
|
||||
public void RebroadcastPeriodically(TrackedSource trackedSource, params TrackedTransactionKey[] txIds)
|
||||
{
|
||||
lock (CollectionLock)
|
||||
{
|
||||
if (TransactionsHashSet.TryGetValue(trackedSource, out var v))
|
||||
{
|
||||
v.AddKeys(txIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
TransactionsHashSet.Add(trackedSource, new RebroadcastedTransaction(txIds) { TrackedSource = trackedSource });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ICollection<RebroadcastedTransaction> RetrieveTransactions()
|
||||
{
|
||||
lock (CollectionLock)
|
||||
{
|
||||
var rebroadcasting = TransactionsHashSet.Select(t => t.Value).ToList();
|
||||
TransactionsHashSet.Clear();
|
||||
return rebroadcasting;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRepositoryProvider _Repositories;
|
||||
private IIndexers _Indexers;
|
||||
Dictionary<NBXplorerNetwork, RebroadcastedTransactions> _BroadcastedTransactionsByCryptoCode;
|
||||
public RebroadcasterHostedService(
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
ExplorerConfiguration configuration,
|
||||
Broadcaster broadcaster,
|
||||
IRepositoryProvider repositories, IIndexers indexers, EventAggregator eventAggregator)
|
||||
{
|
||||
Broadcaster = broadcaster;
|
||||
_Repositories = repositories;
|
||||
_Indexers = indexers;
|
||||
EventAggregator = eventAggregator;
|
||||
_BroadcastedTransactionsByCryptoCode = configuration.ChainConfigurations
|
||||
.Select(r => new RebroadcastedTransactions()
|
||||
{
|
||||
Network = networkProvider.GetFromCryptoCode(r.CryptoCode)
|
||||
}).ToDictionary(t => t.Network);
|
||||
}
|
||||
|
||||
Task _Loop;
|
||||
CancellationTokenSource _Cts = new CancellationTokenSource();
|
||||
|
||||
public Broadcaster Broadcaster { get; }
|
||||
public EventAggregator EventAggregator { get; }
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _Repositories.StartCompletion;
|
||||
_Cts = new CancellationTokenSource();
|
||||
_Loop = RebroadcastLoop(_Cts.Token);
|
||||
}
|
||||
|
||||
public void RebroadcastPeriodically(NBXplorerNetwork network, TrackedSource trackedSource, params TrackedTransactionKey[] txIds)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
_BroadcastedTransactionsByCryptoCode[network].RebroadcastPeriodically(trackedSource, txIds);
|
||||
}
|
||||
|
||||
private async Task RebroadcastLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
// Make sure we don't block main thread
|
||||
await Task.Delay(1).ConfigureAwait(false);
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_ = RebroadcastAll();
|
||||
await Task.Delay(TimeSpan.FromHours(1.0), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<RebroadcastResult> RebroadcastAll()
|
||||
{
|
||||
List<Task<RebroadcastResult>> rebroadcast = new List<Task<RebroadcastResult>>();
|
||||
foreach (var broadcastedTransactions in _BroadcastedTransactionsByCryptoCode.Select(c => c.Value))
|
||||
{
|
||||
var txs = broadcastedTransactions.RetrieveTransactions();
|
||||
if (txs.Count == 0)
|
||||
continue;
|
||||
foreach (var tx in txs)
|
||||
rebroadcast.Add(Rebroadcast(broadcastedTransactions.Network, tx.TrackedSource, tx.OrderedKeys));
|
||||
}
|
||||
var result = new RebroadcastResult();
|
||||
foreach (var broadcast in rebroadcast)
|
||||
{
|
||||
result.Add(await broadcast);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private async Task<RebroadcastResult> Rebroadcast(NBXplorerNetwork network, TrackedSource trackedSource, IEnumerable<TrackedTransactionKey> txIds)
|
||||
{
|
||||
var result = new RebroadcastResult();
|
||||
var repository = _Repositories.GetRepository(network);
|
||||
var rpc = _Indexers.GetIndexer(repository.Network)?.GetConnectedClient();
|
||||
if (rpc is null)
|
||||
return result;
|
||||
List<TrackedTransaction> cleaned = new List<TrackedTransaction>();
|
||||
HashSet<TrackedTransactionKey> processedTrackedTransactionKeys = new HashSet<TrackedTransactionKey>();
|
||||
HashSet<uint256> processedTransactionId = new HashSet<uint256>();
|
||||
foreach (var trackedTxId in txIds)
|
||||
{
|
||||
if (!processedTransactionId.Add(trackedTxId.TxId))
|
||||
continue;
|
||||
var tx = (await repository.GetSavedTransactions(trackedTxId.TxId))?.Select(t => t.Transaction).FirstOrDefault();
|
||||
if (tx == null)
|
||||
continue;
|
||||
|
||||
var broadcastResult = await Broadcaster.Broadcast(network, tx, trackedTxId.TxId);
|
||||
|
||||
if (broadcastResult.AlreadyInMempool)
|
||||
{
|
||||
result.AlreadyInMempool.Add(tx);
|
||||
}
|
||||
if (broadcastResult.Rebroadcasted)
|
||||
{
|
||||
result.Rebroadcasted.Add(tx);
|
||||
}
|
||||
if (broadcastResult.UnknownError)
|
||||
{
|
||||
result.UnknownFailure.Add(tx);
|
||||
}
|
||||
|
||||
if (broadcastResult.MissingInput)
|
||||
{
|
||||
result.MissingInputs.Add(tx);
|
||||
var txs = await repository.GetTransactions(trackedSource, trackedTxId.TxId);
|
||||
foreach (var savedTx in txs)
|
||||
{
|
||||
if (!processedTrackedTransactionKeys.Add(savedTx.Key))
|
||||
continue;
|
||||
if (savedTx.BlockHash == null)
|
||||
{
|
||||
cleaned.Add(savedTx);
|
||||
result.Cleaned.Add(savedTx);
|
||||
}
|
||||
else
|
||||
{
|
||||
var header = await rpc.GetBlockHeaderAsyncEx(savedTx.BlockHash);
|
||||
if (header is null)
|
||||
{
|
||||
cleaned.Add(savedTx);
|
||||
result.Cleaned.Add(savedTx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleaned.Count != 0)
|
||||
{
|
||||
foreach (var tx in cleaned)
|
||||
{
|
||||
EventAggregator.Publish(new EvictedTransactionEvent(tx.TransactionHash));
|
||||
}
|
||||
await repository.Prune(trackedSource, cleaned.ToList());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_Loop == null)
|
||||
return;
|
||||
_Cts.Cancel();
|
||||
try
|
||||
{
|
||||
await _Loop;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using NBXplorer.Logging;
|
||||
using NBitcoin.Scripting;
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
@ -64,7 +64,7 @@ namespace NBXplorer
|
||||
public ScanUTXOSetService(ScanUTXOSetServiceAccessor accessor,
|
||||
IRPCClients rpcClients,
|
||||
KeyPathTemplates keyPathTemplates,
|
||||
IRepositoryProvider repositories)
|
||||
RepositoryProvider repositories)
|
||||
{
|
||||
accessor.Instance = this;
|
||||
RpcClients = rpcClients;
|
||||
@ -116,7 +116,7 @@ namespace NBXplorer
|
||||
private readonly KeyPathTemplates keyPathTemplates;
|
||||
|
||||
public IRPCClients RpcClients { get; }
|
||||
public IRepositoryProvider Repositories { get; }
|
||||
public RepositoryProvider Repositories { get; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -238,7 +238,7 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateRepository(RPCClient client, DerivationSchemeTrackedSource trackedSource, IRepository repo, ScanTxoutOutput[] outputs, ScannedItems scannedItems, ScanUTXOProgress progressObj)
|
||||
private async Task UpdateRepository(RPCClient client, DerivationSchemeTrackedSource trackedSource, Repository repo, ScanTxoutOutput[] outputs, ScannedItems scannedItems, ScanUTXOProgress progressObj)
|
||||
{
|
||||
var clientBatch = client.PrepareBatch();
|
||||
var blockIdsByHeight = new ConcurrentDictionary<int, uint256>();
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class Signaler
|
||||
{
|
||||
Channel<bool> _Channel = Channel.CreateUnbounded<bool>();
|
||||
public void Set()
|
||||
{
|
||||
_Channel.Writer.TryWrite(true);
|
||||
}
|
||||
|
||||
public async Task<bool> Wait(TimeSpan timeout, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using (var cancel = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
cancel.CancelAfter(timeout);
|
||||
try
|
||||
{
|
||||
await Wait(cancel.Token);
|
||||
}
|
||||
catch when (!cancellationToken.IsCancellationRequested) { return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Wait(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (await _Channel.Reader.WaitToReadAsync(cancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
while(_Channel.Reader.TryRead(out _))
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,11 +149,6 @@ namespace NBXplorer
|
||||
});
|
||||
}
|
||||
|
||||
public virtual ITrackedTransactionSerializable CreateBitcoinSerializable()
|
||||
{
|
||||
return new TransactionMatchData(this);
|
||||
}
|
||||
|
||||
Dictionary<OutPoint, int> inputsIndexes;
|
||||
public int IndexOfInput(OutPoint spent)
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using NBXplorer.Backends;
|
||||
using NBXplorer.Backend;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
# Migration from DBTrie backend to Postgres backend
|
||||
|
||||
> [!WARNING]
|
||||
> The last version to support the migration is `2.3.67`. If you are running a version newer than this and need to migrate, please upgrade to `2.3.67` first.
|
||||
|
||||
For an extended period, NBXplorer depended on an embedded database dubbed DBTrie. This internal database imposed limitations for various reasons, prompting us to upgrade NBXplorer to employ a Postgres backend rather than DBTrie.
|
||||
|
||||
Although we continue to support DBTrie, it is now deemed obsolete. We offer a migration pathway for existing deployments.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user