Compare commits

..

1 Commits

Author SHA1 Message Date
nicolas.dorier
f265a4fad6
Remove DBTrie 2023-11-14 23:17:19 +09:00
172 changed files with 6386 additions and 11432 deletions

View File

@ -10,19 +10,61 @@ jobs:
cd .circleci && ./run-tests.sh
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
docker:
docker:
- image: cimg/base:stable
amd64:
machine:
enabled: true
steps:
- setup_remote_docker
- checkout
- checkout
- run:
command: |
docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
docker buildx create --use
docker buildx build -t $DOCKERHUB_REPO:$LATEST_TAG --platform linux/amd64,linux/arm64,linux/arm/v7 --push .
#
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
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-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
workflows:
version: 2
@ -32,12 +74,33 @@ workflows:
publish:
jobs:
- docker:
- 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:
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]+)*(-[a-z0-9-]+)?)|(v[a-z0-9-]+)/
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]+)*/

19
Dockerfile.linuxamd64 Normal file
View File

@ -0,0 +1,19 @@
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"]

View File

@ -1,4 +1,5 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
# 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
WORKDIR /source
COPY NBXplorer/NBXplorer.csproj NBXplorer/NBXplorer.csproj
COPY NBXplorer.Client/NBXplorer.Client.csproj NBXplorer.Client/NBXplorer.Client.csproj
@ -8,12 +9,10 @@ COPY . .
RUN cd NBXplorer && \
dotnet publish --output /app/ --configuration Release
FROM mcr.microsoft.com/dotnet/aspnet:10.0.9-noble
FROM mcr.microsoft.com/dotnet/aspnet:6.0.9-bullseye-slim-arm32v7
WORKDIR /datadir
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

20
Dockerfile.linuxarm64v8 Normal file
View File

@ -0,0 +1,20 @@
# 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"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,49 +1,20 @@
#nullable enable
using NBitcoin;
using System.Collections.Generic;
#if !NO_RECORD
using static NBitcoin.WalletPolicies.MiniscriptNode;
#endif
using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class Derivation
{
public Derivation(Script scriptPubKey, Script? redeem = null)
public Derivation()
{
ScriptPubKey = scriptPubKey;
Redeem = redeem;
}
public Script ScriptPubKey
{
get;
get; set;
}
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
}

View File

@ -1,12 +1,8 @@
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
@ -56,6 +52,7 @@ 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);
@ -82,8 +79,6 @@ 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"))
{
@ -147,14 +142,6 @@ 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);
@ -168,7 +155,7 @@ namespace NBXplorer.DerivationStrategy
/// <param name="publicKey">The public key of the wallet</param>
/// <param name="options">Derivation options</param>
/// <returns></returns>
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
public DerivationStrategyBase CreateDirectDerivationStrategy(ExtPubKey publicKey, DerivationStrategyOptions options = null)
{
return CreateDirectDerivationStrategy(publicKey.GetWif(Network), options);
}
@ -179,10 +166,10 @@ namespace NBXplorer.DerivationStrategy
/// <param name="publicKey">The public key of the wallet</param>
/// <param name="options">Derivation options</param>
/// <returns></returns>
public StandardDerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
public DerivationStrategyBase CreateDirectDerivationStrategy(BitcoinExtPubKey publicKey, DerivationStrategyOptions options = null)
{
options = options ?? new DerivationStrategyOptions();
StandardDerivationStrategyBase strategy = null;
DerivationStrategyBase 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
@ -237,7 +224,7 @@ namespace NBXplorer.DerivationStrategy
public DerivationStrategyBase CreateMultiSigDerivationStrategy(BitcoinExtPubKey[] pubKeys, int sigCount, DerivationStrategyOptions options = null)
{
options = options ?? new DerivationStrategyOptions();
StandardDerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
DerivationStrategyBase derivationStrategy = new MultisigDerivationStrategy(sigCount, pubKeys.ToArray(), options.ScriptPubKeyType == ScriptPubKeyType.Legacy, !options.KeepOrder, options.AdditionalOptions);
if (options.ScriptPubKeyType == ScriptPubKeyType.Legacy)
return new P2SHDerivationStrategy(derivationStrategy, false);
@ -250,6 +237,19 @@ 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(@"-\[([^ \]\-]+)\]");
}
}

View File

@ -6,7 +6,7 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class DirectDerivationStrategy : StandardDerivationStrategyBase
public class DirectDerivationStrategy : DerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -44,11 +44,15 @@ namespace NBXplorer.DerivationStrategy
_Root = root;
Segwit = segwit;
}
public override Derivation GetDerivation(KeyPath keyPath)
public override Derivation GetDerivation()
{
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey;
return new KeyPathDerivation(keyPath, Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey);
var pubKey = _Root.ExtPubKey.PubKey;
return new Derivation() { ScriptPubKey = Segwit ? pubKey.WitHash.ScriptPubKey : pubKey.Hash.ScriptPubKey };
}
public override DerivationStrategyBase GetChild(KeyPath keyPath)
{
return new DirectDerivationStrategy(_Root.ExtPubKey.Derive(keyPath).GetWif(_Root.Network), Segwit, AdditionalOptions);
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()

View File

@ -1,8 +1,4 @@
#nullable enable
using NBitcoin;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -17,42 +13,40 @@ namespace NBXplorer.DerivationStrategy
Direct = 2,
Custom = 3,
}
public abstract class StandardDerivationStrategyBase : DerivationStrategyBase, IHDScriptPubKey
public abstract class DerivationStrategyBase : IHDScriptPubKey
{
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));
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(DerivationFeature feature) => GetLineFor(KeyPathTemplates.Default, feature);
public abstract DerivationLine GetLineFor(KeyPathTemplates keyPathTemplates, DerivationFeature feature);
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();
protected internal abstract string StringValueCore
{
get;
}
string? _StringValue;
string _StringValue;
string StringValue
{
get
@ -72,74 +66,96 @@ namespace NBXplorer.DerivationStrategy
{
return string.Join("", new SortedDictionary<string, string>(AdditionalOptions).Select(pair => $"-[{pair.Key}{(string.IsNullOrEmpty(pair.Value)?string.Empty: $"={pair.Value}")}]"));
}
#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 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);
}
public abstract IEnumerable<ExtPubKey> GetExtPubKeys();
public override int GetHashCode()
{
return StringValue.GetHashCode();
}
public override string ToString()
{
return StringValue;
}
}
#if !NO_RECORD
public class MiniscriptDerivationLine : DerivationLine
{
public MiniscriptDerivationLine(PolicyDerivationStrategy derivationStrategy, DerivationFeature derivationFeature) : base(derivationFeature)
Script IHDScriptPubKey.ScriptPubKey => GetDerivation().ScriptPubKey;
IHDScriptPubKey IHDScriptPubKey.Derive(KeyPath keyPath)
{
DerivationStrategy = derivationStrategy;
Intent = ToAddressIntent(derivationFeature);
return GetChild(keyPath);
}
public static AddressIntent ToAddressIntent(DerivationFeature derivationFeature)
class HDRedeemScriptPubKey : IHDScriptPubKey
{
return derivationFeature switch
private readonly DerivationStrategyBase strategyBase;
public HDRedeemScriptPubKey(DerivationStrategyBase strategyBase)
{
DerivationFeature.Change => AddressIntent.Change,
DerivationFeature.Deposit => AddressIntent.Deposit,
_ => throw new NotSupportedException("MiniscriptDerivationStrategy only support deposit and change features")
};
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 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)
public IHDScriptPubKey AsHDRedeemScriptPubKey()
{
Feature = feature;
return new HDRedeemScriptPubKey(this);
}
public bool CanDeriveHardenedPath()
{
return false;
}
public DerivationFeature Feature { get; }
public abstract Derivation Derive(uint index);
}
public class KeyPathTemplateDerivationLine : DerivationLine
public class DerivationLine
{
public KeyPathTemplateDerivationLine(StandardDerivationStrategyBase derivationStrategyBase, KeyPathTemplates keyPathTemplates, DerivationFeature derivationFeature) : base(derivationFeature)
public DerivationLine(DerivationStrategyBase derivationStrategyBase, KeyPathTemplate keyPathTemplate)
{
if (derivationStrategyBase == null)
throw new ArgumentNullException(nameof(derivationStrategyBase));
if (keyPathTemplates == null)
throw new ArgumentNullException(nameof(keyPathTemplates));
if (keyPathTemplate == null)
throw new ArgumentNullException(nameof(keyPathTemplate));
DerivationStrategyBase = derivationStrategyBase;
KeyPathTemplate = keyPathTemplates.GetKeyPathTemplate(derivationFeature);
KeyPathTemplate = keyPathTemplate;
}
public StandardDerivationStrategyBase DerivationStrategyBase { get; }
public DerivationStrategyBase DerivationStrategyBase { get; }
public KeyPathTemplate KeyPathTemplate { get; }
public override Derivation Derive(uint index)
DerivationStrategyBase _PreLine;
public Derivation Derive(uint index)
{
var kp = KeyPathTemplate.GetKeyPath(index);
return DerivationStrategyBase.GetDerivation(kp);
_PreLine = _PreLine ?? DerivationStrategyBase.GetChild(KeyPathTemplate.PreIndexes);
return _PreLine.GetDerivation(new KeyPath(index).Derive(KeyPathTemplate.PostIndexes));
}
}
}

