Compare commits
242 Commits
cleanupdbt
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5147b5f261 | ||
|
|
6ad4941712 | ||
|
|
98d89f924a | ||
|
|
36e1596b5c | ||
|
|
83e0ab4f68 | ||
|
|
c24b47bb03 | ||
|
|
b9f324b2eb | ||
|
|
e0af3bf493 | ||
|
|
06349f9818 | ||
|
|
c46949ac60 | ||
|
|
5e570a84fb | ||
|
|
060d19f6a8 | ||
|
|
b83178be8d | ||
|
|
f72fc4d321 | ||
|
|
9242d645b6 | ||
|
|
05df9b5037 | ||
|
|
456717935c | ||
|
|
98df6b7fdd | ||
|
|
449568be9a | ||
|
|
abb5fd3a6c | ||
|
|
deb868b2c0 | ||
|
|
f0000ceab6 | ||
|
|
d9cba8b0b5 | ||
|
|
becdc3f47d | ||
|
|
aa462e7af1 | ||
|
|
34d6b0dcf3 | ||
|
|
87037e17e1 | ||
|
|
c806e9022e | ||
|
|
39b1861d7a | ||
|
|
a379506b46 | ||
|
|
68513b4668 | ||
|
|
802945291a | ||
|
|
3756b54138 | ||
|
|
9eeaf71195 | ||
|
|
0e4222a1f2 | ||
|
|
34831b2d85 | ||
|
|
aba8cdc8c6 | ||
|
|
ba76d3b059 | ||
|
|
26b3dddc28 | ||
|
|
1a38ffa497 | ||
|
|
2fb63ae18e | ||
|
|
206fd0017f | ||
|
|
5d78c7034c | ||
|
|
f226238779 | ||
|
|
05d8ed054e | ||
|
|
4ebd7c4c2f | ||
|
|
8670e58923 | ||
|
|
5ea76f64bb | ||
|
|
be086282ae | ||
|
|
58bde3c916 | ||
|
|
8af448f724 | ||
|
|
273333b040 | ||
|
|
d03465c240 | ||
|
|
58e9bba5a2 | ||
|
|
c58c60c9fa | ||
|
|
939a575c51 | ||
|
|
ca97104831 | ||
|
|
a51911f4ce | ||
|
|
e8316d959b | ||
|
|
be51a4af50 | ||
|
|
b7dc5b5c5f | ||
|
|
cdac2c264e | ||
|
|
e494a46de9 | ||
|
|
10ac6d65d8 | ||
|
|
7f84ef983c | ||
|
|
9a784b5cce | ||
|
|
dfe7ca478f | ||
|
|
ea4ac907b6 | ||
|
|
fa8bf56623 | ||
|
|
f4640c8c4f | ||
|
|
fdc8281e88 | ||
|
|
8597067cbe | ||
|
|
ae99da7299 | ||
|
|
4666c13a1b | ||
|
|
3118b71004 | ||
|
|
d61f3db4fa | ||
|
|
10e139c3d1 | ||
|
|
206165736a | ||
|
|
f68c50c4cf | ||
|
|
75cbba9427 | ||
|
|
1916ea7050 | ||
|
|
e43031411d | ||
|
|
7b7cd2314d | ||
|
|
4ce8ef6954 | ||
|
|
80f16ce8bc | ||
|
|
fed67b354f | ||
|
|
da251bdbe0 | ||
|
|
11a3564ab4 | ||
|
|
e3f7933ad1 | ||
|
|
8b9d5fbde7 | ||
|
|
916a52b49e | ||
|
|
2ed4f7850f | ||
|
|
a38ad24023 | ||
|
|
ab27e4b6ce | ||
|
|
e9a235c149 | ||
|
|
03c17c046d | ||
|
|
530a38030e | ||
|
|
9d796828f8 | ||
|
|
eaeeea22a9 | ||
|
|
5170fd92e1 | ||
|
|
e5eaf763e7 | ||
|
|
326d9c5e2b | ||
|
|
8ea14672dd | ||
|
|
4d81c236f5 | ||
|
|
a2afbd951e | ||
|
|
35b449a49f | ||
|
|
5ab3da5031 | ||
|
|
dbfd09e2f9 | ||
|
|
24d24f2f25 | ||
|
|
1eeb0ff3ef | ||
|
|
b31fa111b4 | ||
|
|
fb2691748f | ||
|
|
c64dc124b5 | ||
|
|
97214740c1 | ||
|
|
3e0b2047d3 | ||
|
|
2fa2ca69a0 | ||
|
|
f37ef1c2d5 | ||
|
|
f165b14c52 | ||
|
|
c249b842ef | ||
|
|
531f28817e | ||
|
|
654b103128 | ||
|
|
21122b4a1f | ||
|
|
e547b99a47 | ||
|
|
88f7d8248a | ||
|
|
a201004b42 | ||
|
|
0bf9492e1d | ||
|
|
8275c6effb | ||
|
|
3d746aeff9 | ||
|
|
8f7371dc47 | ||
|
|
048eab8625 | ||
|
|
d4068e7dec | ||
|
|
1352c8dcdf | ||
|
|
e2767a85c5 | ||
|
|
09d56b22be | ||
|
|
1ee089836d | ||
|
|
4c9db11e6c | ||
|
|
dddd825571 | ||
|
|
42c9dda280 | ||
|
|
9af5962b1e | ||
|
|
d5b56d3206 | ||
|
|
ddde4ae301 | ||
|
|
a76965d12c | ||
|
|
7fb40b5a81 | ||
|
|
2e75bdf65e | ||
|
|
ac6e63c20f | ||
|
|
5519caf601 | ||
|
|
1814138225 | ||
|
|
d612420059 | ||
|
|
f3aeea447d | ||
|
|
261a1e73a1 | ||
|
|
7c870bdeaa | ||
|
|
15a07a8b47 | ||
|
|
52258aecd1 | ||
|
|
b672a28c1c | ||
|
|
a33de3bec7 | ||
|
|
f98fa1addb | ||
|
|
b5b90871ae | ||
|
|
e937448b6b | ||
|
|
c84c0fce97 | ||
|
|
4456dd04d0 | ||
|
|
6ba0315de4 | ||
|
|
2c6418c058 | ||
|
|
f978e88b85 | ||
|
|
22b8e0b17a | ||
|
|
2bd46f7820 | ||
|
|
d03b511db6 | ||
|
|
0c9a3569e6 | ||
|
|
f200ea4930 | ||
|
|
dfef4e8c7e | ||
|
|
4bf8e25902 | ||
|
|
019f8c420a | ||
|
|
a2dc680338 | ||
|
|
72a102a8fc | ||
|
|
e7c701f3f7 | ||
|
|
7c710e34b7 | ||
|
|
efc9ae6444 | ||
|
|
b20d1567e0 | ||
|
|
21f5f04f64 | ||
|
|
825d5bc78d | ||
|
|
e83cfdb720 | ||
|
|
96633a3b7d | ||
|
|
900f99545d | ||
|
|
082373501e | ||
|
|
279521c507 | ||
|
|
2000affe7b | ||
|
|
7d70e72d91 | ||
|
|
9b6358221a | ||
|
|
5d4d028b0e | ||
|
|
07b1193237 | ||
|
|
56d2293c30 | ||
|
|
84305c84d8 | ||
|
|
fffa6f0bca | ||
|
|
15eee7ae47 | ||
|
|
a490c24351 | ||
|
|
6eb53ed8d2 | ||
|
|
6ca2560a1a | ||
|
|
f69581f69e | ||
|
|
190eabbc5e | ||
|
|
826dc2d4ac | ||
|
|
95f28ac578 | ||
|
|
8d309e4de5 | ||
|
|
9bcf963a4a | ||
|
|
85f508756a | ||
|
|
5ddba1d816 | ||
|
|
eca963def6 | ||
|
|
cfedff079d | ||
|
|
ceef9651f3 | ||
|
|
1d89e331ed | ||
|
|
7860ebbda4 | ||
|
|
2f855b5cc3 | ||
|
|
8ec608f2bb | ||
|
|
4260cb8e04 | ||
|
|
9b6c0010fb | ||
|
|
a01893d09d | ||
|
|
57048b0d05 | ||
|
|
9a64c246b8 | ||
|
|
fa26cd4fcf | ||
|
|
3b192971f0 | ||
|
|
02e4714bce | ||
|
|
342a16ad07 | ||
|
|
31a1ab4c89 | ||
|
|
5de2a002a0 | ||
|
|
9faab8f97d | ||
|
|
dbef90b927 | ||
|
|
30c1536323 | ||
|
|
ef1f78b9e9 | ||
|
|
d7a9af1d7a | ||
|
|
1ac249db79 | ||
|
|
c547f20d20 | ||
|
|
a9a8f56d73 | ||
|
|
42dbfdb59d | ||
|
|
f8707aca8b | ||
|
|
f76543311d | ||
|
|
8a051016f4 | ||
|
|
506388937e | ||
|
|
600a642ebc | ||
|
|
3cbb864713 | ||
|
|
25a3f579d8 | ||
|
|
a858d56552 | ||
|
|
06ad54141e | ||
|
|
ef84e74bbb | ||
|
|
3579fcd226 |
@ -10,61 +10,19 @@ jobs:
|
||||
cd .circleci && ./run-tests.sh
|
||||
|
||||
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||
amd64:
|
||||
machine:
|
||||
enabled: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -t $DOCKERHUB_REPO:latest-amd64 -f Dockerfile.linuxamd64 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
|
||||
|
||||
arm32v7:
|
||||
machine:
|
||||
enabled: true
|
||||
docker:
|
||||
docker:
|
||||
- image: cimg/base:stable
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||
|
||||
arm64v8:
|
||||
machine:
|
||||
enabled: true
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
command: |
|
||||
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
#
|
||||
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f Dockerfile.linuxarm64v8 .
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
|
||||
|
||||
multiarch:
|
||||
machine:
|
||||
enabled: true
|
||||
steps:
|
||||
- run:
|
||||
command: |
|
||||
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||
#
|
||||
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
|
||||
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
|
||||
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
|
||||
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
|
||||
docker buildx create --use
|
||||
docker buildx build -t $DOCKERHUB_REPO:$LATEST_TAG --platform linux/amd64,linux/arm64,linux/arm/v7 --push .
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@ -74,33 +32,12 @@ workflows:
|
||||
|
||||
publish:
|
||||
jobs:
|
||||
- amd64:
|
||||
filters:
|
||||
# ignore any commit on any branch by default
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# only act on version tags
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
- arm32v7:
|
||||
- docker:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
# only act on version tags v1.0.0.88 or v1.0.2-1
|
||||
# OR feature tags like abc
|
||||
# OR features on specific versions like v1.0.0.88-abc-1
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
- arm64v8:
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
- multiarch:
|
||||
requires:
|
||||
- amd64
|
||||
- arm32v7
|
||||
- arm64v8
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /v[1-9]+(\.[0-9]+)*/
|
||||
only: /(v[1-9]+(\.[0-9]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
# Note that we are using buster rather than bulleyes. Somehow, raspberry pi 4 doesn't like bulleyes.
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
|
||||
WORKDIR /source
|
||||
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
|
||||
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
|
||||
@ -9,10 +8,12 @@ COPY . .
|
||||
RUN cd NBXplorer && \
|
||||
dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim-arm32v7
|
||||
|
||||
WORKDIR /datadir
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0.9-noble
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir /datadir
|
||||
ENV NBXPLORER_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
WORKDIR /source
|
||||
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
|
||||
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
|
||||
# Cache some dependencies
|
||||
RUN cd NBXplorer && dotnet restore && cd ..
|
||||
COPY . .
|
||||
RUN cd NBXplorer && \
|
||||
dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir /datadir
|
||||
ENV NBXPLORER_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
ENTRYPOINT ["dotnet", "NBXplorer.dll"]
|
||||
@ -1,20 +0,0 @@
|
||||
# This is a manifest image, will pull the image with the same arch as the builder machine
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
WORKDIR /source
|
||||
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
|
||||
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
|
||||
# Cache some dependencies
|
||||
RUN cd NBXplorer && dotnet restore && cd ..
|
||||
COPY . .
|
||||
RUN cd NBXplorer && \
|
||||
dotnet publish --output /app/ --configuration Release
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim-arm64v8
|
||||
|
||||
WORKDIR /datadir
|
||||
WORKDIR /app
|
||||
ENV NBXPLORER_DATADIR=/datadir
|
||||
VOLUME /datadir
|
||||
|
||||
COPY --from=builder "/app" .
|
||||
ENTRYPOINT ["dotnet", "NBXplorer.dll"]
|
||||
BIN
NBXplorer.Client/Bitcoin.png
Normal file
BIN
NBXplorer.Client/Bitcoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@ -1,20 +1,49 @@
|
||||
using NBitcoin;
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
using System.Collections.Generic;
|
||||
#if !NO_RECORD
|
||||
using static NBitcoin.WalletPolicies.MiniscriptNode;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class Derivation
|
||||
{
|
||||
public Derivation()
|
||||
public Derivation(Script scriptPubKey, Script? redeem = null)
|
||||
{
|
||||
|
||||
ScriptPubKey = scriptPubKey;
|
||||
Redeem = redeem;
|
||||
}
|
||||
public Script ScriptPubKey
|
||||
{
|
||||
get; set;
|
||||
get;
|
||||
}
|
||||
public Script Redeem
|
||||
public Script? Redeem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyPathDerivation : Derivation
|
||||
{
|
||||
public KeyPathDerivation(KeyPath keyPath, Script scriptPubKey, Script? redeem = null)
|
||||
: base(scriptPubKey, redeem)
|
||||
{
|
||||
KeyPath = keyPath;
|
||||
}
|
||||
|
||||
public KeyPath KeyPath { get; }
|
||||
}
|
||||
#if !NO_RECORD
|
||||
public class PolicyDerivation : Derivation
|
||||
{
|
||||
public PolicyDerivation(NBitcoin.WalletPolicies.DerivationResult details, Script scriptPubKey, Script? redeem = null)
|
||||
: base(scriptPubKey, redeem)
|
||||
{
|
||||
Details = details;
|
||||
}
|
||||
|
||||
public NBitcoin.WalletPolicies.DerivationResult Details { get; }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
using NBitcoin;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
@ -52,7 +56,6 @@ namespace NBXplorer.DerivationStrategy
|
||||
public HashSet<string> AuthorizedOptions { get; } = new HashSet<string>();
|
||||
|
||||
readonly Regex MultiSigRegex = new Regex("^([0-9]{1,2})-of(-[A-Za-z0-9]+)+$");
|
||||
static DirectDerivationStrategy DummyPubKey = new DirectDerivationStrategy(new ExtKey().Neuter().GetWif(Network.RegTest), false);
|
||||
public DerivationStrategyBase Parse(string str)
|
||||
{
|
||||
var strategy = ParseCore(str);
|
||||
@ -79,6 +82,8 @@ namespace NBXplorer.DerivationStrategy
|
||||
if (!Extensions.TryAdd(optionsDictionary, key, value))
|
||||
throw new FormatException($"The option '{key}' is duplicated");
|
||||
}
|
||||
|
||||
var hasOptions = optionsDictionary.Count != 0;
|
||||
str = _OptionRegex.Replace(str, string.Empty);
|
||||
if (optionsDictionary.Remove("legacy"))
|
||||
{
|
||||
@ -142,6 +147,14 @@ namespace NBXplorer.DerivationStrategy
|
||||
.ToArray();
|
||||
return CreateMultiSigDerivationStrategy(pubKeys, sigCount, options);
|
||||
}
|
||||
#if !NO_RECORD
|
||||
else if (PolicyDerivationStrategy._MaybeMiniscript.IsMatch(str))
|
||||
{
|
||||
if (hasOptions)
|
||||
throw new FormatException("The derivation scheme should not contain any option (such as -[legacy])");
|
||||
return PolicyDerivationStrategy.Parse(str, _Network);
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
var key = _Network.Parse<BitcoinExtPubKey>(str);
|
||||
@ -155,7 +168,7 @@ namespace NBXplorer.DerivationStrategy
|
||||
/// <param name="publicKey">The public key of the wallet</param>
|
||||
/// <param name="options">Derivation options</param>
|
||||
/// <returns></returns>
|
||||
public DerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
{
|
||||
return CreateDirectDerivationStrategy(publicKey.GetWif(Network), options);
|
||||
}
|
||||
@ -166,10 +179,10 @@ namespace NBXplorer.DerivationStrategy
|
||||
/// <param name="publicKey">The public key of the wallet</param>
|
||||
/// <param name="options">Derivation options</param>
|
||||
/// <returns></returns>
|
||||
public DerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
|
||||
{
|
||||
options = options ?? new DerivationStrategyOptions();
|
||||
DerivationStrategyBase strategy = null;
|
||||
StandardDerivationStrategyBase strategy = null;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (options.ScriptPubKeyType != ScriptPubKeyType.TaprootBIP86)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
@ -224,7 +237,7 @@ namespace NBXplorer.DerivationStrategy
|
||||
public DerivationStrategyBase CreateMultiSigDerivationStrategy(BitcoinExtPubKey[] pubKeys, int sigCount, DerivationStrategyOptions options = null)
|
||||
{
|
||||
options = options ?? new DerivationStrategyOptions();
|
||||
DerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
|
||||
StandardDerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
|
||||
if (options.ScriptPubKeyType == ScriptPubKeyType.Legacy)
|
||||
return new P2SHDerivationStrategy(derivationStrategy, false);
|
||||
|
||||
@ -237,19 +250,6 @@ namespace NBXplorer.DerivationStrategy
|
||||
}
|
||||
return derivationStrategy;
|
||||
}
|
||||
|
||||
private void ReadBool(ref string str, string attribute, ref bool value)
|
||||
{
|
||||
value = str.Contains($"[{attribute}]");
|
||||
if (value)
|
||||
{
|
||||
str = str.Replace($"[{attribute}]", string.Empty);
|
||||
str = str.Replace("--", "-");
|
||||
if (str.EndsWith("-"))
|
||||
str = str.Substring(0, str.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
readonly static Regex _OptionRegex = new Regex(@"-\[([^ \]\-]+)\]");
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ using NBitcoin;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class DirectDerivationStrategy : DerivationStrategyBase
|
||||
public class DirectDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
BitcoinExtPubKey _Root;
|
||||
|
||||
@ -44,15 +44,11 @@ namespace NBXplorer.DerivationStrategy
|
||||
_Root = root;
|
||||
Segwit = segwit;
|
||||
}
|
||||
public override Derivation GetDerivation()
|
||||
{
|
||||
var pubKey = _Root.ExtPubKey.PubKey;
|
||||
return new Derivation() { ScriptPubKey = Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey };
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetChild(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new DirectDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network), Segwit, AdditionalOptions);
|
||||
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey;
|
||||
return new KeyPathDerivation(keyPath, Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey);
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
using NBitcoin;
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
@ -13,40 +17,42 @@ namespace NBXplorer.DerivationStrategy
|
||||
Direct = 2,
|
||||
Custom = 3,
|
||||
}
|
||||
public abstract class DerivationStrategyBase : IHDScriptPubKey
|
||||
|
||||
public abstract class StandardDerivationStrategyBase : DerivationStrategyBase, IHDScriptPubKey
|
||||
{
|
||||
ReadOnlyDictionary<string, string> Empty = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0));
|
||||
internal StandardDerivationStrategyBase(ReadOnlyDictionary<string, string> additionalOptions) : base(additionalOptions)
|
||||
{
|
||||
}
|
||||
public abstract Derivation GetDerivation(KeyPath keyPath);
|
||||
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature)
|
||||
=> new KeyPathTemplateDerivationLine(this, keyPathTemplates, feature);
|
||||
Script IHDScriptPubKey.ScriptPubKey => GetDerivation(KeyPath.Empty).ScriptPubKey;
|
||||
IHDScriptPubKey? IHDScriptPubKey.Derive(KeyPath keyPath) => keyPath.IsHardenedPath ? null : new HDScriptPubKey(this, keyPath);
|
||||
class HDScriptPubKey(StandardDerivationStrategyBase Parent, KeyPath KeyPath) : IHDScriptPubKey
|
||||
{
|
||||
public Script ScriptPubKey => Parent.GetDerivation(KeyPath).ScriptPubKey;
|
||||
public IHDScriptPubKey? Derive(KeyPath keyPath) => KeyPath.IsHardenedPath ? null : new HDScriptPubKey(Parent, KeyPath.Derive(keyPath));
|
||||
}
|
||||
}
|
||||
public abstract class DerivationStrategyBase
|
||||
{
|
||||
readonly ReadOnlyDictionary<string, string> Empty = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>(0));
|
||||
public ReadOnlyDictionary<string, string> AdditionalOptions { get; }
|
||||
|
||||
internal DerivationStrategyBase(ReadOnlyDictionary<string,string> additionalOptions)
|
||||
internal DerivationStrategyBase(ReadOnlyDictionary<string,string>? additionalOptions)
|
||||
{
|
||||
AdditionalOptions = additionalOptions ?? Empty;
|
||||
}
|
||||
|
||||
public DerivationLine GetLineFor(KeyPathTemplate keyPathTemplate)
|
||||
{
|
||||
return new DerivationLine(this, keyPathTemplate);
|
||||
}
|
||||
public abstract DerivationStrategyBase GetChild(KeyPath keyPath);
|
||||
|
||||
public Derivation GetDerivation(uint i)
|
||||
{
|
||||
return GetChild(new KeyPath(i)).GetDerivation();
|
||||
}
|
||||
public Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null || keyPath.Length == 0)
|
||||
return GetDerivation();
|
||||
return GetChild(keyPath).GetDerivation();
|
||||
}
|
||||
public abstract Derivation GetDerivation();
|
||||
public DerivationLine GetLineFor(DerivationFeature feature) => GetLineFor(KeyPathTemplates.Default, feature);
|
||||
public abstract DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature);
|
||||
|
||||
protected internal abstract string StringValueCore
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
string _StringValue;
|
||||
string? _StringValue;
|
||||
string StringValue
|
||||
{
|
||||
get
|
||||
@ -66,96 +72,74 @@ namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
return string.Join("", new SortedDictionary<string, string>(AdditionalOptions).Select(pair => $"-[{pair.Key}{(string.IsNullOrEmpty(pair.Value)?string.Empty: $"={pair.Value}")}]"));
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
DerivationStrategyBase item = obj as DerivationStrategyBase;
|
||||
if(item == null)
|
||||
return false;
|
||||
return StringValue.Equals(item.StringValue);
|
||||
}
|
||||
public static bool operator ==(DerivationStrategyBase a, DerivationStrategyBase b)
|
||||
{
|
||||
if(System.Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if(((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
return a.StringValue == b.StringValue;
|
||||
}
|
||||
|
||||
public static bool operator !=(DerivationStrategyBase a, DerivationStrategyBase b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
#nullable enable
|
||||
public override bool Equals(object? obj) => obj is DerivationStrategyBase o && StringValue.Equals(o.StringValue);
|
||||
public static bool operator ==(DerivationStrategyBase? a, DerivationStrategyBase? b) => a is null ? b is null : a.Equals(b);
|
||||
public static bool operator !=(DerivationStrategyBase? a, DerivationStrategyBase? b) => !(a == b);
|
||||
public override int GetHashCode() => StringValue.GetHashCode();
|
||||
#nullable restore
|
||||
|
||||
public abstract IEnumerable<ExtPubKey> GetExtPubKeys();
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return StringValue.GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return StringValue;
|
||||
}
|
||||
|
||||
Script IHDScriptPubKey.ScriptPubKey => GetDerivation().ScriptPubKey;
|
||||
IHDScriptPubKey IHDScriptPubKey.Derive(KeyPath keyPath)
|
||||
{
|
||||
return GetChild(keyPath);
|
||||
}
|
||||
|
||||
class HDRedeemScriptPubKey : IHDScriptPubKey
|
||||
{
|
||||
private readonly DerivationStrategyBase strategyBase;
|
||||
public HDRedeemScriptPubKey(DerivationStrategyBase strategyBase)
|
||||
{
|
||||
this.strategyBase = strategyBase;
|
||||
}
|
||||
public Script ScriptPubKey => strategyBase.GetDerivation().Redeem;
|
||||
|
||||
public bool CanDeriveHardenedPath()
|
||||
{
|
||||
return strategyBase.CanDeriveHardenedPath();
|
||||
}
|
||||
public IHDScriptPubKey Derive(KeyPath keyPath)
|
||||
{
|
||||
return strategyBase.GetChild(keyPath).AsHDRedeemScriptPubKey();
|
||||
}
|
||||
}
|
||||
public IHDScriptPubKey AsHDRedeemScriptPubKey()
|
||||
{
|
||||
return new HDRedeemScriptPubKey(this);
|
||||
}
|
||||
|
||||
public bool CanDeriveHardenedPath()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class DerivationLine
|
||||
#if !NO_RECORD
|
||||
public class MiniscriptDerivationLine : DerivationLine
|
||||
{
|
||||
public DerivationLine(DerivationStrategyBase derivationStrategyBase, KeyPathTemplate keyPathTemplate)
|
||||
public MiniscriptDerivationLine(PolicyDerivationStrategy derivationStrategy, DerivationFeature derivationFeature) : base(derivationFeature)
|
||||
{
|
||||
DerivationStrategy = derivationStrategy;
|
||||
Intent = ToAddressIntent(derivationFeature);
|
||||
}
|
||||
|
||||
public static AddressIntent ToAddressIntent(DerivationFeature derivationFeature)
|
||||
{
|
||||
return derivationFeature switch
|
||||
{
|
||||
DerivationFeature.Change => AddressIntent.Change,
|
||||
DerivationFeature.Deposit => AddressIntent.Deposit,
|
||||
_ => throw new NotSupportedException("MiniscriptDerivationStrategy only support deposit and change features")
|
||||
};
|
||||
}
|
||||
|
||||
public PolicyDerivationStrategy DerivationStrategy { get; }
|
||||
public AddressIntent Intent { get; }
|
||||
|
||||
public override Derivation Derive(uint index) => DerivationStrategy.GetDerivation(Intent, index);
|
||||
}
|
||||
#endif
|
||||
public abstract class DerivationLine
|
||||
{
|
||||
protected DerivationLine(DerivationFeature feature)
|
||||
{
|
||||
Feature = feature;
|
||||
}
|
||||
public DerivationFeature Feature { get; }
|
||||
public abstract Derivation Derive(uint index);
|
||||
}
|
||||
public class KeyPathTemplateDerivationLine : DerivationLine
|
||||
{
|
||||
public KeyPathTemplateDerivationLine(StandardDerivationStrategyBase derivationStrategyBase, KeyPathTemplates keyPathTemplates, DerivationFeature derivationFeature) : base(derivationFeature)
|
||||
{
|
||||
if (derivationStrategyBase == null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategyBase));
|
||||
if (keyPathTemplate == null)
|
||||
throw new ArgumentNullException(nameof(keyPathTemplate));
|
||||
if (keyPathTemplates == null)
|
||||
throw new ArgumentNullException(nameof(keyPathTemplates));
|
||||
DerivationStrategyBase = derivationStrategyBase;
|
||||
KeyPathTemplate = keyPathTemplate;
|
||||
KeyPathTemplate = keyPathTemplates.GetKeyPathTemplate(derivationFeature);
|
||||
}
|
||||
|
||||
public DerivationStrategyBase DerivationStrategyBase { get; }
|
||||
public StandardDerivationStrategyBase DerivationStrategyBase { get; }
|
||||
public KeyPathTemplate KeyPathTemplate { get; }
|
||||
|
||||
DerivationStrategyBase _PreLine;
|
||||
|
||||
public Derivation Derive(uint index)
|
||||
public override Derivation Derive(uint index)
|
||||
{
|
||||
_PreLine = _PreLine ?? DerivationStrategyBase.GetChild(KeyPathTemplate.PreIndexes);
|
||||
return _PreLine.GetDerivation(new KeyPath(index).Derive(KeyPathTemplate.PostIndexes));
|
||||
var kp = KeyPathTemplate.GetKeyPath(index);
|
||||
return DerivationStrategyBase.GetDerivation(kp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ using System.Collections.ObjectModel;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class MultisigDerivationStrategy : DerivationStrategyBase
|
||||
public class MultisigDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
public bool LexicographicOrder
|
||||
{
|
||||
@ -62,29 +62,19 @@ namespace NBXplorer.DerivationStrategy
|
||||
get;
|
||||
}
|
||||
|
||||
private void WriteBytes(MemoryStream ms, byte[] v)
|
||||
{
|
||||
ms.Write(v, 0, v.Length);
|
||||
}
|
||||
|
||||
public override Derivation GetDerivation()
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
var pubKeys = new PubKey[this.Keys.Count];
|
||||
Parallel.For(0, pubKeys.Length, i =>
|
||||
{
|
||||
pubKeys[i] = this.Keys[i].ExtPubKey.PubKey;
|
||||
pubKeys[i] = this.Keys[i].ExtPubKey.Derive(keyPath).PubKey;
|
||||
});
|
||||
if(LexicographicOrder)
|
||||
{
|
||||
Array.Sort(pubKeys, LexicographicComparer);
|
||||
}
|
||||
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(RequiredSignatures, pubKeys);
|
||||
return new Derivation() { ScriptPubKey = redeem };
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetChild(KeyPath keyPath)
|
||||
{
|
||||
return new MultisigDerivationStrategy(RequiredSignatures, Keys.Select(k => k.ExtPubKey.Derive(keyPath).GetWif(k.Network)).ToArray(), IsLegacy, LexicographicOrder, AdditionalOptions);
|
||||
return new KeyPathDerivation(keyPath, redeem);
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
|
||||
@ -4,10 +4,10 @@ using NBitcoin;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class P2SHDerivationStrategy : DerivationStrategyBase
|
||||
public class P2SHDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
bool addSuffix;
|
||||
internal P2SHDerivationStrategy(DerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
|
||||
internal P2SHDerivationStrategy(StandardDerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
|
||||
{
|
||||
if(inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
@ -15,7 +15,7 @@ namespace NBXplorer.DerivationStrategy
|
||||
this.addSuffix = addSuffix;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Inner
|
||||
public StandardDerivationStrategyBase Inner
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -30,24 +30,18 @@ namespace NBXplorer.DerivationStrategy
|
||||
}
|
||||
}
|
||||
|
||||
public override Derivation GetDerivation()
|
||||
{
|
||||
var derivation = Inner.GetDerivation();
|
||||
return new Derivation()
|
||||
{
|
||||
ScriptPubKey = derivation.ScriptPubKey.Hash.ScriptPubKey,
|
||||
Redeem = derivation.Redeem ?? derivation.ScriptPubKey
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
return Inner.GetExtPubKeys();
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetChild(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new P2SHDerivationStrategy(Inner.GetChild(keyPath), addSuffix);
|
||||
var derivation = Inner.GetDerivation(keyPath);
|
||||
return new KeyPathDerivation(
|
||||
keyPath,
|
||||
derivation.ScriptPubKey.Hash.ScriptPubKey,
|
||||
derivation.Redeem ?? derivation.ScriptPubKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,40 +4,31 @@ using NBitcoin;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class P2WSHDerivationStrategy : DerivationStrategyBase
|
||||
public class P2WSHDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
internal P2WSHDerivationStrategy(DerivationStrategyBase inner):base(inner.AdditionalOptions)
|
||||
internal P2WSHDerivationStrategy(StandardDerivationStrategyBase inner):base(inner.AdditionalOptions)
|
||||
{
|
||||
if(inner == null)
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
Inner = inner;
|
||||
}
|
||||
|
||||
public DerivationStrategyBase Inner
|
||||
public StandardDerivationStrategyBase Inner
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
protected internal override string StringValueCore => Inner.ToString();
|
||||
|
||||
public override Derivation GetDerivation()
|
||||
{
|
||||
var derivation = Inner.GetDerivation();
|
||||
return new Derivation()
|
||||
{
|
||||
ScriptPubKey = derivation.ScriptPubKey.WitHash.ScriptPubKey,
|
||||
Redeem = derivation.ScriptPubKey
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
return Inner.GetExtPubKeys();
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetChild(KeyPath keyPath)
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
return new P2WSHDerivationStrategy(Inner.GetChild(keyPath));
|
||||
var redeem = Inner.GetDerivation(keyPath).ScriptPubKey;
|
||||
return new KeyPathDerivation(keyPath, redeem.WitHash.ScriptPubKey, redeem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
NBXplorer.Client/DerivationStrategy/PolicyDerivationStrategy.cs
Normal file
212
NBXplorer.Client/DerivationStrategy/PolicyDerivationStrategy.cs
Normal file
@ -0,0 +1,212 @@
|
||||
#nullable enable
|
||||
#if !NO_RECORD
|
||||
using NBitcoin;
|
||||
using NBitcoin.WalletPolicies;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class PolicyDerivationStrategy : DerivationStrategyBase
|
||||
{
|
||||
internal static readonly Regex _MaybeMiniscript = new("^(wsh|sh|pkh|tr|wpkh)\\(");
|
||||
public static bool TryParse(
|
||||
string str,
|
||||
Network network,
|
||||
[MaybeNullWhen(false)] out PolicyDerivationStrategy strategy)
|
||||
{
|
||||
strategy = null;
|
||||
if (!_MaybeMiniscript.IsMatch(str))
|
||||
return false;
|
||||
if (!WalletPolicy.TryParse(str, network, out var policy) || !IsValidPolicy(policy, out _))
|
||||
return false;
|
||||
strategy = new PolicyDerivationStrategy(policy, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PolicyDerivationStrategy Parse(string str, Network network)
|
||||
{
|
||||
if (!_MaybeMiniscript.IsMatch(str))
|
||||
throw new FormatException("The policy should start by either wsh, sh, pkh, tr, wpkh");
|
||||
var policy = WalletPolicy.Parse(str, network);
|
||||
if (!IsValidPolicy(policy, out var err))
|
||||
throw new FormatException(err);
|
||||
return new PolicyDerivationStrategy(policy, false);
|
||||
}
|
||||
|
||||
public PolicyDerivationStrategy(WalletPolicy policy) : this(policy, true)
|
||||
{
|
||||
}
|
||||
|
||||
PolicyDerivationStrategy(WalletPolicy policy, bool check) : base(null)
|
||||
{
|
||||
if (check && !IsValidPolicy(policy, out var error))
|
||||
throw new ArgumentException(paramName: nameof(policy), message: error);
|
||||
Policy = policy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that the policy should have at least one multi path node ([12345678]xpub/**) and no xpriv
|
||||
/// </summary>
|
||||
/// <param name="policy"></param>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public static bool IsValidPolicy(WalletPolicy policy, [MaybeNullWhen(true)] out string error)
|
||||
{
|
||||
var v = new ValidPolicyVisitor();
|
||||
policy.FullDescriptor.Visit(v);
|
||||
error = v.Error;
|
||||
return error is null;
|
||||
}
|
||||
|
||||
class ValidPolicyVisitor : MiniscriptVisitor
|
||||
{
|
||||
public string? Error {
|
||||
get
|
||||
{
|
||||
if (hasSecretKey)
|
||||
return "The policy should not contain any xpriv key";
|
||||
if (!hasMultiPathNode)
|
||||
return "The policy should contain at least one multi path node ([12345678]xpub/**)";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private bool hasMultiPathNode;
|
||||
private bool hasSecretKey;
|
||||
|
||||
public override void Visit(MiniscriptNode node)
|
||||
{
|
||||
if (node is MiniscriptNode.MultipathNode)
|
||||
hasMultiPathNode = true;
|
||||
else if (node is MiniscriptNode.HDKeyNode { Key: BitcoinExtKey })
|
||||
hasSecretKey = true;
|
||||
else
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
public WalletPolicy Policy { get; }
|
||||
private readonly DerivationCache cache = new();
|
||||
private string? _str;
|
||||
|
||||
protected internal override string StringValueCore => _str ??= Policy.ToString(true);
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
=> Policy.KeyInformationVector.Select(kv => GetExtPubKey(kv.Key));
|
||||
|
||||
private ExtPubKey GetExtPubKey(IHDKey key)
|
||||
=> key switch
|
||||
{
|
||||
ExtPubKey extPubKey => extPubKey,
|
||||
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
|
||||
ExtKey extKey => extKey.Neuter(),
|
||||
BitcoinExtKey bitcoinExtKey => bitcoinExtKey.ExtKey.Neuter(),
|
||||
_ => throw new NotSupportedException($"Unsupported key type: {key.GetType()}")
|
||||
};
|
||||
public NBXplorer.DerivationStrategy.Derivation GetDerivation(DerivationFeature feature, uint index)
|
||||
=> GetDerivation(MiniscriptDerivationLine.ToAddressIntent(feature), index);
|
||||
public NBXplorer.DerivationStrategy.Derivation GetDerivation(AddressIntent addressIntent, uint index)
|
||||
{
|
||||
var derived = Policy.FullDescriptor.Derive(new(addressIntent, [(int)index]) { DervivationCache = cache });
|
||||
var scripts = derived[0].Miniscript.ToScripts();
|
||||
return new PolicyDerivation(derived[0], scripts.ScriptPubKey, scripts.RedeemScript);
|
||||
}
|
||||
|
||||
public override DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature) => new MiniscriptDerivationLine(this, feature);
|
||||
|
||||
// Extract the multipath node from the hdkey
|
||||
class MultipathNodeVisitor : MiniscriptVisitor
|
||||
{
|
||||
private readonly ExtPubKey _target;
|
||||
public MiniscriptNode.MultipathNode? Result { get; set; }
|
||||
public MultipathNodeVisitor(IHDKey target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
_target = Normalize(target);
|
||||
}
|
||||
|
||||
private static ExtPubKey Normalize(IHDKey target)
|
||||
=> target switch
|
||||
{
|
||||
BitcoinExtKey extKey => extKey.Neuter().ExtPubKey,
|
||||
ExtKey extKey => extKey.Neuter(),
|
||||
BitcoinExtPubKey bitcoinExtPubKey => bitcoinExtPubKey.ExtPubKey,
|
||||
ExtPubKey a => a,
|
||||
_ => throw new NotSupportedException(target.GetType().ToString())
|
||||
};
|
||||
|
||||
public override void Visit(MiniscriptNode node)
|
||||
{
|
||||
if (Result is not null)
|
||||
return;
|
||||
if (node is MiniscriptNode.MultipathNode { Target: MiniscriptNode.HDKeyNode hd } mp)
|
||||
{
|
||||
if (Normalize(hd.Key).Equals(_target))
|
||||
Result = mp;
|
||||
}
|
||||
else
|
||||
base.Visit(node);
|
||||
}
|
||||
}
|
||||
|
||||
class MiniscriptScriptPubKey : IHDScriptPubKey
|
||||
{
|
||||
private readonly PolicyDerivationStrategy _policyDerivationStrategy;
|
||||
private readonly MiniscriptNode.MultipathNode _multipathNode;
|
||||
private readonly KeyPath _keyPath;
|
||||
|
||||
public MiniscriptScriptPubKey(
|
||||
PolicyDerivationStrategy policyDerivationStrategy,
|
||||
MiniscriptNode.MultipathNode multipathNode,
|
||||
KeyPath? keyPath = null,
|
||||
DerivationCache? cache = null)
|
||||
{
|
||||
_policyDerivationStrategy = policyDerivationStrategy;
|
||||
_multipathNode = multipathNode;
|
||||
_keyPath = keyPath ?? KeyPath.Empty;
|
||||
_cache = cache ?? new();
|
||||
}
|
||||
|
||||
private readonly DerivationCache _cache;
|
||||
public IHDScriptPubKey? Derive(KeyPath keyPath) =>
|
||||
_keyPath.Derive(keyPath) is { Length: <= 2 } kp
|
||||
&& (kp.Length == 0 || GetAddressIntent(kp.Indexes[0]) is not null)
|
||||
&& !kp.IsHardenedPath
|
||||
? new MiniscriptScriptPubKey(_policyDerivationStrategy, _multipathNode, kp, _cache) : null;
|
||||
|
||||
public Script ScriptPubKey
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_keyPath is not { Indexes: [var intentIdx, var index], IsHardenedPath: false }
|
||||
|| GetAddressIntent(intentIdx) is not {} intent)
|
||||
throw new InvalidOperationException("Invalid keypath (it should be non hardened with two component)");
|
||||
var derived = _policyDerivationStrategy.Policy.FullDescriptor.Derive(new(intent, new[] { (int)index })
|
||||
{
|
||||
DervivationCache = _cache
|
||||
});
|
||||
return derived[0].Miniscript.ToScripts().ScriptPubKey;
|
||||
}
|
||||
}
|
||||
|
||||
private AddressIntent? GetAddressIntent(uint intentIdx)
|
||||
=> intentIdx == _multipathNode.DepositIndex ? AddressIntent.Deposit :
|
||||
intentIdx == _multipathNode.ChangeIndex ? AddressIntent.Change : null;
|
||||
}
|
||||
|
||||
public IHDScriptPubKey? GetHDScriptPubKey(IHDKey accountKey)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountKey);
|
||||
var visitor = new MultipathNodeVisitor(accountKey);
|
||||
visitor.Visit(Policy.FullDescriptor.RootNode);
|
||||
if (visitor.Result is null)
|
||||
return null;
|
||||
return new MiniscriptScriptPubKey(this, visitor.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -6,7 +6,7 @@ using NBitcoin;
|
||||
|
||||
namespace NBXplorer.DerivationStrategy
|
||||
{
|
||||
public class TaprootDerivationStrategy : DerivationStrategyBase
|
||||
public class TaprootDerivationStrategy : StandardDerivationStrategyBase
|
||||
{
|
||||
BitcoinExtPubKey _Root;
|
||||
|
||||
@ -35,21 +35,16 @@ namespace NBXplorer.DerivationStrategy
|
||||
throw new ArgumentNullException(nameof(root));
|
||||
_Root = root;
|
||||
}
|
||||
public override Derivation GetDerivation()
|
||||
public override Derivation GetDerivation(KeyPath keyPath)
|
||||
{
|
||||
#if NO_SPAN
|
||||
throw new NotSupportedException("Deriving taproot address is not supported on this platform.");
|
||||
#else
|
||||
var pubKey = _Root.ExtPubKey.PubKey.GetTaprootFullPubKey();
|
||||
return new Derivation() { ScriptPubKey = pubKey.ScriptPubKey };
|
||||
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey.GetTaprootFullPubKey();
|
||||
return new KeyPathDerivation(keyPath, pubKey.ScriptPubKey);
|
||||
#endif
|
||||
}
|
||||
|
||||
public override DerivationStrategyBase GetChild(KeyPath keyPath)
|
||||
{
|
||||
return new TaprootDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network), AdditionalOptions);
|
||||
}
|
||||
|
||||
public override IEnumerable<ExtPubKey> GetExtPubKeys()
|
||||
{
|
||||
yield return _Root.ExtPubKey;
|
||||
|
||||
@ -13,6 +13,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Net.WebSockets;
|
||||
using NBitcoin.RPC;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
@ -150,7 +152,7 @@ namespace NBXplorer
|
||||
}
|
||||
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<TransactionResult>(HttpMethod.Get, null, "v1/cryptos/{0}/transactions/" + txId, new[] { CryptoCode }, cancellation).ConfigureAwait(false);
|
||||
return await SendAsync<TransactionResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/transactions/{txId}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public TransactionResult GetTransaction(uint256 txId, CancellationToken cancellation = default)
|
||||
@ -162,7 +164,7 @@ namespace NBXplorer
|
||||
{
|
||||
if (extKey == null)
|
||||
throw new ArgumentNullException(nameof(extKey));
|
||||
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, "v1/cryptos/{0}/derivations/{1}/prune", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
|
||||
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/prune", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequest, CancellationToken cancellation = default)
|
||||
@ -170,6 +172,16 @@ namespace NBXplorer
|
||||
return PruneAsync(extKey, pruneRequest, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
internal class RawStr
|
||||
{
|
||||
private string str;
|
||||
public RawStr(string str)
|
||||
{
|
||||
this.str = str;
|
||||
}
|
||||
public override string ToString() => str;
|
||||
};
|
||||
internal static RawStr Raw(string str) => new RawStr(str);
|
||||
public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (extKey == null)
|
||||
@ -184,7 +196,7 @@ namespace NBXplorer
|
||||
var argsString = string.Join("&", args.ToArray());
|
||||
if (argsString != string.Empty)
|
||||
argsString = $"?{argsString}";
|
||||
await SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan{2}", new object[] { Network.CryptoCode, extKey, argsString }, cancellation).ConfigureAwait(false);
|
||||
await SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
public void ScanUTXOSet(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
|
||||
{
|
||||
@ -193,7 +205,7 @@ namespace NBXplorer
|
||||
|
||||
public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
|
||||
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public ScanUTXOInformation GetScanUTXOSetInformation(DerivationStrategyBase extKey, CancellationToken cancellation = default)
|
||||
@ -217,25 +229,28 @@ namespace NBXplorer
|
||||
await session.ConnectAsync(cancellation).ConfigureAwait(false);
|
||||
return session;
|
||||
}
|
||||
public WebsocketNotificationSessionLegacy CreateWebsocketNotificationSessionLegacy(CancellationToken cancellation = default)
|
||||
{
|
||||
return CreateWebsocketNotificationSessionLegacyAsync(cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<WebsocketNotificationSessionLegacy> CreateWebsocketNotificationSessionLegacyAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
var session = new WebsocketNotificationSessionLegacy(this);
|
||||
await session.ConnectAsync(cancellation).ConfigureAwait(false);
|
||||
return session;
|
||||
}
|
||||
|
||||
public UTXOChanges GetUTXOs(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetUTXOsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public async Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
public Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
if (trackedSource is DerivationSchemeTrackedSource dsts)
|
||||
{
|
||||
return await SendAsync<UTXOChanges>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos", new object[] { CryptoCode, dsts.DerivationStrategy.ToString() }, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
else if (trackedSource is AddressTrackedSource asts)
|
||||
{
|
||||
return await SendAsync<UTXOChanges>(HttpMethod.Get, null, "v1/cryptos/{0}/addresses/{1}/utxos", new object[] { CryptoCode, asts.Address }, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
throw UnSupported(trackedSource);
|
||||
return SendAsync<UTXOChanges>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/utxos", cancellation);
|
||||
}
|
||||
|
||||
public void WaitServerStarted(CancellationToken cancellation = default)
|
||||
@ -266,7 +281,7 @@ namespace NBXplorer
|
||||
}
|
||||
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
{
|
||||
return TrackAsync(TrackedSource.Create(strategy), cancellation);
|
||||
return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation);
|
||||
}
|
||||
|
||||
public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
|
||||
@ -277,27 +292,18 @@ namespace NBXplorer
|
||||
{
|
||||
if (strategy == null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, strategy.ToString() }, cancellation).ConfigureAwait(false);
|
||||
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, $"v1/cryptos/{CryptoCode}/derivations/{strategy}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Track(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
TrackAsync(trackedSource, cancellation).GetAwaiter().GetResult();
|
||||
TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public Task TrackAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
if (trackedSource is DerivationSchemeTrackedSource dsts)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, dsts.DerivationStrategy.ToString() }, cancellation);
|
||||
}
|
||||
else if (trackedSource is AddressTrackedSource asts)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, null, "v1/cryptos/{0}/addresses/{1}", new[] { CryptoCode, asts.Address.ToString() }, cancellation);
|
||||
}
|
||||
else
|
||||
throw UnSupported(trackedSource);
|
||||
return SendAsync<string>(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation);
|
||||
}
|
||||
|
||||
private Exception UnSupported(TrackedSource trackedSource)
|
||||
@ -316,7 +322,7 @@ namespace NBXplorer
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/balance", new[] { CryptoCode, userDerivationScheme.ToString() }, cancellation);
|
||||
return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation);
|
||||
}
|
||||
|
||||
|
||||
@ -326,12 +332,30 @@ namespace NBXplorer
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/addresses/{1}/balance", new[] { CryptoCode, address.ToString() }, cancellation);
|
||||
return GetBalanceAsync(TrackedSource.Create(address), cancellation);
|
||||
}
|
||||
public Task<GetBalanceResponse> GetBalanceAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation);
|
||||
}
|
||||
public async Task<bool> IsTrackedAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
{
|
||||
|
||||
var responseMessage = await SendAsync(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}", cancellation);
|
||||
switch (responseMessage.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.OK:
|
||||
return true;
|
||||
case HttpStatusCode.NotFound:
|
||||
return false;
|
||||
default:
|
||||
await ParseResponse(responseMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, keyPaths, "v1/cryptos/{0}/derivations/{1}/addresses/cancelreservation", new[] { CryptoCode, strategy.ToString() }, cancellation);
|
||||
return SendAsync<string>(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation);
|
||||
}
|
||||
|
||||
public StatusResult GetStatus(CancellationToken cancellation = default)
|
||||
@ -346,40 +370,39 @@ namespace NBXplorer
|
||||
{
|
||||
if (strategy is null)
|
||||
throw new ArgumentNullException(nameof(strategy));
|
||||
return SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/wipe", new[] { CryptoCode, strategy.ToString() }, cancellation);
|
||||
return SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/utxos/wipe", cancellation);
|
||||
}
|
||||
|
||||
public Task<StatusResult> GetStatusAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", null, cancellation);
|
||||
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", cancellation);
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(strategy, cancellation).GetAwaiter().GetResult();
|
||||
return GetTransactionsAsync(strategy, from, to, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
|
||||
return GetTransactionsAsync(trackedSource, from, to, cancellation).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetTransactionsAsync(TrackedSource.Create(strategy), cancellation);
|
||||
return GetTransactionsAsync(TrackedSource.Create(strategy), from, to, cancellation);
|
||||
}
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
|
||||
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
|
||||
{
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
if (trackedSource is DerivationSchemeTrackedSource dsts)
|
||||
string fromV = string.Empty;
|
||||
string toV = string.Empty;
|
||||
if (from is DateTimeOffset f)
|
||||
{
|
||||
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions", null, cancellation);
|
||||
fromV = NBitcoin.Utils.DateTimeToUnixTime(f).ToString();
|
||||
}
|
||||
else if (trackedSource is AddressTrackedSource asts)
|
||||
if (to is DateTimeOffset t)
|
||||
{
|
||||
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions", null, cancellation);
|
||||
toV = NBitcoin.Utils.DateTimeToUnixTime(t).ToString();
|
||||
}
|
||||
else
|
||||
throw UnSupported(trackedSource);
|
||||
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions?from={fromV}&to={toV}", cancellation);
|
||||
}
|
||||
|
||||
|
||||
@ -404,23 +427,14 @@ namespace NBXplorer
|
||||
throw new ArgumentNullException(nameof(txId));
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
if (trackedSource is DerivationSchemeTrackedSource dsts)
|
||||
{
|
||||
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions/{txId}", null, cancellation);
|
||||
}
|
||||
else if (trackedSource is AddressTrackedSource asts)
|
||||
{
|
||||
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions/{txId}", null, cancellation);
|
||||
}
|
||||
else
|
||||
throw UnSupported(trackedSource);
|
||||
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation);
|
||||
}
|
||||
|
||||
public Task RescanAsync(RescanRequest rescanRequest, CancellationToken cancellation = default)
|
||||
{
|
||||
if (rescanRequest == null)
|
||||
throw new ArgumentNullException(nameof(rescanRequest));
|
||||
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", null, cancellation);
|
||||
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", cancellation);
|
||||
}
|
||||
|
||||
public void Rescan(RescanRequest rescanRequest, CancellationToken cancellation = default)
|
||||
@ -437,7 +451,7 @@ namespace NBXplorer
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", null, cancellation).ConfigureAwait(false);
|
||||
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch (NBXplorerException ex) when (ex.Error?.HttpCode == 404)
|
||||
{
|
||||
@ -452,16 +466,17 @@ namespace NBXplorer
|
||||
|
||||
public async Task<KeyPathInformation> GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/scripts/" + script.ToHex(), new object[] { CryptoCode, strategy }, cancellation).ConfigureAwait(false);
|
||||
return await GetKeyInformationAsync(new DerivationSchemeTrackedSource(strategy), script, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
public async Task<KeyPathInformation> GetKeyInformationAsync(TrackedSource trackedSource, Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Obsolete("Use GetKeyInformationAsync(DerivationStrategyBase strategy, Script script) instead")]
|
||||
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, "v1/cryptos/{0}/scripts/" + script.ToHex(), new[] { CryptoCode }, cancellation).ConfigureAwait(false);
|
||||
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[Obsolete("Use GetKeyInformation(DerivationStrategyBase strategy, Script script) instead")]
|
||||
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult();
|
||||
@ -480,7 +495,7 @@ namespace NBXplorer
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation).ConfigureAwait(false);
|
||||
return await GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch (NBXplorerException ex) when (fallbackFeeRate != null && ex.Error.Code == "fee-estimation-unavailable")
|
||||
{
|
||||
@ -489,7 +504,7 @@ namespace NBXplorer
|
||||
}
|
||||
public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, CancellationToken cancellation = default)
|
||||
{
|
||||
return GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation);
|
||||
return GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation);
|
||||
}
|
||||
public CreatePSBTResponse CreatePSBT(DerivationStrategyBase derivationStrategy, CreatePSBTRequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
@ -501,7 +516,7 @@ namespace NBXplorer
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations/{1}/psbt/create", new object[] { CryptoCode, derivationStrategy }, cancellation);
|
||||
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations/{derivationStrategy}/psbt/create", cancellation);
|
||||
}
|
||||
|
||||
public UpdatePSBTResponse UpdatePSBT(UpdatePSBTRequest request, CancellationToken cancellation = default)
|
||||
@ -512,7 +527,7 @@ namespace NBXplorer
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/psbt/update", new object[] { CryptoCode }, cancellation);
|
||||
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/psbt/update", cancellation);
|
||||
}
|
||||
public BroadcastResult Broadcast(Transaction tx, CancellationToken cancellation = default)
|
||||
{
|
||||
@ -530,7 +545,7 @@ namespace NBXplorer
|
||||
|
||||
public Task<BroadcastResult> BroadcastAsync(Transaction tx, bool testMempoolAccept, CancellationToken cancellation = default)
|
||||
{
|
||||
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), "v1/cryptos/{0}/transactions?testMempoolAccept={1}", new[] { CryptoCode, testMempoolAccept.ToString() }, cancellation);
|
||||
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), $"v1/cryptos/{CryptoCode}/transactions?testMempoolAccept={testMempoolAccept}", cancellation);
|
||||
}
|
||||
|
||||
public TMetadata GetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, CancellationToken cancellationToken = default)
|
||||
@ -543,7 +558,7 @@ namespace NBXplorer
|
||||
throw new ArgumentNullException(nameof(derivationScheme));
|
||||
if (key == null)
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
return GetAsync<TMetadata>("v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
|
||||
return GetAsync<TMetadata>($"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
|
||||
}
|
||||
|
||||
public void SetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
|
||||
@ -553,13 +568,13 @@ namespace NBXplorer
|
||||
|
||||
public Task SetMetadataAsync<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<string>(HttpMethod.Post, value, "v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
|
||||
return SendAsync<string>(HttpMethod.Post, value, $"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
|
||||
}
|
||||
|
||||
public Task<GenerateWalletResponse> GenerateWalletAsync(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
request ??= new GenerateWalletRequest();
|
||||
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations", new object[] { CryptoCode }, cancellationToken);
|
||||
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations", cancellationToken);
|
||||
}
|
||||
|
||||
public GenerateWalletResponse GenerateWallet(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
|
||||
@ -568,6 +583,37 @@ namespace NBXplorer
|
||||
return GenerateWalletAsync(request, cancellationToken).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task<GroupInformation> CreateGroupAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, null, $"v1/groups", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> GetGroupAsync(string groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Get, null, $"v1/groups/{groupId}", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> AddGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, children, $"v1/groups/{groupId}/children", cancellationToken);
|
||||
}
|
||||
public Task<GroupInformation> RemoveGroupChildrenAsync(string groupId, GroupChild[] children, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Delete, children, $"v1/groups/{groupId}/children", cancellationToken);
|
||||
}
|
||||
|
||||
public Task AddGroupAddressAsync(string cryptoCode, string groupId, string[] addresses, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return SendAsync<GroupInformation>(HttpMethod.Post, addresses, $"v1/cryptos/{cryptoCode}/groups/{groupId}/addresses", cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ImportUTXOs(string cryptoCode, ImportUTXORequest request, CancellationToken cancellation = default)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
if (cryptoCode == null)
|
||||
throw new ArgumentNullException(nameof(cryptoCode));
|
||||
await SendAsync(HttpMethod.Post, request, $"v1/cryptos/{cryptoCode}/rescan-utxos", cancellation);
|
||||
}
|
||||
|
||||
private static readonly HttpClient SharedClient = new HttpClient();
|
||||
internal HttpClient Client = SharedClient;
|
||||
|
||||
@ -611,13 +657,23 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetFullUri(string relativePath, params object[] parameters)
|
||||
static FormattableString EncodeUrlParameters(FormattableString url)
|
||||
{
|
||||
return FormattableStringFactory.Create(
|
||||
url.Format,
|
||||
url.GetArguments()
|
||||
.Select(a =>
|
||||
a is RawStr ? a :
|
||||
a is FormattableString o ? EncodeUrlParameters(o) :
|
||||
Uri.EscapeDataString(a?.ToString() ?? ""))
|
||||
.ToArray());
|
||||
}
|
||||
internal string GetFullUri(FormattableString relativePath)
|
||||
{
|
||||
relativePath = String.Format(relativePath, parameters ?? new object[0]);
|
||||
var uri = Address.AbsoluteUri;
|
||||
if (!uri.EndsWith("/", StringComparison.Ordinal))
|
||||
uri += "/";
|
||||
uri += relativePath;
|
||||
uri += EncodeUrlParameters(relativePath).ToString();
|
||||
if (!IncludeTransaction)
|
||||
{
|
||||
if (uri.IndexOf('?') == -1)
|
||||
@ -627,13 +683,13 @@ namespace NBXplorer
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
private Task<T> GetAsync<T>(string relativePath, object[] parameters, CancellationToken cancellation)
|
||||
private Task<T> GetAsync<T>(FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters, cancellation);
|
||||
return SendAsync<T>(HttpMethod.Get, null, relativePath, cancellation);
|
||||
}
|
||||
internal async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, object[] parameters, CancellationToken cancellation)
|
||||
internal async Task<T> SendAsync<T>(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
HttpRequestMessage message = CreateMessage(method, body, relativePath, parameters);
|
||||
HttpRequestMessage message = CreateMessage(method, body, relativePath);
|
||||
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
if ((int)result.StatusCode == 404)
|
||||
{
|
||||
@ -647,16 +703,35 @@ namespace NBXplorer
|
||||
{
|
||||
if (Auth.RefreshCache())
|
||||
{
|
||||
message = CreateMessage(method, body, relativePath, parameters);
|
||||
result = await Client.SendAsync(message).ConfigureAwait(false);
|
||||
message = CreateMessage(method, body, relativePath);
|
||||
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return await ParseResponse<T>(result).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, string relativePath, object[] parameters)
|
||||
internal async Task<HttpResponseMessage> SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
|
||||
{
|
||||
var uri = GetFullUri(relativePath, parameters);
|
||||
var message = CreateMessage(method, body, relativePath);
|
||||
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
|
||||
if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout)
|
||||
{
|
||||
throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException());
|
||||
}
|
||||
if ((int)result.StatusCode == 401)
|
||||
{
|
||||
if (Auth.RefreshCache())
|
||||
{
|
||||
message = CreateMessage(method, body, relativePath);
|
||||
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, FormattableString relativePath)
|
||||
{
|
||||
var uri = GetFullUri(relativePath);
|
||||
var message = new HttpRequestMessage(method, uri);
|
||||
Auth.SetAuthorization(message);
|
||||
if (body != null)
|
||||
@ -709,5 +784,18 @@ namespace NBXplorer
|
||||
throw error.AsException();
|
||||
}
|
||||
}
|
||||
|
||||
private FormattableString GetBasePath(TrackedSource trackedSource)
|
||||
{
|
||||
if (trackedSource is null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
return trackedSource switch
|
||||
{
|
||||
DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}",
|
||||
AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}",
|
||||
GroupTrackedSource wts => $"v1/cryptos/{CryptoCode}/groups/{wts.GroupId}",
|
||||
_ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,27 +37,6 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index)
|
||||
{
|
||||
|
||||
if((uint)index > (uint)array.Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return new ArraySegment<T>(array.Array, array.Offset + index, array.Count - index);
|
||||
}
|
||||
|
||||
public static ArraySegment<T> Slice<T>(this ArraySegment<T> array, int index, int count)
|
||||
{
|
||||
if((uint)index > (uint)array.Count || (uint)count > (uint)(array.Count - index))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
return new ArraySegment<T>(array.Array, array.Offset + index, count);
|
||||
}
|
||||
|
||||
public static async Task CloseSocket(this WebSocket socket, WebSocketCloseStatus status, string statusDescription, CancellationToken cancellation = default)
|
||||
{
|
||||
try
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBitcoin.JsonConverters;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class PSBTDestinationJsonConverter : JsonConverter
|
||||
{
|
||||
public PSBTDestinationJsonConverter(Network network)
|
||||
{
|
||||
Network = network;
|
||||
}
|
||||
|
||||
public Network Network { get; }
|
||||
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(PSBTDestination).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
if (reader.TokenType != JsonToken.String)
|
||||
{
|
||||
throw new JsonObjectException($"Unexpected json token type, expected is {JsonToken.String} and actual is {reader.TokenType}", reader);
|
||||
}
|
||||
var str = reader.Value.ToString();
|
||||
try
|
||||
{
|
||||
return PSBTDestination.Parse(str, Network);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new JsonObjectException(ex.Message, reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not null)
|
||||
{
|
||||
writer.WriteValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -75,7 +75,7 @@ namespace NBXplorer
|
||||
if (longPolling)
|
||||
parameters.Add($"longPolling={longPolling}");
|
||||
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{parametersString}", null, cancellation);
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{ExplorerClient.Raw(parametersString)}", cancellation);
|
||||
|
||||
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
|
||||
.OfType<NewEventBase>()
|
||||
@ -94,7 +94,7 @@ namespace NBXplorer
|
||||
if (limit != 10)
|
||||
parameters.Add($"limit={limit}");
|
||||
var parametersString = parameters.Count == 0 ? string.Empty : $"?{String.Join("&", parameters.ToArray<object>())}";
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{parametersString}", null, cancellation);
|
||||
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{ExplorerClient.Raw(parametersString)}", cancellation);
|
||||
|
||||
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
|
||||
.OfType<NewEventBase>()
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class CreatePSBTRequest
|
||||
{
|
||||
[JsonProperty("PSBTVersion")]
|
||||
public int? PSBTVersion { get; set; }
|
||||
/// <summary>
|
||||
/// A seed to specific to get a deterministic PSBT (useful for tests)
|
||||
/// </summary>
|
||||
@ -63,10 +68,15 @@ namespace NBXplorer.Models
|
||||
/// </summary>
|
||||
public List<OutPoint> IncludeOnlyOutpoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If `true`, all the UTXOs that have been selected will be used as input in the PSBT. (default to false)
|
||||
/// </summary>
|
||||
public bool? SpendAllMatchingOutpoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Use a specific change address (Optional, default: null, mutually exclusive with ReserveChangeAddress)
|
||||
/// </summary>
|
||||
public BitcoinAddress ExplicitChangeAddress { get; set; }
|
||||
public PSBTDestination ExplicitChangeAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rebase the hdkey paths (if no rebase, the key paths are relative to the xpub that NBXplorer knows about)
|
||||
@ -101,9 +111,66 @@ namespace NBXplorer.Models
|
||||
/// </summary>
|
||||
public RootedKeyPath AccountKeyPath { get; set; }
|
||||
}
|
||||
public class ScriptDestination : IDestination
|
||||
{
|
||||
public ScriptDestination(Script scriptPubKey)
|
||||
{
|
||||
ScriptPubKey = scriptPubKey;
|
||||
}
|
||||
public Script ScriptPubKey { get; }
|
||||
}
|
||||
|
||||
public abstract class PSBTDestination
|
||||
{
|
||||
public static implicit operator PSBTDestination(Script script) => new ScriptType(script);
|
||||
public static implicit operator PSBTDestination(BitcoinAddress address) => new AddressType(address);
|
||||
public class ScriptType : PSBTDestination
|
||||
{
|
||||
public ScriptType(Script scriptPubKey)
|
||||
{
|
||||
if (scriptPubKey is null)
|
||||
throw new ArgumentNullException(nameof(scriptPubKey));
|
||||
ScriptPubKey = scriptPubKey;
|
||||
}
|
||||
public override Script ScriptPubKey { get; }
|
||||
public override string ToString() => ScriptPubKey.ToHex();
|
||||
}
|
||||
public class AddressType : PSBTDestination
|
||||
{
|
||||
public AddressType(BitcoinAddress address)
|
||||
{
|
||||
if (address is null)
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
Address = address;
|
||||
}
|
||||
public BitcoinAddress Address { get; }
|
||||
public override Script ScriptPubKey => Address.ScriptPubKey;
|
||||
public override string ToString() => Address.ToString();
|
||||
}
|
||||
public abstract Script ScriptPubKey { get; }
|
||||
public static PSBTDestination Create(Script script) => new ScriptType(script);
|
||||
public static PSBTDestination Create(BitcoinAddress address) => new AddressType(address);
|
||||
|
||||
public static PSBTDestination Parse(string str, Network network)
|
||||
{
|
||||
if (str is null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
if (network is null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (HexEncoder.IsWellFormed(str))
|
||||
return new ScriptType(Script.FromHex(str));
|
||||
else
|
||||
{
|
||||
return new AddressType(BitcoinAddress.Create(str, network));
|
||||
}
|
||||
}
|
||||
}
|
||||
public class CreatePSBTDestination
|
||||
{
|
||||
public BitcoinAddress Destination { get; set; }
|
||||
/// <summary>
|
||||
/// The destination as an address or a script. (in hex)
|
||||
/// </summary>
|
||||
public PSBTDestination Destination { get; set; }
|
||||
/// <summary>
|
||||
/// Will Send this amount to this destination (Mutually exclusive with: SweepAll)
|
||||
/// </summary>
|
||||
@ -120,11 +187,11 @@ namespace NBXplorer.Models
|
||||
public class FeePreference
|
||||
{
|
||||
/// <summary>
|
||||
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, ExplicitFee, FallbackFeeRate)
|
||||
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, FallbackFeeRate)
|
||||
/// </summary>
|
||||
public FeeRate ExplicitFeeRate { get; set; }
|
||||
/// <summary>
|
||||
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, ExplicitFeeRate, FallbackFeeRate)
|
||||
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, FallbackFeeRate)
|
||||
/// </summary>
|
||||
public Money ExplicitFee { get; set; }
|
||||
/// <summary>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
@ -14,6 +15,7 @@ namespace NBXplorer.Models
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.ScriptPubKeyTypeConverter))]
|
||||
public NBitcoin.ScriptPubKeyType? ScriptPubKeyType { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
[Obsolete("We will remove this feature in a future release.")]
|
||||
public bool ImportKeysToRPC { get; set; }
|
||||
public bool SavePrivateKeys { get; set; }
|
||||
public Dictionary<string, string> AdditionalOptions { get; set; }
|
||||
|
||||
@ -6,6 +6,7 @@ namespace NBXplorer.Models
|
||||
{
|
||||
public class GenerateWalletResponse
|
||||
{
|
||||
public string TrackedSource { get; set; }
|
||||
public string Mnemonic { get; set; }
|
||||
public string Passphrase { get; set; }
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.WordlistJsonConverter))]
|
||||
@ -17,7 +18,7 @@ namespace NBXplorer.Models
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
|
||||
public NBitcoin.RootedKeyPath AccountKeyPath { get; set; }
|
||||
public string AccountDescriptor { get; set; }
|
||||
public DerivationStrategyBase DerivationScheme { get; set; }
|
||||
public StandardDerivationStrategyBase DerivationScheme { get; set; }
|
||||
|
||||
public Mnemonic GetMnemonic()
|
||||
{
|
||||
|
||||
@ -89,10 +89,10 @@ namespace NBXplorer.Models
|
||||
get; set;
|
||||
} = new List<MatchedOutput>();
|
||||
|
||||
public List<MatchedOutput> Inputs
|
||||
public List<MatchedInput> Inputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<MatchedOutput>();
|
||||
} = new List<MatchedInput>();
|
||||
public DateTimeOffset Timestamp
|
||||
{
|
||||
get;
|
||||
@ -106,5 +106,6 @@ namespace NBXplorer.Models
|
||||
public uint256 ReplacedBy { get; set; }
|
||||
public uint256 Replacing { get; set; }
|
||||
public bool Replaceable { get; set; }
|
||||
public TransactionMetadata Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
22
NBXplorer.Client/Models/GroupInformation.cs
Normal file
22
NBXplorer.Client/Models/GroupInformation.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GroupChild
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public string CryptoCode { get; set; }
|
||||
public string TrackedSource { get; set; }
|
||||
}
|
||||
public class GroupInformation
|
||||
{
|
||||
public string TrackedSource { get; set; }
|
||||
public string GroupId { get; set; }
|
||||
public GroupChild[] Children { get; set; }
|
||||
|
||||
public GroupChild AsGroupChild() => new () { TrackedSource = TrackedSource };
|
||||
}
|
||||
}
|
||||
10
NBXplorer.Client/Models/ImportUTXORequest.cs
Normal file
10
NBXplorer.Client/Models/ImportUTXORequest.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NBXplorer.Models;
|
||||
|
||||
public class ImportUTXORequest
|
||||
{
|
||||
[JsonProperty("UTXOs")]
|
||||
public OutPoint[] Utxos { get; set; }
|
||||
}
|
||||
@ -1,26 +1,13 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class KeyPathInformation
|
||||
{
|
||||
public KeyPathInformation()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public KeyPathInformation(Derivation derivation, DerivationSchemeTrackedSource derivationStrategy, DerivationFeature feature, KeyPath keyPath, NBXplorerNetwork network)
|
||||
{
|
||||
ScriptPubKey = derivation.ScriptPubKey;
|
||||
Redeem = derivation.Redeem;
|
||||
TrackedSource = derivationStrategy;
|
||||
DerivationStrategy = derivationStrategy.DerivationStrategy;
|
||||
Feature = feature;
|
||||
KeyPath = keyPath;
|
||||
Address = network.CreateAddress(derivationStrategy.DerivationStrategy, keyPath, ScriptPubKey);
|
||||
}
|
||||
public TrackedSource TrackedSource { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature Feature
|
||||
@ -48,9 +35,10 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public int GetIndex(KeyPathTemplates keyPathTemplates)
|
||||
{
|
||||
return (int)keyPathTemplates.GetKeyPathTemplate(Feature).GetIndex(KeyPath);
|
||||
}
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? Index { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,25 +71,6 @@ namespace NBXplorer
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryMatchTemplate(KeyPath keyPath, out uint index)
|
||||
{
|
||||
index = 0;
|
||||
if (keyPath.Length != 1 + PreIndexes.Length + PostIndexes.Length)
|
||||
return false;
|
||||
for (int i = 0; i < PreIndexes.Length; i++)
|
||||
{
|
||||
if (PreIndexes[i] != keyPath[i])
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < PostIndexes.Length; i++)
|
||||
{
|
||||
if (PostIndexes[i] != keyPath[i + 1 + PreIndexes.Length])
|
||||
return false;
|
||||
}
|
||||
index = keyPath[PreIndexes.Length];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseCore(string i, out uint index)
|
||||
{
|
||||
if (i.Length == 0)
|
||||
@ -147,12 +128,5 @@ namespace NBXplorer
|
||||
builder.Append($"/{PostIndexes}");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public uint GetIndex(KeyPath keypath)
|
||||
{
|
||||
if (TryMatchTemplate(keypath, out var index))
|
||||
return index;
|
||||
throw new ArgumentException("Impossible to get the index of this keypath", nameof(keypath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,14 +13,7 @@ namespace NBXplorer
|
||||
private readonly KeyPathTemplate customKeyPathTemplate;
|
||||
private static readonly KeyPathTemplates _Default = new KeyPathTemplates();
|
||||
private readonly DerivationFeature[] derivationFeatures;
|
||||
|
||||
public static KeyPathTemplates Default
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Default;
|
||||
}
|
||||
}
|
||||
public static KeyPathTemplates Default => _Default;
|
||||
|
||||
private KeyPathTemplates() : this(null)
|
||||
{
|
||||
@ -29,81 +22,27 @@ namespace NBXplorer
|
||||
public KeyPathTemplates(KeyPathTemplate customKeyPathTemplate)
|
||||
{
|
||||
this.customKeyPathTemplate = customKeyPathTemplate;
|
||||
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>();
|
||||
derivationFeatures.Add(DerivationFeature.Deposit);
|
||||
derivationFeatures.Add(DerivationFeature.Change);
|
||||
derivationFeatures.Add(DerivationFeature.Direct);
|
||||
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>
|
||||
{
|
||||
DerivationFeature.Deposit,
|
||||
DerivationFeature.Change,
|
||||
DerivationFeature.Direct
|
||||
};
|
||||
if (customKeyPathTemplate != null)
|
||||
derivationFeatures.Add(DerivationFeature.Custom);
|
||||
this.derivationFeatures = derivationFeatures.ToArray();
|
||||
}
|
||||
|
||||
public KeyPathTemplate GetKeyPathTemplate(DerivationFeature derivationFeature)
|
||||
{
|
||||
switch (derivationFeature)
|
||||
=> derivationFeature switch
|
||||
{
|
||||
case DerivationFeature.Deposit:
|
||||
return depositKeyPathTemplate;
|
||||
case DerivationFeature.Change:
|
||||
return changeKeyPathTemplate;
|
||||
case DerivationFeature.Direct:
|
||||
return directKeyPathTemplate;
|
||||
case DerivationFeature.Custom when customKeyPathTemplate != null:
|
||||
return customKeyPathTemplate;
|
||||
default:
|
||||
throw new NotSupportedException(derivationFeature.ToString());
|
||||
}
|
||||
}
|
||||
DerivationFeature.Deposit => depositKeyPathTemplate,
|
||||
DerivationFeature.Change => changeKeyPathTemplate,
|
||||
DerivationFeature.Direct => directKeyPathTemplate,
|
||||
DerivationFeature.Custom when customKeyPathTemplate != null => customKeyPathTemplate,
|
||||
_ => throw new NotSupportedException($"The derivation feature {derivationFeature} is not supported by the key path templates.")
|
||||
};
|
||||
|
||||
public KeyPathTemplate GetKeyPathTemplate(KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null)
|
||||
throw new ArgumentNullException(nameof(keyPath));
|
||||
|
||||
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return depositKeyPathTemplate;
|
||||
}
|
||||
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return changeKeyPathTemplate;
|
||||
}
|
||||
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return directKeyPathTemplate;
|
||||
}
|
||||
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return customKeyPathTemplate;
|
||||
}
|
||||
else
|
||||
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
|
||||
}
|
||||
|
||||
public DerivationFeature GetDerivationFeature(KeyPath keyPath)
|
||||
{
|
||||
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return DerivationFeature.Deposit;
|
||||
}
|
||||
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return DerivationFeature.Change;
|
||||
}
|
||||
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return DerivationFeature.Direct;
|
||||
}
|
||||
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
|
||||
{
|
||||
return DerivationFeature.Custom;
|
||||
}
|
||||
else
|
||||
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
|
||||
}
|
||||
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures()
|
||||
{
|
||||
return derivationFeatures;
|
||||
}
|
||||
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures() => derivationFeatures;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@ namespace NBXplorer.Models
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<MatchedInput> Inputs
|
||||
{
|
||||
get; set;
|
||||
} = new List<MatchedInput>();
|
||||
public List<MatchedOutput> Outputs
|
||||
{
|
||||
get; set;
|
||||
@ -62,7 +66,19 @@ namespace NBXplorer.Models
|
||||
public KeyPath KeyPath { get; set; }
|
||||
public Script ScriptPubKey { get; set; }
|
||||
public int Index { get; set; }
|
||||
public int KeyIndex { get; set; }
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature? Feature { get; set; }
|
||||
public IMoney Value { get; set; }
|
||||
public BitcoinAddress Address { get; set; }
|
||||
}
|
||||
|
||||
public class MatchedInput : MatchedOutput
|
||||
{
|
||||
public int InputIndex { get; set; }
|
||||
public uint256 TransactionId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -48,12 +48,6 @@ namespace NBXplorer.Models
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string Backend { get; set; }
|
||||
public double RepositoryPingTime
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public bool IsFullySynched
|
||||
{
|
||||
get; set;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
@ -10,22 +13,30 @@ namespace NBXplorer.Models
|
||||
{
|
||||
if (str == null)
|
||||
throw new ArgumentNullException(nameof(str));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
trackedSource = null;
|
||||
var strSpan = str.AsSpan();
|
||||
if (strSpan.StartsWith("DERIVATIONSCHEME:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (network is null)
|
||||
return false;
|
||||
if (!DerivationSchemeTrackedSource.TryParse(strSpan, out var derivationSchemeTrackedSource, network))
|
||||
return false;
|
||||
trackedSource = derivationSchemeTrackedSource;
|
||||
}
|
||||
else if (strSpan.StartsWith("ADDRESS:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (network is null)
|
||||
return false;
|
||||
if (!AddressTrackedSource.TryParse(strSpan, out var addressTrackedSource, network.NBitcoinNetwork))
|
||||
return false;
|
||||
trackedSource = addressTrackedSource;
|
||||
}
|
||||
else if (strSpan.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
|
||||
{
|
||||
if (!GroupTrackedSource.TryParse(strSpan, out var walletTrackedSource))
|
||||
return false;
|
||||
trackedSource = walletTrackedSource;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
@ -97,6 +108,51 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupTrackedSource : TrackedSource
|
||||
{
|
||||
public string GroupId { get; }
|
||||
|
||||
public static GroupTrackedSource Generate()
|
||||
{
|
||||
Span<byte> r = stackalloc byte[13];
|
||||
// 13 is most consistent on number of chars and more than we need to avoid generating twice same id
|
||||
RandomNumberGenerator.Fill(r);
|
||||
return new GroupTrackedSource(Encoders.Base58.EncodeData(r));
|
||||
}
|
||||
|
||||
public GroupTrackedSource(string groupId)
|
||||
{
|
||||
GroupId = groupId;
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> trackedSource, out GroupTrackedSource walletTrackedSource)
|
||||
{
|
||||
walletTrackedSource = null;
|
||||
if (!trackedSource.StartsWith("GROUP:".AsSpan(), StringComparison.Ordinal))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
walletTrackedSource = new GroupTrackedSource(trackedSource.Slice("GROUP:".Length).ToString());
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "GROUP:" + GroupId;
|
||||
}
|
||||
public override string ToPrettyString()
|
||||
{
|
||||
return "G:" + GroupId;
|
||||
}
|
||||
|
||||
public static GroupTrackedSource Parse(string trackedSource)
|
||||
{
|
||||
return TryParse(trackedSource, out var g) ? g : throw new FormatException("Invalid group tracked source format");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressTrackedSource : TrackedSource, IDestination
|
||||
{
|
||||
// Note that we should in theory access BitcoinAddress. But parsing BitcoinAddress is very expensive, so we keep storing plain strings
|
||||
@ -119,8 +175,6 @@ namespace NBXplorer.Models
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> strSpan, out TrackedSource addressTrackedSource, Network network)
|
||||
{
|
||||
if (strSpan == null)
|
||||
throw new ArgumentNullException(nameof(strSpan));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
addressTrackedSource = null;
|
||||
@ -158,8 +212,6 @@ namespace NBXplorer.Models
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> strSpan, out DerivationSchemeTrackedSource derivationSchemeTrackedSource, NBXplorerNetwork network)
|
||||
{
|
||||
if (strSpan == null)
|
||||
throw new ArgumentNullException(nameof(strSpan));
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
derivationSchemeTrackedSource = null;
|
||||
@ -188,5 +240,12 @@ namespace NBXplorer.Models
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
#if !NO_RECORD
|
||||
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
|
||||
=> DerivationStrategy is PolicyDerivationStrategy ? new[] { DerivationFeature.Deposit, DerivationFeature.Change } : keyPathTemplates.GetSupportedDerivationFeatures();
|
||||
#else
|
||||
public IEnumerable<DerivationFeature> GetDerivationFeatures(KeyPathTemplates keyPathTemplates)
|
||||
=> keyPathTemplates.GetSupportedDerivationFeatures();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
40
NBXplorer.Client/Models/TransactionMetadata.cs
Normal file
40
NBXplorer.Client/Models/TransactionMetadata.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class TransactionMetadata
|
||||
{
|
||||
public class ChunkMetadata
|
||||
{
|
||||
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
|
||||
public Money Fees { get; set; }
|
||||
[JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Weight { get; set; }
|
||||
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("vsize", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int? VirtualSize { get; set; }
|
||||
[JsonProperty("fees", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBXplorer.JsonConverters.MoneyJsonConverter))]
|
||||
public Money Fees { get; set; }
|
||||
[JsonProperty("feeRate", DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.FeeRateJsonConverter))]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
public ChunkMetadata Chunk { get; set; }
|
||||
public static TransactionMetadata Parse(string json) => JsonConvert.DeserializeObject<TransactionMetadata>(json);
|
||||
public string ToString(bool indented) => JsonConvert.SerializeObject(this, indented ? Formatting.Indented : Formatting.None);
|
||||
public override string ToString() => ToString(true);
|
||||
[JsonExtensionData]
|
||||
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
|
||||
}
|
||||
}
|
||||
@ -55,5 +55,7 @@ namespace NBXplorer.Models
|
||||
set;
|
||||
}
|
||||
public uint256 ReplacedBy { get; set; }
|
||||
|
||||
public TransactionMetadata Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
@ -42,6 +45,11 @@ namespace NBXplorer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public List<UTXO> SpentUnconfirmed
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = new List<UTXO>();
|
||||
|
||||
UTXOChange _Confirmed = new UTXOChange();
|
||||
public UTXOChange Confirmed
|
||||
@ -85,7 +93,7 @@ namespace NBXplorer.Models
|
||||
|
||||
public Key[] GetKeys(ExtKey extKey, bool excludeUnconfirmedUTXOs = false)
|
||||
{
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
|
||||
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Where(u => u.KeyPath is not null).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
|
||||
}
|
||||
}
|
||||
public class UTXOChange
|
||||
@ -151,16 +159,31 @@ namespace NBXplorer.Models
|
||||
if (Value is Money v)
|
||||
{
|
||||
var coin = new Coin(Outpoint, new TxOut(v, ScriptPubKey));
|
||||
if (derivationStrategy != null)
|
||||
if (Redeem is not null)
|
||||
{
|
||||
var derivation = derivationStrategy.GetDerivation(KeyPath);
|
||||
if (derivation.ScriptPubKey != coin.ScriptPubKey)
|
||||
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
|
||||
if (derivation.Redeem != null)
|
||||
coin = coin.ToScriptCoin(derivation.Redeem);
|
||||
}
|
||||
else if (Redeem is not null)
|
||||
coin = coin.ToScriptCoin(Redeem);
|
||||
}
|
||||
else
|
||||
{
|
||||
DerivationStrategy.Derivation derivation = null;
|
||||
if (derivationStrategy is StandardDerivationStrategyBase kd && KeyPath is not null)
|
||||
{
|
||||
derivation = kd.GetDerivation(KeyPath);
|
||||
}
|
||||
#if !NO_RECORD
|
||||
else if (derivationStrategy is PolicyDerivationStrategy md && Feature is { } f)
|
||||
{
|
||||
derivation = md.GetDerivation(f, (uint)KeyIndex);
|
||||
}
|
||||
#endif
|
||||
if (derivation is not null)
|
||||
{
|
||||
if (derivation.ScriptPubKey != coin.ScriptPubKey)
|
||||
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
|
||||
if (derivation.Redeem != null)
|
||||
coin = coin.ToScriptCoin(derivation.Redeem);
|
||||
}
|
||||
}
|
||||
return coin;
|
||||
}
|
||||
return null;
|
||||
@ -254,5 +277,7 @@ namespace NBXplorer.Models
|
||||
_Confirmations = checked((long)value);
|
||||
}
|
||||
}
|
||||
|
||||
public int KeyIndex { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,34 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
|
||||
<Company>Digital Garage</Company>
|
||||
<Version>4.2.5</Version>
|
||||
<Version>5.0.6</Version>
|
||||
<Copyright>Copyright © Digital Garage 2017</Copyright>
|
||||
<Description>Client API for the minimalist HD Wallet Tracker NBXplorer</Description>
|
||||
<PackageIconUrl>https://aois.blob.core.windows.net/public/Bitcoin.png</PackageIconUrl>
|
||||
<PackageIcon>Bitcoin.png</PackageIcon>
|
||||
<PackageTags>bitcoin</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/dgarage/NBXplorer/</PackageProjectUrl>
|
||||
<PackageProjectUrl>https://github.com/btcpayserver/NBXplorer/</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/dgarage/NBXplorer</RepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/NBXplorer</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
|
||||
<DefineConstants>$(DefineConstants);NO_SPAN</DefineConstants>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
|
||||
<DefineConstants>$(DefineConstants);NO_RECORD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591;1573;1572;1584;1570;3021</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="7.0.31" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.19" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||
<PackageReference Include="NBitcoin" Version="10.0.6" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="6.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="NBXplorer.Tests" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="Bitcoin.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -50,6 +50,7 @@ namespace NBXplorer
|
||||
internal set;
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public virtual BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
|
||||
{
|
||||
return scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
|
||||
|
||||
@ -4,6 +4,9 @@ using NBitcoin.Altcoins.Elements;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.DataEncoders;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
#if !NO_RECORD
|
||||
using NBitcoin.WalletPolicies;
|
||||
#endif
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
@ -23,14 +26,19 @@ namespace NBXplorer
|
||||
return factory;
|
||||
}
|
||||
|
||||
public BitcoinAddress BlindIfNeeded(DerivationStrategyBase derivationStrategy, BitcoinAddress address, KeyPath keyPath)
|
||||
{
|
||||
if (derivationStrategy.Unblinded() || address is BitcoinBlindedAddress)
|
||||
return address;
|
||||
var blindingPubKey = GenerateBlindingKey(derivationStrategy, keyPath, address.ScriptPubKey, NBitcoinNetwork).PubKey;
|
||||
return new BitcoinBlindedAddress(blindingPubKey, address);
|
||||
}
|
||||
|
||||
[Obsolete]
|
||||
public override BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
|
||||
{
|
||||
if (derivationStrategy.Unblinded())
|
||||
{
|
||||
return base.CreateAddress(derivationStrategy, keyPath, scriptPubKey);
|
||||
}
|
||||
var blindingPubKey = GenerateBlindingKey(derivationStrategy, keyPath, scriptPubKey, NBitcoinNetwork).PubKey;
|
||||
return new BitcoinBlindedAddress(blindingPubKey, base.CreateAddress(derivationStrategy, keyPath, scriptPubKey));
|
||||
var addr = scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
|
||||
return BlindIfNeeded(derivationStrategy, addr, keyPath);
|
||||
}
|
||||
|
||||
public static Key GenerateSlip77BlindingKeyFromMnemonic(Mnemonic mnemonic, Script script)
|
||||
@ -46,7 +54,7 @@ namespace NBXplorer
|
||||
return new Key(Hashes.HMACSHA256(masterBlindingKey.ToBytes(), script.ToBytes()));
|
||||
}
|
||||
|
||||
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script script, Network network)
|
||||
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey, Network network)
|
||||
{
|
||||
if (derivationStrategy.Unblinded())
|
||||
{
|
||||
@ -57,11 +65,11 @@ namespace NBXplorer
|
||||
{
|
||||
if (HexEncoder.IsWellFormed(key))
|
||||
{
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), script);
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), scriptPubKey);
|
||||
}
|
||||
try
|
||||
{
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), script);
|
||||
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), scriptPubKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -70,7 +78,7 @@ namespace NBXplorer
|
||||
try
|
||||
{
|
||||
var data = new Mnemonic(key);
|
||||
return GenerateSlip77BlindingKeyFromMnemonic(data, derivationStrategy.GetDerivation(keyPath).ScriptPubKey);
|
||||
return GenerateSlip77BlindingKeyFromMnemonic(data, scriptPubKey);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -79,10 +87,12 @@ namespace NBXplorer
|
||||
|
||||
throw new InvalidOperationException("The key provided for slip77 derivation was invalid.");
|
||||
}
|
||||
|
||||
var blindingKey = new Key(derivationStrategy.GetChild(keyPath).GetChild(new KeyPath("0")).GetDerivation()
|
||||
.ScriptPubKey.WitHash.ToBytes());
|
||||
return blindingKey;
|
||||
else if (derivationStrategy is StandardDerivationStrategyBase kpd && keyPath is not null)
|
||||
{
|
||||
var blindingKey = new Key(kpd.GetDerivation(keyPath.Derive(new KeyPath(0))).ScriptPubKey.WitHash.ToBytes());
|
||||
return blindingKey;
|
||||
}
|
||||
throw new InvalidOperationException("-[blinded] doesn't work on miniscript derivation strategies, use [slip77=key] instead");
|
||||
}
|
||||
}
|
||||
private void InitLiquid(ChainName networkType)
|
||||
|
||||
24
NBXplorer.Client/NBXplorerNetworkProvider.Pepecoin.cs
Normal file
24
NBXplorer.Client/NBXplorerNetworkProvider.Pepecoin.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
private void InitPepecoin(ChainName networkType)
|
||||
{
|
||||
Add(new NBXplorerNetwork(NBitcoin.Altcoins.Pepecoin.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 10000,
|
||||
ChainLoadingTimeout = TimeSpan.FromHours(1),
|
||||
ChainCacheLoadingTimeout = TimeSpan.FromMinutes(2),
|
||||
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3434'") : new KeyPath("1'")
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetPEPE()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Pepecoin.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@ namespace NBXplorer
|
||||
InitBitcore(networkType);
|
||||
InitLitecoin(networkType);
|
||||
InitDogecoin(networkType);
|
||||
InitPepecoin(networkType);
|
||||
InitBCash(networkType);
|
||||
InitGroestlcoin(networkType);
|
||||
InitBGold(networkType);
|
||||
|
||||
128
NBXplorer.Client/NBitcoinNBXplorerExtensions.cs
Normal file
128
NBXplorer.Client/NBitcoinNBXplorerExtensions.cs
Normal file
@ -0,0 +1,128 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
|
||||
namespace NBitcoin;
|
||||
|
||||
public static class NBitcoinNBXplorerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the keys</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
|
||||
RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.HDKeysFor(hd, accountKey, accountKeyPath);
|
||||
return Array.Empty<PSBTHDKeyMatch>();
|
||||
}
|
||||
/// <summary>
|
||||
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the keys</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> HDKeysFor(psbt, derivationStrategy, accountKey, null);
|
||||
|
||||
/// <summary>
|
||||
/// Filter the keys which contains the <paramref name="accountKey"/> and <paramref name="accountKeyPath"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="coin">The coins to get the keys from</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey,
|
||||
RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return coin.HDKeysFor(hd, accountKey, accountKeyPath);
|
||||
return Array.Empty<PSBTHDKeyMatch>();
|
||||
}
|
||||
|
||||
static IHDScriptPubKey? ToHDScriptPubKey(DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
{
|
||||
if (derivationStrategy is null)
|
||||
throw new ArgumentNullException(nameof(derivationStrategy));
|
||||
if (derivationStrategy is StandardDerivationStrategyBase standard)
|
||||
return standard;
|
||||
#if !NO_RECORD
|
||||
else if (derivationStrategy is PolicyDerivationStrategy policy && policy.GetHDScriptPubKey(accountKey) is IHDScriptPubKey hd)
|
||||
return hd;
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter the keys that contain the <paramref name="accountKey"/> in the HDKeys and whose input/output
|
||||
/// the same scriptPubKeys as <paramref name="derivationStrategy"/>.
|
||||
/// </summary>
|
||||
/// <param name="coin">The coins to get the keys from</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>HD Keys matching master root key</returns>
|
||||
public static IEnumerable<PSBTHDKeyMatch> HDKeysFor(this PSBTCoin coin, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> HDKeysFor(coin, derivationStrategy, accountKey, null);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the balance change if you were signing this transaction.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the balance</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <param name="accountKeyPath">The account key path</param>
|
||||
/// <returns>The balance change</returns>
|
||||
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.GetBalance(hd, accountKey, accountKeyPath);
|
||||
return Money.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the balance change if you were signing this transaction.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT from which to get the balance</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key that will be used to sign (i.e., 49'/0'/0')</param>
|
||||
/// <returns>The balance change</returns>
|
||||
public static Money GetBalance(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> GetBalance(psbt, derivationStrategy, accountKey, null);
|
||||
|
||||
/// <summary>
|
||||
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT to sign</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key with which to sign</param>
|
||||
/// <param name="accountKeyPath">The account key path (eg. [masterFP]/49'/0'/0')</param>
|
||||
/// <returns>The signed PSBT</returns>
|
||||
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey, RootedKeyPath? accountKeyPath)
|
||||
{
|
||||
if (ToHDScriptPubKey(derivationStrategy, accountKey) is {} hd)
|
||||
return psbt.SignAll(hd, accountKey, accountKeyPath);
|
||||
return psbt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign all inputs that derive addresses from <paramref name="derivationStrategy"/> and that need to be signed by <paramref name="accountKey"/>.
|
||||
/// </summary>
|
||||
/// <param name="psbt">The PSBT to sign</param>
|
||||
/// <param name="derivationStrategy">The derivation scheme</param>
|
||||
/// <param name="accountKey">The account key with which to sign</param>
|
||||
/// <returns>The signed PSBT</returns>
|
||||
public static PSBT SignAll(this PSBT psbt, DerivationStrategyBase derivationStrategy, IHDKey accountKey)
|
||||
=> SignAll(psbt, derivationStrategy, accountKey, null);
|
||||
}
|
||||
9
NBXplorer.Client/PushNuget.sh
Executable file
9
NBXplorer.Client/PushNuget.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
rm -rf "bin/Release/"
|
||||
dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg
|
||||
package=$(find ./bin/Release -name "*.nupkg" -type f | head -n 1)
|
||||
dotnet nuget push "${package[0]}" --source "https://api.nuget.org/v3/index.json" --api-key "$NUGET_API_KEY"
|
||||
ver=$(basename "${package[0]}" | sed -E 's/NBXplorer\.Client\.([0-9]+(\.[0-9]+){1,3}).*/\1/')
|
||||
git tag -a "Client/v$ver" -m "Client/$ver"
|
||||
git push origin "Client/v$ver"
|
||||
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using NBXplorer.JsonConverters;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
@ -28,6 +29,7 @@ namespace NBXplorer
|
||||
if (_Network != null)
|
||||
{
|
||||
settings.Converters.Insert(0, new JsonConverters.CachedSerializer(_Network));
|
||||
settings.Converters.Insert(1, new PSBTDestinationJsonConverter(_Network.NBitcoinNetwork));
|
||||
settings.Converters.Add(new JsonConverters.KeyPathTemplateJsonConverter());
|
||||
}
|
||||
ReplaceConverter<NBitcoin.JsonConverters.MoneyJsonConverter>(settings, new NBXplorer.JsonConverters.MoneyJsonConverter());
|
||||
|
||||
@ -7,71 +7,16 @@ using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.Protocol;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class WebsocketNotificationSession : NotificationSessionBase, IDisposable
|
||||
public class WebsocketNotificationSessionLegacy : WebsocketNotificationSession
|
||||
{
|
||||
|
||||
private readonly ExplorerClient _Client;
|
||||
public ExplorerClient Client
|
||||
protected override FormattableString GetConnectPath() => $"v1/cryptos/{_Client.CryptoCode}/connect";
|
||||
internal WebsocketNotificationSessionLegacy(ExplorerClient client) : base(client)
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Client;
|
||||
}
|
||||
}
|
||||
internal WebsocketNotificationSession(ExplorerClient client)
|
||||
{
|
||||
if(client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
_Client = client;
|
||||
}
|
||||
|
||||
internal async Task ConnectAsync(CancellationToken cancellation)
|
||||
{
|
||||
var uri = _Client.GetFullUri($"v1/cryptos/{_Client.CryptoCode}/connect", null);
|
||||
uri = ToWebsocketUri(uri);
|
||||
WebSocket socket = null;
|
||||
try
|
||||
{
|
||||
socket = await ConnectAsyncCore(uri, cancellation);
|
||||
}
|
||||
catch(WebSocketException) // For some reason the ErrorCode is not properly set, so we can check for error 401
|
||||
{
|
||||
if(!_Client.Auth.RefreshCache())
|
||||
throw;
|
||||
socket = await ConnectAsyncCore(uri, cancellation);
|
||||
}
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings();
|
||||
new Serializer(_Client.Network).ConfigureSerializer(settings);
|
||||
_MessageListener = new WebsocketMessageListener(socket, settings);
|
||||
}
|
||||
|
||||
private async Task<ClientWebSocket> ConnectAsyncCore(string uri, CancellationToken cancellation)
|
||||
{
|
||||
var socket = new ClientWebSocket();
|
||||
_Client.Auth.SetWebSocketAuth(socket);
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(new Uri(uri, UriKind.Absolute), cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch { socket.Dispose(); throw; }
|
||||
return socket;
|
||||
}
|
||||
|
||||
private static string ToWebsocketUri(string uri)
|
||||
{
|
||||
if(uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("https://", "wss://");
|
||||
if(uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("http://", "ws://");
|
||||
return uri;
|
||||
}
|
||||
|
||||
WebsocketMessageListener _MessageListener;
|
||||
UTF8Encoding UTF8 = new UTF8Encoding(false, true);
|
||||
|
||||
public void ListenNewBlock(CancellationToken cancellation = default)
|
||||
{
|
||||
ListenNewBlockAsync(cancellation).GetAwaiter().GetResult();
|
||||
@ -128,7 +73,7 @@ namespace NBXplorer
|
||||
|
||||
public Task ListenDerivationSchemesAsync(DerivationStrategyBase[] derivationSchemes, CancellationToken cancellation = default)
|
||||
{
|
||||
return _MessageListener.Send(new Models.NewTransactionEventRequest() { DerivationSchemes = derivationSchemes.Select(d=>d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
|
||||
return _MessageListener.Send(new Models.NewTransactionEventRequest() { DerivationSchemes = derivationSchemes.Select(d => d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
|
||||
}
|
||||
|
||||
public void ListenTrackedSources(TrackedSource[] trackedSources, CancellationToken cancellation = default)
|
||||
@ -141,6 +86,70 @@ namespace NBXplorer
|
||||
return _MessageListener.Send(new Models.NewTransactionEventRequest() { TrackedSources = trackedSources.Select(d => d.ToString()).ToArray(), CryptoCode = _Client.CryptoCode }, null, cancellation);
|
||||
}
|
||||
|
||||
}
|
||||
public class WebsocketNotificationSession : NotificationSessionBase, IDisposable
|
||||
{
|
||||
|
||||
protected readonly ExplorerClient _Client;
|
||||
public ExplorerClient Client
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Client;
|
||||
}
|
||||
}
|
||||
internal WebsocketNotificationSession(ExplorerClient client)
|
||||
{
|
||||
if(client == null)
|
||||
throw new ArgumentNullException(nameof(client));
|
||||
_Client = client;
|
||||
}
|
||||
|
||||
internal async Task ConnectAsync(CancellationToken cancellation)
|
||||
{
|
||||
var uri = _Client.GetFullUri(GetConnectPath());
|
||||
uri = ToWebsocketUri(uri);
|
||||
WebSocket socket = null;
|
||||
try
|
||||
{
|
||||
socket = await ConnectAsyncCore(uri, cancellation);
|
||||
}
|
||||
catch (WebSocketException) // For some reason the ErrorCode is not properly set, so we can check for error 401
|
||||
{
|
||||
if (!_Client.Auth.RefreshCache())
|
||||
throw;
|
||||
socket = await ConnectAsyncCore(uri, cancellation);
|
||||
}
|
||||
JsonSerializerSettings settings = new JsonSerializerSettings();
|
||||
new Serializer(_Client.Network).ConfigureSerializer(settings);
|
||||
_MessageListener = new WebsocketMessageListener(socket, settings);
|
||||
}
|
||||
|
||||
protected virtual FormattableString GetConnectPath() => $"v1/cryptos/connect?cryptoCode={_Client.Network.CryptoCode}";
|
||||
|
||||
private async Task<ClientWebSocket> ConnectAsyncCore(string uri, CancellationToken cancellation)
|
||||
{
|
||||
var socket = new ClientWebSocket();
|
||||
_Client.Auth.SetWebSocketAuth(socket);
|
||||
try
|
||||
{
|
||||
await socket.ConnectAsync(new Uri(uri, UriKind.Absolute), cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch { socket.Dispose(); throw; }
|
||||
return socket;
|
||||
}
|
||||
|
||||
private static string ToWebsocketUri(string uri)
|
||||
{
|
||||
if(uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("https://", "wss://");
|
||||
if(uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
uri = uri.Replace("http://", "ws://");
|
||||
return uri;
|
||||
}
|
||||
|
||||
protected WebsocketMessageListener _MessageListener;
|
||||
|
||||
public override Task<NewEventBase> NextEventAsync(CancellationToken cancellation = default)
|
||||
{
|
||||
return _MessageListener.NextMessageAsync(cancellation);
|
||||
|
||||
@ -8,6 +8,8 @@ using Xunit;
|
||||
using System.Net.Http;
|
||||
using Xunit.Abstractions;
|
||||
using NBXplorer.Analytics;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBitcoin.Altcoins;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public enum Backend
|
||||
{
|
||||
Postgres,
|
||||
DBTrie
|
||||
}
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
public static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
TaskCompletionSource<bool> _Evt = null;
|
||||
IWebHost _Host = null;
|
||||
CancellationTokenSource _Closed = new CancellationTokenSource();
|
||||
public CustomServer()
|
||||
{
|
||||
var port = FreeTcpPort();
|
||||
_Host = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(req =>
|
||||
{
|
||||
while(_Act == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
_Closed.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
_Act(req);
|
||||
_Act = null;
|
||||
_Evt.TrySetResult(true);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
})
|
||||
.UseKestrel()
|
||||
.UseUrls("http://127.0.0.1:" + port)
|
||||
.Build();
|
||||
_Host.Start();
|
||||
}
|
||||
|
||||
public Uri GetUri()
|
||||
{
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
{
|
||||
_Evt.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch(TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_Closed.Cancel();
|
||||
_Host.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,8 +16,10 @@ using Microsoft.Extensions.Configuration;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.IO;
|
||||
using System.Diagnostics;
|
||||
using NBXplorer.Backends.Postgres;
|
||||
using NBXplorer.Client;
|
||||
using System.Data;
|
||||
using static NBXplorer.Backend.DbConnectionHelper;
|
||||
using NBXplorer.Backend;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
@ -84,7 +86,7 @@ namespace NBXplorer.Tests
|
||||
await connection.ExecuteAsync("ANALYZE;");
|
||||
goto retry;
|
||||
}
|
||||
Assert.False(true, "Unacceptable response time for " + script);
|
||||
Assert.Fail("Unacceptable response time for " + script);
|
||||
}
|
||||
}
|
||||
|
||||
@ -143,6 +145,9 @@ namespace NBXplorer.Tests
|
||||
await AssertBalance(5.0m);
|
||||
|
||||
// Another double spent, but this time having an output
|
||||
await AssertFetchMatches(conn,
|
||||
new NewOutRaw[] { new ("t2ss", 0, "a1", 51, "") },
|
||||
new NewInRaw[] { new ("t2ss", 0, "t1", 1) }, true);
|
||||
Assert.True(await conn.ExecuteScalarAsync<bool>("CALL fetch_matches ('BTC', " +
|
||||
"ARRAY[" +
|
||||
"('t2ss', 0, 'a1', 51, '')" +
|
||||
@ -155,6 +160,7 @@ namespace NBXplorer.Tests
|
||||
Assert.Equal(conflict.spent_idx, 1);
|
||||
Assert.Equal(conflict.replacing_tx_id, "t2ss");
|
||||
Assert.Equal(conflict.replaced_tx_id, "t2s");
|
||||
Assert.True(conflict.is_new);
|
||||
await conn.ExecuteAsync("CALL save_matches('BTC');");
|
||||
await AssertBalance(5.0m + 51m);
|
||||
|
||||
@ -163,8 +169,36 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
await conn.QueryFirstAsync($"SELECT * FROM txs WHERE tx_id='{txid}'");
|
||||
}
|
||||
await conn.ExecuteScalarAsync("CALL save_matches('BTC')");
|
||||
|
||||
// Same match, as previously, but this time is_new should be false
|
||||
// as the conflict has been detected earlier.
|
||||
await AssertFetchMatches(conn,
|
||||
new NewOutRaw[] { new("t2ss", 0, "a1", 51, "") },
|
||||
new NewInRaw[] { new("t2ss", 0, "t1", 1) }, true);
|
||||
conflict = await conn.QueryFirstAsync("SELECT * FROM matched_conflicts");
|
||||
Assert.Equal(conflict.spent_tx_id, "t1");
|
||||
Assert.Equal(conflict.spent_idx, 1);
|
||||
Assert.Equal(conflict.replacing_tx_id, "t2ss");
|
||||
Assert.Equal(conflict.replaced_tx_id, "t2s");
|
||||
Assert.False(conflict.is_new);
|
||||
|
||||
// No matches
|
||||
await AssertFetchMatches(conn,
|
||||
new NewOutRaw[] { },
|
||||
new NewInRaw[] { }, false);
|
||||
}
|
||||
|
||||
private async Task AssertFetchMatches(DbConnection conn, NewOutRaw[] outs, NewInRaw[] ins, bool expectedHasMatches)
|
||||
{
|
||||
DynamicParameters parameters = new DynamicParameters();
|
||||
parameters.Add("in_code", "BTC");
|
||||
parameters.Add("in_outs", outs);
|
||||
parameters.Add("in_ins", ins);
|
||||
parameters.Add("has_match", dbType: System.Data.DbType.Boolean, direction: ParameterDirection.InputOutput);
|
||||
await conn.QueryAsync<int>("fetch_matches", parameters, commandType: CommandType.StoredProcedure);
|
||||
Assert.Equal(expectedHasMatches, parameters.Get<bool>("has_match"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanCalculateHistogram()
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
|
||||
ARG SupportDBTrie=true
|
||||
ENV SupportDBTrie $SupportDBTrie
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
|
||||
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
|
||||
{
|
||||
@ -57,11 +57,11 @@ namespace NBXplorer.Tests
|
||||
|
||||
public static DerivationLine GetLineFor(this DerivationStrategyBase strategy, DerivationFeature feature)
|
||||
{
|
||||
return strategy.GetLineFor(KeyPathTemplates.Default.GetKeyPathTemplate(feature));
|
||||
return strategy.GetLineFor(KeyPathTemplates.Default, feature);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -70,12 +70,6 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
return client.EnsureGenerateAsync(blockCount).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public static RPCClient WithCapabilitiesOf(this RPCClient client, RPCClient target)
|
||||
{
|
||||
client.Capabilities = target.Capabilities;
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -69,20 +69,12 @@ namespace NBXplorer.Tests
|
||||
|
||||
public void LogInformation(string msg)
|
||||
{
|
||||
if(msg != null)
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
}
|
||||
public class Logs
|
||||
{
|
||||
public static ILog Tester
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public static XUnitLoggerProvider LogProvider
|
||||
{
|
||||
get;
|
||||
set;
|
||||
if (msg != null)
|
||||
try
|
||||
{
|
||||
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,18 +8,13 @@ using Xunit.Abstractions;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public class MaintenanceUtilities
|
||||
public class MaintenanceUtilities(ITestOutputHelper helper) : UnitTestBase(helper)
|
||||
{
|
||||
public MaintenanceUtilities(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLoggerProvider(helper);
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Maintenance", "Maintenance")]
|
||||
public async Task GenerateFullSchema()
|
||||
{
|
||||
using var t = ServerTester.Create(Backend.Postgres);
|
||||
using var t = CreateTester();
|
||||
var script = await GenerateDbScript(t);
|
||||
File.WriteAllText(GetFullSchemaFile(), script);
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net6.0</TargetFramework>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net10.0</TargetFramework>
|
||||
<LangVersion>12</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" />
|
||||
@ -13,10 +11,11 @@
|
||||
<EmbeddedResource Include="Scripts\generate-whale.sql" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="3.0.22" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.9" />
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="5.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
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 +11,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 +40,17 @@ 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, 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
|
||||
{
|
||||
|
||||
@ -29,6 +29,12 @@ namespace NBXplorer.Tests
|
||||
//Network = NBitcoin.Altcoins.Dogecoin.Instance.Regtest;
|
||||
//RPCStringAmount = false;
|
||||
|
||||
//Tests of PEPE are broken because it outpoint locking seems to work differently
|
||||
//CryptoCode = "PEPE";
|
||||
//nodeDownloadData = NodeDownloadData.Pepecoin.v1_1_0;
|
||||
//Network = NBitcoin.Altcoins.Pepecoin.Instance.Regtest;
|
||||
//RPCStringAmount = false;
|
||||
|
||||
//CryptoCode = "DASH";
|
||||
//nodeDownloadData = NodeDownloadData.Dash.v0_12_2;
|
||||
//Network = NBitcoin.Altcoins.Dash.Instance.Regtest;
|
||||
@ -62,7 +68,7 @@ namespace NBXplorer.Tests
|
||||
//Network = NBitcoin.Altcoins.Viacoin.Instance.Regtest;
|
||||
|
||||
//CryptoCode = "GRS";
|
||||
//nodeDownloadData = NodeDownloadData.Groestlcoin.v25_0;
|
||||
//nodeDownloadData = NodeDownloadData.Groestlcoin.v29_0;
|
||||
//Network = NBitcoin.Altcoins.Groestlcoin.Instance.Regtest;
|
||||
|
||||
//CryptoCode = "BTX";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBXplorer.Configuration;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Tests;
|
||||
using System;
|
||||
@ -16,9 +16,11 @@ using NBitcoin.RPC;
|
||||
using System.Net;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NBitcoin.WalletPolicies;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBitcoin.Scripting;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
@ -26,22 +28,14 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
private readonly string _Directory;
|
||||
|
||||
public static ServerTester Create(Backend backend, [CallerMemberNameAttribute] string caller = null)
|
||||
public static ServerTester Create(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
|
||||
{
|
||||
return new ServerTester(backend, caller);
|
||||
return new ServerTester(logs, caller, true);
|
||||
}
|
||||
|
||||
public static ServerTester Create([CallerMemberNameAttribute]string caller = null)
|
||||
public static ServerTester CreateNoAutoStart(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
|
||||
{
|
||||
return Create(Backend.DBTrie, 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(logs, caller, false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -65,12 +59,13 @@ namespace NBXplorer.Tests
|
||||
get; set;
|
||||
}
|
||||
|
||||
public TesterLogs Logs { get; }
|
||||
public string Caller { get; }
|
||||
public ServerTester(Backend backend, string directory, bool autoStart = true)
|
||||
public ServerTester(TesterLogs logs, string directory, bool autoStart = true)
|
||||
{
|
||||
_Name = directory;
|
||||
Backend = backend;
|
||||
SetEnvironment();
|
||||
Logs = logs;
|
||||
Caller = directory;
|
||||
var rootTestData = "TestData";
|
||||
directory = Path.Combine(rootTestData, directory);
|
||||
@ -81,32 +76,11 @@ namespace NBXplorer.Tests
|
||||
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;
|
||||
set;
|
||||
} = NBitcoin.Tests.RPCWalletType.Legacy;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
@ -139,28 +113,28 @@ namespace NBXplorer.Tests
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static int FreeTcpPort()
|
||||
{
|
||||
TcpListener l = new TcpListener(IPAddress.Loopback, 0);
|
||||
l.Start();
|
||||
int port = ((IPEndPoint)l.LocalEndpoint).Port;
|
||||
l.Stop();
|
||||
return port;
|
||||
}
|
||||
|
||||
public int TrimEvents { get; set; } = -1;
|
||||
public bool UseRabbitMQ { get; set; } = false;
|
||||
public List<(string key, string value)> AdditionalConfiguration { get; set; } = new List<(string key, string value)>();
|
||||
public List<string> AdditionalFlags = new List<string>();
|
||||
internal string PostgresConnectionString;
|
||||
private void StartNBXplorer()
|
||||
{
|
||||
var additionalFlags = new List<string>();
|
||||
var port = CustomServer.FreeTcpPort();
|
||||
var port = 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()));
|
||||
@ -170,33 +144,17 @@ namespace NBXplorer.Tests
|
||||
keyValues.Add(("verbose", "1"));
|
||||
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcauth", Explorer.GetRPCAuth()));
|
||||
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcurl", Explorer.CreateRPCClient().Address.AbsoluteUri));
|
||||
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}rpcdefaultwallet", "default"));
|
||||
keyValues.Add(("exposerpc", "1"));
|
||||
keyValues.Add(("rpcnotest", "1"));
|
||||
keyValues.Add(("trimevents", TrimEvents.ToString()));
|
||||
keyValues.Add(("mingapsize", "3"));
|
||||
keyValues.Add(("maxgapsize", "8"));
|
||||
keyValues.Add(($"{CryptoCode.ToLowerInvariant()}nodeendpoint", $"{Explorer.Endpoint.Address}:{Explorer.Endpoint.Port}"));
|
||||
keyValues.Add(("asbcnstr", AzureServiceBusTestConfig.ConnectionString));
|
||||
keyValues.Add(("asbblockq", AzureServiceBusTestConfig.NewBlockQueue));
|
||||
keyValues.Add(("asbtranq", AzureServiceBusTestConfig.NewTransactionQueue));
|
||||
keyValues.Add(("asbblockt", AzureServiceBusTestConfig.NewBlockTopic));
|
||||
keyValues.Add(("asbtrant", AzureServiceBusTestConfig.NewTransactionTopic));
|
||||
if (UseRabbitMQ)
|
||||
{
|
||||
keyValues.Add(("rmqhost", RabbitMqTestConfig.RabbitMqHostName));
|
||||
keyValues.Add(("rmqvirtual", RabbitMqTestConfig.RabbitMqVirtualHost));
|
||||
keyValues.Add(("rmquser", RabbitMqTestConfig.RabbitMqUsername));
|
||||
keyValues.Add(("rmqpass", RabbitMqTestConfig.RabbitMqPassword));
|
||||
keyValues.Add(("rmqtranex", RabbitMqTestConfig.RabbitMqTransactionExchange));
|
||||
keyValues.Add(("rmqblockex", RabbitMqTestConfig.RabbitMqBlockExchange));
|
||||
}
|
||||
var args = keyValues.SelectMany(kv => new[] { $"--{kv.key}", kv.value }
|
||||
.Concat(new[] { $"--{CryptoCode.ToLowerInvariant()}hastxindex" }))
|
||||
var args = keyValues.SelectMany(kv => new[] { $"--{kv.key}", kv.value })
|
||||
.Concat(AdditionalFlags)
|
||||
.Concat(additionalFlags).ToArray();
|
||||
Host = new WebHostBuilder()
|
||||
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
|
||||
.UseKestrel()
|
||||
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
|
||||
.ConfigureLogging(l =>
|
||||
{
|
||||
l.SetMinimumLevel(LogLevel.Information)
|
||||
@ -204,13 +162,20 @@ namespace NBXplorer.Tests
|
||||
.AddFilter("Microsoft", LogLevel.Error)
|
||||
.AddFilter("Hangfire", LogLevel.Error)
|
||||
.AddFilter("NBXplorer.Authentication.BasicAuthenticationHandler", LogLevel.Critical)
|
||||
.ClearProviders()
|
||||
.AddProvider(Logs.LogProvider);
|
||||
})
|
||||
.UseStartup<Startup>()
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder
|
||||
.UseKestrel()
|
||||
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
|
||||
.UseStartup<Startup>();
|
||||
})
|
||||
.Build();
|
||||
NBXplorer.Logging.Logs.Configure(Host.Services.GetRequiredService<ILoggerFactory>());
|
||||
NBXplorerNetwork = ((NBXplorerNetworkProvider)Host.Services.GetService(typeof(NBXplorerNetworkProvider))).GetFromCryptoCode(CryptoCode);
|
||||
RPC = ((IRPCClients)Host.Services.GetService(typeof(IRPCClients))).Get(NBXplorerNetwork);
|
||||
RPC = ((RPCClientProvider)Host.Services.GetService(typeof(RPCClientProvider))).Get(NBXplorerNetwork);
|
||||
var conf = (ExplorerConfiguration)Host.Services.GetService(typeof(ExplorerConfiguration));
|
||||
Host.Start();
|
||||
Configuration = conf;
|
||||
@ -245,10 +210,10 @@ namespace NBXplorer.Tests
|
||||
public HttpClient HttpClient { get; internal set; }
|
||||
|
||||
string datadir;
|
||||
|
||||
public void ResetExplorer(bool deleteAll = true)
|
||||
{
|
||||
Host.Dispose();
|
||||
_ = Host.StopAsync();
|
||||
Host.WaitForShutdown();
|
||||
if (deleteAll)
|
||||
{
|
||||
PostgresConnectionString = null;
|
||||
@ -271,7 +236,7 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
get
|
||||
{
|
||||
var address = Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.FirstOrDefault();
|
||||
var address = Host.GetServerFeatures<IServerAddressesFeature>().Addresses.First();
|
||||
return new Uri(address);
|
||||
}
|
||||
}
|
||||
@ -302,7 +267,7 @@ namespace NBXplorer.Tests
|
||||
}
|
||||
|
||||
|
||||
public IWebHost Host
|
||||
public IHost Host
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
@ -414,9 +379,11 @@ namespace NBXplorer.Tests
|
||||
var k = PrivateKeyOf(key, path);
|
||||
try
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
await RPC.ImportPrivKeyAsync(k).ConfigureAwait(false);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
catch (RPCException ex) when (ex.RPCCode == RPCErrorCode.RPC_WALLET_ERROR)
|
||||
catch (RPCException ex) when (ex.RPCCode is RPCErrorCode.RPC_WALLET_ERROR or RPCErrorCode.RPC_METHOD_NOT_FOUND)
|
||||
{
|
||||
string[] desc;
|
||||
if (this.RPC.Capabilities.SupportSegwit)
|
||||
@ -435,8 +402,8 @@ namespace NBXplorer.Tests
|
||||
new JArray(
|
||||
new JObject()
|
||||
{
|
||||
["desc"] = OutputDescriptor.AddChecksum(d),
|
||||
["timestamp"] = this.RPC.Network.Consensus.CoinbaseMaturity
|
||||
["desc"] = Miniscript.AddChecksum(d),
|
||||
["timestamp"] = "now"
|
||||
})
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
@ -457,7 +424,7 @@ namespace NBXplorer.Tests
|
||||
return key.ExtKey.Derive(new KeyPath(path)).Neuter().PubKey.Hash.GetAddress(Network);
|
||||
}
|
||||
|
||||
public BitcoinAddress AddressOf(DerivationStrategyBase scheme, string path)
|
||||
public BitcoinAddress AddressOf(StandardDerivationStrategyBase scheme, string path)
|
||||
{
|
||||
return scheme.GetDerivation(KeyPath.Parse(path)).ScriptPubKey.GetDestinationAddress(Network);
|
||||
}
|
||||
@ -466,14 +433,14 @@ namespace NBXplorer.Tests
|
||||
{
|
||||
return (DirectDerivationStrategy)CreateDerivationStrategy(pubKey, false);
|
||||
}
|
||||
public DerivationStrategyBase CreateDerivationStrategy(ExtPubKey pubKey, bool p2sh)
|
||||
public StandardDerivationStrategyBase CreateDerivationStrategy(ExtPubKey pubKey, bool p2sh)
|
||||
{
|
||||
key = key ?? new ExtKey();
|
||||
pubKey = pubKey ?? key.Neuter();
|
||||
string suffix = this.RPC.Capabilities.SupportSegwit ? "" : "-[legacy]";
|
||||
suffix += p2sh ? "-[p2sh]" : "";
|
||||
scriptPubKeyType = p2sh ? ScriptPubKeyType.SegwitP2SH : ScriptPubKeyType.Segwit;
|
||||
return NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
|
||||
return (StandardDerivationStrategyBase)NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
|
||||
}
|
||||
ExtKey key;
|
||||
ScriptPubKeyType scriptPubKeyType;
|
||||
@ -491,8 +458,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();
|
||||
|
||||
@ -1,74 +0,0 @@
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public static class RabbitMqTestConfig
|
||||
{
|
||||
//Put your rabbit mq settings here
|
||||
public static string RabbitMqHostName => "localhost";
|
||||
public static string RabbitMqVirtualHost => "/";
|
||||
public static string RabbitMqUsername => "guest";
|
||||
public static string RabbitMqPassword => "guest";
|
||||
|
||||
public static string RabbitMqBlockExchange => "NewBlock";
|
||||
public static string RabbitMqTransactionExchange => "NewTransaction";
|
||||
}
|
||||
|
||||
public static class AzureServiceBusTestConfig
|
||||
{
|
||||
public static string ConnectionString
|
||||
{
|
||||
get
|
||||
{
|
||||
//Put your service bus connection string here - requires READ / WRITE permissions
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static string NewBlockQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
return "newblock";
|
||||
}
|
||||
}
|
||||
public static string NewBlockTopic
|
||||
{
|
||||
get
|
||||
{
|
||||
return "newbitcoinblock";
|
||||
}
|
||||
}
|
||||
|
||||
public static string NewBlockSubscription
|
||||
{
|
||||
get
|
||||
{
|
||||
return "NewBlock";
|
||||
}
|
||||
}
|
||||
|
||||
public static string NewTransactionQueue
|
||||
{
|
||||
get
|
||||
{
|
||||
return "newtransaction";
|
||||
}
|
||||
}
|
||||
|
||||
public static string NewTransactionTopic
|
||||
{
|
||||
get
|
||||
{
|
||||
return "newbitcointransaction";
|
||||
}
|
||||
}
|
||||
|
||||
public static string NewTransactionSubscription
|
||||
{
|
||||
get
|
||||
{
|
||||
return "NewTransaction";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
9
NBXplorer.Tests/TesterLogs.cs
Normal file
9
NBXplorer.Tests/TesterLogs.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace NBXplorer.Tests;
|
||||
|
||||
public class TesterLogs(ITestOutputHelper helper)
|
||||
{
|
||||
public XUnitLog Tester { get; } = new XUnitLog(helper) { Name = "Tests" };
|
||||
public XUnitLoggerProvider LogProvider { get; } = new(helper);
|
||||
}
|
||||
@ -11,4 +11,12 @@ namespace NBXplorer.Tests
|
||||
Timeout = 60_000;
|
||||
}
|
||||
}
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class FactWithTimeoutAttribute : FactAttribute
|
||||
{
|
||||
public FactWithTimeoutAttribute()
|
||||
{
|
||||
Timeout = 60_000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using static NBXplorer.Backend.DbConnectionHelper;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
@ -21,18 +22,20 @@ namespace NBXplorer.Tests
|
||||
|
||||
public TrackedTransaction Build()
|
||||
{
|
||||
var tx = new TrackedTransaction(new TrackedTransactionKey(_TransactionId, _BlockId, true), _Parent._TrackedSource, null as Coin[], null)
|
||||
{
|
||||
Inserted = _TimeStamp,
|
||||
FirstSeen = _TimeStamp
|
||||
};
|
||||
var record = new SaveTransactionRecord(null, _TransactionId, _BlockId, null, null, false, _TimeStamp);
|
||||
var tx = TrackedTransaction.Create(_Parent._TrackedSource, record);
|
||||
foreach (var input in _Inputs)
|
||||
{
|
||||
tx.SpentOutpoints.Add(input.Coin.Outpoint);
|
||||
tx.SpentOutpoints.Add(input.Coin.Outpoint, 0);
|
||||
}
|
||||
foreach (var output in _Outputs)
|
||||
{
|
||||
tx.ReceivedCoins.Add(output.Coin);
|
||||
tx.MatchedOutputs.Add(new MatchedOutput()
|
||||
{
|
||||
Index = (int)output.Coin.Outpoint.N,
|
||||
Value = output.Coin.Amount,
|
||||
ScriptPubKey = output.Coin.ScriptPubKey
|
||||
});
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
131
NBXplorer.Tests/UnitTest1.Groups.cs
Normal file
131
NBXplorer.Tests/UnitTest1.Groups.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using Dapper;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Backend;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace NBXplorer.Tests
|
||||
{
|
||||
public partial class UnitTest1
|
||||
{
|
||||
[Fact]
|
||||
public async Task CanCRUDGroups()
|
||||
{
|
||||
using var tester = CreateTester();
|
||||
var g1 = await tester.Client.CreateGroupAsync();
|
||||
void AssertG1Empty()
|
||||
{
|
||||
Assert.NotNull(g1.GroupId);
|
||||
Assert.NotNull(g1.TrackedSource);
|
||||
Assert.Equal($"GROUP:{g1.GroupId}", g1.TrackedSource);
|
||||
Assert.Empty(g1.Children);
|
||||
}
|
||||
AssertG1Empty();
|
||||
g1 = await tester.Client.GetGroupAsync(g1.GroupId);
|
||||
AssertG1Empty();
|
||||
Assert.Null(await tester.Client.GetGroupAsync("lol"));
|
||||
Assert.Null(await tester.Client.AddGroupChildrenAsync("lol", Array.Empty<GroupChild>()));
|
||||
|
||||
await AssertNBXplorerException(409, tester.Client.AddGroupChildrenAsync(g1.GroupId, [g1.AsGroupChild()]));
|
||||
Assert.Null(await tester.Client.AddGroupChildrenAsync(g1.GroupId, [new GroupChild() { TrackedSource = "GROUP:Test" }]));
|
||||
Assert.Null(await tester.Client.AddGroupChildrenAsync("Test", [new GroupChild() { TrackedSource = g1.TrackedSource }]));
|
||||
|
||||
var g2 = await tester.Client.CreateGroupAsync();
|
||||
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
|
||||
Assert.NotNull(g1);
|
||||
// Nothing happen if twice
|
||||
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
|
||||
Assert.Equal(g2.TrackedSource, Assert.Single(g1.Children).TrackedSource);
|
||||
await AssertNBXplorerException(409, tester.Client.AddGroupChildrenAsync(g2.GroupId, [g1.AsGroupChild()]));
|
||||
g1 = await tester.Client.RemoveGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
|
||||
AssertG1Empty();
|
||||
|
||||
var g3 = await tester.Client.CreateGroupAsync();
|
||||
g1 = await tester.Client.AddGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild(), g3.AsGroupChild()]);
|
||||
Assert.Equal(2, g1.Children.Length);
|
||||
|
||||
// Adding address in g2 should add the addresse to g1 but not g3
|
||||
var addresses = Enumerable.Range(0,10).Select(_ => new Key().GetAddress(ScriptPubKeyType.Legacy, tester.Network).ToString()).ToArray();
|
||||
await tester.Client.AddGroupAddressAsync("BTC", g2.GroupId, addresses);
|
||||
// Idempotent
|
||||
await tester.Client.AddGroupAddressAsync("BTC", g2.GroupId, addresses);
|
||||
|
||||
async Task AssertAddresses(GroupInformation g)
|
||||
{
|
||||
var groupAddresses = await GetGroupAddressesAsync(tester, "BTC", g.GroupId);
|
||||
Assert.Equal(groupAddresses.Length, addresses.Length);
|
||||
foreach (var a in addresses)
|
||||
{
|
||||
Assert.Contains(a, groupAddresses);
|
||||
}
|
||||
}
|
||||
await AssertAddresses(g1);
|
||||
await AssertAddresses(g2);
|
||||
var g3Addrs = await GetGroupAddressesAsync(tester, "BTC", g3.GroupId);
|
||||
Assert.Empty(g3Addrs);
|
||||
|
||||
// Removing g2 should remove all its addresses
|
||||
g1 = await tester.Client.RemoveGroupChildrenAsync(g1.GroupId, [g2.AsGroupChild()]);
|
||||
await AssertAddresses(g2);
|
||||
var g1Addrs = await GetGroupAddressesAsync(tester, "BTC", g1.GroupId);
|
||||
Assert.Empty(g1Addrs);
|
||||
|
||||
await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { TrackedSource= "DERIVATIONSCHEME:tpubDC45vUDsFAAqwYKz5hSLi5yJLNduJzpmTw6QTMRPrwdXURoyL81H8oZAaL8EiwEgg92qgMa9h1bB4Y1BZpy9CTNPfjfxvFcWxeiKBHCqSdc" }]));
|
||||
await AssertNBXplorerException(400, tester.Client.AddGroupChildrenAsync(g2.GroupId, [new GroupChild() { CryptoCode="BTC", TrackedSource = "DERIVATIONSCHEME:lol" }]));
|
||||
}
|
||||
|
||||
private async Task<string[]> GetGroupAddressesAsync(ServerTester tester, string code, string groupId)
|
||||
{
|
||||
await using var conn = await tester.GetService<DbConnectionFactory>().CreateConnection();
|
||||
return (await conn.QueryAsync<string>("SELECT s.addr FROM wallets_scripts JOIN scripts s USING (code, script) WHERE code=@code AND wallet_id=@wid", new
|
||||
{
|
||||
code = code,
|
||||
wid = Repository.GetWalletKey(new GroupTrackedSource(groupId)).wid
|
||||
})).ToArray();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanAliceAndBobShareWallet()
|
||||
{
|
||||
using var tester = CreateTester();
|
||||
var bobW = tester.Client.GenerateWallet(new GenerateWalletRequest() { ScriptPubKeyType = ScriptPubKeyType.Segwit });
|
||||
var aliceW = tester.Client.GenerateWallet(new GenerateWalletRequest() { ScriptPubKeyType = ScriptPubKeyType.Segwit });
|
||||
|
||||
var shared = await tester.Client.CreateGroupAsync();
|
||||
await tester.Client.AddGroupChildrenAsync(shared.GroupId, new[] { bobW, aliceW }.Select(w => new GroupChild() { CryptoCode = "BTC", TrackedSource = w.TrackedSource }).ToArray());
|
||||
|
||||
var unused = tester.Client.GetUnused(bobW.DerivationScheme, DerivationStrategy.DerivationFeature.Deposit);
|
||||
var txid = tester.SendToAddress(unused.Address, Money.Coins(1.0m));
|
||||
var gts = GroupTrackedSource.Parse(shared.TrackedSource);
|
||||
tester.Notifications.WaitForTransaction(gts, txid);
|
||||
|
||||
var balance = await tester.Client.GetBalanceAsync(gts);
|
||||
Assert.Equal(Money.Coins(1.0m), balance.Unconfirmed);
|
||||
|
||||
var txs = await tester.Client.GetTransactionsAsync(gts);
|
||||
var tx = Assert.Single(txs.UnconfirmedTransactions.Transactions);
|
||||
Assert.Equal(txid, tx.TransactionId);
|
||||
Assert.NotNull(tx.Outputs[0].Address);
|
||||
|
||||
// Can we track manually added address?
|
||||
await tester.Client.AddGroupAddressAsync("BTC", shared.GroupId, ["n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa"]);
|
||||
txid = tester.SendToAddress(BitcoinAddress.Create("n3XyBWEKWLxm5EzrrvLCJyCQrRhVWQ8YGa", tester.Network), Money.Coins(1.2m));
|
||||
var txEvt = tester.Notifications.WaitForTransaction(gts, txid);
|
||||
Assert.Single(txEvt.Outputs);
|
||||
Assert.NotNull(tx.Outputs[0].Address);
|
||||
|
||||
balance = await tester.Client.GetBalanceAsync(gts);
|
||||
Assert.Equal(Money.Coins(1.0m + 1.2m), balance.Unconfirmed);
|
||||
}
|
||||
|
||||
private async Task<NBXplorerException> AssertNBXplorerException(int httpCode, Task<GroupInformation> task)
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<NBXplorerException>(() => task);
|
||||
Assert.Equal(httpCode, ex.Error.HttpCode);
|
||||
return ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
12
NBXplorer.Tests/UnitTestBase.cs
Normal file
12
NBXplorer.Tests/UnitTestBase.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace NBXplorer.Tests;
|
||||
|
||||
public class UnitTestBase(ITestOutputHelper helper)
|
||||
{
|
||||
public TesterLogs Logs { get; set; } = new TesterLogs(helper);
|
||||
|
||||
public ServerTester CreateTester([CallerMemberName] string caller = null) => ServerTester.Create(Logs, caller);
|
||||
public ServerTester CreateTesterNoAutoStart([CallerMemberName] string caller = null) => ServerTester.CreateNoAutoStart(Logs, caller);
|
||||
}
|
||||
@ -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:
|
||||
@ -19,9 +17,9 @@ services:
|
||||
- postgres
|
||||
- pgadmin
|
||||
postgres:
|
||||
image: postgres:13
|
||||
image: postgres:18.1
|
||||
container_name: nbxplorertests_postgres_1
|
||||
command: [ "-c", "random_page_cost=1.0", "-c", "shared_preload_libraries=pg_stat_statements" ]
|
||||
command: [ "-c", "random_page_cost=1.0", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
ports:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
dotnet test --filter "Azure!=Azure&Broker!=RabbitMq&Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null
|
||||
dotnet test --filter "Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
{
|
||||
"parallelizeTestCollections": false
|
||||
"parallelizeTestCollections": false,
|
||||
"methodDisplay": "method"
|
||||
}
|
||||
@ -14,10 +14,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFiles", "SolutionFi
|
||||
.dockerignore = .dockerignore
|
||||
docs\API.md = docs\API.md
|
||||
.circleci\config.yml = .circleci\config.yml
|
||||
docker-compose.mutiny.yml = docker-compose.mutiny.yml
|
||||
docker-compose.regtest.yml = docker-compose.regtest.yml
|
||||
Dockerfile.linuxamd64 = Dockerfile.linuxamd64
|
||||
Dockerfile.linuxarm32v7 = Dockerfile.linuxarm32v7
|
||||
Dockerfile.linuxarm64v8 = Dockerfile.linuxarm64v8
|
||||
Dockerfile = Dockerfile
|
||||
global.json = global.json
|
||||
.circleci\run-tests.sh = .circleci\run-tests.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
||||
4
NBXplorer.sln.DotSettings
Normal file
4
NBXplorer.sln.DotSettings
Normal file
@ -0,0 +1,4 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=psbt/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=NBXplorer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Xplorer/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@ -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,16 +68,14 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
public AddressPoolService(NBXplorerNetworkProvider networks, IRepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
|
||||
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider)
|
||||
{
|
||||
this.networks = networks;
|
||||
this.repositoryProvider = repositoryProvider;
|
||||
this.keyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
Dictionary<NBXplorerNetwork, AddressPool> _AddressPoolByNetwork;
|
||||
private readonly NBXplorerNetworkProvider networks;
|
||||
private readonly IRepositoryProvider repositoryProvider;
|
||||
private readonly KeyPathTemplates keyPathTemplates;
|
||||
private readonly RepositoryProvider repositoryProvider;
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
@ -111,9 +109,10 @@ namespace NBXplorer
|
||||
var derivationStrategy = (m.TrackedSource as Models.DerivationSchemeTrackedSource)?.DerivationStrategy;
|
||||
if (derivationStrategy == null)
|
||||
continue;
|
||||
foreach (var feature in m.KnownKeyPathMapping.Select(kv => keyPathTemplates.GetDerivationFeature(kv.Value)))
|
||||
foreach (var feature in m.InOuts.Select(kv => kv.Feature).Distinct())
|
||||
{
|
||||
refill.Add(GenerateAddresses(network, derivationStrategy, feature));
|
||||
if (feature is not null)
|
||||
refill.Add(GenerateAddresses(network, derivationStrategy, feature.Value));
|
||||
}
|
||||
}
|
||||
return Task.WhenAll(refill.ToArray());
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -39,13 +39,6 @@ namespace NBXplorer
|
||||
{
|
||||
_TxById = new Dictionary<uint256, AnnotatedTransaction>(transactions.Count);
|
||||
ConfirmedTransactions = new List<AnnotatedTransaction>(transactions.Count);
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
foreach (var keyPathInfo in tx.KnownKeyPathMapping)
|
||||
{
|
||||
_KeyPaths.TryAdd(keyPathInfo.Key, keyPathInfo.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Let's remove the dups and let's get the current height of the transactions
|
||||
foreach (var trackedTx in transactions)
|
||||
@ -82,7 +75,7 @@ namespace NBXplorer
|
||||
// No way to have double spent in confirmed transactions
|
||||
try
|
||||
{
|
||||
spentBy.Add(spent, annotatedTransaction.Record.TransactionHash);
|
||||
spentBy.Add(spent.Outpoint, annotatedTransaction.Record.TransactionHash);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -98,7 +91,7 @@ namespace NBXplorer
|
||||
HashSet<uint256> toRemove = new HashSet<uint256>();
|
||||
foreach (var annotatedTransaction in unconfs.Values)
|
||||
{
|
||||
foreach (var spent in annotatedTransaction.Record.SpentOutpoints)
|
||||
foreach (var spent in annotatedTransaction.Record.SpentOutpoints.Select(o => o.Outpoint))
|
||||
{
|
||||
// All children of a replaced transaction should be replaced
|
||||
if (replaced.TryGetValue(spent.Hash, out var parent) && parent.ReplacedBy is uint256)
|
||||
@ -214,7 +207,7 @@ namespace NBXplorer
|
||||
// but we don't want user cancelling a chain of transaction
|
||||
foreach (var parentOutpoint in tx.Record.SpentOutpoints)
|
||||
{
|
||||
if (_TxById.TryGetValue(parentOutpoint.Hash, out var parent) && parent.Height is null)
|
||||
if (_TxById.TryGetValue(parentOutpoint.Outpoint.Hash, out var parent) && parent.Height is null)
|
||||
{
|
||||
parent.Replaceable = false;
|
||||
}
|
||||
@ -261,21 +254,6 @@ namespace NBXplorer
|
||||
}
|
||||
}
|
||||
|
||||
public MatchedOutput GetUTXO(OutPoint outpoint)
|
||||
{
|
||||
if (_TxById.TryGetValue(outpoint.Hash, out var tx))
|
||||
{
|
||||
return tx.Record.GetReceivedOutputs().Where(c => c.Index == outpoint.N).FirstOrDefault();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@ -11,7 +11,7 @@ namespace NBXplorer.Authentication
|
||||
{
|
||||
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
|
||||
{
|
||||
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
|
||||
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
10
NBXplorer/Backend/BitcoinDWaiterState.cs
Normal file
10
NBXplorer/Backend/BitcoinDWaiterState.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public enum BitcoinDWaiterState
|
||||
{
|
||||
NotStarted,
|
||||
CoreSynching,
|
||||
NBXplorerSynching,
|
||||
Ready
|
||||
}
|
||||
}
|
||||
63
NBXplorer/Backend/DbConnectionFactory.cs
Normal file
63
NBXplorer/Backend/DbConnectionFactory.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Configuration;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class DbConnectionFactory : IAsyncDisposable
|
||||
{
|
||||
public DbConnectionFactory(ILogger<DbConnectionFactory> logger,
|
||||
IConfiguration configuration,
|
||||
ExplorerConfiguration conf)
|
||||
{
|
||||
Logger = logger;
|
||||
ExplorerConfiguration = conf;
|
||||
ConnectionString = configuration.GetRequired("POSTGRES");
|
||||
_DS = CreateDataSourceBuilder(null).Build();
|
||||
}
|
||||
|
||||
public NpgsqlDataSourceBuilder CreateDataSourceBuilder(Action<NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
var connStrBuilder = new NpgsqlConnectionStringBuilder(ConnectionString);
|
||||
// Since we create lots of connection in the indexer loop, this saves one round
|
||||
// trip.
|
||||
connStrBuilder.NoResetOnClose = true;
|
||||
// This force connections to recreate, fixing some issues where connection
|
||||
// take more and more RAM on postgres.
|
||||
connStrBuilder.ConnectionLifetime = (int)TimeSpan.FromMinutes(10).TotalSeconds;
|
||||
action?.Invoke(connStrBuilder);
|
||||
var builder = new NpgsqlDataSourceBuilder(connStrBuilder.ConnectionString);
|
||||
DbConnectionHelper.Register(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
NpgsqlDataSource _DS;
|
||||
public string ConnectionString { get; }
|
||||
public ILogger<DbConnectionFactory> Logger { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
|
||||
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
|
||||
{
|
||||
return new DbConnectionHelper(network, await CreateConnection())
|
||||
{
|
||||
MinPoolSize = ExplorerConfiguration.MinGapSize,
|
||||
MaxPoolSize = ExplorerConfiguration.MaxGapSize
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DbConnection> CreateConnection(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _DS.ReliableOpenConnectionAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
return _DS.DisposeAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,32 +1,32 @@
|
||||
#nullable enable
|
||||
using Dapper;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using Npgsql.TypeMapping;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class DbConnectionHelper : IDisposable, IAsyncDisposable
|
||||
{
|
||||
public DbConnectionHelper(NBXplorerNetwork network,
|
||||
DbConnection connection,
|
||||
KeyPathTemplates keyPathTemplates)
|
||||
DbConnection connection)
|
||||
{
|
||||
derivationStrategyFactory = new DerivationStrategyFactory(network.NBitcoinNetwork);
|
||||
Network = network;
|
||||
Connection = connection;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
DerivationStrategyFactory derivationStrategyFactory;
|
||||
|
||||
public NBXplorerNetwork Network { get; }
|
||||
public DbConnection Connection { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
public int MinPoolSize { get; set; }
|
||||
public int MaxPoolSize { get; set; }
|
||||
|
||||
@ -40,59 +40,28 @@ namespace NBXplorer.Backends.Postgres
|
||||
return Connection.DisposeAsync();
|
||||
}
|
||||
|
||||
public record NewOut(uint256 txId, int idx, Script script, IMoney value);
|
||||
public record NewOut(uint256 txId, int idx, Script script, IMoney value)
|
||||
{
|
||||
public static NewOut FromCoin(ICoin c)
|
||||
=> new(c.Outpoint.Hash, (int)c.Outpoint.N, c.TxOut.ScriptPubKey, c.Amount);
|
||||
}
|
||||
public record NewIn(uint256 txId, int idx, uint256 spentTxId, int spentIdx);
|
||||
public record NewOutRaw(string tx_id, long idx, string script, long value, string asset_id);
|
||||
public record NewInRaw(string tx_id, long idx, string spent_tx_id, long spent_idx);
|
||||
internal record OutpointRaw(string tx_id, long idx);
|
||||
|
||||
public static void Register(INpgsqlTypeMapper typeMapper)
|
||||
public static void Register(NpgsqlDataSourceBuilder dsBuilder)
|
||||
{
|
||||
typeMapper.MapComposite<NewOutRaw>("new_out");
|
||||
typeMapper.MapComposite<NewInRaw>("new_in");
|
||||
typeMapper.MapComposite<OutpointRaw>("outpoint");
|
||||
typeMapper.MapComposite<PostgresRepository.DescriptorScriptInsert>("nbxv1_ds");
|
||||
dsBuilder.MapComposite<NewOutRaw>("new_out");
|
||||
dsBuilder.MapComposite<NewInRaw>("new_in");
|
||||
dsBuilder.MapComposite<OutpointRaw>("outpoint");
|
||||
dsBuilder.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
|
||||
}
|
||||
|
||||
public Task<bool> FetchMatches(IEnumerable<Transaction> txs, SlimChainedBlock slimBlock, Money? minUtxoValue)
|
||||
public async Task<bool> FetchMatches(MatchQuery matchQuery, CancellationToken cancellationToken)
|
||||
{
|
||||
var outCount = txs.Select(t => t.Outputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewOut> outs = new List<DbConnectionHelper.NewOut>(outCount);
|
||||
var inCount = txs.Select(t => t.Inputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewIn> ins = new List<DbConnectionHelper.NewIn>(inCount);
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
if (!tx.IsCoinBase)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var input in tx.Inputs)
|
||||
{
|
||||
ins.Add(new DbConnectionHelper.NewIn(tx.GetHash(), i, input.PrevOut.Hash, (int)input.PrevOut.N));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
int io = -1;
|
||||
foreach (var output in tx.Outputs)
|
||||
{
|
||||
io++;
|
||||
if (minUtxoValue != null && output.Value < minUtxoValue)
|
||||
continue;
|
||||
outs.Add(new DbConnectionHelper.NewOut(tx.GetHash(), io, output.ScriptPubKey, output.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return FetchMatches(outs, ins);
|
||||
}
|
||||
public async Task<bool> FetchMatches(IEnumerable<NewOut>? newOuts, IEnumerable<NewIn>? newIns)
|
||||
{
|
||||
newOuts ??= Array.Empty<NewOut>();
|
||||
newIns ??= Array.Empty<NewIn>();
|
||||
newOuts.TryGetNonEnumeratedCount(out int outCount);
|
||||
newIns.TryGetNonEnumeratedCount(out int inCount);
|
||||
|
||||
var outs = new List<NewOutRaw>(outCount);
|
||||
var ins = new List<NewInRaw>(inCount);
|
||||
foreach (var o in newOuts)
|
||||
var outs = new List<NewOutRaw>(matchQuery.Outs.Count);
|
||||
var ins = new List<NewInRaw>(matchQuery.Ins.Count);
|
||||
foreach (var o in matchQuery.Outs)
|
||||
{
|
||||
long value;
|
||||
string assetId;
|
||||
@ -113,17 +82,41 @@ namespace NBXplorer.Backends.Postgres
|
||||
}
|
||||
outs.Add(new NewOutRaw(o.txId.ToString(), o.idx, o.script.ToHex(), value, assetId));
|
||||
}
|
||||
foreach (var ni in newIns)
|
||||
foreach (var ni in matchQuery.Ins)
|
||||
{
|
||||
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 });
|
||||
|
||||
DynamicParameters parameters = new DynamicParameters();
|
||||
parameters.Add("in_code", Network.CryptoCode);
|
||||
parameters.Add("in_outs", outs);
|
||||
parameters.Add("in_ins", ins);
|
||||
parameters.Add("has_match", dbType: System.Data.DbType.Boolean, direction: ParameterDirection.InputOutput);
|
||||
var command = new CommandDefinition(
|
||||
commandText: "fetch_matches",
|
||||
parameters: parameters,
|
||||
commandType: CommandType.StoredProcedure,
|
||||
commandTimeout: ((NpgsqlConnection)Connection).CommandTimeout * 3,
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
await Connection.QueryAsync<int>(command);
|
||||
return parameters.Get<bool>("has_match");
|
||||
}
|
||||
public record SaveTransactionRecord(Transaction? Transaction, uint256? Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset? SeenAt)
|
||||
|
||||
public record SaveTransactionRecord(Transaction? Transaction, uint256 Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset SeenAt)
|
||||
{
|
||||
public static SaveTransactionRecord Create(TrackedTransaction t) => new SaveTransactionRecord(t.Transaction, t.TransactionHash, t.BlockHash, t.BlockIndex, t.BlockHeight, t.IsCoinBase, new DateTimeOffset?(t.FirstSeen));
|
||||
public static SaveTransactionRecord Create(Transaction? tx = null, uint256? txHash = null, SlimChainedBlock? slimBlock = null, int? blockIndex = null, DateTimeOffset? seenAt = null) => new SaveTransactionRecord(
|
||||
tx,
|
||||
txHash ?? tx?.GetHash() ?? throw new ArgumentException("tx or txHash is expected"),
|
||||
slimBlock?.Hash,
|
||||
blockIndex,
|
||||
slimBlock?.Height,
|
||||
tx?.IsCoinBase is true,
|
||||
seenAt ?? DateTimeOffset.UtcNow
|
||||
);
|
||||
}
|
||||
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions)
|
||||
|
||||
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions, Dictionary<uint256, MempoolEntry> mempoolEntries)
|
||||
{
|
||||
var parameters = transactions
|
||||
.DistinctBy(o => o.Id)
|
||||
@ -132,19 +125,26 @@ namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
code = Network.CryptoCode,
|
||||
blk_id = tx.BlockId?.ToString(),
|
||||
id = tx.Id?.ToString() ?? tx.Transaction?.GetHash()?.ToString(),
|
||||
id = tx.Id.ToString(),
|
||||
raw = tx.Transaction?.ToBytes(),
|
||||
mempool = tx.BlockId is null,
|
||||
seen_at = tx.SeenAt,
|
||||
blk_idx = tx.BlockIndex is int i ? i : 0,
|
||||
blk_height = tx.BlockHeight,
|
||||
immature = tx.Immature
|
||||
immature = tx.Immature,
|
||||
metadata = mempoolEntries.TryGetValue(tx.Id, out var meta) ? meta.ToTransactionMetadata().ToString(false) : null
|
||||
})
|
||||
.Where(o => o.id is not null)
|
||||
.ToArray();
|
||||
await Connection.ExecuteAsync("INSERT INTO txs(code, tx_id, raw, immature, seen_at) VALUES (@code, @id, @raw, @immature, COALESCE(@seen_at, CURRENT_TIMESTAMP)) " +
|
||||
" ON CONFLICT (code, tx_id) " +
|
||||
" DO UPDATE SET seen_at=LEAST(COALESCE(@seen_at, CURRENT_TIMESTAMP), txs.seen_at), raw = COALESCE(@raw, txs.raw), immature=EXCLUDED.immature", parameters);
|
||||
await Connection.ExecuteAsync("""
|
||||
INSERT INTO txs(code, tx_id, raw, immature, seen_at, metadata) VALUES (@code, @id, @raw, @immature, COALESCE(@seen_at, CURRENT_TIMESTAMP), @metadata::JSONB)
|
||||
ON CONFLICT (code, tx_id)
|
||||
DO UPDATE SET
|
||||
seen_at=LEAST(COALESCE(@seen_at, CURRENT_TIMESTAMP), txs.seen_at),
|
||||
raw = COALESCE(@raw, txs.raw),
|
||||
immature=EXCLUDED.immature,
|
||||
metadata=COALESCE(@metadata::JSONB, txs.metadata);
|
||||
""", parameters);
|
||||
await Connection.ExecuteAsync("INSERT INTO blks_txs VALUES (@code, @blk_id, @id, @blk_idx) ON CONFLICT DO NOTHING", parameters.Where(p => p.blk_id is not null).AsList());
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
var 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",
|
||||
@ -206,6 +206,11 @@ namespace NBXplorer.Backends.Postgres
|
||||
return null;
|
||||
return Network.Serializer.ToObject<TMetadata>(result);
|
||||
}
|
||||
public async Task<HashSet<uint256>> GetUnconfirmedTxs()
|
||||
{
|
||||
var txs = await Connection.QueryAsync<string>("SELECT tx_id FROM txs WHERE code=@code AND mempool IS TRUE;", new { code = Network.CryptoCode });
|
||||
return new HashSet<uint256>(txs.Select(t => uint256.Parse(t)));
|
||||
}
|
||||
|
||||
public async Task NewBlock(SlimChainedBlock newTip)
|
||||
{
|
||||
@ -1,4 +1,4 @@
|
||||
namespace NBXplorer.Backends
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class GenerateAddressQuery
|
||||
{
|
||||
74
NBXplorer/Backend/GetTransactionQuery.cs
Normal file
74
NBXplorer/Backend/GetTransactionQuery.cs
Normal file
@ -0,0 +1,74 @@
|
||||
#nullable enable
|
||||
using Dapper;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public abstract record GetTransactionQuery
|
||||
{
|
||||
public static TrackedSourceTxId Create(TrackedSource TrackedSource, uint256? TxId = null, DateTimeOffset? from = null, DateTimeOffset? to = null) => new TrackedSourceTxId(TrackedSource, TxId, from, to);
|
||||
public static ScriptsTxIds Create(KeyPathInformation[] KeyInfos, uint256[] TxIds) => new ScriptsTxIds(KeyInfos, TxIds);
|
||||
public record TrackedSourceTxId(TrackedSource TrackedSource, uint256? TxId, DateTimeOffset? From, DateTimeOffset? To) : GetTransactionQuery
|
||||
{
|
||||
string? walletId;
|
||||
public override string GetSql(DynamicParameters parameters, NBXplorerNetwork network)
|
||||
{
|
||||
string txIdCond = String.Empty, fromCond = String.Empty, toCond = String.Empty;
|
||||
if (TxId is not null)
|
||||
{
|
||||
txIdCond = " AND tx_id=@tx_id";
|
||||
parameters.Add("@tx_id", TxId.ToString());
|
||||
}
|
||||
if (From is DateTimeOffset f)
|
||||
{
|
||||
fromCond = " AND @from <= seen_at";
|
||||
parameters.Add("@from", f);
|
||||
}
|
||||
if (To is DateTimeOffset t)
|
||||
{
|
||||
toCond = " AND seen_at <= @to";
|
||||
parameters.Add("@to", t);
|
||||
}
|
||||
walletId = Repository.GetWalletKey(TrackedSource, network).wid;
|
||||
parameters.Add("@walletId", walletId);
|
||||
parameters.Add("@code", network.CryptoCode);
|
||||
|
||||
return $"""
|
||||
SELECT wallet_id, tx_id, idx, blk_id, blk_height, blk_idx, is_out, spent_tx_id, spent_idx, script, s.addr, value, asset_id, immature, keypath, key_idx, seen_at, feature
|
||||
FROM nbxv1_tracked_txs LEFT JOIN scripts s USING (code, script)
|
||||
WHERE code=@code AND wallet_id=@walletId{txIdCond}{fromCond}{toCond}
|
||||
""";
|
||||
}
|
||||
public override TrackedSource? GetTrackedSource(string wallet_id) => walletId == wallet_id ? TrackedSource : null;
|
||||
}
|
||||
|
||||
public record ScriptsTxIds(KeyPathInformation[] KeyInfos, uint256[] TxIds) : GetTransactionQuery
|
||||
{
|
||||
Dictionary<string, TrackedSource> widToTrackedSource = new Dictionary<string, TrackedSource>();
|
||||
public override string GetSql(DynamicParameters parameters, NBXplorerNetwork network)
|
||||
{
|
||||
widToTrackedSource.Clear();
|
||||
foreach (var k in KeyInfos)
|
||||
{
|
||||
widToTrackedSource.TryAdd(Repository.GetWalletKey(k.TrackedSource, network).wid, k.TrackedSource);
|
||||
}
|
||||
parameters.Add("@code", network.CryptoCode);
|
||||
parameters.Add("@tx_ids", TxIds.Select(t => t.ToString()).ToArray());
|
||||
return """
|
||||
SELECT wallet_id, t.tx_id, idx, blk_id, blk_height, blk_idx, is_out, spent_tx_id, spent_idx, script, s.addr, value, asset_id, immature, keypath, key_idx, seen_at, feature
|
||||
FROM nbxv1_tracked_txs LEFT JOIN scripts s USING (code, script)
|
||||
JOIN unnest(@tx_ids) t(tx_id) USING (tx_id)
|
||||
WHERE code=@code
|
||||
""";
|
||||
}
|
||||
public override TrackedSource? GetTrackedSource(string wallet_id) => widToTrackedSource.TryGetValue(wallet_id, out var trackedSource) ? trackedSource : null;
|
||||
}
|
||||
|
||||
public abstract string GetSql(DynamicParameters parameters, NBXplorerNetwork network);
|
||||
public abstract TrackedSource? GetTrackedSource(string wallet_id);
|
||||
}
|
||||
}
|
||||
633
NBXplorer/Backend/Indexer.cs
Normal file
633
NBXplorer/Backend/Indexer.cs
Normal file
@ -0,0 +1,633 @@
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.Protocol.Behaviors;
|
||||
using NBitcoin.Protocol;
|
||||
using NBitcoin.RPC;
|
||||
using NBitcoin;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.Events;
|
||||
using NBXplorer.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
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;
|
||||
Task _watchdogLoop;
|
||||
|
||||
// This one will check if the indexer is "stuck" and disconnect the node if it is the case
|
||||
async Task WatchdogLoop()
|
||||
{
|
||||
var cancellationToken = cts.Token;
|
||||
wait:
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(5.0), cancellationToken);
|
||||
var lastBlock = await SeemsStuck(cancellationToken);
|
||||
if (lastBlock is null)
|
||||
goto wait;
|
||||
await Task.Delay(TimeSpan.FromMinutes(2.0), cancellationToken);
|
||||
var lastBlock2 = await SeemsStuck(cancellationToken);
|
||||
if (lastBlock != lastBlock2)
|
||||
goto wait;
|
||||
_Connection?.Dispose($"Sync seems stuck after block {lastBlock.Hash} ({lastBlock.Hash}), restarting the connection.");
|
||||
goto wait;
|
||||
}
|
||||
catch when (cts.Token.IsCancellationRequested)
|
||||
{
|
||||
goto end;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Unhandled exception in the indexer watchdog");
|
||||
goto wait;
|
||||
}
|
||||
end:;
|
||||
}
|
||||
|
||||
async Task<SlimChainedBlock> SeemsStuck(CancellationToken cancellationToken)
|
||||
{
|
||||
if (State is not (BitcoinDWaiterState.NBXplorerSynching or BitcoinDWaiterState.Ready) ||
|
||||
lastIndexedBlock is not { } lastBlock ||
|
||||
GetConnectedClient() is not RPCClient rpc)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var blockchainInfo = await rpc.GetBlockchainInfoAsyncEx(cancellationToken);
|
||||
return blockchainInfo.BestBlockHash != lastBlock.Hash ? lastBlock : null;
|
||||
}
|
||||
|
||||
async Task IndexerLoop()
|
||||
{
|
||||
TimeSpan retryDelay = TimeSpan.FromSeconds(0);
|
||||
retry:
|
||||
try
|
||||
{
|
||||
await IndexerLoopCore(cts.Token);
|
||||
if (!cts.Token.IsCancellationRequested)
|
||||
goto retry;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class Connection : IDisposable
|
||||
{
|
||||
public Channel<Object> Events;
|
||||
public Channel<Block> Blocks;
|
||||
public Node Node;
|
||||
public Connection(Node node)
|
||||
{
|
||||
Node = node;
|
||||
Events = Channel.CreateUnbounded<object>(new() { AllowSynchronousContinuations = false });
|
||||
Blocks = Channel.CreateUnbounded<Block>(new() { AllowSynchronousContinuations = false });
|
||||
}
|
||||
bool _Disposed = false;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(null);
|
||||
}
|
||||
public void Dispose(string reason)
|
||||
{
|
||||
if (_Disposed)
|
||||
return;
|
||||
Node.DisconnectAsync(reason);
|
||||
Events.Writer.TryComplete();
|
||||
Blocks.Writer.TryComplete();
|
||||
_Disposed = true;
|
||||
}
|
||||
}
|
||||
Connection _Connection;
|
||||
private async Task IndexerLoopCore(CancellationToken token)
|
||||
{
|
||||
await ConnectNode(token);
|
||||
var connection = _Connection;
|
||||
await foreach (var item in connection.Events.Reader.ReadAllAsync(token))
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network);
|
||||
if (item is PullBlocks pb)
|
||||
{
|
||||
var headers = ConsolidatePullBlocks(connection.Events.Reader, pb);
|
||||
var slimChainedBlocks = await RPCClient.GetBlockHeadersAsync(headers.Select(b => b.GetHash()).ToList(), token);
|
||||
headers = headers.Where(b => slimChainedBlocks.ByHashes.ContainsKey(b.GetHash())).ToList();
|
||||
foreach (var batch in headers.Chunk(maxinflight))
|
||||
{
|
||||
_ = connection.Node.SendMessageAsync(
|
||||
new GetDataPayload(
|
||||
batch.Select(b => new InventoryVector(connection.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 connection.Blocks.Reader.ReadAllAsync(token))
|
||||
{
|
||||
if (!remaining.Remove(block.Header.GetHash()))
|
||||
continue;
|
||||
if (lastIndexedBlock is null || block.Header.HashPrevBlock == lastIndexedBlock.Hash)
|
||||
{
|
||||
SlimChainedBlock slimChainedBlock = slimChainedBlocks.ByHashes[block.Header.GetHash()].ToSlimChainedBlock();
|
||||
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)
|
||||
{
|
||||
// If there is a fork, we should index the unordered blocks
|
||||
bool unconfedBlocks = false;
|
||||
bool fork = await RPCClient.GetBlockHeaderAsyncEx(lastIndexedBlock.Hash, token) == null;
|
||||
foreach (var b in Enumerable.Zip(unorderedBlocks, slimChainedBlocks)
|
||||
.Where(b => fork || b.Second.Height > lastIndexedBlock.Height)
|
||||
.OrderBy(b => b.Second.Height)
|
||||
.ToList())
|
||||
{
|
||||
var slimBlock = b.Second;
|
||||
if (fork && !unconfedBlocks)
|
||||
{
|
||||
await conn.MakeOrphanFrom(slimBlock.Height);
|
||||
unconfedBlocks = true;
|
||||
}
|
||||
await SaveMatches(conn, b.First, slimBlock.ToSlimChainedBlock());
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
await SaveProgress(conn);
|
||||
await UpdateState(connection.Node);
|
||||
}
|
||||
if (connection.Node.State == NodeState.HandShaked)
|
||||
await AskNextHeaders(connection.Node, token);
|
||||
}
|
||||
if (item is Transaction tx)
|
||||
{
|
||||
var txs = PullTransactions(connection.Events.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 async Task ConnectNode(CancellationToken token)
|
||||
{
|
||||
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...");
|
||||
await node.VersionHandshakeAsync(handshakeTimeout.Token);
|
||||
Logger.LogInformation($"Handshaked");
|
||||
await node.SendMessageAsync(new SendHeadersPayload());
|
||||
|
||||
await RPCArgs.TestRPCAsync(Network, RPCClient, token, Logger);
|
||||
HasTxIndex = await RPCClient.SupportTxIndex() is true;
|
||||
if (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, token))?.ToSlimChainedBlock();
|
||||
State = BitcoinDWaiterState.NBXplorerSynching;
|
||||
// Refresh the NetworkInfo that may have become different while it was synching.
|
||||
NetworkInfo = await RPCClient.GetNetworkInfoAsync();
|
||||
|
||||
_Connection?.Dispose("Creating new connection");
|
||||
_Connection = new Connection(node);
|
||||
node.MessageReceived += Node_MessageReceived;
|
||||
node.Disconnected += Node_Disconnected;
|
||||
var locator = await AskNextHeaders(node, token);
|
||||
lastIndexedBlock = await Repository.GetLastIndexedSlimChainedBlock(locator);
|
||||
if (lastIndexedBlock is null)
|
||||
{
|
||||
var locatorTip = await RPCClient.GetBlockHeaderAsyncEx(locator.Blocks[0], token);
|
||||
lastIndexedBlock = locatorTip?.ToSlimChainedBlock();
|
||||
}
|
||||
await UpdateState(node);
|
||||
}
|
||||
}
|
||||
|
||||
bool firstConnect = true;
|
||||
private async Task<BlockLocator> AskNextHeaders(Node node, CancellationToken token)
|
||||
{
|
||||
var indexProgress = await Repository.GetIndexProgress();
|
||||
if (indexProgress is null)
|
||||
{
|
||||
indexProgress = await GetDefaultCurrentLocation(token);
|
||||
}
|
||||
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(Node node)
|
||||
{
|
||||
if (node.State != NodeState.HandShaked)
|
||||
return;
|
||||
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(CancellationToken token)
|
||||
{
|
||||
if (ChainConfiguration.StartHeight > BlockchainInfo.Headers)
|
||||
throw new InvalidOperationException($"{Network.CryptoCode}: StartHeight ({ChainConfiguration.StartHeight}) should not be above the current tip ({BlockchainInfo.Headers})");
|
||||
BlockLocator blockLocator = null;
|
||||
if (ChainConfiguration.StartHeight == -1)
|
||||
{
|
||||
var bestBlock = await RPCClient.GetBestBlockHashAsync(token);
|
||||
var bh = await RPCClient.GetBlockHeaderAsyncEx(bestBlock, token);
|
||||
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, token);
|
||||
var header2 = await RPCClient.GetBlockHeaderAsyncEx(header.GetHash(), token);
|
||||
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);
|
||||
// If we are synching, the block time is better approximation of the received time
|
||||
var seenAt = State == BitcoinDWaiterState.NBXplorerSynching
|
||||
? block.Header.BlockTime
|
||||
: DateTimeOffset.UtcNow;
|
||||
await SaveMatches(conn, block.Transactions, slimChainedBlock, true, seenAt);
|
||||
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, DateTimeOffset? seenAt = null)
|
||||
{
|
||||
foreach (var tx in transactions)
|
||||
tx.PrecomputeHash(false, true);
|
||||
var now = seenAt ?? DateTimeOffset.UtcNow;
|
||||
if (slimChainedBlock != null)
|
||||
{
|
||||
await conn.NewBlock(slimChainedBlock);
|
||||
}
|
||||
var matches = await Repository.GetMatches(conn, transactions, slimChainedBlock, now, useCache: true, cancellationToken: cts.Token);
|
||||
_ = 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,
|
||||
Metadata = matches[i].Metadata
|
||||
},
|
||||
Inputs = matches[i].MatchedInputs,
|
||||
Outputs = matches[i].MatchedOutputs,
|
||||
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);
|
||||
private void Node_MessageReceived(Node node, IncomingMessage message)
|
||||
{
|
||||
var connection = _Connection;
|
||||
if (message.Message.Payload is HeadersPayload h && h.Headers.Count != 0)
|
||||
{
|
||||
connection.Events.Writer.TryWrite(new PullBlocks(h.Headers));
|
||||
}
|
||||
else if (message.Message.Payload is BlockPayload b)
|
||||
{
|
||||
connection.Blocks.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);
|
||||
}
|
||||
}
|
||||
else if (message.Message.Payload is TxPayload tx)
|
||||
{
|
||||
connection.Events.Writer.TryWrite(tx.Object);
|
||||
}
|
||||
}
|
||||
|
||||
private void Node_Disconnected(Node node)
|
||||
{
|
||||
if (node.DisconnectReason.Exception != null)
|
||||
{
|
||||
Logger.LogError(node.DisconnectReason.Exception, $"Node disconnected with exception");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInformation($"Node disconnected ({node.DisconnectReason.Reason})");
|
||||
}
|
||||
_Connection?.Dispose();
|
||||
node.MessageReceived -= Node_MessageReceived;
|
||||
node.Disconnected -= Node_Disconnected;
|
||||
State = BitcoinDWaiterState.NotStarted;
|
||||
}
|
||||
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
await Task.Yield(); // So it doesn't crash the calling Task.WhenAll
|
||||
cts = new CancellationTokenSource();
|
||||
_indexerLoop = IndexerLoop();
|
||||
_watchdogLoop = WatchdogLoop();
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cts?.Cancel();
|
||||
_Connection?.Dispose("NBXplorer stopping...");
|
||||
if (_indexerLoop is not null)
|
||||
await _indexerLoop;
|
||||
if (_watchdogLoop is not null)
|
||||
await _watchdogLoop;
|
||||
}
|
||||
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; }
|
||||
public bool HasTxIndex { get; private set; }
|
||||
|
||||
NBXplorerNetwork network;
|
||||
private int maxinflight = 10;
|
||||
|
||||
public async Task SaveMatches(Transaction transaction)
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnectionHelper(Network);
|
||||
await SaveMatches(conn, new List<Transaction>(1) { transaction }, null, false);
|
||||
}
|
||||
|
||||
public RPCClient GetConnectedClient() => State switch
|
||||
{
|
||||
BitcoinDWaiterState.CoreSynching or BitcoinDWaiterState.NBXplorerSynching or BitcoinDWaiterState.Ready => RPCClient,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
89
NBXplorer/Backend/Indexers.cs
Normal file
89
NBXplorer/Backend/Indexers.cs
Normal file
@ -0,0 +1,89 @@
|
||||
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 class Indexers : IHostedService
|
||||
{
|
||||
|
||||
Dictionary<string, Indexer> _Indexers = new Dictionary<string, Indexer>();
|
||||
|
||||
public AddressPoolService AddressPoolService { get; }
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
public RPCClientProvider 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,
|
||||
RPCClientProvider 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),
|
||||
(Repository)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;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
NBXplorer/Backend/MatchQuery.cs
Normal file
59
NBXplorer/Backend/MatchQuery.cs
Normal file
@ -0,0 +1,59 @@
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class MatchQuery
|
||||
{
|
||||
public MatchQuery(List<DbConnectionHelper.NewIn> ins, List<DbConnectionHelper.NewOut> outs)
|
||||
{
|
||||
Ins = ins;
|
||||
Outs = outs;
|
||||
}
|
||||
public MatchQuery(IEnumerable<ICoin> coins)
|
||||
{
|
||||
Outs = coins.Select(c => DbConnectionHelper.NewOut.FromCoin(c)).ToList();
|
||||
Ins = new List<DbConnectionHelper.NewIn>();
|
||||
}
|
||||
public List<DbConnectionHelper.NewIn> Ins { get; }
|
||||
public List<DbConnectionHelper.NewOut> Outs { get; }
|
||||
|
||||
public static MatchQuery FromTransactions(IEnumerable<Transaction> txs, Money? minUtxoValue)
|
||||
{
|
||||
var outCount = txs.Select(t => t.Outputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewOut> outs = new List<DbConnectionHelper.NewOut>(outCount);
|
||||
var inCount = txs.Select(t => t.Inputs.Count).Sum();
|
||||
List<DbConnectionHelper.NewIn> ins = new List<DbConnectionHelper.NewIn>(inCount);
|
||||
foreach (var tx in txs)
|
||||
{
|
||||
var hash = tx.GetHash();
|
||||
if (!tx.IsCoinBase)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var input in tx.Inputs)
|
||||
{
|
||||
ins.Add(new DbConnectionHelper.NewIn(hash, i, input.PrevOut.Hash, (int)input.PrevOut.N));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
int io = -1;
|
||||
foreach (var output in tx.Outputs)
|
||||
{
|
||||
io++;
|
||||
if (minUtxoValue != null && output.Value < minUtxoValue)
|
||||
continue;
|
||||
IMoney val = output switch
|
||||
{
|
||||
ElementsTxOut { Asset: { AssetId: { } assetId } } el => new AssetMoney(assetId, el.Value),
|
||||
_ => output.Value
|
||||
};
|
||||
outs.Add(new DbConnectionHelper.NewOut(hash, io, output.ScriptPubKey, val));
|
||||
}
|
||||
}
|
||||
return new MatchQuery(ins, outs);
|
||||
}
|
||||
}
|
||||
}
|
||||
1325
NBXplorer/Backend/Repository.cs
Normal file
1325
NBXplorer/Backend/Repository.cs
Normal file
File diff suppressed because it is too large
Load Diff
128
NBXplorer/Backend/RepositoryProvider.cs
Normal file
128
NBXplorer/Backend/RepositoryProvider.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using NBitcoin;
|
||||
using Dapper;
|
||||
using NBXplorer.Configuration;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBXplorer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class RepositoryProvider : IHostedService
|
||||
{
|
||||
Dictionary<string, Repository> _Repositories = new Dictionary<string, Repository>();
|
||||
ExplorerConfiguration _Configuration;
|
||||
|
||||
public Task StartCompletion => Task.CompletedTask;
|
||||
|
||||
public NBXplorerNetworkProvider Networks { get; }
|
||||
public DbConnectionFactory ConnectionFactory { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
|
||||
public RepositoryProvider(NBXplorerNetworkProvider networks,
|
||||
ExplorerConfiguration configuration,
|
||||
DbConnectionFactory connectionFactory,
|
||||
KeyPathTemplates keyPathTemplates)
|
||||
{
|
||||
Networks = networks;
|
||||
_Configuration = configuration;
|
||||
ConnectionFactory = connectionFactory;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
|
||||
public Repository GetRepository(string cryptoCode)
|
||||
{
|
||||
_Repositories.TryGetValue(cryptoCode.ToUpperInvariant(), out Repository repository);
|
||||
return repository;
|
||||
}
|
||||
public Repository GetRepository(NBXplorerNetwork network)
|
||||
{
|
||||
return GetRepository(network.CryptoCode);
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var net in Networks.GetAll())
|
||||
{
|
||||
var settings = GetChainSetting(net);
|
||||
if (settings != null)
|
||||
{
|
||||
var repo = new Repository(ConnectionFactory, net, KeyPathTemplates, settings.RPC, _Configuration);
|
||||
repo.MaxPoolSize = _Configuration.MaxGapSize;
|
||||
repo.MinPoolSize = _Configuration.MinGapSize;
|
||||
repo.MinUtxoValue = settings.MinUtxoValue;
|
||||
_Repositories.Add(net.CryptoCode, repo);
|
||||
}
|
||||
}
|
||||
foreach (var repo in _Repositories.Select(kv => kv.Value))
|
||||
{
|
||||
if (GetChainSetting(repo.Network) is ChainConfiguration chainConf &&
|
||||
chainConf.Rescan &&
|
||||
(chainConf.RescanIfTimeBefore is null || chainConf.RescanIfTimeBefore.Value >= DateTimeOffset.UtcNow))
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{repo.Network.CryptoCode}: Rescanning the chain...");
|
||||
await repo.SetIndexProgress(null);
|
||||
}
|
||||
}
|
||||
if (_Configuration.TrimEvents > 0)
|
||||
{
|
||||
Logs.Explorer.LogInformation("Trimming the event table if needed...");
|
||||
int trimmed = 0;
|
||||
foreach (var repo in _Repositories.Select(kv => kv.Value))
|
||||
{
|
||||
if (GetChainSetting(repo.Network) is ChainConfiguration chainConf)
|
||||
{
|
||||
trimmed += await repo.TrimmingEvents(_Configuration.TrimEvents, cancellationToken);
|
||||
}
|
||||
}
|
||||
if (trimmed != 0)
|
||||
Logs.Explorer.LogInformation($"Trimmed {trimmed} events in total...");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetMigrationId()
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
var v = await conn.ExecuteScalarAsync<string>("SELECT data_json FROM nbxv1_settings WHERE code='' AND key='MigrationId'");
|
||||
return v is null ? null : v[1..^1];
|
||||
}
|
||||
public async Task SetMigrationId(uint256 newId)
|
||||
{
|
||||
await using var conn = await ConnectionFactory.CreateConnection();
|
||||
await conn.ExecuteScalarAsync<string>(
|
||||
"INSERT INTO nbxv1_settings AS ns (code, key, data_json) VALUES ('', 'MigrationId', @data::JSONB) " +
|
||||
"RETURNING data_json", new { data = $"\"{newId}\"" });
|
||||
}
|
||||
|
||||
private ChainConfiguration GetChainSetting(NBXplorerNetwork net)
|
||||
{
|
||||
return _Configuration.ChainConfigurations.FirstOrDefault(c => c.CryptoCode == net.CryptoCode);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class LegacyDescriptorMetadata
|
||||
{
|
||||
public const string TypeName = "NBXv1-Derivation";
|
||||
[JsonProperty]
|
||||
public string Type { get; set; }
|
||||
[JsonProperty]
|
||||
public DerivationStrategyBase Derivation { get; set; }
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public KeyPathTemplate KeyPathTemplate { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public DerivationFeature Feature { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,12 @@
|
||||
using NBitcoin;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer.Backends
|
||||
namespace NBXplorer.Backend
|
||||
{
|
||||
public class SavedTransaction
|
||||
{
|
||||
public uint256 TxId { get; set; }
|
||||
public NBitcoin.Transaction Transaction
|
||||
{
|
||||
get; set;
|
||||
@ -23,5 +25,7 @@ namespace NBXplorer.Backends
|
||||
set;
|
||||
}
|
||||
public uint256 ReplacedBy { get; set; }
|
||||
|
||||
public TransactionMetadata Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
@ -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,79 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBXplorer.Configuration;
|
||||
using Npgsql;
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Backends.Postgres
|
||||
{
|
||||
public class DbConnectionFactory
|
||||
{
|
||||
public DbConnectionFactory(ILogger<DbConnectionFactory> logger,
|
||||
IConfiguration configuration,
|
||||
ExplorerConfiguration conf,
|
||||
KeyPathTemplates keyPathTemplates)
|
||||
{
|
||||
Logger = logger;
|
||||
ExplorerConfiguration = conf;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
ConnectionString = configuration.GetRequired("POSTGRES");
|
||||
}
|
||||
|
||||
public string ConnectionString { get; }
|
||||
public ILogger<DbConnectionFactory> Logger { get; }
|
||||
public ExplorerConfiguration ExplorerConfiguration { get; }
|
||||
public KeyPathTemplates KeyPathTemplates { get; }
|
||||
|
||||
public Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
|
||||
{
|
||||
return CreateConnectionHelper(network, null);
|
||||
}
|
||||
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network, Action<Npgsql.NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
return new DbConnectionHelper(network, await CreateConnection(action), KeyPathTemplates)
|
||||
{
|
||||
MinPoolSize = ExplorerConfiguration.MinGapSize,
|
||||
MaxPoolSize = ExplorerConfiguration.MaxGapSize
|
||||
};
|
||||
}
|
||||
public Task<DbConnection> CreateConnection()
|
||||
{
|
||||
return CreateConnection(null);
|
||||
}
|
||||
public async Task<DbConnection> CreateConnection(Action<Npgsql.NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
int maxRetries = 10;
|
||||
int retries = maxRetries;
|
||||
retry:
|
||||
var conn = new Npgsql.NpgsqlConnection(GetConnectionString(action));
|
||||
try
|
||||
{
|
||||
await conn.OpenAsync();
|
||||
}
|
||||
catch (PostgresException ex) when (ex.IsTransient && retries > 0)
|
||||
{
|
||||
retries--;
|
||||
await conn.DisposeAsync();
|
||||
await Task.Delay((maxRetries - retries) * 100);
|
||||
goto retry;
|
||||
}
|
||||
catch
|
||||
{
|
||||
conn.Dispose();
|
||||
throw;
|
||||
}
|
||||
return conn;
|
||||
}
|
||||
|
||||
private string GetConnectionString(Action<NpgsqlConnectionStringBuilder> action)
|
||||
{
|
||||
if (action is null)
|
||||
return ConnectionString;
|
||||
NpgsqlConnectionStringBuilder builder = new NpgsqlConnectionStringBuilder(ConnectionString);
|
||||
action(builder);
|
||||
return builder.ConnectionString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
36
NBXplorer/BlockHeaders.cs
Normal file
36
NBXplorer/BlockHeaders.cs
Normal file
@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NBXplorer;
|
||||
|
||||
public record RPCBlockHeader(uint256 Hash, uint256? Previous, int Height, DateTimeOffset Time, uint256 MerkleRoot)
|
||||
{
|
||||
public SlimChainedBlock ToSlimChainedBlock() => new(Hash, Previous, Height);
|
||||
}
|
||||
public class BlockHeaders : IEnumerable<RPCBlockHeader>
|
||||
{
|
||||
public readonly Dictionary<uint256, RPCBlockHeader> ByHashes;
|
||||
public readonly Dictionary<int, RPCBlockHeader> ByHeight;
|
||||
public BlockHeaders(IList<RPCBlockHeader> headers)
|
||||
{
|
||||
ByHashes = new Dictionary<uint256, RPCBlockHeader>(headers.Count);
|
||||
ByHeight = new Dictionary<int, RPCBlockHeader>(headers.Count);
|
||||
foreach (var header in headers)
|
||||
{
|
||||
ByHashes.TryAdd(header.Hash, header);
|
||||
ByHeight.TryAdd(header.Height, header);
|
||||
}
|
||||
}
|
||||
public IEnumerator<RPCBlockHeader> GetEnumerator()
|
||||
{
|
||||
return ByHeight.Values.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
@ -47,7 +47,7 @@ namespace NBXplorer
|
||||
"bad-txns-inputs-missingorspent" or
|
||||
"txn-already-known" or
|
||||
"missing-inputs" or
|
||||
"Transaction already in block chain" => new Reject.MissingInput(),
|
||||
"Transaction already in block chain" or "Transaction outputs already in utxo set" => new Reject.MissingInput(),
|
||||
"mempool min fee not met" => new Reject.NotEnoughFee(),
|
||||
_ => new Reject.Unknown(rejectReason)
|
||||
};
|
||||
|
||||
@ -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();
|
||||
@ -43,23 +40,9 @@ namespace NBXplorer.Configuration
|
||||
app.Option($"--{crypto}startheight", $"The height where starting the scan (default: where your rpc server was synched when you first started this program)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}minutxovalue", $"The minimum value of tracked UTXOs, any UTXO with value less than this is ignored. (default: 1 (satoshi))", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}nodeendpoint", $"The p2p connection to a Bitcoin node, make sure you are whitelisted (default: default p2p node on localhost, depends on network)", CommandOptionType.SingleValue);
|
||||
app.Option($"--{crypto}hastxindex", "If true, NBXplorer will try to fetch missing transactions from the local node. If the node support getindexinfo, this setting is ignored. (default: false)", CommandOptionType.BoolValue);
|
||||
app.Option($"--{crypto}exposerpc", $"Expose the node RPCs through the REST API (default: false)", CommandOptionType.SingleValue);
|
||||
}
|
||||
|
||||
app.Option("--asbcnstr", "[For Azure Service Bus] Azure Service Bus Connection string. New Block and New Transaction messages will be pushed to queues when this values is set", CommandOptionType.SingleValue);
|
||||
app.Option("--asbblockq", "[For Azure Service Bus] Name of Queue to push new block message to. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--asbtranq", "[For Azure Service Bus] Name of Queue to push new transaction message to. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--asbblockt", "[For Azure Service Bus] Name of Topic to push new block message to. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--asbtrant", "[For Azure Service Bus] Name of Topic to push new transaction message to. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
|
||||
app.Option("--rmqhost", "[For RabbitMq] RabbitMq host name. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--rmquser", "[For RabbitMq] RabbitMq username. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--rmqpass", "[For RabbitMq] RabbitMq password. Leave blank to turn off", CommandOptionType.SingleValue);
|
||||
app.Option("--rmqvirtual", "[For RabbitMq] RabbitMq virtual host.", CommandOptionType.SingleValue);
|
||||
app.Option("--rmqtranex", "[For RabbitMq] Name of exchange to push transaction messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--rmqblockex", "[For RabbitMq] Name of exchange to push block messages.", CommandOptionType.SingleValue);
|
||||
|
||||
app.Option("--customkeypathtemplate", $"Define an additional derivation path tracked by NBXplorer (Format: m/1/392/*/29, default: empty)", CommandOptionType.SingleValue);
|
||||
app.Option("--maxgapsize", $"The maximum gap address count on which the explorer will track derivation schemes (default: 30)", CommandOptionType.SingleValue);
|
||||
app.Option("--mingapsize", $"The minimum gap address count on which the explorer will track derivation schemes (default: 20)", CommandOptionType.SingleValue);
|
||||
@ -67,19 +50,9 @@ 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);
|
||||
@ -124,9 +97,11 @@ namespace NBXplorer.Configuration
|
||||
}
|
||||
return n.ChainName;
|
||||
}
|
||||
|
||||
var net = conf.GetOrDefault<bool>("regtest", false) ? ChainName.Regtest :
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainName.Testnet :
|
||||
conf.GetOrDefault<bool>("signet", false) ? new ChainName("signet") : ChainName.Mainnet;
|
||||
conf.GetOrDefault<bool>("testnet", false) ? ChainName.Testnet :
|
||||
conf.GetOrDefault<bool>("signet", false) ? Bitcoin.Instance.Signet.ChainName :
|
||||
conf.GetOrDefault<bool>("mutinynet", false) ? Bitcoin.Instance.Mutinynet.ChainName : ChainName.Mainnet;
|
||||
return net;
|
||||
}
|
||||
|
||||
@ -191,15 +166,6 @@ namespace NBXplorer.Configuration
|
||||
builder.AppendLine("#port=" + settings.DefaultPort);
|
||||
builder.AppendLine("#bind=127.0.0.1");
|
||||
builder.AppendLine($"#{networkType.ToString().ToLowerInvariant()}=1");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("####Azure Service Bus####");
|
||||
builder.AppendLine("## Azure Service Bus configuration - set connection string to use Service Bus. Set Queue and / or Topic names to publish message to queues / topics");
|
||||
builder.AppendLine("#asbcnstr=Endpoint=sb://<yourdomain>.servicebus.windows.net/;SharedAccessKeyName=<your key name here>;SharedAccessKey=<your key here>");
|
||||
builder.AppendLine("#asbblockq=<new block queue name>");
|
||||
builder.AppendLine("#asbtranq=<new transaction queue name>");
|
||||
builder.AppendLine("#asbblockt=<new block topic name>");
|
||||
builder.AppendLine("#asbtrant=<new transaction topic name>");
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
@ -49,7 +49,6 @@ namespace NBXplorer.Configuration
|
||||
set;
|
||||
}
|
||||
public bool NoWarmup { get; set; }
|
||||
public bool HasTxIndex { get; set; }
|
||||
public bool ExposeRPC { get; set; }
|
||||
}
|
||||
public class ExplorerConfiguration
|
||||
@ -77,15 +76,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;
|
||||
@ -148,7 +138,6 @@ namespace NBXplorer.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
chainConfiguration.HasTxIndex = config.GetOrDefault<bool>($"{network.CryptoCode}.hastxindex", false);
|
||||
chainConfiguration.ExposeRPC = config.GetOrDefault<bool>($"{network.CryptoCode}.exposerpc", exposeRPCGlobal);
|
||||
chainConfiguration.NoWarmup = config.GetOrDefault<bool>($"nowarmup", false);
|
||||
ChainConfigurations.Add(chainConfiguration);
|
||||
@ -173,11 +162,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 +173,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);
|
||||
@ -206,38 +187,17 @@ namespace NBXplorer.Configuration
|
||||
CustomKeyPathTemplate = v;
|
||||
}
|
||||
|
||||
AzureServiceBusConnectionString = config.GetOrDefault<string>("asbcnstr", "");
|
||||
AzureServiceBusBlockQueue = config.GetOrDefault<string>("asbblockq", "");
|
||||
AzureServiceBusTransactionQueue = config.GetOrDefault<string>("asbtranq", "");
|
||||
AzureServiceBusBlockTopic = config.GetOrDefault<string>("asbblockt", "");
|
||||
AzureServiceBusTransactionTopic = config.GetOrDefault<string>("asbtrant", "");
|
||||
var obsolete = string.Join(", ",
|
||||
new[] { "dbtrie", "automigrate", "nomigrateevts", "nomigraterawtxs", "cachechain", "deleteaftermigration", "dbcache" }
|
||||
.Where(o => !string.IsNullOrEmpty(config[o])));
|
||||
|
||||
RabbitMqHostName = config.GetOrDefault<string>("rmqhost", "");
|
||||
RabbitMqVirtualHost = config.GetOrDefault<string>("rmqvirtual", "");
|
||||
RabbitMqUsername = config.GetOrDefault<string>("rmquser", "");
|
||||
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)
|
||||
if (obsolete != string.Empty)
|
||||
{
|
||||
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 (Directory.Exists(Path.Combine(DataDir, "db")))
|
||||
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.5.2 and follow https://github.com/btcpayserver/NBXplorer/blob/master/docs/Postgres-Migration.md.");
|
||||
else
|
||||
Logs.Explorer.LogWarning($"Options '{obsolete}' is obsolete and ignored...");
|
||||
}
|
||||
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
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -255,13 +215,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;
|
||||
@ -273,41 +226,6 @@ namespace NBXplorer.Configuration
|
||||
set;
|
||||
}
|
||||
public int TrimEvents { get; set; }
|
||||
public string AzureServiceBusConnectionString
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string AzureServiceBusBlockQueue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string AzureServiceBusBlockTopic
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string AzureServiceBusTransactionQueue
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public string AzureServiceBusTransactionTopic
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public string RabbitMqHostName { get; set; }
|
||||
public string RabbitMqVirtualHost { get; set; }
|
||||
public string RabbitMqUsername { get; set; }
|
||||
public string RabbitMqPassword { get; set; }
|
||||
public string RabbitMqTransactionExchange { get; set; }
|
||||
public string RabbitMqBlockExchange { get; set; }
|
||||
|
||||
public KeyPathTemplate CustomKeyPathTemplate { get; set; }
|
||||
public EndPoint SocksEndpoint { get; set; }
|
||||
|
||||
12
NBXplorer/Controllers/CommonRoutes.cs
Normal file
12
NBXplorer/Controllers/CommonRoutes.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace NBXplorer.Controllers;
|
||||
|
||||
public static class CommonRoutes
|
||||
{
|
||||
public const string BaseCryptoEndpoint = "cryptos/{cryptoCode}";
|
||||
public const string BaseDerivationEndpoint = $"{BaseCryptoEndpoint}/derivations";
|
||||
public const string DerivationEndpoint = $"{BaseCryptoEndpoint}/derivations/{{derivationScheme}}";
|
||||
public const string AddressEndpoint = $"{BaseCryptoEndpoint}/addresses/{{address}}";
|
||||
public const string BaseGroupEndpoint = $"groups";
|
||||
public const string GroupEndpoint = $"{BaseGroupEndpoint}/{{groupId}}";
|
||||
public const string TransactionsPath = "transactions/{txId?}";
|
||||
}
|
||||
@ -1,52 +1,56 @@
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
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.Threading.Tasks;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Dapper;
|
||||
using NBXplorer.Backend;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NBXplorer.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
[Route("v1")]
|
||||
[Route($"v1/{CommonRoutes.DerivationEndpoint}")]
|
||||
[Route($"v1/{CommonRoutes.AddressEndpoint}")]
|
||||
[Route($"v1/{CommonRoutes.BaseCryptoEndpoint}/{CommonRoutes.GroupEndpoint}")]
|
||||
[Authorize]
|
||||
public class PostgresMainController : ControllerBase, IUTXOService
|
||||
public class CommonRoutesController : Controller
|
||||
{
|
||||
public PostgresMainController(
|
||||
DbConnectionFactory connectionFactory,
|
||||
NBXplorerNetworkProvider networkProvider,
|
||||
IRPCClients rpcClients,
|
||||
IIndexers indexers,
|
||||
KeyPathTemplates keyPathTemplates,
|
||||
IRepositoryProvider repositoryProvider) : base(networkProvider, rpcClients, repositoryProvider, indexers)
|
||||
{
|
||||
ConnectionFactory = connectionFactory;
|
||||
KeyPathTemplates = keyPathTemplates;
|
||||
}
|
||||
|
||||
public GroupsController GroupsController{ get; }
|
||||
public AddressPoolService AddressPoolService{ get; }
|
||||
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)
|
||||
public CommonRoutesController(DbConnectionFactory connectionFactory, AddressPoolService addressPoolService, GroupsController groupsController)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
GroupsController = groupsController;
|
||||
AddressPoolService = addressPoolService;
|
||||
ConnectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> IsTracked(TrackedSourceContext trackedSourceContext)
|
||||
{
|
||||
var trackedSource = trackedSourceContext.TrackedSource;
|
||||
var network = trackedSourceContext.Network;
|
||||
var repo = trackedSourceContext.Repository;
|
||||
if(await repo.WalletExists( repo.GetWalletKey(trackedSource)))
|
||||
return Ok();
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("balance")]
|
||||
public async Task<IActionResult> GetBalance(TrackedSourceContext trackedSourceContext)
|
||||
{
|
||||
var trackedSource = trackedSourceContext.TrackedSource;
|
||||
var network = trackedSourceContext.Network;
|
||||
var repo = trackedSourceContext.Repository;
|
||||
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
|
||||
@ -87,7 +91,6 @@ namespace NBXplorer.Controllers
|
||||
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
|
||||
return Json(balance, network.JsonSerializerSettings);
|
||||
}
|
||||
|
||||
private IMoney Format(NBXplorerNetwork network, MoneyBag bag)
|
||||
{
|
||||
if (network.IsElement)
|
||||
@ -99,47 +102,34 @@ namespace NBXplorer.Controllers
|
||||
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)
|
||||
[HttpGet("utxos")]
|
||||
public async Task<IActionResult> GetUTXOs(TrackedSourceContext trackedSourceContext)
|
||||
{
|
||||
var trackedSource = GetTrackedSource(derivationScheme, address);
|
||||
if (trackedSource == null)
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
var network = GetNetwork(cryptoCode, false);
|
||||
var repo = (PostgresRepository)RepositoryProvider.GetRepository(cryptoCode);
|
||||
|
||||
var trackedSource = trackedSourceContext.TrackedSource;
|
||||
var repo = trackedSourceContext.Repository;
|
||||
var network = trackedSourceContext.Network;
|
||||
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())
|
||||
var derivationScheme = (trackedSource as DerivationSchemeTrackedSource)?.DerivationStrategy;
|
||||
if (network.IsElement && derivationScheme is not null && !derivationScheme.Unblinded())
|
||||
{
|
||||
addrColumns = "ds.metadata->>'blindedAddress' as address";
|
||||
}
|
||||
|
||||
string descriptorJoin = string.Empty;
|
||||
string descriptorColumns = "NULL as redeem, NULL as keypath, NULL as feature";
|
||||
string descriptorColumns = "NULL as redeem, NULL as keypath, NULL as keyindex, 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";
|
||||
descriptorColumns = "ds.metadata->>'redeem' redeem, nbxv1_get_keypath(d.metadata, ds.idx) AS keypath, ds.idx, d.metadata->>'feature' feature";
|
||||
}
|
||||
|
||||
var utxos = (await conn.QueryAsync<(
|
||||
@ -151,6 +141,7 @@ namespace NBXplorer.Controllers
|
||||
string address,
|
||||
string redeem,
|
||||
string keypath,
|
||||
int keyIndex,
|
||||
string feature,
|
||||
bool mempool,
|
||||
bool input_mempool,
|
||||
@ -172,7 +163,8 @@ namespace NBXplorer.Controllers
|
||||
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)
|
||||
TransactionHash = uint256.Parse(utxo.tx_id),
|
||||
KeyIndex = utxo.keyIndex
|
||||
};
|
||||
u.Outpoint = new OutPoint(u.TransactionHash, u.Index);
|
||||
if (utxo.blk_height is long)
|
||||
@ -181,24 +173,39 @@ namespace NBXplorer.Controllers
|
||||
}
|
||||
|
||||
if (utxo.keypath is not null)
|
||||
{
|
||||
u.KeyPath = KeyPath.Parse(utxo.keypath);
|
||||
if (utxo.feature is not null)
|
||||
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);
|
||||
if (utxo.input_mempool)
|
||||
changes.Unconfirmed.SpentOutpoints.Add(u.Outpoint);
|
||||
}
|
||||
else if (!utxo.input_mempool)
|
||||
changes.Unconfirmed.UTXOs.Add(u);
|
||||
if (utxo.input_mempool && !utxo.mempool)
|
||||
changes.Unconfirmed.SpentOutpoints.Add(u.Outpoint);
|
||||
else // (utxo.mempool && utxo.input_mempool)
|
||||
changes.SpentUnconfirmed.Add(u);
|
||||
}
|
||||
return Json(changes, network.JsonSerializerSettings);
|
||||
}
|
||||
|
||||
public Task<IActionResult> GetUTXOs(string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||
|
||||
[HttpPost("metadata/{key}")]
|
||||
[HttpPost($"~/v1/{CommonRoutes.GroupEndpoint}/metadata/{{key}}")]
|
||||
public async Task<IActionResult> SetMetadata(TrackedSourceContext trackedSourceContext, string key, [FromBody] JToken value = null)
|
||||
{
|
||||
return this.GetUTXOs(cryptoCode, derivationStrategy, null);
|
||||
await trackedSourceContext.Repository.SaveMetadata(trackedSourceContext.TrackedSource, key, value);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("metadata/{key}")]
|
||||
[HttpGet($"~/v1/{CommonRoutes.GroupEndpoint}/metadata/{{key}}")]
|
||||
public async Task<IActionResult> GetMetadata(TrackedSourceContext trackedSourceContext, string key)
|
||||
{
|
||||
var result = await trackedSourceContext.Repository.GetMetadata<JToken>(trackedSourceContext.TrackedSource, key);
|
||||
return result == null ? NotFound() : Json(result, trackedSourceContext.Repository.Serializer.Settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user