Compare commits

..

2 Commits

Author SHA1 Message Date
nicolas.dorier
8d3f5477a6
Remove appveyor 2021-12-27 14:48:22 +09:00
nicolas.dorier
9f8f087aeb
bump nbx to 6.0 2021-12-27 14:42:58 +09:00
260 changed files with 13854 additions and 19218 deletions

View File

@ -1,28 +1,77 @@
version: 2
jobs:
test:
machine:
- image: ubuntu-2004:202201-02
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0.101-bullseye-slim
steps:
- checkout
- run:
command: |
cd .circleci && ./run-tests.sh
dotnet --info
dotnet build -c Release
dotnet test -c Release --no-build ./NBXplorer.Tests/NBXplorer.Tests.csproj --filter "Azure!=Azure&Broker!=RabbitMq" -v n --logger "console;verbosity=normal" < /dev/null
# 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
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
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 +81,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]+)*/

View File

@ -1,7 +0,0 @@
#!/bin/sh
set -e
cd ../NBXplorer.Tests
docker-compose -v
docker-compose build
docker-compose run tests

View File

@ -121,4 +121,5 @@ bower_components
output
.vs
NBXplorer.Tests/
**/launchSettings.json

17
.gitattributes vendored
View File

@ -1,17 +0,0 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Declare files that will always have CRLF line endings on checkout.
*.sh text eol=lf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

View File

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
FROM mcr.microsoft.com/dotnet/sdk:6.0.101-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,14 +8,12 @@ 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/6.0.1-bullseye-slim
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
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "NBXplorer.dll"]
ENTRYPOINT ["dotnet", "NBXplorer.dll"]

21
Dockerfile.linuxarm32v7 Normal file
View File

@ -0,0 +1,21 @@
# 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.101-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
# Note that we are using buster rather than bulleyes. Somehow, raspberry pi 4 doesn't like bulleyes.
FROM mcr.microsoft.com/dotnet/aspnet:6.0.1-bullseye-slim-arm32v7
WORKDIR /datadir
WORKDIR /app
ENV NBXPLORER_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "NBXplorer.dll"]

21
Dockerfile.linuxarm64v8 Normal file
View File

@ -0,0 +1,21 @@
# 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.101-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
# Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program
FROM mcr.microsoft.com/dotnet/aspnet:6.0.1-bullseye-slim-arm64v8
WORKDIR /datadir
WORKDIR /app
ENV NBXPLORER_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "NBXplorer.dll"]

View File