View File

@ -9,7 +9,7 @@ using System.Collections.ObjectModel;
namespace NBXplorer.DerivationStrategy
{
public class MultisigDerivationStrategy : StandardDerivationStrategyBase
public class MultisigDerivationStrategy : DerivationStrategyBase
{
public bool LexicographicOrder
{
@ -62,19 +62,29 @@ namespace NBXplorer.DerivationStrategy
get;
}
public override Derivation GetDerivation(KeyPath keyPath)
private void WriteBytes(MemoryStream ms, byte[] v)
{
ms.Write(v, 0, v.Length);
}
public override Derivation GetDerivation()
{
var pubKeys = new PubKey[this.Keys.Count];
Parallel.For(0, pubKeys.Length, i =>
{
pubKeys[i] = this.Keys[i].ExtPubKey.Derive(keyPath).PubKey;
pubKeys[i] = this.Keys[i].ExtPubKey.PubKey;
});
if(LexicographicOrder)
{
Array.Sort(pubKeys, LexicographicComparer);
}
var redeem = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(RequiredSignatures, pubKeys);
return new KeyPathDerivation(keyPath, redeem);
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);
}
public override IEnumerable<ExtPubKey> GetExtPubKeys()

View File

@ -4,10 +4,10 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class P2SHDerivationStrategy : StandardDerivationStrategyBase
public class P2SHDerivationStrategy : DerivationStrategyBase
{
bool addSuffix;
internal P2SHDerivationStrategy(StandardDerivationStrategyBase inner, bool addSuffix):base(inner.AdditionalOptions)
internal P2SHDerivationStrategy(DerivationStrategyBase 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 StandardDerivationStrategyBase Inner
public DerivationStrategyBase Inner
{
get; set;
}
@ -30,18 +30,24 @@ 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 Derivation GetDerivation(KeyPath keyPath)
public override DerivationStrategyBase GetChild(KeyPath keyPath)
{
var derivation = Inner.GetDerivation(keyPath);
return new KeyPathDerivation(
keyPath,
derivation.ScriptPubKey.Hash.ScriptPubKey,
derivation.Redeem ?? derivation.ScriptPubKey);
return new P2SHDerivationStrategy(Inner.GetChild(keyPath), addSuffix);
}
}
}

View File

@ -4,31 +4,40 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class P2WSHDerivationStrategy : StandardDerivationStrategyBase
public class P2WSHDerivationStrategy : DerivationStrategyBase
{
internal P2WSHDerivationStrategy(StandardDerivationStrategyBase inner):base(inner.AdditionalOptions)
internal P2WSHDerivationStrategy(DerivationStrategyBase inner):base(inner.AdditionalOptions)
{
if(inner == null)
throw new ArgumentNullException(nameof(inner));
Inner = inner;
}
public StandardDerivationStrategyBase Inner
public DerivationStrategyBase 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 Derivation GetDerivation(KeyPath keyPath)
public override DerivationStrategyBase GetChild(KeyPath keyPath)
{
var redeem = Inner.GetDerivation(keyPath).ScriptPubKey;
return new KeyPathDerivation(keyPath, redeem.WitHash.ScriptPubKey, redeem);
return new P2WSHDerivationStrategy(Inner.GetChild(keyPath));
}
}
}

View File

@ -1,212 +0,0 @@
#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

View File

@ -6,7 +6,7 @@ using NBitcoin;
namespace NBXplorer.DerivationStrategy
{
public class TaprootDerivationStrategy : StandardDerivationStrategyBase
public class TaprootDerivationStrategy : DerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -35,16 +35,21 @@ namespace NBXplorer.DerivationStrategy
throw new ArgumentNullException(nameof(root));
_Root = root;
}
public override Derivation GetDerivation(KeyPath keyPath)
public override Derivation GetDerivation()
{
#if NO_SPAN
throw new NotSupportedException("Deriving taproot address is not supported on this platform.");
#else
var pubKey = _Root.ExtPubKey.Derive(keyPath).PubKey.GetTaprootFullPubKey();
return new KeyPathDerivation(keyPath, pubKey.ScriptPubKey);
var pubKey = _Root.ExtPubKey.PubKey.GetTaprootFullPubKey();
return new Derivation() { ScriptPubKey = 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;

View File

@ -13,8 +13,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
using NBitcoin.RPC;
using System.Runtime.CompilerServices;
using System.Linq;
namespace NBXplorer
{
@ -152,7 +150,7 @@ namespace NBXplorer
}
public async Task<TransactionResult> GetTransactionAsync(uint256 txId, CancellationToken cancellation = default)
{
return await SendAsync<TransactionResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/transactions/{txId}", cancellation).ConfigureAwait(false);
return await SendAsync<TransactionResult>(HttpMethod.Get, null, "v1/cryptos/{0}/transactions/" + txId, new[] { CryptoCode }, cancellation).ConfigureAwait(false);
}
public TransactionResult GetTransaction(uint256 txId, CancellationToken cancellation = default)
@ -164,7 +162,7 @@ namespace NBXplorer
{
if (extKey == null)
throw new ArgumentNullException(nameof(extKey));
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/prune", cancellation).ConfigureAwait(false);
return await SendAsync<PruneResponse>(HttpMethod.Post, pruneRequest, "v1/cryptos/{0}/derivations/{1}/prune", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
}
public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequest, CancellationToken cancellation = default)
@ -172,16 +170,6 @@ 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)
@ -196,7 +184,7 @@ namespace NBXplorer
var argsString = string.Join("&", args.ToArray());
if (argsString != string.Empty)
argsString = $"?{argsString}";
await SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);
await SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan{2}", new object[] { Network.CryptoCode, extKey, argsString }, cancellation).ConfigureAwait(false);
}
public void ScanUTXOSet(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
{
@ -205,7 +193,7 @@ namespace NBXplorer
public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
{
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan", cancellation).ConfigureAwait(false);
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/utxos/scan", new object[] { Network.CryptoCode, extKey }, cancellation).ConfigureAwait(false);
}
public ScanUTXOInformation GetScanUTXOSetInformation(DerivationStrategyBase extKey, CancellationToken cancellation = default)
@ -229,28 +217,25 @@ 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 Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
public async Task<UTXOChanges> GetUTXOsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
return SendAsync<UTXOChanges>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/utxos", cancellation);
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);
}
public void WaitServerStarted(CancellationToken cancellation = default)
@ -281,7 +266,7 @@ namespace NBXplorer
}
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
{
return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation);
return TrackAsync(TrackedSource.Create(strategy), cancellation);
}
public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
@ -292,18 +277,27 @@ namespace NBXplorer
{
if (strategy == null)
throw new ArgumentNullException(nameof(strategy));
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, $"v1/cryptos/{CryptoCode}/derivations/{strategy}", cancellation).ConfigureAwait(false);
await SendAsync<string>(HttpMethod.Post, trackDerivationRequest, "v1/cryptos/{0}/derivations/{1}", new[] { CryptoCode, strategy.ToString() }, cancellation).ConfigureAwait(false);
}
public void Track(TrackedSource trackedSource, CancellationToken cancellation = default)
{
TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult();
TrackAsync(trackedSource, cancellation).GetAwaiter().GetResult();
}
public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default)
public Task TrackAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
return SendAsync<string>(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation);
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);
}
private Exception UnSupported(TrackedSource trackedSource)
@ -322,7 +316,7 @@ namespace NBXplorer
}
public Task<GetBalanceResponse> GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
{
return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation);
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/balance", new[] { CryptoCode, userDerivationScheme.ToString() }, cancellation);
}
@ -332,30 +326,12 @@ namespace NBXplorer
}
public Task<GetBalanceResponse> GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default)
{
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;
}
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, "v1/cryptos/{0}/addresses/{1}/balance", new[] { CryptoCode, address.ToString() }, cancellation);
}
public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
{
return SendAsync<string>(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation);
return SendAsync<string>(HttpMethod.Post, keyPaths, "v1/cryptos/{0}/derivations/{1}/addresses/cancelreservation", new[] { CryptoCode, strategy.ToString() }, cancellation);
}
public StatusResult GetStatus(CancellationToken cancellation = default)
@ -370,39 +346,40 @@ namespace NBXplorer
{
if (strategy is null)
throw new ArgumentNullException(nameof(strategy));
return SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/utxos/wipe", cancellation);
return SendAsync<bool>(HttpMethod.Post, null, "v1/cryptos/{0}/derivations/{1}/utxos/wipe", new[] { CryptoCode, strategy.ToString() }, cancellation);
}
public Task<StatusResult> GetStatusAsync(CancellationToken cancellation = default)
{
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", cancellation);
return SendAsync<StatusResult>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/status", null, cancellation);
}
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
public GetTransactionsResponse GetTransactions(DerivationStrategyBase strategy, CancellationToken cancellation = default)
{
return GetTransactionsAsync(strategy, from, to, cancellation).GetAwaiter().GetResult();
return GetTransactionsAsync(strategy, cancellation).GetAwaiter().GetResult();
}
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
public GetTransactionsResponse GetTransactions(TrackedSource trackedSource, CancellationToken cancellation = default)
{
return GetTransactionsAsync(trackedSource, from, to, cancellation).GetAwaiter().GetResult();
return GetTransactionsAsync(trackedSource, cancellation).GetAwaiter().GetResult();
}
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
{
return GetTransactionsAsync(TrackedSource.Create(strategy), from, to, cancellation);
return GetTransactionsAsync(TrackedSource.Create(strategy), cancellation);
}
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, DateTimeOffset? from = null, DateTimeOffset? to = null, CancellationToken cancellation = default)
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
string fromV = string.Empty;
string toV = string.Empty;
if (from is DateTimeOffset f)
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
fromV = NBitcoin.Utils.DateTimeToUnixTime(f).ToString();
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions", null, cancellation);
}
if (to is DateTimeOffset t)
else if (trackedSource is AddressTrackedSource asts)
{
toV = NBitcoin.Utils.DateTimeToUnixTime(t).ToString();
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions", null, cancellation);
}
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions?from={fromV}&to={toV}", cancellation);
else
throw UnSupported(trackedSource);
}
@ -427,14 +404,23 @@ namespace NBXplorer
throw new ArgumentNullException(nameof(txId));
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation);
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);
}
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", cancellation);
return SendAsync<byte[]>(HttpMethod.Post, rescanRequest, $"v1/cryptos/{CryptoCode}/rescan", null, cancellation);
}
public void Rescan(RescanRequest rescanRequest, CancellationToken cancellation = default)
@ -451,7 +437,7 @@ namespace NBXplorer
{
try
{
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", cancellation).ConfigureAwait(false);
return await GetAsync<KeyPathInformation>($"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/unused?feature={feature}&skip={skip}&reserve={reserve}", null, cancellation).ConfigureAwait(false);
}
catch (NBXplorerException ex) when (ex.Error?.HttpCode == 404)
{
@ -466,17 +452,16 @@ namespace NBXplorer
public async Task<KeyPathInformation> GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
{
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);
}
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, "v1/cryptos/{0}/derivations/{1}/scripts/" + script.ToHex(), new object[] { CryptoCode, strategy }, 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);
}
[Obsolete("Use GetKeyInformation(DerivationStrategyBase strategy, Script script) instead")]
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default)
{
return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult();
@ -495,7 +480,7 @@ namespace NBXplorer
{
try
{
return await GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation).ConfigureAwait(false);
return await GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation).ConfigureAwait(false);
}
catch (NBXplorerException ex) when (fallbackFeeRate != null && ex.Error.Code == "fee-estimation-unavailable")
{
@ -504,7 +489,7 @@ namespace NBXplorer
}
public Task<GetFeeRateResult> GetFeeRateAsync(int blockCount, CancellationToken cancellation = default)
{
return GetAsync<GetFeeRateResult>($"v1/cryptos/{CryptoCode}/fees/{blockCount}", cancellation);
return GetAsync<GetFeeRateResult>("v1/cryptos/{0}/fees/{1}", new object[] { CryptoCode, blockCount }, cancellation);
}
public CreatePSBTResponse CreatePSBT(DerivationStrategyBase derivationStrategy, CreatePSBTRequest request, CancellationToken cancellation = default)
{
@ -516,7 +501,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/{CryptoCode}/derivations/{derivationStrategy}/psbt/create", cancellation);
return this.SendAsync<CreatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations/{1}/psbt/create", new object[] { CryptoCode, derivationStrategy }, cancellation);
}
public UpdatePSBTResponse UpdatePSBT(UpdatePSBTRequest request, CancellationToken cancellation = default)
@ -527,7 +512,7 @@ namespace NBXplorer
{
if (request == null)
throw new ArgumentNullException(nameof(request));
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/psbt/update", cancellation);
return this.SendAsync<UpdatePSBTResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/psbt/update", new object[] { CryptoCode }, cancellation);
}
public BroadcastResult Broadcast(Transaction tx, CancellationToken cancellation = default)
{
@ -545,7 +530,7 @@ namespace NBXplorer
public Task<BroadcastResult> BroadcastAsync(Transaction tx, bool testMempoolAccept, CancellationToken cancellation = default)
{
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), $"v1/cryptos/{CryptoCode}/transactions?testMempoolAccept={testMempoolAccept}", cancellation);
return SendAsync<BroadcastResult>(HttpMethod.Post, tx.ToBytes(), "v1/cryptos/{0}/transactions?testMempoolAccept={1}", new[] { CryptoCode, testMempoolAccept.ToString() }, cancellation);
}
public TMetadata GetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, CancellationToken cancellationToken = default)
@ -558,7 +543,7 @@ namespace NBXplorer
throw new ArgumentNullException(nameof(derivationScheme));
if (key == null)
throw new ArgumentNullException(nameof(key));
return GetAsync<TMetadata>($"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
return GetAsync<TMetadata>("v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
}
public void SetMetadata<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
@ -568,13 +553,13 @@ namespace NBXplorer
public Task SetMetadataAsync<TMetadata>(DerivationStrategyBase derivationScheme, string key, TMetadata value, CancellationToken cancellationToken = default)
{
return SendAsync<string>(HttpMethod.Post, value, $"v1/cryptos/{CryptoCode}/derivations/{derivationScheme}/metadata/{key}", cancellationToken);
return SendAsync<string>(HttpMethod.Post, value, "v1/cryptos/{0}/derivations/{1}/metadata/{2}", new object[] { CryptoCode, derivationScheme, key }, cancellationToken);
}
public Task<GenerateWalletResponse> GenerateWalletAsync(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
{
request ??= new GenerateWalletRequest();
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, $"v1/cryptos/{CryptoCode}/derivations", cancellationToken);
return SendAsync<GenerateWalletResponse>(HttpMethod.Post, request, "v1/cryptos/{0}/derivations", new object[] { CryptoCode }, cancellationToken);
}
public GenerateWalletResponse GenerateWallet(GenerateWalletRequest request = null, CancellationToken cancellationToken = default)
@ -583,37 +568,6 @@ 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;
@ -657,23 +611,13 @@ namespace NBXplorer
}
}
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)
internal string GetFullUri(string relativePath, params object[] parameters)
{
relativePath = String.Format(relativePath, parameters ?? new object[0]);
var uri = Address.AbsoluteUri;
if (!uri.EndsWith("/", StringComparison.Ordinal))
uri += "/";
uri += EncodeUrlParameters(relativePath).ToString();
uri += relativePath;
if (!IncludeTransaction)
{
if (uri.IndexOf('?') == -1)
@ -683,13 +627,13 @@ namespace NBXplorer
}
return uri;
}
private Task<T> GetAsync<T>(FormattableString relativePath, CancellationToken cancellation)
private Task<T> GetAsync<T>(string relativePath, object[] parameters, CancellationToken cancellation)
{
return SendAsync<T>(HttpMethod.Get, null, relativePath, cancellation);
return SendAsync<T>(HttpMethod.Get, null, relativePath, parameters, cancellation);
}
internal async Task<T> SendAsync<T>(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
internal async Task<T> SendAsync<T>(HttpMethod method, object body, string relativePath, object[] parameters, CancellationToken cancellation)
{
HttpRequestMessage message = CreateMessage(method, body, relativePath);
HttpRequestMessage message = CreateMessage(method, body, relativePath, parameters);
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
if ((int)result.StatusCode == 404)
{
@ -703,35 +647,16 @@ namespace NBXplorer
{
if (Auth.RefreshCache())
{
message = CreateMessage(method, body, relativePath);
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
message = CreateMessage(method, body, relativePath, parameters);
result = await Client.SendAsync(message).ConfigureAwait(false);
}
}
return await ParseResponse<T>(result).ConfigureAwait(false);
}
internal async Task<HttpResponseMessage> SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
{
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)
internal HttpRequestMessage CreateMessage(HttpMethod method, object body, string relativePath, object[] parameters)
{
var uri = GetFullUri(relativePath);
var uri = GetFullUri(relativePath, parameters);
var message = new HttpRequestMessage(method, uri);
Auth.SetAuthorization(message);
if (body != null)
@ -784,18 +709,5 @@ 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}"
};
}
}
}