@ -2,11 +2,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBXplorer.Client" Version="4.2.0" />
<PackageReference Include="NBXplorer.Client" Version="3.0.20" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NBXplorer
{

View File

@ -1,18 +0,0 @@
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using System.Text;
namespace NBXplorer.Client
{
public static class DBUtils
{
public static string nbxv1_get_wallet_id(string cryptoCode, string addressOrDerivation)
{
return Encoders.Base64.EncodeData(Hashes.SHA256(new UTF8Encoding(false).GetBytes($"{cryptoCode}|{addressOrDerivation}")), 0, 21);
}
public static string nbxv1_get_descriptor_id(string cryptoCode, string strategy, string feature)
{
return Encoders.Base64.EncodeData(Hashes.SHA256(new UTF8Encoding(false).GetBytes($"{cryptoCode}|{strategy}|{feature}")), 0, 21);
}
}
}

View File

@ -1,49 +1,23 @@
#nullable enable
using NBitcoin;
using NBitcoin;
using System;
using System.Collections.Generic;
#if !NO_RECORD
using static NBitcoin.WalletPolicies.MiniscriptNode;
#endif
using System.Text;
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,11 @@
using NBitcoin;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace NBXplorer.DerivationStrategy
@ -23,7 +22,7 @@ namespace NBXplorer.DerivationStrategy
get; set;
}
public ReadOnlyDictionary<string, string> AdditionalOptions { get; set; }
public ReadOnlyDictionary<string, bool> AdditionalOptions { get; set; }
}
public class DerivationStrategyFactory
{
@ -56,6 +55,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);
@ -70,20 +70,15 @@ namespace NBXplorer.DerivationStrategy
bool taproot = false;
ScriptPubKeyType type = ScriptPubKeyType.Segwit;
IDictionary<string, string> optionsDictionary = new Dictionary<string, string>(5);
Dictionary<string, bool> optionsDictionary = new Dictionary<string, bool>(5);
foreach (Match optionMatch in _OptionRegex.Matches(str))
{
var rawKey = optionMatch.Groups[1].Value.ToLowerInvariant();
var splitKey = rawKey.Split(new[]{'='}, StringSplitOptions.RemoveEmptyEntries);
var key = splitKey[0];
var value = splitKey.Length > 1 ? splitKey[1]: null;
var key = optionMatch.Groups[1].Value.ToLowerInvariant();
if (!AuthorizedOptions.Contains(key))
throw new FormatException($"The option '{key}' is not supported by this network");
if (!Extensions.TryAdd(optionsDictionary, key, value))
if (!optionsDictionary.TryAdd(key, true))
throw new FormatException($"The option '{key}' is duplicated");
}
var hasOptions = optionsDictionary.Count != 0;
str = _OptionRegex.Replace(str, string.Empty);
if (optionsDictionary.Remove("legacy"))
{
@ -133,7 +128,7 @@ namespace NBXplorer.DerivationStrategy
{
KeepOrder = keepOrder,
ScriptPubKeyType = type,
AdditionalOptions = new ReadOnlyDictionary<string, string>(optionsDictionary)
AdditionalOptions = new ReadOnlyDictionary<string, bool>(optionsDictionary)
};
var match = MultiSigRegex.Match(str);
if (match.Success)
@ -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
@ -210,7 +197,7 @@ namespace NBXplorer.DerivationStrategy
/// <param name="publicKey">The public key of the wallet</param>
/// <param name="options">Derivation options</param>
/// <returns></returns>
public TaprootDerivationStrategy CreateTaprootDerivationStrategy(BitcoinExtPubKey publicKey, ReadOnlyDictionary<string, string> options = null)
public TaprootDerivationStrategy CreateTaprootDerivationStrategy(BitcoinExtPubKey publicKey, ReadOnlyDictionary<string, bool> options = null)
{
return new TaprootDerivationStrategy(publicKey, options);
}
@ -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

@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using NBitcoin;
using NBitcoin.Crypto;
namespace NBXplorer.DerivationStrategy
{
public class DirectDerivationStrategy : StandardDerivationStrategyBase
public class DirectDerivationStrategy : DerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -37,18 +38,22 @@ namespace NBXplorer.DerivationStrategy
}
}
public DirectDerivationStrategy(BitcoinExtPubKey root, bool segwit, ReadOnlyDictionary<string, string> additionalOptions = null) : base(additionalOptions)
public DirectDerivationStrategy(BitcoinExtPubKey root, bool segwit, ReadOnlyDictionary<string, bool> additionalOptions = null) : base(additionalOptions)
{
if(root == null)
throw new ArgumentNullException(nameof(root));
_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,12 +1,10 @@
#nullable enable
using NBitcoin;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using NBitcoin;
using NBitcoin.Crypto;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
namespace NBXplorer.DerivationStrategy
{
@ -17,42 +15,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));
public ReadOnlyDictionary<string, string> AdditionalOptions { get; }
ReadOnlyDictionary<string, bool> Empty = new ReadOnlyDictionary<string, bool>(new Dictionary<string, bool>(0));
public ReadOnlyDictionary<string, bool> AdditionalOptions { get; }
internal DerivationStrategyBase(ReadOnlyDictionary<string,string>? additionalOptions)
internal DerivationStrategyBase(ReadOnlyDictionary<string,bool> 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
@ -70,76 +66,98 @@ namespace NBXplorer.DerivationStrategy
private string GetSuffixOptionsString()
{
return string.Join("", new SortedDictionary<string, string>(AdditionalOptions).Select(pair => $"-[{pair.Key}{(string.IsNullOrEmpty(pair.Value)?string.Empty: $"={pair.Value}")}]"));
return string.Join("", new SortedDictionary<string, bool>(AdditionalOptions).Where(pair => pair.Value).Select(pair => $"-[{pair.Key}]"));
}
public override bool Equals(object obj)
{
DerivationStrategyBase item = obj as DerivationStrategyBase;
if(item == null)
return false;
return StringValue.Equals(item.StringValue);
}
public static bool operator ==(DerivationStrategyBase a, DerivationStrategyBase b)
{
if(System.Object.ReferenceEquals(a, b))
return true;
if(((object)a == null) || ((object)b == null))
return false;
return a.StringValue == b.StringValue;
}
public static bool operator !=(DerivationStrategyBase a, DerivationStrategyBase b)
{
return !(a == b);
}
#nullable enable
public override bool Equals(object? obj) => obj is DerivationStrategyBase o && StringValue.Equals(o.StringValue);
public static bool operator ==(DerivationStrategyBase? a, DerivationStrategyBase? b) => a is null ? b is null : a.Equals(b);
public static bool operator !=(DerivationStrategyBase? a, DerivationStrategyBase? b) => !(a == b);
public override int GetHashCode() => StringValue.GetHashCode();
#nullable restore
public abstract IEnumerable<ExtPubKey> GetExtPubKeys();
public override int GetHashCode()
{
return StringValue.GetHashCode();
}
public override string ToString()
{
return StringValue;
}
}
#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