View File

@ -37,6 +37,27 @@ 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

View File

@ -1,54 +0,0 @@
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());
}
}
}
}

View File

@ -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{ExplorerClient.Raw(parametersString)}", cancellation);
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events{parametersString}", null, 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{ExplorerClient.Raw(parametersString)}", cancellation);
var evts = await Client.SendAsync<JArray>(HttpMethod.Get, null, $"v1/cryptos/{Client.CryptoCode}/events/latest{parametersString}", null, cancellation);
var evtsObj = evts.Select(ev => NewEventBase.ParseEvent((JObject)ev, Client.Serializer.Settings))
.OfType<NewEventBase>()

View File

@ -1,15 +1,10 @@
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>
@ -68,15 +63,10 @@ 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 PSBTDestination ExplicitChangeAddress { get; set; }
public BitcoinAddress ExplicitChangeAddress { get; set; }
/// <summary>
/// Rebase the hdkey paths (if no rebase, the key paths are relative to the xpub that NBXplorer knows about)
@ -111,66 +101,9 @@ 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
{
/// <summary>
/// The destination as an address or a script. (in hex)
/// </summary>
public PSBTDestination Destination { get; set; }
public BitcoinAddress Destination { get; set; }
/// <summary>
/// Will Send this amount to this destination (Mutually exclusive with: SweepAll)
/// </summary>
@ -187,11 +120,11 @@ namespace NBXplorer.Models
public class FeePreference
{
/// <summary>
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, FallbackFeeRate)
/// An explicit fee rate for the transaction in Satoshi per vBytes (Mutually exclusive with: BlockTarget, ExplicitFee, FallbackFeeRate)
/// </summary>
public FeeRate ExplicitFeeRate { get; set; }
/// <summary>
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, FallbackFeeRate)
/// An explicit fee for the transaction in Satoshi (Mutually exclusive with: BlockTarget, ExplicitFeeRate, FallbackFeeRate)
/// </summary>
public Money ExplicitFee { get; set; }
/// <summary>

View File

@ -1,5 +1,4 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json;
using System.Collections.Generic;
namespace NBXplorer.Models
@ -15,7 +14,6 @@ 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; }

View File

@ -6,7 +6,6 @@ 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))]
@ -18,7 +17,7 @@ namespace NBXplorer.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.KeyPathJsonConverter))]
public NBitcoin.RootedKeyPath AccountKeyPath { get; set; }
public string AccountDescriptor { get; set; }
public StandardDerivationStrategyBase DerivationScheme { get; set; }
public DerivationStrategyBase DerivationScheme { get; set; }
public Mnemonic GetMnemonic()
{

View File

@ -89,10 +89,10 @@ namespace NBXplorer.Models
get; set;
} = new List<MatchedOutput>();
public List<MatchedInput> Inputs
public List<MatchedOutput> Inputs
{
get; set;
} = new List<MatchedInput>();
} = new List<MatchedOutput>();
public DateTimeOffset Timestamp
{
get;
@ -106,6 +106,5 @@ namespace NBXplorer.Models
public uint256 ReplacedBy { get; set; }
public uint256 Replacing { get; set; }
public bool Replaceable { get; set; }
public TransactionMetadata Metadata { get; set; }
}
}

View File

@ -1,22 +0,0 @@
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 };
}
}

View File

@ -1,10 +0,0 @@
using NBitcoin;
using Newtonsoft.Json;
namespace NBXplorer.Models;
public class ImportUTXORequest
{
[JsonProperty("UTXOs")]
public OutPoint[] Utxos { get; set; }
}

View File

@ -1,13 +1,26 @@
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
@ -35,10 +48,9 @@ namespace NBXplorer.Models
{
get; set;
}
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int? Index { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; } = new Dictionary<string, JToken>();
public int GetIndex(KeyPathTemplates keyPathTemplates)
{
return (int)keyPathTemplates.GetKeyPathTemplate(Feature).GetIndex(KeyPath);
}
}
}

View File

@ -71,6 +71,25 @@ 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)
@ -128,5 +147,12 @@ 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));
}
}
}

View File

@ -13,7 +13,14 @@ namespace NBXplorer
private readonly KeyPathTemplate customKeyPathTemplate;
private static readonly KeyPathTemplates _Default = new KeyPathTemplates();
private readonly DerivationFeature[] derivationFeatures;
public static KeyPathTemplates Default => _Default;
public static KeyPathTemplates Default
{
get
{
return _Default;
}
}
private KeyPathTemplates() : this(null)
{
@ -22,27 +29,81 @@ namespace NBXplorer
public KeyPathTemplates(KeyPathTemplate customKeyPathTemplate)
{
this.customKeyPathTemplate = customKeyPathTemplate;
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>
{
DerivationFeature.Deposit,
DerivationFeature.Change,
DerivationFeature.Direct
};
List<DerivationFeature> derivationFeatures = new List<DerivationFeature>();
derivationFeatures.Add(DerivationFeature.Deposit);
derivationFeatures.Add(DerivationFeature.Change);
derivationFeatures.Add(DerivationFeature.Direct);
if (customKeyPathTemplate != null)
derivationFeatures.Add(DerivationFeature.Custom);
this.derivationFeatures = derivationFeatures.ToArray();
}
public KeyPathTemplate GetKeyPathTemplate(DerivationFeature derivationFeature)
=> derivationFeature switch
{
switch (derivationFeature)
{
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.")
};
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());
}
}
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures() => derivationFeatures;
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;
}
}
}

View File

@ -28,10 +28,6 @@ namespace NBXplorer.Models
get; set;
}
public List<MatchedInput> Inputs
{
get; set;
} = new List<MatchedInput>();
public List<MatchedOutput> Outputs
{
get; set;
@ -66,19 +62,7 @@ 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;
}
}
}

View File

@ -48,6 +48,12 @@ namespace NBXplorer.Models
{
get; set;
}
public string Backend { get; set; }
public double RepositoryPingTime
{
get;
set;
}
public bool IsFullySynched
{
get; set;

View File

@ -1,9 +1,6 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace NBXplorer.Models
{
@ -13,30 +10,22 @@ 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;
@ -108,51 +97,6 @@ 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
@ -175,6 +119,8 @@ 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;
@ -212,6 +158,8 @@ 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;
@ -240,12 +188,5 @@ 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
}
}

View File

@ -1,40 +0,0 @@
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>();
}
}

View File

@ -55,7 +55,5 @@ namespace NBXplorer.Models
set;
}
public uint256 ReplacedBy { get; set; }
public TransactionMetadata Metadata { get; set; }
}
}

View File

@ -4,9 +4,6 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
namespace NBXplorer.Models
{
@ -45,11 +42,6 @@ namespace NBXplorer.Models
}
}
public List<UTXO> SpentUnconfirmed
{
get;
set;
} = new List<UTXO>();
UTXOChange _Confirmed = new UTXOChange();
public UTXOChange Confirmed
@ -93,7 +85,7 @@ namespace NBXplorer.Models
public Key[] GetKeys(ExtKey extKey, bool excludeUnconfirmedUTXOs = false)
{
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Where(u => u.KeyPath is not null).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
return GetUnspentUTXOs(excludeUnconfirmedUTXOs).Select(u => extKey.Derive(u.KeyPath).PrivateKey).ToArray();
}
}
public class UTXOChange
@ -159,31 +151,16 @@ namespace NBXplorer.Models
if (Value is Money v)
{
var coin = new Coin(Outpoint, new TxOut(v, ScriptPubKey));
if (Redeem is not null)
if (derivationStrategy != 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;
@ -277,7 +254,5 @@ namespace NBXplorer.Models
_Confirmations = checked((long)value);
}
}
public int KeyIndex { get; set; }
}
}

View File

@ -1,43 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<Company>Digital Garage</Company>
<Version>5.0.6</Version>
<Version>4.2.5</Version>
<Copyright>Copyright © Digital Garage 2017</Copyright>
<Description>Client API for the minimalist HD Wallet Tracker NBXplorer</Description>
<PackageIcon>Bitcoin.png</PackageIcon>
<PackageIconUrl>https://aois.blob.core.windows.net/public/Bitcoin.png</PackageIconUrl>
<PackageTags>bitcoin</PackageTags>
<PackageProjectUrl>https://github.com/btcpayserver/NBXplorer/</PackageProjectUrl>
<PackageProjectUrl>https://github.com/dgarage/NBXplorer/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/btcpayserver/NBXplorer</RepositoryUrl>
<RepositoryUrl>https://github.com/dgarage/NBXplorer</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<LangVersion>12</LangVersion>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);NO_RECORD</DefineConstants>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<DefineConstants>$(DefineConstants);NO_SPAN</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>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573;1572;1584;1570;3021</NoWarn>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="10.0.6" />
<PackageReference Include="NBitcoin.Altcoins" Version="6.0.3" />
<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" />
</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>

View File

@ -50,7 +50,6 @@ namespace NBXplorer
internal set;
}
[Obsolete]
public virtual BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
{
return scriptPubKey.GetDestinationAddress(NBitcoinNetwork);

View File

@ -4,9 +4,6 @@ using NBitcoin.Altcoins.Elements;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
namespace NBXplorer
{
@ -26,19 +23,14 @@ 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)
{
var addr = scriptPubKey.GetDestinationAddress(NBitcoinNetwork);
return BlindIfNeeded(derivationStrategy, addr, keyPath);
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));
}
public static Key GenerateSlip77BlindingKeyFromMnemonic(Mnemonic mnemonic, Script script)
@ -54,7 +46,7 @@ namespace NBXplorer
return new Key(Hashes.HMACSHA256(masterBlindingKey.ToBytes(), script.ToBytes()));
}
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey, Network network)
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script script, Network network)
{
if (derivationStrategy.Unblinded())
{
@ -65,11 +57,11 @@ namespace NBXplorer
{
if (HexEncoder.IsWellFormed(key))
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), scriptPubKey);
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), script);
}
try
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), scriptPubKey);
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), script);
}
catch (Exception)
{
@ -78,7 +70,7 @@ namespace NBXplorer
try
{
var data = new Mnemonic(key);
return GenerateSlip77BlindingKeyFromMnemonic(data, scriptPubKey);
return GenerateSlip77BlindingKeyFromMnemonic(data, derivationStrategy.GetDerivation(keyPath).ScriptPubKey);
}
catch (Exception)
{
@ -87,12 +79,10 @@ namespace NBXplorer
throw new InvalidOperationException("The key provided for slip77 derivation was invalid.");
}
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");
var blindingKey = new Key(derivationStrategy.GetChild(keyPath).GetChild(new KeyPath("0")).GetDerivation()
.ScriptPubKey.WitHash.ToBytes());
return blindingKey;
}
}
private void InitLiquid(ChainName networkType)

View File

@ -1,24 +0,0 @@
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);
}
}
}

View File

@ -13,7 +13,6 @@ namespace NBXplorer
InitBitcore(networkType);
InitLitecoin(networkType);
InitDogecoin(networkType);
InitPepecoin(networkType);
InitBCash(networkType);
InitGroestlcoin(networkType);
InitBGold(networkType);

View File

@ -1,128 +0,0 @@
#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);
}

View File

@ -1,9 +0,0 @@
#!/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"

View File

@ -3,7 +3,6 @@ using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using NBXplorer.JsonConverters;
namespace NBXplorer
{
@ -29,7 +28,6 @@ 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());

View File

@ -7,16 +7,71 @@ using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin.Protocol;
namespace NBXplorer
{
public class WebsocketNotificationSessionLegacy : WebsocketNotificationSession
public class WebsocketNotificationSession : NotificationSessionBase, IDisposable
{
protected override FormattableString GetConnectPath() => $"v1/cryptos/{_Client.CryptoCode}/connect";
internal WebsocketNotificationSessionLegacy(ExplorerClient client) : base(client)
private 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($"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();
@ -73,7 +128,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)
@ -86,70 +141,6 @@ 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);

View File

@ -8,8 +8,6 @@ using Xunit;
using System.Net.Http;
using Xunit.Abstractions;
using NBXplorer.Analytics;
using NBXplorer.DerivationStrategy;
using NBitcoin.Altcoins;
namespace NBXplorer.Tests
{

View File

@ -0,0 +1,84 @@
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();
}
}
}

View File