@ -1,15 +1,17 @@
using NBitcoin;
using System.Linq;
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using NBitcoin.Crypto;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
namespace NBXplorer.DerivationStrategy
{
public class MultisigDerivationStrategy : StandardDerivationStrategyBase
public class MultisigDerivationStrategy : DerivationStrategyBase
{
public bool LexicographicOrder
{
@ -49,7 +51,7 @@ namespace NBXplorer.DerivationStrategy
}
internal MultisigDerivationStrategy(int reqSignature, BitcoinExtPubKey[] keys, bool isLegacy, bool lexicographicOrder,
ReadOnlyDictionary<string, string> additionalOptions) : base(additionalOptions)
ReadOnlyDictionary<string, bool> additionalOptions) : base(additionalOptions)
{
Keys = new ReadOnlyCollection<BitcoinExtPubKey>(keys);
RequiredSignatures = reqSignature;
@ -62,19 +64,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

@ -1,13 +1,17 @@
using System;
using NBXplorer.DerivationStrategy;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using NBitcoin.Crypto;
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 +19,7 @@ namespace NBXplorer.DerivationStrategy
this.addSuffix = addSuffix;
}
public StandardDerivationStrategyBase Inner
public DerivationStrategyBase Inner
{
get; set;
}
@ -30,18 +34,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

@ -1,34 +1,47 @@
using System;
using NBXplorer.DerivationStrategy;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using NBitcoin.Crypto;
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

@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using NBitcoin;
using NBitcoin.Crypto;
namespace NBXplorer.DerivationStrategy
{
public class TaprootDerivationStrategy : StandardDerivationStrategyBase
public class TaprootDerivationStrategy : DerivationStrategyBase
{
BitcoinExtPubKey _Root;
@ -29,22 +30,27 @@ namespace NBXplorer.DerivationStrategy
}
}
public TaprootDerivationStrategy(BitcoinExtPubKey root, ReadOnlyDictionary<string, string> additionalOptions = null) : base(additionalOptions)
public TaprootDerivationStrategy(BitcoinExtPubKey root, ReadOnlyDictionary<string, bool> additionalOptions = null) : base(additionalOptions)
{
if (root == null)
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

@ -1,5 +1,7 @@
using NBitcoin;
using System.Linq;
using NBitcoin.DataEncoders;
using NBitcoin.JsonConverters;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
@ -13,8 +15,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
using NBitcoin.RPC;
using System.Runtime.CompilerServices;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace NBXplorer
{
@ -152,7 +153,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 +165,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 +173,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 +187,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 +196,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 +220,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 +269,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 +280,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 +319,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 +329,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 +349,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 +407,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 +440,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 +455,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 +483,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 +492,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 +504,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 +515,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 +533,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 +546,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 +556,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 +571,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 +614,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 +630,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 +650,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 +712,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

@ -1,8 +1,11 @@
using NBitcoin;
using NBitcoin.RPC;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -16,11 +19,6 @@ namespace NBXplorer
throw new ArgumentOutOfRangeException(nameof(size));
if (values == null)
throw new ArgumentNullException(nameof(values));
if (values is IList<T> l && l.Count <= size)
{
yield return l;
yield break;
}
var batch = new List<T>();
foreach(var v in values)
{
@ -37,6 +35,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,8 +1,11 @@
using System.Linq;
using NBitcoin;
using System.Linq;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
namespace NBXplorer.JsonConverters

View File

@ -1,7 +1,10 @@
using System.Reflection;
using NBitcoin;
using System.Reflection;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.JsonConverters;
namespace NBXplorer.JsonConverters

View File

@ -1,25 +0,0 @@
using Newtonsoft.Json;
using System;
namespace NBXplorer.JsonConverters
{
public class KeyPathTemplateJsonConverter : JsonConverter<KeyPathTemplate>
{
public override KeyPathTemplate ReadJson(JsonReader reader, Type objectType, KeyPathTemplate existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new NBitcoin.JsonConverters.JsonObjectException($"Unexpected json token type, expected String, actual {reader.TokenType}", reader);
if (!KeyPathTemplate.TryParse((string)reader.Value, out var template))
throw new NBitcoin.JsonConverters.JsonObjectException($"Invalid KeyPathTemplate", reader);
return template;
}
public override void WriteJson(JsonWriter writer, KeyPathTemplate value, JsonSerializer serializer)
{
if (value is KeyPathTemplate kt)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -2,6 +2,8 @@
using System.Linq;
using System.Reflection;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using NBitcoin.JsonConverters;

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

@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.JsonConverters
{

View File

@ -1,5 +1,8 @@
using Newtonsoft.Json;
using NBitcoin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using NBXplorer.Models;
using NBitcoin.JsonConverters;

View File

@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.JsonConverters
{

View File

@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.JsonConverters
{

View File

@ -1,11 +1,13 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using Newtonsoft.Json.Linq;
using NBXplorer.Models;
using System.Collections.Concurrent;
namespace NBXplorer
{
@ -75,7 +77,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 +96,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,4 +1,7 @@
using NBitcoin.RPC;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.RPC;
namespace NBXplorer.Models
{

View File

@ -1,15 +1,12 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
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>
@ -38,10 +35,6 @@ namespace NBXplorer.Models
/// </summary>
public bool? RBF { get; set; }
/// <summary>
/// Whether this transaction should merge the outputs.
/// </summary>
public bool? MergeOutputs { get; set; }
/// <summary>
/// The destinations where to send the money
/// </summary>
public List<CreatePSBTDestination> Destinations { get; set; } = new List<CreatePSBTDestination>();
@ -68,15 +61,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 +99,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 +118,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,8 @@
using NBitcoin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,6 +1,9 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using NBXplorer.JsonConverters;
using NBitcoin;
namespace NBXplorer.Models
{
@ -15,9 +18,7 @@ namespace NBXplorer.Models
[JsonConverter(typeof(NBXplorer.JsonConverters.ScriptPubKeyTypeConverter))]
public NBitcoin.ScriptPubKeyType? ScriptPubKeyType { get; set; }
public string Passphrase { get; set; }
[Obsolete("We will remove this feature in a future release.")]
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
public Dictionary<string, string> AdditionalOptions { get; set; }
}
}

View File

@ -1,12 +1,15 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using NBXplorer.JsonConverters;
namespace NBXplorer.Models
{
public class GenerateWalletResponse
{
public string TrackedSource { get; set; }
public string Mnemonic { get; set; }
public string Passphrase { get; set; }
[JsonConverter(typeof(NBXplorer.JsonConverters.WordlistJsonConverter))]
@ -17,8 +20,7 @@ namespace NBXplorer.Models
public BitcoinExtKey AccountHDKey { get; set; }
[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

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -2,6 +2,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
@ -63,11 +64,11 @@ namespace NBXplorer.Models
{
get; set;
}
public long Confirmations
public int Confirmations
{
get; set;
}
public long? Height
public int? Height
{
get; set;
}
@ -89,10 +90,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 +107,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,29 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
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 +51,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()
{
return (int)KeyPath.Indexes[KeyPath.Indexes.Length - 1];
}
}
}

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NBXplorer
{
@ -71,6 +72,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)

View File

@ -2,6 +2,8 @@
using NBXplorer.DerivationStrategy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NBXplorer
{
@ -13,7 +15,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 +31,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

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
@ -26,13 +28,6 @@ namespace NBXplorer.Models
Code = code;
Message = message;
}
public NBXplorerError(int httpCode, string code, string message, string reason)
{
HttpCode = httpCode;
Code = code;
Message = message;
Reason = reason;
}
public int HttpCode
{
get; set;
@ -45,10 +40,6 @@ namespace NBXplorer.Models
{
get; set;
}
public string Reason
{
get; set;
}
public NBXplorerException AsException()
{

View File

@ -22,8 +22,6 @@ namespace NBXplorer.Models
get; set;
}
public long Confirmations { get; set; }
[JsonIgnore]
public override string EventType => "newblock";

View File

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
@ -28,10 +29,6 @@ namespace NBXplorer.Models
get; set;
}
public List<MatchedInput> Inputs
{
get; set;
} = new List<MatchedInput>();
public List<MatchedOutput> Outputs
{
get; set;
@ -40,8 +37,6 @@ namespace NBXplorer.Models
[JsonIgnore]
public override string EventType => "newtransaction";
public List<uint256> Replacing { get; set; }
public override string ToString()
{
var conf = (BlockId == null ? "unconfirmed" : "confirmed");
@ -66,19 +61,6 @@ 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

@ -1,4 +1,8 @@
namespace NBXplorer.Models
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
public class PruneRequest
{

View File

@ -1,4 +1,8 @@
namespace NBXplorer.Models
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
public class PruneResponse
{

View File

@ -1,5 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,7 +1,9 @@
using NBXplorer.DerivationStrategy;
using System.Linq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,5 +1,8 @@
using NBitcoin;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
@ -48,6 +51,11 @@ namespace NBXplorer.Models
{
get; set;
}
public double RepositoryPingTime
{
get;
set;
}
public bool IsFullySynched
{
get; set;

View File

@ -1,5 +1,8 @@
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,9 +1,10 @@
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Linq;
using System.Threading.Tasks;
namespace NBXplorer.Models
{
@ -13,30 +14,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;
@ -44,6 +37,7 @@ namespace NBXplorer.Models
return true;
}
public override bool Equals(object obj)
{
TrackedSource item = obj as TrackedSource;
@ -99,58 +93,6 @@ namespace NBXplorer.Models
{
return ToString();
}
public static TrackedSource Parse(string str, NBXplorerNetwork network)
{
if (!TryParse(str, out var trackedSource, network))
throw new FormatException("Invalid TrackedSource");
return trackedSource;
}
}
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
@ -175,6 +117,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 +156,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 +186,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

@ -1,12 +1,14 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{
public class TransactionResult
{
long _Confirmations;
public long Confirmations
int _Confirmations;
public int Confirmations
{
get
{
@ -44,7 +46,7 @@ namespace NBXplorer.Models
}
}
public long? Height
public int? Height
{
get;
set;
@ -54,8 +56,5 @@ namespace NBXplorer.Models
get;
set;
}
public uint256 ReplacedBy { get; set; }
public TransactionMetadata Metadata { get; set; }
}
}

View File

@ -1,12 +1,13 @@
using NBitcoin;
using System.Linq;
using NBitcoin.Protocol;
using System;
using System.Collections.Generic;
using System.Text;
using NBitcoin.Crypto;
using System.IO;
using Newtonsoft.Json;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
namespace NBXplorer.Models
{
@ -45,11 +46,6 @@ namespace NBXplorer.Models
}
}
public List<UTXO> SpentUnconfirmed
{
get;
set;
} = new List<UTXO>();
UTXOChange _Confirmed = new UTXOChange();
public UTXOChange Confirmed
@ -93,7 +89,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,30 +155,13 @@ 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)
{
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);
}
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);
}
return coin;
}
@ -218,10 +197,7 @@ namespace NBXplorer.Models
}
}
public BitcoinAddress Address { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public Script Redeem { get; set; }
IMoney _Value;
public IMoney Value
{
@ -265,19 +241,17 @@ namespace NBXplorer.Models
}
long _Confirmations;
public long Confirmations
uint _Confirmations;
public int Confirmations
{
get
{
return checked((long)_Confirmations);
return checked((int)_Confirmations);
}
set
{
_Confirmations = checked((long)value);
_Confirmations = checked((uint)value);
}
}
public int KeyIndex { get; set; }
}
}

View File

@ -1,4 +1,8 @@
using Newtonsoft.Json.Linq;
using NBXplorer.Models;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,7 +1,9 @@
using NBitcoin;
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer.Models
{

View File

@ -1,43 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<Company>Digital Garage</Company>
<Version>5.0.6</Version>
<Version>4.1.3</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="6.0.18" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.8" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<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

@ -1,7 +1,9 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
namespace NBXplorer
{
@ -57,7 +59,7 @@ namespace NBXplorer
if (_Settings.TryGetValue(networkType, out v))
return v;
var settings = new NBXplorerDefaultSettings();
settings.DefaultDataDirectory = GetDirectory("NBXplorer", GetFolderName(networkType), false);
settings.DefaultDataDirectory = StandardConfiguration.DefaultDataDirectory.GetDirectory("NBXplorer", GetFolderName(networkType), false);
settings.DefaultConfigurationFile = Path.Combine(settings.DefaultDataDirectory, "settings.config");
settings.DefaultCookieFile = Path.Combine(settings.DefaultDataDirectory, ".cookie");
settings.DefaultPort = (networkType == ChainName.Mainnet ? 24444 :
@ -68,48 +70,5 @@ namespace NBXplorer
return settings;
}
}
static string GetDirectory(string appDirectory, string subDirectory, bool createIfNotExists = true)
{
string directory = null;
var home = Environment.GetEnvironmentVariable("HOME");
var localAppData = Environment.GetEnvironmentVariable("APPDATA");
if (!string.IsNullOrEmpty(home) && string.IsNullOrEmpty(localAppData))
{
directory = home;
directory = Path.Combine(directory, "." + appDirectory.ToLowerInvariant());
}
else
{
if (!string.IsNullOrEmpty(localAppData))
{
directory = localAppData;
directory = Path.Combine(directory, appDirectory);
}
else if (createIfNotExists)
{
throw new DirectoryNotFoundException("Could not find suitable datadir environment variables HOME or APPDATA are not set");
}
else
return string.Empty;
}
if (createIfNotExists)
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
directory = Path.Combine(directory, subDirectory);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
else
{
directory = Path.Combine(directory, subDirectory);
}
return directory;
}
}
}

View File

@ -2,6 +2,10 @@
using NBXplorer.DerivationStrategy;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace NBXplorer
{
@ -13,10 +17,6 @@ namespace NBXplorer
CryptoCode = networkSet.CryptoCode;
DefaultSettings = NBXplorerDefaultSettings.GetDefaultSettings(networkType);
}
public static uint256 UnknownTxId = uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
public static string UnknownAssetId = uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").ToString();
public static AssetMoney UnknownAssetMoney = new AssetMoney(uint256.Parse("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), 1);
public bool IsElement => NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance;
public Network NBitcoinNetwork
{
get;
@ -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

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,8 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,5 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{
@ -12,6 +14,7 @@ namespace NBXplorer
MinRPCVersion = 140200,
ChainLoadingTimeout = TimeSpan.FromHours(1),
ChainCacheLoadingTimeout = TimeSpan.FromMinutes(2),
SupportCookieAuthentication = false,
CoinType = NetworkType == ChainName.Mainnet ? new KeyPath("3'") : new KeyPath("1'")
});
}

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,12 +1,10 @@
using NBitcoin;
using System;
using System.Threading;
using System.Threading.Tasks;
using NBitcoin.Altcoins.Elements;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using NBXplorer.DerivationStrategy;
#if !NO_RECORD
using NBitcoin.WalletPolicies;
#endif
using NBXplorer.Models;
namespace NBXplorer
{
@ -22,77 +20,28 @@ namespace NBXplorer
{
var factory = base.CreateStrategyFactory();
factory.AuthorizedOptions.Add("unblinded");
factory.AuthorizedOptions.Add("slip77");
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).PubKey;
return new BitcoinBlindedAddress(blindingPubKey, base.CreateAddress(derivationStrategy, keyPath, scriptPubKey));
}
public static Key GenerateSlip77BlindingKeyFromMnemonic(Mnemonic mnemonic, Script script)
{
var seed = mnemonic.DeriveSeed();
var slip21 = Slip21Node.FromSeed(seed);
var slip77 = slip21.GetSlip77Node();
return slip77.DeriveSlip77BlindingKey(script);
}
public static Key GenerateSlip77BlindingKeyFromMasterBlindingKey(Key masterBlindingKey, Script script)
{
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)
{
if (derivationStrategy.Unblinded())
{
throw new InvalidOperationException("This derivation scheme is set to only track unblinded addresses");
}
if (derivationStrategy.Slip77(out var key))
{
if (HexEncoder.IsWellFormed(key))
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(new Key(Encoders.Hex.DecodeData(key)), scriptPubKey);
}
try
{
return GenerateSlip77BlindingKeyFromMasterBlindingKey(Key.Parse(key, network), scriptPubKey);
}
catch (Exception)
{
// ignored
}
try
{
var data = new Mnemonic(key);
return GenerateSlip77BlindingKeyFromMnemonic(data, scriptPubKey);
}
catch (Exception)
{
// ignored
}
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)
@ -114,11 +63,7 @@ namespace NBXplorer
{
public static bool Unblinded(this DerivationStrategyBase derivationStrategyBase)
{
return derivationStrategyBase.AdditionalOptions.TryGetValue("unblinded", out _);
}
public static bool Slip77(this DerivationStrategyBase derivationStrategyBase ,out string key)
{
return derivationStrategyBase.AdditionalOptions.TryGetValue("slip77", out key);
return derivationStrategyBase.AdditionalOptions.TryGetValue("unblinded", out var unblinded) is true && unblinded;
}
}
}

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

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

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

View File

@ -1,4 +1,7 @@
using NBitcoin;
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{

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,4 +1,7 @@
using NBXplorer.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -13,4 +16,3 @@ namespace NBXplorer
public abstract Task<NewEventBase> NextEventAsync(CancellationToken cancellation = default);
}
}

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,8 @@ using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using NBXplorer.JsonConverters;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{
@ -24,13 +25,10 @@ namespace NBXplorer
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
Settings.DateParseHandling = DateParseHandling.None;
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(settings, Network);
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

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace NBXplorer
{

View File

@ -1,22 +1,79 @@
using NBitcoin;
using System.Linq;
using NBXplorer.DerivationStrategy;
using NBXplorer.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
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 +130,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 +143,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

@ -1,4 +1,8 @@
namespace NBXplorer
using System;
using System.Collections.Generic;
using System.Text;
namespace NBXplorer
{
public class WellknownMetadataKeys
{
@ -6,8 +10,6 @@
public const string Mnemonic = nameof(Mnemonic);
public const string MasterHDKey = nameof(MasterHDKey);
public const string AccountHDKey = nameof(AccountHDKey);
public const string AccountDescriptor = nameof(AccountDescriptor);
public const string Birthdate = nameof(Birthdate);
public const string AccountKeyPath = nameof(AccountKeyPath);
}
}

View File

@ -3,13 +3,18 @@ using System.Linq;
using System.IO;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using System.Runtime.InteropServices;
using System.Net.Http;
using Xunit.Abstractions;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using Microsoft.IdentityModel.Tokens;
using NBitcoin.Crypto;
using Microsoft.Extensions.DependencyInjection;
using NBXplorer.Analytics;
using NBXplorer.DerivationStrategy;
using NBitcoin.Altcoins;
namespace NBXplorer.Tests
{
@ -130,7 +135,7 @@ namespace NBXplorer.Tests
catch { }
}
using var client = new HttpClient();
var resp = await client.GetAsync($"https://mempool.space/api/block/{blockId}/raw");
var resp = await client.GetAsync($"https://api.qbit.ninja/blocks/{blockId}?format=raw");
resp.EnsureSuccessStatusCode();
var bytes = await resp.Content.ReadAsByteArrayAsync();
var block = Block.Load(bytes, Network.Main);

View File

@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Hosting;
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
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();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
{
"Servers": {
"1": {
"Name": "docker_postgres",
"Group": "Servers",
"Host": "postgres",
"Port": 5432,
"MaintenanceDB": "postgres",
"Username": "postgres",
"SSLMode": "disable"
}
}
}

View File

@ -1,6 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0.301-noble AS builder
WORKDIR /source
COPY . .
RUN cd NBXplorer.Tests && dotnet build
WORKDIR /source/NBXplorer.Tests
ENTRYPOINT ["./tests-entrypoint.sh"]

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