@ -17,8 +17,6 @@ using System.Runtime.CompilerServices;
using System.IO;
using System.Diagnostics;
using NBXplorer.Client;
using System.Data;
using static NBXplorer.Backend.DbConnectionHelper;
using NBXplorer.Backend;
namespace NBXplorer.Tests
@ -86,7 +84,7 @@ namespace NBXplorer.Tests
await connection.ExecuteAsync("ANALYZE;");
goto retry;
}
Assert.Fail("Unacceptable response time for " + script);
Assert.False(true, "Unacceptable response time for " + script);
}
}
@ -145,9 +143,6 @@ 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, '')" +
@ -160,7 +155,6 @@ 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);
@ -169,36 +163,8 @@ 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()

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
FROM mcr.microsoft.com/dotnet/sdk:6.0.401-bullseye-slim AS builder
WORKDIR /source
COPY . .
RUN cd NBXplorer.Tests && dotnet build

View File

@ -57,7 +57,7 @@ namespace NBXplorer.Tests
public static DerivationLine GetLineFor(this DerivationStrategyBase strategy, DerivationFeature feature)
{
return strategy.GetLineFor(KeyPathTemplates.Default, feature);
return strategy.GetLineFor(KeyPathTemplates.Default.GetKeyPathTemplate(feature));
}
static BitcoinAddress Dummy = new Key().PubKey.GetAddress(ScriptPubKeyType.Legacy, Network.Main);
@ -70,6 +70,12 @@ namespace NBXplorer.Tests
{
return client.EnsureGenerateAsync(blockCount).GetAwaiter().GetResult();
}
public static RPCClient WithCapabilitiesOf(this RPCClient client, RPCClient target)
{
client.Capabilities = target.Capabilities;
return client;
}
}
}

View File

@ -69,12 +69,20 @@ namespace NBXplorer.Tests
public void LogInformation(string msg)
{
if (msg != null)
try
{
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
catch { }
if(msg != null)
_Helper.WriteLine(DateTimeOffset.UtcNow + " :" + Name + ": " + msg);
}
}
public class Logs
{
public static ILog Tester
{
get; set;
}
public static XUnitLoggerProvider LogProvider
{
get;
set;
}
}
}

View File

@ -8,13 +8,18 @@ using Xunit.Abstractions;
namespace NBXplorer.Tests
{
public class MaintenanceUtilities(ITestOutputHelper helper) : UnitTestBase(helper)
public class MaintenanceUtilities
{
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 = CreateTester();
using var t = ServerTester.Create();
var script = await GenerateDbScript(t);
File.WriteAllText(GetFullSchemaFile(), script);
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net10.0</TargetFramework>
<LangVersion>12</LangVersion>
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">net6.0</TargetFramework>
<LangVersion>10.0</LangVersion>
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -11,11 +11,10 @@
<EmbeddedResource Include="Scripts\generate-whale.sql" />
</ItemGroup>
<ItemGroup>
<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">
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
@ -30,9 +29,4 @@
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Update="Data\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -2,7 +2,6 @@
using Microsoft.Extensions.DependencyInjection;
using NBitcoin;
using NBXplorer.Backend;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@ -45,11 +44,12 @@ namespace NBXplorer.Tests
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<RepositoryProvider>();
services.AddSingleton<HostedServices.DatabaseSetupHostedService>();
var provider = services.BuildServiceProvider();
_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"));

View File

@ -29,12 +29,6 @@ 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;
@ -68,7 +62,7 @@ namespace NBXplorer.Tests
//Network = NBitcoin.Altcoins.Viacoin.Instance.Regtest;
//CryptoCode = "GRS";
//nodeDownloadData = NodeDownloadData.Groestlcoin.v29_0;
//nodeDownloadData = NodeDownloadData.Groestlcoin.v25_0;
//Network = NBitcoin.Altcoins.Groestlcoin.Instance.Regtest;
//CryptoCode = "BTX";

View File

@ -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,11 +16,9 @@ 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
{
@ -28,14 +26,13 @@ namespace NBXplorer.Tests
{
private readonly string _Directory;
public static ServerTester Create(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
public static ServerTester Create([CallerMemberNameAttribute] string caller = null)
{
return new ServerTester(logs, caller, true);
return new ServerTester(caller);
}
public static ServerTester CreateNoAutoStart(TesterLogs logs, [CallerMemberNameAttribute] string caller = null)
public static ServerTester CreateNoAutoStart([CallerMemberNameAttribute]string caller = null)
{
return new ServerTester(logs, caller, false);
return new ServerTester(caller, false);
}
public void Dispose()
@ -59,13 +56,11 @@ namespace NBXplorer.Tests
get; set;
}
public TesterLogs Logs { get; }
public string Caller { get; }
public ServerTester(TesterLogs logs, string directory, bool autoStart = true)
public ServerTester(string directory, bool autoStart = true)
{
_Name = directory;
SetEnvironment();
Logs = logs;
Caller = directory;
var rootTestData = "TestData";
directory = Path.Combine(rootTestData, directory);
@ -75,12 +70,11 @@ namespace NBXplorer.Tests
if (autoStart)
Start();
}
public RPCWalletType? RPCWalletType
{
get;
set;
}
} = NBitcoin.Tests.RPCWalletType.Legacy;
public void Start()
{
@ -113,24 +107,16 @@ 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 = FreeTcpPort();
var port = CustomServer.FreeTcpPort();
List<(string key, string value)> keyValues = new List<(string key, string value)>();
keyValues.Add(("conf", Path.Combine(datadir, "settings.config")));
PostgresConnectionString ??= GetTestPostgres(null, _Name);
@ -144,17 +130,33 @@ 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}"));
var args = keyValues.SelectMany(kv => new[] { $"--{kv.key}", kv.value })
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" }))
.Concat(AdditionalFlags)
.Concat(additionalFlags).ToArray();
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
Host = new WebHostBuilder()
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
.UseKestrel()
.ConfigureLogging(l =>
{
l.SetMinimumLevel(LogLevel.Information)
@ -162,20 +164,13 @@ namespace NBXplorer.Tests
.AddFilter("Microsoft", LogLevel.Error)
.AddFilter("Hangfire", LogLevel.Error)
.AddFilter("NBXplorer.Authentication.BasicAuthenticationHandler", LogLevel.Critical)
.ClearProviders()
.AddProvider(Logs.LogProvider);
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseKestrel()
.UseConfiguration(new DefaultConfiguration().CreateConfiguration(args))
.UseStartup<Startup>();
})
.UseStartup<Startup>()
.Build();
NBXplorer.Logging.Logs.Configure(Host.Services.GetRequiredService<ILoggerFactory>());
NBXplorerNetwork = ((NBXplorerNetworkProvider)Host.Services.GetService(typeof(NBXplorerNetworkProvider))).GetFromCryptoCode(CryptoCode);
RPC = ((RPCClientProvider)Host.Services.GetService(typeof(RPCClientProvider))).Get(NBXplorerNetwork);
RPC = ((IRPCClients)Host.Services.GetService(typeof(IRPCClients))).Get(NBXplorerNetwork);
var conf = (ExplorerConfiguration)Host.Services.GetService(typeof(ExplorerConfiguration));
Host.Start();
Configuration = conf;
@ -210,10 +205,10 @@ namespace NBXplorer.Tests
public HttpClient HttpClient { get; internal set; }
string datadir;
public void ResetExplorer(bool deleteAll = true)
{
_ = Host.StopAsync();
Host.WaitForShutdown();
Host.Dispose();
if (deleteAll)
{
PostgresConnectionString = null;
@ -236,7 +231,7 @@ namespace NBXplorer.Tests
{
get
{
var address = Host.GetServerFeatures<IServerAddressesFeature>().Addresses.First();
var address = Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.FirstOrDefault();
return new Uri(address);
}
}
@ -267,7 +262,7 @@ namespace NBXplorer.Tests
}
public IHost Host
public IWebHost Host
{
get; set;
}
@ -379,11 +374,9 @@ 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 is RPCErrorCode.RPC_WALLET_ERROR or RPCErrorCode.RPC_METHOD_NOT_FOUND)
catch (RPCException ex) when (ex.RPCCode == RPCErrorCode.RPC_WALLET_ERROR)
{
string[] desc;
if (this.RPC.Capabilities.SupportSegwit)
@ -402,8 +395,8 @@ namespace NBXplorer.Tests
new JArray(
new JObject()
{
["desc"] = Miniscript.AddChecksum(d),
["timestamp"] = "now"
["desc"] = OutputDescriptor.AddChecksum(d),
["timestamp"] = this.RPC.Network.Consensus.CoinbaseMaturity
})
}
}).ConfigureAwait(false);
@ -424,7 +417,7 @@ namespace NBXplorer.Tests
return key.ExtKey.Derive(new KeyPath(path)).Neuter().PubKey.Hash.GetAddress(Network);
}
public BitcoinAddress AddressOf(StandardDerivationStrategyBase scheme, string path)
public BitcoinAddress AddressOf(DerivationStrategyBase scheme, string path)
{
return scheme.GetDerivation(KeyPath.Parse(path)).ScriptPubKey.GetDestinationAddress(Network);
}
@ -433,14 +426,14 @@ namespace NBXplorer.Tests
{
return (DirectDerivationStrategy)CreateDerivationStrategy(pubKey, false);
}
public StandardDerivationStrategyBase CreateDerivationStrategy(ExtPubKey pubKey, bool p2sh)
public DerivationStrategyBase 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 (StandardDerivationStrategyBase)NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
return NBXplorerNetwork.DerivationStrategyFactory.Parse($"{pubKey.ToString(this.Network)}{suffix}");
}
ExtKey key;
ScriptPubKeyType scriptPubKeyType;

View File

@ -0,0 +1,74 @@
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";
}
}
}
}

View File

@ -1,9 +0,0 @@
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);
}

View File

@ -3,7 +3,6 @@ using System.Linq;
using System.Collections.Generic;
using NBitcoin;
using NBXplorer.Models;
using static NBXplorer.Backend.DbConnectionHelper;
namespace NBXplorer.Tests
{
@ -22,20 +21,18 @@ namespace NBXplorer.Tests
public TrackedTransaction Build()
{
var record = new SaveTransactionRecord(null, _TransactionId, _BlockId, null, null, false, _TimeStamp);
var tx = TrackedTransaction.Create(_Parent._TrackedSource, record);
var tx = new TrackedTransaction(new TrackedTransactionKey(_TransactionId, _BlockId, true), _Parent._TrackedSource, null as Coin[], null)
{
Inserted = _TimeStamp,
FirstSeen = _TimeStamp
};
foreach (var input in _Inputs)
{
tx.SpentOutpoints.Add(input.Coin.Outpoint, 0);
tx.SpentOutpoints.Add(input.Coin.Outpoint);
}
foreach (var output in _Outputs)
{
tx.MatchedOutputs.Add(new MatchedOutput()
{
Index = (int)output.Coin.Outpoint.N,
Value = output.Coin.Amount,
ScriptPubKey = output.Coin.ScriptPubKey
});
tx.ReceivedCoins.Add(output.Coin);
}
return tx;
}

View File

@ -1,131 +0,0 @@
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

View File

@ -1,12 +0,0 @@
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);
}

View File

@ -17,9 +17,9 @@ services:
- postgres
- pgadmin
postgres:
image: postgres:18.1
image: postgres:13
container_name: nbxplorertests_postgres_1
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"]
command: [ "-c", "random_page_cost=1.0", "-c", "shared_preload_libraries=pg_stat_statements" ]
environment:
POSTGRES_HOST_AUTH_METHOD: trust
ports:

View File

@ -1,4 +1,4 @@
#!/bin/sh
set -e
dotnet test --filter "Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null
dotnet test --filter "Azure!=Azure&Broker!=RabbitMq&Benchmark!=Benchmark&Maintenance!=Maintenance" --no-build -v n --logger "console;verbosity=normal" < /dev/null

View File

@ -1,4 +1,3 @@
{
"parallelizeTestCollections": false,
"methodDisplay": "method"
"parallelizeTestCollections": false
}

View File

@ -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 = Dockerfile
global.json = global.json
Dockerfile.linuxamd64 = Dockerfile.linuxamd64
Dockerfile.linuxarm32v7 = Dockerfile.linuxarm32v7
Dockerfile.linuxarm64v8 = Dockerfile.linuxarm64v8
.circleci\run-tests.sh = .circleci\run-tests.sh
EndProjectSection
EndProject

View File

@ -1,4 +0,0 @@
<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>

View File

@ -68,14 +68,16 @@ namespace NBXplorer
}
}
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider)
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
{
this.networks = networks;
this.repositoryProvider = repositoryProvider;
this.keyPathTemplates = keyPathTemplates;
}
Dictionary<NBXplorerNetwork, AddressPool> _AddressPoolByNetwork;
private readonly NBXplorerNetworkProvider networks;
private readonly RepositoryProvider repositoryProvider;
private readonly KeyPathTemplates keyPathTemplates;
public async Task StartAsync(CancellationToken cancellationToken)
{
@ -109,10 +111,9 @@ namespace NBXplorer
var derivationStrategy = (m.TrackedSource as Models.DerivationSchemeTrackedSource)?.DerivationStrategy;
if (derivationStrategy == null)
continue;
foreach (var feature in m.InOuts.Select(kv => kv.Feature).Distinct())
foreach (var feature in m.KnownKeyPathMapping.Select(kv => keyPathTemplates.GetDerivationFeature(kv.Value)))
{
if (feature is not null)
refill.Add(GenerateAddresses(network, derivationStrategy, feature.Value));
refill.Add(GenerateAddresses(network, derivationStrategy, feature));
}
}
return Task.WhenAll(refill.ToArray());

View File

@ -39,6 +39,13 @@ 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)
@ -75,7 +82,7 @@ namespace NBXplorer
// No way to have double spent in confirmed transactions
try
{
spentBy.Add(spent.Outpoint, annotatedTransaction.Record.TransactionHash);
spentBy.Add(spent, annotatedTransaction.Record.TransactionHash);
}
catch
{
@ -91,7 +98,7 @@ namespace NBXplorer
HashSet<uint256> toRemove = new HashSet<uint256>();
foreach (var annotatedTransaction in unconfs.Values)
{
foreach (var spent in annotatedTransaction.Record.SpentOutpoints.Select(o => o.Outpoint))
foreach (var spent in annotatedTransaction.Record.SpentOutpoints)
{
// All children of a replaced transaction should be replaced
if (replaced.TryGetValue(spent.Hash, out var parent) && parent.ReplacedBy is uint256)
@ -207,7 +214,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.Outpoint.Hash, out var parent) && parent.Height is null)
if (_TxById.TryGetValue(parentOutpoint.Hash, out var parent) && parent.Height is null)
{
parent.Replaceable = false;
}
@ -254,6 +261,17 @@ 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>();
Dictionary<uint256, AnnotatedTransaction> _TxById = new Dictionary<uint256, AnnotatedTransaction>();
public AnnotatedTransaction GetByTxId(uint256 txId)
{

View File

@ -9,7 +9,6 @@ namespace NBXplorer
{
this.youngToOld = youngToOld;
}
private static readonly AnnotatedTransactionComparer _Youngness = new AnnotatedTransactionComparer(true);
private static readonly AnnotatedTransactionComparer _Oldness = new AnnotatedTransactionComparer(false);
public static AnnotatedTransactionComparer OldToYoung
{

View File

@ -11,7 +11,7 @@ namespace NBXplorer.Authentication
{
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder)
public BasicAuthenticationHandler(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}

View File

@ -1,10 +0,0 @@
namespace NBXplorer.Backend
{
public enum BitcoinDWaiterState
{
NotStarted,
CoreSynching,
NBXplorerSynching,
Ready
}
}

View File

@ -4,60 +4,76 @@ 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 class DbConnectionFactory
{
public DbConnectionFactory(ILogger<DbConnectionFactory> logger,
IConfiguration configuration,
ExplorerConfiguration conf)
ExplorerConfiguration conf,
KeyPathTemplates keyPathTemplates)
{
Logger = logger;
ExplorerConfiguration = conf;
KeyPathTemplates = keyPathTemplates;
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 KeyPathTemplates KeyPathTemplates { get; }
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
public Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
{
return new DbConnectionHelper(network, await CreateConnection())
return CreateConnectionHelper(network, null);
}
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network, Action<NpgsqlConnectionStringBuilder> action)
{
return new DbConnectionHelper(network, await CreateConnection(action), KeyPathTemplates)
{
MinPoolSize = ExplorerConfiguration.MinGapSize,
MaxPoolSize = ExplorerConfiguration.MaxGapSize
};
}
public async Task<DbConnection> CreateConnection(CancellationToken cancellationToken = default)
public Task<DbConnection> CreateConnection()
{
return await _DS.ReliableOpenConnectionAsync(cancellationToken);
return CreateConnection(null);
}
public async Task<DbConnection> CreateConnection(Action<NpgsqlConnectionStringBuilder> action)
{
int maxRetries = 10;
int retries = maxRetries;
retry:
var conn = new 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;
}
public ValueTask DisposeAsync()
private string GetConnectionString(Action<NpgsqlConnectionStringBuilder> action)
{
return _DS.DisposeAsync();
if (action is null)
return ConnectionString;
NpgsqlConnectionStringBuilder builder = new NpgsqlConnectionStringBuilder(ConnectionString);
action(builder);
return builder.ConnectionString;
}
}
}

View File

@ -1,15 +1,12 @@
#nullable enable
using Dapper;
using NBitcoin;
using NBitcoin.RPC;
using NBXplorer.DerivationStrategy;
using Npgsql;
using Npgsql.TypeMapping;
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.Backend
@ -17,16 +14,19 @@ namespace NBXplorer.Backend
public class DbConnectionHelper : IDisposable, IAsyncDisposable
{
public DbConnectionHelper(NBXplorerNetwork network,
DbConnection connection)
DbConnection connection,
KeyPathTemplates keyPathTemplates)
{
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,28 +40,59 @@ namespace NBXplorer.Backend
return Connection.DisposeAsync();
}
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 NewOut(uint256 txId, int idx, Script script, IMoney value);
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(NpgsqlDataSourceBuilder dsBuilder)
public static void Register(INpgsqlTypeMapper typeMapper)
{
dsBuilder.MapComposite<NewOutRaw>("new_out");
dsBuilder.MapComposite<NewInRaw>("new_in");
dsBuilder.MapComposite<OutpointRaw>("outpoint");
dsBuilder.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
typeMapper.MapComposite<NewOutRaw>("new_out");
typeMapper.MapComposite<NewInRaw>("new_in");
typeMapper.MapComposite<OutpointRaw>("outpoint");
typeMapper.MapComposite<Repository.DescriptorScriptInsert>("nbxv1_ds");
}
public async Task<bool> FetchMatches(MatchQuery matchQuery, CancellationToken cancellationToken)
public Task<bool> FetchMatches(IEnumerable<Transaction> txs, SlimChainedBlock slimBlock, Money? minUtxoValue)
{
var outs = new List<NewOutRaw>(matchQuery.Outs.Count);
var ins = new List<NewInRaw>(matchQuery.Ins.Count);
foreach (var o in matchQuery.Outs)
var outCount = txs.Select(t => t.Outputs.Count).Sum();
List<NewOut> outs = new List<NewOut>(outCount);
var inCount = txs.Select(t => t.Inputs.Count).Sum();
List<NewIn> ins = new List<NewIn>(inCount);
foreach (var tx in txs)
{
if (!tx.IsCoinBase)
{
int i = 0;
foreach (var input in tx.Inputs)
{
ins.Add(new 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 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)
{
long value;
string assetId;
@ -82,41 +113,17 @@ namespace NBXplorer.Backend
}
outs.Add(new NewOutRaw(o.txId.ToString(), o.idx, o.script.ToHex(), value, assetId));
}
foreach (var ni in matchQuery.Ins)
foreach (var ni in newIns)
{
ins.Add(new NewInRaw(ni.txId.ToString(), ni.idx, ni.spentTxId.ToString(), ni.spentIdx));
}
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");
return await Connection.ExecuteScalarAsync<bool>("CALL fetch_matches(@code, @outs, @ins, 'f');", new { code = Network.CryptoCode, outs, ins });
}
public record SaveTransactionRecord(Transaction? Transaction, uint256 Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset SeenAt)
public record SaveTransactionRecord(Transaction? Transaction, uint256? Id, uint256? BlockId, int? BlockIndex, long? BlockHeight, bool Immature, DateTimeOffset? SeenAt)
{
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 static SaveTransactionRecord Create(TrackedTransaction t) => new SaveTransactionRecord(t.Transaction, t.TransactionHash, t.BlockHash, t.BlockIndex, t.BlockHeight, t.IsCoinBase, new DateTimeOffset?(t.FirstSeen));
}
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions, Dictionary<uint256, MempoolEntry> mempoolEntries)
public async Task SaveTransactions(IEnumerable<SaveTransactionRecord> transactions)
{
var parameters = transactions
.DistinctBy(o => o.Id)
@ -125,32 +132,25 @@ namespace NBXplorer.Backend
{
code = Network.CryptoCode,
blk_id = tx.BlockId?.ToString(),
id = tx.Id.ToString(),
id = tx.Id?.ToString() ?? tx.Transaction?.GetHash()?.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,
metadata = mempoolEntries.TryGetValue(tx.Id, out var meta) ? meta.ToTransactionMetadata().ToString(false) : null
immature = tx.Immature
})
.Where(o => o.id is not null)
.ToArray();
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 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 blks_txs VALUES (@code, @blk_id, @id, @blk_idx) ON CONFLICT DO NOTHING", parameters.Where(p => p.blk_id is not null).AsList());
}
public async Task MakeOrphanFrom(int height)
{
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height = height });
await Connection.ExecuteAsync("UPDATE blks SET confirmed='f' WHERE code=@code AND height >= @height;", new { code = Network.CryptoCode, height });
}
public async Task<Dictionary<OutPoint, TxOut>> GetOutputs(IEnumerable<OutPoint> outPoints)
@ -159,7 +159,7 @@ namespace NBXplorer.Backend
List<OutpointRaw> rawOutpoints = new List<OutpointRaw>(outpointCount);
foreach (var o in outPoints)
rawOutpoints.Add(new OutpointRaw(o.Hash.ToString(), o.N));
var result = new Dictionary<OutPoint, TxOut>();
Dictionary<OutPoint, TxOut> result = new Dictionary<OutPoint, TxOut>();
foreach (var r in await Connection.QueryAsync<(string tx_id, long idx, string script, long value, string asset_id)>(
"SELECT o.tx_id, o.idx, o.script, o.value, o.asset_id FROM unnest(@outpoints) outpoints " +
"JOIN outs o ON code=@code AND o.tx_id=outpoints.tx_id AND o.idx=outpoints.idx",
@ -169,7 +169,7 @@ namespace NBXplorer.Backend
outpoints = rawOutpoints
}))
{
var txout = this.Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
var txout = Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
txout.Value = Money.Satoshis(r.value);
txout.ScriptPubKey = Script.FromHex(r.script);
result.TryAdd(new OutPoint(uint256.Parse(r.tx_id), (uint)r.idx), txout);
@ -206,11 +206,6 @@ namespace NBXplorer.Backend
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)
{

Some files were not shown because too many files have changed in this diff Show More