Add Liquid Support (#198)
This commit is contained in:
parent
5ee8b32a56
commit
649e82a9f7
406
NBXplorer.Client/AssetMoney.cs
Normal file
406
NBXplorer.Client/AssetMoney.cs
Normal file
@ -0,0 +1,406 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class AssetMoney : IComparable, IComparable<AssetMoney>, IEquatable<AssetMoney>, IMoney
|
||||
{
|
||||
long _Quantity;
|
||||
public long Quantity
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Quantity;
|
||||
}
|
||||
// used as a central point where long.MinValue checking can be enforced
|
||||
private set
|
||||
{
|
||||
CheckLongMinValue(value);
|
||||
_Quantity = value;
|
||||
}
|
||||
}
|
||||
private static void CheckLongMinValue(long value)
|
||||
{
|
||||
if (value == long.MinValue)
|
||||
throw new OverflowException("satoshis amount should be greater than long.MinValue");
|
||||
}
|
||||
|
||||
private readonly uint256 _Id;
|
||||
|
||||
/// <summary>
|
||||
/// AssetId of the current amount
|
||||
/// </summary>
|
||||
public uint256 AssetId
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get absolute value of the instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public AssetMoney Abs()
|
||||
{
|
||||
var a = this;
|
||||
if (a.Quantity < 0)
|
||||
a = -a;
|
||||
return a;
|
||||
}
|
||||
#region ctor
|
||||
|
||||
public AssetMoney(uint256 assetId)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
}
|
||||
public AssetMoney(uint256 assetId, int quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, uint quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
public AssetMoney(uint256 assetId, long quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
Quantity = quantity;
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, ulong quantity)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
|
||||
// overflow check.
|
||||
// ulong.MaxValue is greater than long.MaxValue
|
||||
checked
|
||||
{
|
||||
Quantity = (long)quantity;
|
||||
}
|
||||
}
|
||||
|
||||
public AssetMoney(uint256 assetId, decimal amount, int divisibility)
|
||||
{
|
||||
if (assetId == null)
|
||||
throw new ArgumentNullException(nameof(assetId));
|
||||
_Id = assetId;
|
||||
// sanity check. Only valid units are allowed
|
||||
checked
|
||||
{
|
||||
int dec = Pow10(divisibility);
|
||||
var satoshi = amount * dec;
|
||||
Quantity = (long)satoshi;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static int Pow10(int divisibility)
|
||||
{
|
||||
if (divisibility < 0)
|
||||
throw new ArgumentOutOfRangeException("divisibility", "divisibility should be higher than 0");
|
||||
int dec = 1;
|
||||
for (int i = 0; i < divisibility; i++)
|
||||
{
|
||||
dec = dec * 10;
|
||||
}
|
||||
return dec;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Split the Money in parts without loss
|
||||
/// </summary>
|
||||
/// <param name="parts">The number of parts (must be more than 0)</param>
|
||||
/// <returns>The splitted money</returns>
|
||||
public IEnumerable<AssetMoney> Split(int parts)
|
||||
{
|
||||
if (parts <= 0)
|
||||
throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts");
|
||||
long remain;
|
||||
long result = DivRem(_Quantity, parts, out remain);
|
||||
|
||||
for (int i = 0; i < parts; i++)
|
||||
{
|
||||
yield return new AssetMoney(_Id, result + (remain > 0 ? 1 : 0));
|
||||
remain--;
|
||||
}
|
||||
}
|
||||
|
||||
private static long DivRem(long a, long b, out long result)
|
||||
{
|
||||
result = a % b;
|
||||
return a / b;
|
||||
}
|
||||
|
||||
public decimal ToDecimal(int divisibility)
|
||||
{
|
||||
var dec = Pow10(divisibility);
|
||||
// overflow safe because (long / int) always fit in decimal
|
||||
// decimal operations are checked by default
|
||||
return (decimal)Quantity / (int)dec;
|
||||
}
|
||||
|
||||
#region IEquatable<AssetMoney> Members
|
||||
|
||||
public bool Equals(AssetMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return false;
|
||||
CheckAssetId(other, "other");
|
||||
return _Quantity.Equals(other.Quantity);
|
||||
}
|
||||
|
||||
internal void CheckAssetId(AssetMoney other, string param)
|
||||
{
|
||||
if (other.AssetId != AssetId)
|
||||
throw new ArgumentException("AssetMoney instance of different assets can't be computed together", param);
|
||||
}
|
||||
|
||||
public int CompareTo(AssetMoney other)
|
||||
{
|
||||
if (other == null)
|
||||
return 1;
|
||||
CheckAssetId(other, "other");
|
||||
return _Quantity.CompareTo(other.Quantity);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IComparable Members
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
return 1;
|
||||
AssetMoney m = obj as AssetMoney;
|
||||
if (m != null)
|
||||
return _Quantity.CompareTo(m.Quantity);
|
||||
#if !NETSTANDARD1X
|
||||
return _Quantity.CompareTo(obj);
|
||||
#else
|
||||
return _Quantity.CompareTo((long)obj);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static AssetMoney operator -(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return new AssetMoney(left.AssetId, checked(left.Quantity - right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator -(AssetMoney left)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
return new AssetMoney(left.AssetId, checked(-left.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator +(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return new AssetMoney(left.AssetId, checked(left.Quantity + right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(int left, AssetMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(AssetMoney right, int left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(right.Quantity * left));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(long left, AssetMoney right)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static AssetMoney operator *(AssetMoney right, long left)
|
||||
{
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
return new AssetMoney(right.AssetId, checked(left * right.Quantity));
|
||||
}
|
||||
|
||||
public static bool operator <(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity < right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator >(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity > right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator <=(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity <= right.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator >=(AssetMoney left, AssetMoney right)
|
||||
{
|
||||
if (left == null)
|
||||
throw new ArgumentNullException(nameof(left));
|
||||
if (right == null)
|
||||
throw new ArgumentNullException(nameof(right));
|
||||
left.CheckAssetId(right, "right");
|
||||
return left.Quantity >= right.Quantity;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
AssetMoney item = obj as AssetMoney;
|
||||
if (item == null)
|
||||
return false;
|
||||
if (item.AssetId != AssetId)
|
||||
return false;
|
||||
return _Quantity.Equals(item.Quantity);
|
||||
}
|
||||
|
||||
public static bool operator ==(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
if (Object.ReferenceEquals(a, b))
|
||||
return true;
|
||||
if (((object)a == null) || ((object)b == null))
|
||||
return false;
|
||||
|
||||
if (a.AssetId != b.AssetId)
|
||||
return false;
|
||||
return a.Quantity == b.Quantity;
|
||||
}
|
||||
|
||||
public static bool operator !=(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Tuple.Create(_Quantity, AssetId).GetHashCode();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Format("{0}-{1}", Quantity, AssetId);
|
||||
}
|
||||
|
||||
public static AssetMoney Min(AssetMoney a, AssetMoney b)
|
||||
{
|
||||
if (a == null)
|
||||
throw new ArgumentNullException(nameof(a));
|
||||
if (b == null)
|
||||
throw new ArgumentNullException(nameof(b));
|
||||
a.CheckAssetId(b, "b");
|
||||
if (a <= b)
|
||||
return a;
|
||||
return b;
|
||||
}
|
||||
|
||||
#region IMoney Members
|
||||
|
||||
|
||||
IMoney IMoney.Add(IMoney money)
|
||||
{
|
||||
var assetMoney = (AssetMoney)money;
|
||||
return this + assetMoney;
|
||||
}
|
||||
|
||||
IMoney IMoney.Sub(IMoney money)
|
||||
{
|
||||
var assetMoney = (AssetMoney)money;
|
||||
return this - assetMoney;
|
||||
}
|
||||
|
||||
IMoney IMoney.Negate()
|
||||
{
|
||||
return this * -1;
|
||||
}
|
||||
|
||||
int IComparable.CompareTo(object obj)
|
||||
{
|
||||
return this.CompareTo(obj);
|
||||
}
|
||||
|
||||
int IComparable<IMoney>.CompareTo(IMoney other)
|
||||
{
|
||||
return this.CompareTo(other);
|
||||
}
|
||||
|
||||
bool IEquatable<IMoney>.Equals(IMoney other)
|
||||
{
|
||||
return this.Equals(other);
|
||||
}
|
||||
|
||||
bool IMoney.IsCompatible(IMoney money)
|
||||
{
|
||||
if (money == null)
|
||||
throw new ArgumentNullException(nameof(money));
|
||||
AssetMoney assetMoney = money as AssetMoney;
|
||||
if (assetMoney == null)
|
||||
return false;
|
||||
return assetMoney.AssetId == AssetId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IMoney Members
|
||||
|
||||
|
||||
IEnumerable<IMoney> IMoney.Split(int parts)
|
||||
{
|
||||
return Split(parts);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
78
NBXplorer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
78
NBXplorer.Client/JsonConverters/MoneyJsonConverter.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NBitcoin;
|
||||
using NBitcoin.JsonConverters;
|
||||
|
||||
namespace NBXplorer.JsonConverters
|
||||
{
|
||||
public class MoneyJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(IMoney).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
|
||||
}
|
||||
|
||||
class AssetCoinJson
|
||||
{
|
||||
public uint256 AssetId { get; set; }
|
||||
public long? Value { get; set; }
|
||||
public AssetMoney ToAssetMoney(string path)
|
||||
{
|
||||
if (AssetId == null)
|
||||
throw new JsonObjectException("'assetId' is missing", path);
|
||||
if (Value is null)
|
||||
throw new JsonObjectException("'value' is missing", path);
|
||||
return new AssetMoney(AssetId, Value.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (reader.TokenType == JsonToken.Null)
|
||||
return null;
|
||||
AssertJsonType(reader, new[] { JsonToken.Integer, JsonToken.StartObject, JsonToken.StartArray });
|
||||
if (reader.TokenType == JsonToken.Integer)
|
||||
{
|
||||
return new Money((long)reader.Value);
|
||||
}
|
||||
else if (reader.TokenType == JsonToken.StartObject)
|
||||
{
|
||||
return serializer.Deserialize<AssetCoinJson>(reader).ToAssetMoney(reader.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new MoneyBag(serializer.Deserialize<AssetCoinJson[]>(reader).Select(c => c.ToAssetMoney(reader.Path)).ToArray());
|
||||
}
|
||||
}
|
||||
catch (InvalidCastException)
|
||||
{
|
||||
throw new JsonObjectException("Money amount should be in satoshi", reader);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is Money v)
|
||||
writer.WriteValue(v.Satoshi);
|
||||
else if (value is AssetMoney av)
|
||||
{
|
||||
serializer.Serialize(writer, new AssetCoinJson() { Value = av.Quantity, AssetId = av.AssetId });
|
||||
}
|
||||
else if (value is MoneyBag mb)
|
||||
{
|
||||
serializer.Serialize(writer, mb.OfType<AssetMoney>().Select(av2 => new AssetCoinJson() { Value = av2.Quantity, AssetId = av2.AssetId }).ToArray());
|
||||
}
|
||||
}
|
||||
static void AssertJsonType(JsonReader reader, JsonToken[] anyExpectedTypes)
|
||||
{
|
||||
if (!anyExpectedTypes.Contains(reader.TokenType))
|
||||
throw new JsonObjectException($"Unexpected json token type, expected are {string.Join(", ", anyExpectedTypes)} and actual is {reader.TokenType}", reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
NBXplorer.Client/Models/GetBalanceResponse.cs
Normal file
14
NBXplorer.Client/Models/GetBalanceResponse.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace NBXplorer.Models
|
||||
{
|
||||
public class GetBalanceResponse
|
||||
{
|
||||
public IMoney Unconfirmed { get; set; }
|
||||
public IMoney Confirmed { get; set; }
|
||||
public IMoney Total { get; set; }
|
||||
}
|
||||
}
|
||||
@ -89,7 +89,7 @@ namespace NBXplorer.Models
|
||||
get;
|
||||
set;
|
||||
}
|
||||
public Money BalanceChange
|
||||
public IMoney BalanceChange
|
||||
{
|
||||
get;
|
||||
set;
|
||||
|
||||
@ -13,19 +13,18 @@ namespace NBXplorer.Models
|
||||
{
|
||||
|
||||
}
|
||||
public KeyPathInformation(DerivationFeature feature, KeyPath keyPath, DerivationStrategyBase derivationStrategy, NBXplorerNetwork network)
|
||||
|
||||
public KeyPathInformation(Derivation derivation, DerivationSchemeTrackedSource derivationStrategy, DerivationFeature feature, KeyPath keyPath, NBXplorerNetwork network)
|
||||
{
|
||||
var derivation = derivationStrategy.GetDerivation(keyPath);
|
||||
ScriptPubKey = derivation.ScriptPubKey;
|
||||
Redeem = derivation.Redeem;
|
||||
TrackedSource = new DerivationSchemeTrackedSource(derivationStrategy);
|
||||
DerivationStrategy = derivationStrategy;
|
||||
TrackedSource = derivationStrategy;
|
||||
DerivationStrategy = derivationStrategy.DerivationStrategy;
|
||||
Feature = feature;
|
||||
KeyPath = keyPath;
|
||||
Address = network.CreateAddress(derivationStrategy, keyPath, ScriptPubKey);
|
||||
Address = network.CreateAddress(derivationStrategy.DerivationStrategy, keyPath, ScriptPubKey);
|
||||
}
|
||||
public TrackedSource TrackedSource { get; set; }
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public DerivationFeature Feature
|
||||
{
|
||||
@ -57,4 +56,4 @@ namespace NBXplorer.Models
|
||||
return (int)KeyPath.Indexes[KeyPath.Indexes.Length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,6 @@ namespace NBXplorer.Models
|
||||
public KeyPath KeyPath { get; set; }
|
||||
public Script ScriptPubKey { get; set; }
|
||||
public int Index { get; set; }
|
||||
public Money Value { get; set; }
|
||||
public IMoney Value { get; set; }
|
||||
}
|
||||
}
|
||||
@ -129,12 +129,12 @@ namespace NBXplorer.Models
|
||||
|
||||
}
|
||||
|
||||
public UTXO(Coin coin)
|
||||
public UTXO(ICoin coin)
|
||||
{
|
||||
Outpoint = coin.Outpoint;
|
||||
Index = (int)coin.Outpoint.N;
|
||||
TransactionHash = coin.Outpoint.Hash;
|
||||
Value = coin.TxOut.Value;
|
||||
Value = coin.Amount;
|
||||
ScriptPubKey = coin.TxOut.ScriptPubKey;
|
||||
}
|
||||
|
||||
@ -152,16 +152,20 @@ namespace NBXplorer.Models
|
||||
|
||||
public Coin AsCoin(DerivationStrategy.DerivationStrategyBase derivationStrategy)
|
||||
{
|
||||
var coin = new Coin(Outpoint, new TxOut(Value, ScriptPubKey));
|
||||
if (derivationStrategy != null)
|
||||
if (Value is Money v)
|
||||
{
|
||||
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);
|
||||
var coin = new Coin(Outpoint, new TxOut(v, ScriptPubKey));
|
||||
if (derivationStrategy != null)
|
||||
{
|
||||
var derivation = derivationStrategy.GetDerivation(KeyPath);
|
||||
if (derivation.ScriptPubKey != coin.ScriptPubKey)
|
||||
throw new InvalidOperationException($"This Derivation Strategy does not own this coin");
|
||||
if (derivation.Redeem != null)
|
||||
coin = coin.ToScriptCoin(derivation.Redeem);
|
||||
}
|
||||
return coin;
|
||||
}
|
||||
return coin;
|
||||
return null;
|
||||
}
|
||||
|
||||
OutPoint _Outpoint = new OutPoint();
|
||||
@ -194,8 +198,8 @@ namespace NBXplorer.Models
|
||||
}
|
||||
|
||||
|
||||
Money _Value;
|
||||
public Money Value
|
||||
IMoney _Value;
|
||||
public IMoney Value
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@ -16,10 +16,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NBitcoin" Version="5.0.3" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="2.0.2" />
|
||||
<PackageReference Include="NBitcoin" Version="5.0.4" />
|
||||
<PackageReference Include="NBitcoin.Altcoins" Version="2.0.3" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
|
||||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
51
NBXplorer.Client/NBXplorerNetworkProvider.Liquid.cs
Normal file
51
NBXplorer.Client/NBXplorerNetworkProvider.Liquid.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class NBXplorerNetworkProvider
|
||||
{
|
||||
public class LiquidNBXplorerNetwork : NBXplorerNetwork
|
||||
{
|
||||
public LiquidNBXplorerNetwork(INetworkSet networkSet, NetworkType networkType, DerivationStrategyFactory derivationStrategyFactory = null) : base(networkSet, networkType, derivationStrategyFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public override BitcoinAddress CreateAddress(DerivationStrategyBase derivationStrategy, KeyPath keyPath, Script scriptPubKey)
|
||||
{
|
||||
var blindingKey = GenerateBlindingKey(derivationStrategy, keyPath);
|
||||
if (blindingKey == null)
|
||||
{
|
||||
return base.CreateAddress(derivationStrategy, keyPath, scriptPubKey);
|
||||
}
|
||||
var blindingPubKey = blindingKey.PubKey;
|
||||
return new BitcoinBlindedAddress(blindingPubKey, base.CreateAddress(derivationStrategy, keyPath, scriptPubKey));
|
||||
}
|
||||
|
||||
public static Key GenerateBlindingKey(DerivationStrategyBase derivationStrategy, KeyPath keyPath)
|
||||
{
|
||||
var blindingKey = new Key(derivationStrategy.GetChild(keyPath).GetChild(new KeyPath("0")).GetDerivation()
|
||||
.ScriptPubKey.WitHash.ToBytes());
|
||||
return blindingKey;
|
||||
}
|
||||
}
|
||||
private void InitLiquid(NetworkType networkType)
|
||||
{
|
||||
Add(new LiquidNBXplorerNetwork(NBitcoin.Altcoins.Liquid.Instance, networkType)
|
||||
{
|
||||
MinRPCVersion = 150000,
|
||||
CoinType = networkType == NetworkType.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
|
||||
});
|
||||
}
|
||||
|
||||
public NBXplorerNetwork GetLBTC()
|
||||
{
|
||||
return GetFromCryptoCode(NBitcoin.Altcoins.Liquid.Instance.CryptoCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,8 @@ namespace NBXplorer
|
||||
InitGobyte(networkType);
|
||||
InitColossus(networkType);
|
||||
InitChaincoin(networkType);
|
||||
foreach(var chain in _Networks.Values)
|
||||
InitLiquid(networkType);
|
||||
foreach (var chain in _Networks.Values)
|
||||
{
|
||||
chain.DerivationStrategyFactory ??= new DerivationStrategy.DerivationStrategyFactory(chain.NBitcoinNetwork);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using NBitcoin;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
@ -22,16 +23,25 @@ namespace NBXplorer
|
||||
|
||||
public void ConfigureSerializer(JsonSerializerSettings settings)
|
||||
{
|
||||
if(settings == null)
|
||||
if (settings == null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
NBitcoin.JsonConverters.Serializer.RegisterFrontConverters(settings, Network);
|
||||
if (Network != null)
|
||||
if (_Network != null)
|
||||
{
|
||||
settings.Converters.Insert(0, new JsonConverters.CachedSerializer(_Network));
|
||||
}
|
||||
ReplaceConverter<NBitcoin.JsonConverters.MoneyJsonConverter>(settings, new NBXplorer.JsonConverters.MoneyJsonConverter());
|
||||
settings.Converters.Insert(0, new JsonConverters.FeeRateJsonConverter());
|
||||
}
|
||||
|
||||
private static void ReplaceConverter<T>(JsonSerializerSettings settings, JsonConverter jsonConverter) where T : JsonConverter
|
||||
{
|
||||
var moneyConverter = settings.Converters.OfType<T>().Single();
|
||||
var index = settings.Converters.IndexOf(moneyConverter);
|
||||
settings.Converters.RemoveAt(index);
|
||||
settings.Converters.Insert(index, new NBXplorer.JsonConverters.MoneyJsonConverter());
|
||||
}
|
||||
|
||||
public T ToObject<T>(string str)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(str, Settings);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Azure.ServiceBus" Version="4.0.0" />
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="2.0.0" />
|
||||
<PackageReference Include="NBitcoin.TestFramework" Version="2.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
|
||||
|
||||
@ -75,6 +75,10 @@ namespace NBXplorer.Tests
|
||||
//Network = NBitcoin.Altcoins.Colossus.Instance.Regtest;
|
||||
//RPCSupportSegwit = false;
|
||||
|
||||
//CryptoCode = "LBTC";
|
||||
//nodeDownloadData = NodeDownloadData.Elements.v0_18_1_1;
|
||||
//NBXplorerNetwork = new NBXplorerNetwork(NBitcoin.Altcoins.Liquid.Instance, NetworkType.Regtest);
|
||||
//
|
||||
CryptoCode = "BTC";
|
||||
nodeDownloadData = NodeDownloadData.Bitcoin.v0_18_0;
|
||||
NBXplorerNetwork = new NBXplorerNetwork(Network.RegTest.NetworkSet, NetworkType.Regtest);
|
||||
|
||||
@ -22,7 +22,7 @@ namespace NBXplorer.Tests
|
||||
|
||||
public TrackedTransaction Build()
|
||||
{
|
||||
var tx = new TrackedTransaction(new TrackedTransactionKey(_TransactionId, _BlockId, true), _Parent._TrackedSource)
|
||||
var tx = new TrackedTransaction(new TrackedTransactionKey(_TransactionId, _BlockId, true), _Parent._TrackedSource, null as Coin[], null)
|
||||
{
|
||||
Inserted = _TimeStamp,
|
||||
FirstSeen = _TimeStamp
|
||||
|
||||
@ -15,6 +15,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@ -27,7 +28,7 @@ namespace NBXplorer.Tests
|
||||
Logs.Tester = new XUnitLog(helper) { Name = "Tests" };
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
|
||||
private NBXplorerNetwork GetNetwork(Network network)
|
||||
{
|
||||
return new NBXplorerNetwork(network.NetworkSet, network.NetworkType, new DerivationStrategyFactory(network));
|
||||
@ -1807,7 +1808,7 @@ namespace NBXplorer.Tests
|
||||
utxo = tester.Client.GetUTXOs(pubkey);
|
||||
Assert.Equal(2, utxo.Unconfirmed.UTXOs.Count);
|
||||
Assert.IsType<Coin>(utxo.Unconfirmed.UTXOs[0].AsCoin(pubkey));
|
||||
Assert.Equal(Money.Coins(0.6m) + Money.Coins(0.15m), utxo.Unconfirmed.UTXOs[0].Value + utxo.Unconfirmed.UTXOs[1].Value);
|
||||
Assert.Equal(Money.Coins(0.6m) + Money.Coins(0.15m), utxo.Unconfirmed.UTXOs[0].Value.Add(utxo.Unconfirmed.UTXOs[1].Value));
|
||||
Assert.Empty(utxo.Unconfirmed.SpentOutpoints);
|
||||
}
|
||||
}
|
||||
@ -2402,7 +2403,7 @@ namespace NBXplorer.Tests
|
||||
|
||||
private static AnnotatedTransaction CreateRandomAnnotatedTransaction(DerivationSchemeTrackedSource trackedSource, int? height = null, int? seen = null)
|
||||
{
|
||||
var a = new AnnotatedTransaction(height, new TrackedTransaction(new TrackedTransactionKey(RandomUtils.GetUInt256(), null, true), trackedSource), true);
|
||||
var a = new AnnotatedTransaction(height, new TrackedTransaction(new TrackedTransactionKey(RandomUtils.GetUInt256(), null, true), trackedSource, null as Coin[], null), true);
|
||||
if (seen is int v)
|
||||
{
|
||||
a.Record.FirstSeen = NBitcoin.Utils.UnixTimeToDateTime(v);
|
||||
@ -2870,6 +2871,88 @@ namespace NBXplorer.Tests
|
||||
Assert.Equal(path2, KeyPathTemplate.Parse(template).GetKeyPath(1).ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ElementsTests()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
if (tester.Network.NetworkSet != NBitcoin.Altcoins.Liquid.Instance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var userDerivationScheme = tester.Client.GenerateWallet(new GenerateWalletRequest()
|
||||
{
|
||||
SavePrivateKeys = true,
|
||||
ImportKeysToRPC= true
|
||||
}).DerivationScheme;
|
||||
await tester.Client.TrackAsync(userDerivationScheme, Cancel);
|
||||
|
||||
//test: Elements shouldgenerate blinded addresses by default
|
||||
var address =
|
||||
Assert.IsType<BitcoinBlindedAddress>(tester.Client.GetUnused(userDerivationScheme,
|
||||
DerivationFeature.Deposit).Address);
|
||||
|
||||
|
||||
using (var session = await tester.Client.CreateWebsocketNotificationSessionAsync(Timeout))
|
||||
{
|
||||
await session.ListenAllTrackedSourceAsync(cancellation: Timeout);
|
||||
|
||||
//test: Client should return Elements transaction types when event is published
|
||||
var evtTask = session.NextEventAsync(Timeout);
|
||||
var txid = await tester.SendToAddressAsync(address, Money.Coins(1.0m));
|
||||
|
||||
var evt = Assert.IsType<NewTransactionEvent>(await evtTask);
|
||||
|
||||
|
||||
//test: Elements should have unblinded the outputs
|
||||
var output = Assert.Single(evt.Outputs);
|
||||
var assetMoney = Assert.IsType<AssetMoney>(output.Value);
|
||||
Assert.Equal(Money.Coins(1.0m).Satoshi, assetMoney.Quantity);
|
||||
Assert.NotNull(assetMoney.AssetId);
|
||||
|
||||
// but not the transaction itself
|
||||
var tx = Assert.IsAssignableFrom<ElementsTransaction>(evt.TransactionData.Transaction);
|
||||
Assert.Equal(txid, tx.GetHash());
|
||||
var elementsTxOut = Assert.IsAssignableFrom<ElementsTxOut>(tx.Outputs[output.Index]);
|
||||
Assert.Null(elementsTxOut.Value);
|
||||
//test: Get Transaction should give an ElementsTransaction
|
||||
tx = Assert.IsAssignableFrom<ElementsTransaction>((await tester.Client.GetTransactionAsync(txid)).Transaction);
|
||||
Assert.Equal(txid, tx.GetHash());
|
||||
|
||||
//test: receive a tx to deriv scheme but to a confidential address with a different blinding key than our derivation method
|
||||
evtTask = session.NextEventAsync(Timeout);
|
||||
txid = await tester.SendToAddressAsync(new BitcoinBlindedAddress(new Key().PubKey, address.UnblindedAddress), Money.Coins(2.0m));
|
||||
evt = Assert.IsType<NewTransactionEvent>(await evtTask);
|
||||
var unblindabletx = (Assert.IsAssignableFrom<ElementsTransaction>(Assert.IsType<NewTransactionEvent>(evt)
|
||||
.TransactionData.Transaction));
|
||||
Assert.Equal(txid, unblindabletx.GetHash());
|
||||
Assert.Contains(unblindabletx.Outputs, txout => Assert.IsAssignableFrom<ElementsTxOut>(txout).Value == null);
|
||||
|
||||
//test: The ouptut of the event should have null value
|
||||
output = Assert.Single(evt.Outputs);
|
||||
Assert.Null(output.Value);
|
||||
|
||||
var txInfos = tester.Client.GetTransactions(userDerivationScheme).UnconfirmedTransactions.Transactions;
|
||||
var assetMoney2 = Assert.IsType<AssetMoney>(Assert.Single(Assert.IsType<MoneyBag>(txInfos[1].BalanceChange)));
|
||||
Assert.Empty(Assert.IsType<MoneyBag>(txInfos[0].BalanceChange));
|
||||
Assert.Equal(assetMoney, assetMoney2);
|
||||
|
||||
tester.RPC.Generate(6);
|
||||
var received = tester.RPC.SendCommand("getreceivedbyaddress", address.ToString());
|
||||
var receivedMoney = received.Result["bitcoin"].Value<decimal>();
|
||||
// Assert.Equal(1.0m, receivedMoney);
|
||||
// Note that you would expect to have only 1.0 here because you would
|
||||
// expect the second 2.0 to not be unblindable by RPC
|
||||
// but because RPC originated this transaction, it can unblind it without knowing the blinding key
|
||||
Assert.Equal(3.0m, receivedMoney);
|
||||
|
||||
var balance = tester.Client.GetBalance(userDerivationScheme);
|
||||
Assert.Equal(assetMoney, balance.Total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CanGenerateWallet()
|
||||
{
|
||||
|
||||
46
NBXplorer/Altcoins/Liquid/AssetCoin.cs
Normal file
46
NBXplorer/Altcoins/Liquid/AssetCoin.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using NBitcoin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer.Altcoins.Liquid
|
||||
{
|
||||
public class AssetCoin : ICoin
|
||||
{
|
||||
private readonly ICoin innerCoin;
|
||||
|
||||
public AssetCoin(AssetMoney assetMoney, ICoin innerCoin)
|
||||
{
|
||||
if (assetMoney == null)
|
||||
throw new ArgumentNullException(nameof(assetMoney));
|
||||
if (innerCoin == null)
|
||||
throw new ArgumentNullException(nameof(innerCoin));
|
||||
Money = assetMoney;
|
||||
this.innerCoin = innerCoin;
|
||||
}
|
||||
public AssetMoney Money { get; }
|
||||
IMoney ICoin.Amount => Money;
|
||||
|
||||
public OutPoint Outpoint => innerCoin.Outpoint;
|
||||
|
||||
public TxOut TxOut => innerCoin.TxOut;
|
||||
|
||||
public bool CanGetScriptCode => innerCoin.CanGetScriptCode;
|
||||
|
||||
public HashVersion GetHashVersion()
|
||||
{
|
||||
return innerCoin.GetHashVersion();
|
||||
}
|
||||
|
||||
public Script GetScriptCode()
|
||||
{
|
||||
return innerCoin.GetScriptCode();
|
||||
}
|
||||
|
||||
public void OverrideScriptCode(Script scriptCode)
|
||||
{
|
||||
innerCoin.OverrideScriptCode(scriptCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ using Newtonsoft.Json;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Diagnostics;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
|
||||
namespace NBXplorer.Controllers
|
||||
{
|
||||
@ -151,6 +152,11 @@ namespace NBXplorer.Controllers
|
||||
{
|
||||
await rpc.ImportAddressAsync(result.Address, null, false);
|
||||
}
|
||||
if (repository is LiquidRepository && result.Address is BitcoinBlindedAddress bitcoinBlindedAddress)
|
||||
{
|
||||
var blindingkey = NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(strategyBase, result.KeyPath);
|
||||
_ = await rpc.ImportBlindingKey(bitcoinBlindedAddress, blindingkey);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -614,7 +620,15 @@ namespace NBXplorer.Controllers
|
||||
if (txId != null && txId == txInfo.TransactionId)
|
||||
fetchedTransactionInfo = txInfo;
|
||||
|
||||
txInfo.BalanceChange = txInfo.Outputs.Select(o => o.Value).Sum() - txInfo.Inputs.Select(o => o.Value).Sum();
|
||||
if (network.NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance)
|
||||
{
|
||||
txInfo.BalanceChange = new MoneyBag(txInfo.Outputs.Select(o => o.Value).OfType<AssetMoney>().ToArray())
|
||||
- new MoneyBag(txInfo.Inputs.Select(o => o.Value).OfType<AssetMoney>().ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
txInfo.BalanceChange = txInfo.Outputs.Select(o => o.Value).OfType<Money>().Sum() - txInfo.Inputs.Select(o => o.Value).OfType<Money>().Sum();
|
||||
}
|
||||
}
|
||||
item.TxSet.Transactions.Reverse(); // So the youngest transaction is generally first
|
||||
}
|
||||
@ -814,13 +828,20 @@ namespace NBXplorer.Controllers
|
||||
Confirmed = CalculateBalance(network, transactions.ConfirmedTransactions),
|
||||
Unconfirmed = CalculateBalance(network, transactions.UnconfirmedTransactions)
|
||||
};
|
||||
balance.Total = balance.Confirmed + balance.Unconfirmed;
|
||||
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
|
||||
return Json(balance, jsonResult.SerializerSettings);
|
||||
}
|
||||
|
||||
private Money CalculateBalance(NBXplorerNetwork network, TransactionInformationSet transactions)
|
||||
private IMoney CalculateBalance(NBXplorerNetwork network, TransactionInformationSet transactions)
|
||||
{
|
||||
return transactions.Transactions.Select(t => t.BalanceChange).Sum();
|
||||
if (network.NBitcoinNetwork.NetworkSet == NBitcoin.Altcoins.Liquid.Instance)
|
||||
{
|
||||
return new MoneyBag(transactions.Transactions.Select(t => t.BalanceChange).ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
return transactions.Transactions.Select(t => t.BalanceChange).OfType<Money>().Sum();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
|
||||
223
NBXplorer/LiquidRepository.cs
Normal file
223
NBXplorer/LiquidRepository.cs
Normal file
@ -0,0 +1,223 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using DBriize;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Altcoins.Elements;
|
||||
using NBXplorer.Altcoins.Liquid;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Models;
|
||||
using System;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
public class LiquidRepository : Repository
|
||||
{
|
||||
private readonly RPCClient _rpcClient;
|
||||
|
||||
internal LiquidRepository(DBriizeEngine engine, NBXplorerNetwork network, KeyPathTemplates keyPathTemplates,
|
||||
RPCClient rpcClient) : base(engine, network, keyPathTemplates)
|
||||
{
|
||||
_rpcClient = rpcClient;
|
||||
}
|
||||
|
||||
class ElementsTrackedTransaction : TrackedTransaction
|
||||
{
|
||||
public ElementsTrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, IEnumerable<Coin> receivedCoins, Dictionary<Script, KeyPath> knownScriptMapping) :
|
||||
base(key, trackedSource, receivedCoins, knownScriptMapping)
|
||||
{
|
||||
ClearCoinValues();
|
||||
Unblind(receivedCoins, false);
|
||||
}
|
||||
public ElementsTrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, Transaction transaction, Dictionary<Script, KeyPath> knownScriptMapping) :
|
||||
base(key, trackedSource, transaction, knownScriptMapping)
|
||||
{
|
||||
ClearCoinValues();
|
||||
Unblind(transaction.Outputs.AsCoins(), false);
|
||||
}
|
||||
|
||||
private void ClearCoinValues()
|
||||
{
|
||||
foreach (var coin in ReceivedCoins.OfType<Coin>())
|
||||
{
|
||||
coin.Amount = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override ITrackedTransactionSerializable CreateBitcoinSerializable()
|
||||
{
|
||||
return new ElementsTransactionMatchData(this);
|
||||
}
|
||||
|
||||
public void Unblind(ElementsTransaction unblindedTransaction, bool saveUnblindData)
|
||||
{
|
||||
Unblind(unblindedTransaction.Outputs.AsCoins(), saveUnblindData);
|
||||
}
|
||||
|
||||
public void Unblind(IEnumerable<ICoin> unblindedCoins, bool saveUnblindData)
|
||||
{
|
||||
foreach (var coin in unblindedCoins)
|
||||
{
|
||||
AssetMoney assetMoney = null;
|
||||
if (coin is AssetCoin assetCoin)
|
||||
{
|
||||
assetMoney = assetCoin.Money;
|
||||
}
|
||||
if (coin.TxOut is ElementsTxOut elementsTxOut &&
|
||||
elementsTxOut.Asset.AssetId != null &&
|
||||
elementsTxOut.Value != null)
|
||||
{
|
||||
assetMoney = new AssetMoney(elementsTxOut.Asset.AssetId, elementsTxOut.Value.Satoshi);
|
||||
}
|
||||
if (assetMoney != null &&
|
||||
TryGetReceivedCoinByIndex((int)coin.Outpoint.N) is Coin existingCoin)
|
||||
{
|
||||
if (saveUnblindData)
|
||||
Unblinded.TryAdd((int)existingCoin.Outpoint.N, assetMoney);
|
||||
this.ReceivedCoins.Remove(existingCoin);
|
||||
this.ReceivedCoins.Add(new AssetCoin(assetMoney, existingCoin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ICoin TryGetReceivedCoinByIndex(int index)
|
||||
{
|
||||
return this.ReceivedCoins.FirstOrDefault(r => r.Outpoint.N == index);
|
||||
}
|
||||
public void Unblind(IEnumerable<ElementsTransactionMatchData.UnblindData> unblindData)
|
||||
{
|
||||
foreach (var unblind in unblindData)
|
||||
{
|
||||
if (TryGetReceivedCoinByIndex(unblind.Index) is Coin existingCoin)
|
||||
{
|
||||
this.ReceivedCoins.Remove(existingCoin);
|
||||
var money = new AssetMoney(unblind.AssetId, unblind.Value);
|
||||
this.ReceivedCoins.Add(new AssetCoin(money, existingCoin));
|
||||
this.Unblinded.Add(unblind.Index, money);
|
||||
}
|
||||
}
|
||||
}
|
||||
public Dictionary<int, AssetMoney> Unblinded = new Dictionary<int, AssetMoney>();
|
||||
}
|
||||
class ElementsTransactionMatchData : TrackedTransaction.TransactionMatchData
|
||||
{
|
||||
internal class UnblindData : IBitcoinSerializable
|
||||
{
|
||||
|
||||
long _Value;
|
||||
public long Value
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
uint256 _AssetId;
|
||||
public uint256 AssetId
|
||||
{
|
||||
get
|
||||
{
|
||||
return _AssetId;
|
||||
}
|
||||
set
|
||||
{
|
||||
_AssetId = value;
|
||||
}
|
||||
}
|
||||
|
||||
int _Index;
|
||||
public int Index
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Index;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Index = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadWrite(BitcoinStream stream)
|
||||
{
|
||||
stream.ReadWrite(ref _Index);
|
||||
stream.ReadWrite(ref _AssetId);
|
||||
stream.ReadWrite(ref _Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
List<UnblindData> _UnblindData = new List<UnblindData>();
|
||||
internal List<UnblindData> Unblind
|
||||
{
|
||||
get
|
||||
{
|
||||
return _UnblindData;
|
||||
}
|
||||
set
|
||||
{
|
||||
_UnblindData = value;
|
||||
}
|
||||
}
|
||||
public ElementsTransactionMatchData(TrackedTransactionKey key) : base(key)
|
||||
{
|
||||
|
||||
}
|
||||
public ElementsTransactionMatchData(ElementsTrackedTransaction trackedTransaction) : base(trackedTransaction)
|
||||
{
|
||||
foreach (var unblind in trackedTransaction.Unblinded)
|
||||
_UnblindData.Add(new UnblindData() { Index = unblind.Key, AssetId = unblind.Value.AssetId, Value = unblind.Value.Quantity });
|
||||
}
|
||||
|
||||
public override void ReadWrite(BitcoinStream stream)
|
||||
{
|
||||
base.ReadWrite(stream);
|
||||
stream.ReadWrite(ref _UnblindData);
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task AfterMatch(TrackedTransaction tx)
|
||||
{
|
||||
if (tx.TrackedSource is DerivationSchemeTrackedSource ts &&
|
||||
tx.Transaction is ElementsTransaction elementsTransaction &&
|
||||
tx is ElementsTrackedTransaction elementsTracked)
|
||||
{
|
||||
var unblinded = await _rpcClient.UnblindTransaction(
|
||||
tx.KnownKeyPathMapping
|
||||
.Select(kv => (KeyPath: kv.Value,
|
||||
BlindingKey: NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(ts.DerivationStrategy, kv.Value),
|
||||
UnconfidentialAddress: kv.Key.GetDestinationAddress(Network.NBitcoinNetwork)))
|
||||
.Select(o => new UnblindTransactionBlindingAddressKey()
|
||||
{
|
||||
Address = o.UnconfidentialAddress.AddBlindingKey(o.BlindingKey.PubKey),
|
||||
BlindingKey = o.BlindingKey
|
||||
}).ToList(), elementsTransaction, Network.NBitcoinNetwork);
|
||||
elementsTracked.Unblind(unblinded, true);
|
||||
}
|
||||
}
|
||||
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, Transaction tx, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new ElementsTrackedTransaction(transactionKey, trackedSource, tx, knownScriptMapping);
|
||||
}
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, IEnumerable<Coin> coins, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new ElementsTrackedTransaction(transactionKey, trackedSource, coins, knownScriptMapping);
|
||||
}
|
||||
public override TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, ITrackedTransactionSerializable tx)
|
||||
{
|
||||
var trackedTransaction = (ElementsTrackedTransaction)base.CreateTrackedTransaction(trackedSource, tx);
|
||||
trackedTransaction.Unblind(((ElementsTransactionMatchData)tx).Unblind);
|
||||
return trackedTransaction;
|
||||
}
|
||||
protected override ITrackedTransactionSerializable CreateBitcoinSerializableTrackedTransaction(TrackedTransactionKey trackedTransactionKey)
|
||||
{
|
||||
return new ElementsTransactionMatchData(trackedTransactionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,22 @@
|
||||
"applicationUrl": "http://localhost:4774",
|
||||
"launchUrl": "v1/cryptos/ltc/status"
|
||||
},
|
||||
|
||||
"NBXplorer - LBTC on Regtest from Docker": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"NBXPLORER_NETWORK": "regtest",
|
||||
"NBXPLORER_CHAINS":"lbtc",
|
||||
"NBXPLORER_LBTCRPCURL": "http://127.0.0.1:19332/",
|
||||
"NBXPLORER_LBTCNODEENDPOINT": "127.0.0.1:19444",
|
||||
"NBXPLORER_LBTCRPCUSER": "liquid",
|
||||
"NBXPLORER_LBTCRPCPASSWORD": "liquid",
|
||||
"NBXPLORER_NOAUTH": "true"
|
||||
},
|
||||
"applicationUrl": "http://localhost:4774",
|
||||
"launchUrl": "v1/cryptos/lbtc/status"
|
||||
},
|
||||
"NBXplorer - DASH on Testnet": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "--chains=dash --network=testnet",
|
||||
|
||||
@ -2,10 +2,7 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Linq;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Crypto;
|
||||
using NBitcoin.JsonConverters;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
@ -14,15 +11,10 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using NBitcoin.DataEncoders;
|
||||
using DBriize.Utils;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using DBriize.Exceptions;
|
||||
using NBitcoin.Altcoins;
|
||||
using NBitcoin.RPC;
|
||||
using NBXplorer.Logging;
|
||||
using NBXplorer.Configuration;
|
||||
using static NBXplorer.RepositoryProvider;
|
||||
using static NBXplorer.Repository;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static NBXplorer.TrackedTransaction;
|
||||
|
||||
@ -76,7 +68,7 @@ namespace NBXplorer
|
||||
var settings = GetChainSetting(net);
|
||||
if (settings != null)
|
||||
{
|
||||
var repo = new Repository(_Engine, net, keyPathTemplates);
|
||||
var repo = net.NBitcoinNetwork.NetworkSet == Liquid.Instance ? new LiquidRepository(_Engine, net, keyPathTemplates, settings.RPC) : new Repository(_Engine, net, keyPathTemplates);
|
||||
repo.MaxPoolSize = configuration.MaxGapSize;
|
||||
repo.MinPoolSize = configuration.MinGapSize;
|
||||
_Repositories.Add(net.CryptoCode, repo);
|
||||
@ -92,9 +84,9 @@ namespace NBXplorer
|
||||
public async Task StartAsync()
|
||||
{
|
||||
await Task.WhenAll(_Repositories.Select(kv => kv.Value.StartAsync()).ToArray());
|
||||
foreach (var repo in _Repositories.Select(kv => kv.Value))
|
||||
foreach (var repo in _Repositories.Select(kv => kv.Value))
|
||||
{
|
||||
if(GetChainSetting(repo.Network) is ChainConfiguration chainConf && chainConf.Rescan)
|
||||
if (GetChainSetting(repo.Network) is ChainConfiguration chainConf && chainConf.Rescan)
|
||||
{
|
||||
Logs.Configuration.LogInformation($"{repo.Network.CryptoCode}: Rescanning the chain...");
|
||||
await repo.SetIndexProgress(null);
|
||||
@ -310,7 +302,7 @@ namespace NBXplorer
|
||||
return new Index(tx, $"{_Suffix}Events", string.Empty);
|
||||
}
|
||||
|
||||
NBXplorerNetwork _Network;
|
||||
protected NBXplorerNetwork _Network;
|
||||
private readonly KeyPathTemplates keyPathTemplates;
|
||||
|
||||
public NBXplorerNetwork Network
|
||||
@ -376,7 +368,7 @@ namespace NBXplorer
|
||||
|
||||
public Task<KeyPathInformation> GetUnused(DerivationStrategyBase strategy, DerivationFeature derivationFeature, int n, bool reserve)
|
||||
{
|
||||
return _TxContext.DoAsync<KeyPathInformation>((tx) =>
|
||||
return _TxContext.DoAsync((tx) =>
|
||||
{
|
||||
tx.ValuesLazyLoadingIsOn = false;
|
||||
var availableTable = GetAvailableKeysIndex(tx, strategy, derivationFeature);
|
||||
@ -384,6 +376,7 @@ namespace NBXplorer
|
||||
var rows = availableTable.SelectForwardSkip(n);
|
||||
if (rows.Length == 0)
|
||||
return null;
|
||||
|
||||
var keyInfo = ToObject<KeyPathInformation>(rows[0].Value).AddAddress(Network.NBitcoinNetwork);
|
||||
if (reserve)
|
||||
{
|
||||
@ -418,10 +411,10 @@ namespace NBXplorer
|
||||
{
|
||||
var index = highestGenerated + i + 1;
|
||||
var derivation = feature.Derive((uint)index);
|
||||
var info = new KeyPathInformation(
|
||||
var info = new KeyPathInformation(derivation,
|
||||
new DerivationSchemeTrackedSource(strategy),
|
||||
derivationFeature,
|
||||
keyPathTemplates.GetKeyPathTemplate(derivationFeature).GetKeyPath(index, false),
|
||||
strategy,
|
||||
Network);
|
||||
keyPathInformations[i] = info;
|
||||
});
|
||||
@ -685,6 +678,7 @@ namespace NBXplorer
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
private T ToObject<T>(byte[] value)
|
||||
{
|
||||
var result = Serializer.ToObject<T>(Unzip(value));
|
||||
@ -738,23 +732,21 @@ namespace NBXplorer
|
||||
public async Task<TrackedTransaction[]> GetTransactions(TrackedSource trackedSource, uint256 txId = null, CancellationToken cancellation = default)
|
||||
{
|
||||
Dictionary<uint256, long> firstSeenList = new Dictionary<uint256, long>();
|
||||
HashSet<TransactionMatchData> needRemove = new HashSet<TransactionMatchData>();
|
||||
HashSet<TransactionMatchData> needUpdate = new HashSet<TransactionMatchData>();
|
||||
HashSet<ITrackedTransactionSerializable> needRemove = new HashSet<ITrackedTransactionSerializable>();
|
||||
HashSet<ITrackedTransactionSerializable> needUpdate = new HashSet<ITrackedTransactionSerializable>();
|
||||
var transactions = await _TxContext.DoAsync(tx =>
|
||||
{
|
||||
var table = GetTransactionsIndex(tx, trackedSource);
|
||||
tx.ValuesLazyLoadingIsOn = false;
|
||||
var result = new List<TransactionMatchData>();
|
||||
var result = new List<ITrackedTransactionSerializable>();
|
||||
foreach (var row in table.SelectForwardSkip(0, txId?.ToString()))
|
||||
{
|
||||
MemoryStream ms = new MemoryStream(row.Value);
|
||||
BitcoinStream bs = new BitcoinStream(ms, false);
|
||||
bs.ConsensusFactory = Network.NBitcoinNetwork.Consensus.ConsensusFactory;
|
||||
TransactionMatchData data = new TransactionMatchData(TrackedTransactionKey.Parse(row.Key));
|
||||
var data = CreateBitcoinSerializableTrackedTransaction(TrackedTransactionKey.Parse(row.Key));
|
||||
data.ReadWrite(bs);
|
||||
result.Add(data);
|
||||
if (data.KnownKeyPathMapping == null)
|
||||
needUpdate.Add(data);
|
||||
long firstSeen;
|
||||
if (firstSeenList.TryGetValue(data.Key.TxId, out firstSeen))
|
||||
{
|
||||
@ -770,7 +762,7 @@ namespace NBXplorer
|
||||
return result;
|
||||
}, cancellation);
|
||||
|
||||
TransactionMatchData previousConfirmed = null;
|
||||
ITrackedTransactionSerializable previousConfirmed = null;
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
if (tx.Key.BlockHash != null)
|
||||
@ -806,14 +798,6 @@ namespace NBXplorer
|
||||
}
|
||||
if (needUpdate.Count != 0 || needRemove.Count != 0)
|
||||
{
|
||||
// This is legacy data, need an update
|
||||
foreach (var data in needUpdate.Where(t => t.KnownKeyPathMapping == null))
|
||||
{
|
||||
data.KnownKeyPathMapping = (await this.GetMatches(data.Transaction, data.Key.BlockHash, DateTimeOffset.UtcNow, false))
|
||||
.Where(m => m.TrackedSource.Equals(trackedSource))
|
||||
.Select(m => m.KnownKeyPathMapping)
|
||||
.First();
|
||||
}
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
// This can be eventually consistent, let's not waste one round trip waiting for this
|
||||
_TxContext.DoAsync(tx =>
|
||||
@ -831,7 +815,15 @@ namespace NBXplorer
|
||||
});
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
}
|
||||
return transactions.Where(tt => !needRemove.Contains(tt)).Select(c => c.ToTrackedTransaction(trackedSource)).ToArray();
|
||||
return transactions.Where(tt => !needRemove.Contains(tt)).Select(c => ToTrackedTransaction(c, trackedSource)).ToArray();
|
||||
}
|
||||
|
||||
TrackedTransaction ToTrackedTransaction(ITrackedTransactionSerializable tx, TrackedSource trackedSource)
|
||||
{
|
||||
var trackedTransaction = CreateTrackedTransaction(trackedSource, tx);
|
||||
trackedTransaction.Inserted = tx.TickCount == 0 ? NBitcoin.Utils.UnixTimeToDateTime(0) : new DateTimeOffset((long)tx.TickCount, TimeSpan.Zero);
|
||||
trackedTransaction.FirstSeen = tx.FirstSeenTickCount == 0 ? NBitcoin.Utils.UnixTimeToDateTime(0) : new DateTimeOffset((long)tx.FirstSeenTickCount, TimeSpan.Zero);
|
||||
return trackedTransaction;
|
||||
}
|
||||
|
||||
public async Task SaveMetadata<TMetadata>(TrackedSource source, string key, TMetadata value) where TMetadata : class
|
||||
@ -846,7 +838,7 @@ namespace NBXplorer
|
||||
tx.Commit();
|
||||
});
|
||||
}
|
||||
public async Task<TMetadata> GetMetadata<TMetadata>(TrackedSource source, string key) where TMetadata: class
|
||||
public async Task<TMetadata> GetMetadata<TMetadata>(TrackedSource source, string key) where TMetadata : class
|
||||
{
|
||||
return await _TxContext.DoAsync(tx =>
|
||||
{
|
||||
@ -877,7 +869,8 @@ namespace NBXplorer
|
||||
{
|
||||
foreach (var kv in value.KnownKeyPathMapping)
|
||||
{
|
||||
var info = new KeyPathInformation(keyPathTemplates.GetDerivationFeature(kv.Value), kv.Value, s.DerivationStrategy, Network);
|
||||
var derivation = s.DerivationStrategy.GetDerivation(kv.Value);
|
||||
var info = new KeyPathInformation(derivation, s, keyPathTemplates.GetDerivationFeature(kv.Value), kv.Value, _Network);
|
||||
var availableIndex = GetAvailableKeysIndex(tx, s.DerivationStrategy, info.Feature);
|
||||
var reservedIndex = GetReservedKeysIndex(tx, s.DerivationStrategy, info.Feature);
|
||||
var index = info.GetIndex();
|
||||
@ -896,7 +889,7 @@ namespace NBXplorer
|
||||
var ms = new MemoryStream();
|
||||
BitcoinStream bs = new BitcoinStream(ms, true);
|
||||
bs.ConsensusFactory = Network.NBitcoinNetwork.Consensus.ConsensusFactory;
|
||||
TransactionMatchData data = new TransactionMatchData(value);
|
||||
var data = value.CreateBitcoinSerializable();
|
||||
bs.ReadWrite(data);
|
||||
table.Insert(data.Key.ToString(), ms.ToArrayEfficient());
|
||||
}
|
||||
@ -1026,15 +1019,12 @@ namespace NBXplorer
|
||||
var matchesGroupingKey = $"{keyInfo.DerivationStrategy?.ToString() ?? keyInfo.ScriptPubKey.ToHex()}-[{tx.GetHash()}]";
|
||||
if (!matches.TryGetValue(matchesGroupingKey, out TrackedTransaction match))
|
||||
{
|
||||
match = new TrackedTransaction(
|
||||
match = CreateTrackedTransaction(keyInfo.TrackedSource,
|
||||
new TrackedTransactionKey(tx.GetHash(), blockId, false),
|
||||
keyInfo.TrackedSource,
|
||||
tx,
|
||||
new Dictionary<Script, KeyPath>())
|
||||
{
|
||||
FirstSeen = now,
|
||||
Inserted = now
|
||||
};
|
||||
new Dictionary<Script, KeyPath>());
|
||||
match.FirstSeen = now;
|
||||
match.Inserted = now;
|
||||
matches.Add(matchesGroupingKey, match);
|
||||
}
|
||||
if (keyInfo.KeyPath != null)
|
||||
@ -1045,6 +1035,7 @@ namespace NBXplorer
|
||||
foreach (var m in matches.Values)
|
||||
{
|
||||
m.KnownKeyPathMappingUpdated();
|
||||
await AfterMatch(m);
|
||||
}
|
||||
|
||||
foreach (var tx in txs)
|
||||
@ -1057,5 +1048,27 @@ namespace NBXplorer
|
||||
}
|
||||
return matches.Values.Count == 0 ? Array.Empty<TrackedTransaction>() : matches.Values.ToArray();
|
||||
}
|
||||
public virtual TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, Transaction tx, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new TrackedTransaction(transactionKey, trackedSource, tx, knownScriptMapping);
|
||||
}
|
||||
public virtual TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, TrackedTransactionKey transactionKey, IEnumerable<Coin> coins, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
return new TrackedTransaction(transactionKey, trackedSource, coins, knownScriptMapping);
|
||||
}
|
||||
public virtual TrackedTransaction CreateTrackedTransaction(TrackedSource trackedSource, ITrackedTransactionSerializable tx)
|
||||
{
|
||||
return tx.Key.IsPruned
|
||||
? CreateTrackedTransaction(trackedSource, tx.Key, tx.GetCoins(), tx.KnownKeyPathMapping)
|
||||
: CreateTrackedTransaction(trackedSource, tx.Key, tx.Transaction, tx.KnownKeyPathMapping);
|
||||
}
|
||||
protected virtual ITrackedTransactionSerializable CreateBitcoinSerializableTrackedTransaction(TrackedTransactionKey trackedTransactionKey)
|
||||
{
|
||||
return new TrackedTransaction.TransactionMatchData(trackedTransactionKey);
|
||||
}
|
||||
protected virtual Task AfterMatch(TrackedTransaction tx)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,12 +283,25 @@ namespace NBXplorer
|
||||
await repo.UpdateAddressPool(trackedSource, progressObj.HighestKeyIndexFound);
|
||||
await gettingBlockHeaders;
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
await repo.SaveMatches(data.Select(o => new TrackedTransaction(new TrackedTransactionKey(o.TxId, o.BlockId, true), trackedSource, o.Coins, o.KeyPathInformations)
|
||||
await repo.SaveMatches(data.Select(o =>
|
||||
{
|
||||
Inserted = now,
|
||||
FirstSeen = blockHeadersByBlockId.TryGetValue(o.BlockId, out var header) && header != null ? header.BlockTime : NBitcoin.Utils.UnixTimeToDateTime(0)
|
||||
var trackedTransaction = repo.CreateTrackedTransaction(trackedSource, new TrackedTransactionKey(o.TxId, o.BlockId, true), o.Coins, ToDictionary(o.KeyPathInformations));
|
||||
trackedTransaction.Inserted = now;
|
||||
trackedTransaction.FirstSeen = blockHeadersByBlockId.TryGetValue(o.BlockId, out var header) && header != null ? header.BlockTime : NBitcoin.Utils.UnixTimeToDateTime(0);
|
||||
return trackedTransaction;
|
||||
}).ToArray());
|
||||
}
|
||||
private static Dictionary<Script, KeyPath> ToDictionary(IEnumerable<KeyPathInformation> knownScriptMapping)
|
||||
{
|
||||
if (knownScriptMapping == null)
|
||||
return null;
|
||||
var result = new Dictionary<Script, KeyPath>();
|
||||
foreach (var keypathInfo in knownScriptMapping)
|
||||
{
|
||||
result.TryAdd(keypathInfo.ScriptPubKey, keypathInfo.KeyPath);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ScannedItems GetScannedItems(ScanUTXOWorkItem workItem, ScanUTXOProgress progress, NBXplorerNetwork network)
|
||||
{
|
||||
@ -302,11 +315,8 @@ namespace NBXplorer
|
||||
.Select(index =>
|
||||
{
|
||||
var derivation = lineDerivation.Derive((uint)index);
|
||||
var info = new KeyPathInformation(
|
||||
feature,
|
||||
keyPathTemplate.GetKeyPath(index, false),
|
||||
derivationStrategy.DerivationStrategy,
|
||||
network);
|
||||
var info = new KeyPathInformation(derivation, derivationStrategy, feature,
|
||||
keyPathTemplate.GetKeyPath(index, false), network);
|
||||
items.Descriptors.Add(new ScanTxoutSetObject(ScanTxoutDescriptor.Raw(info.ScriptPubKey)));
|
||||
items.KeyPathInformations.TryAdd(info.ScriptPubKey, info);
|
||||
return info;
|
||||
|
||||
@ -7,10 +7,11 @@ using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using static NBXplorer.Repository;
|
||||
|
||||
namespace NBXplorer{
|
||||
namespace NBXplorer
|
||||
{
|
||||
public partial class TrackedTransaction
|
||||
{
|
||||
class CoinOutpointEqualityComparer : IEqualityComparer<Coin>
|
||||
class CoinOutpointEqualityComparer : IEqualityComparer<ICoin>
|
||||
{
|
||||
|
||||
private static readonly CoinOutpointEqualityComparer _Instance = new CoinOutpointEqualityComparer();
|
||||
@ -21,39 +22,19 @@ namespace NBXplorer{
|
||||
return _Instance;
|
||||
}
|
||||
}
|
||||
public bool Equals(Coin x, Coin y)
|
||||
public bool Equals(ICoin x, ICoin y)
|
||||
{
|
||||
return x.Outpoint == y.Outpoint;
|
||||
}
|
||||
|
||||
public int GetHashCode(Coin obj)
|
||||
public int GetHashCode(ICoin obj)
|
||||
{
|
||||
return obj.Outpoint.GetHashCode();
|
||||
}
|
||||
}
|
||||
public TrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource) : this(key, trackedSource, null as Coin[], null as Dictionary<Script,KeyPath>)
|
||||
{
|
||||
|
||||
}
|
||||
public TrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, IEnumerable<Coin> receivedCoins, IEnumerable<KeyPathInformation> knownScriptMapping)
|
||||
: this(key, trackedSource, receivedCoins, ToDictionary(knownScriptMapping))
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TrackedSource TrackedSource { get; }
|
||||
|
||||
private static Dictionary<Script, KeyPath> ToDictionary(IEnumerable<KeyPathInformation> knownScriptMapping)
|
||||
{
|
||||
if (knownScriptMapping == null)
|
||||
return null;
|
||||
var result = new Dictionary<Script, KeyPath>();
|
||||
foreach (var keypathInfo in knownScriptMapping)
|
||||
{
|
||||
result.TryAdd(keypathInfo.ScriptPubKey, keypathInfo.KeyPath);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public TrackedTransaction(TrackedTransactionKey key, TrackedSource trackedSource, IEnumerable<Coin> receivedCoins, Dictionary<Script, KeyPath> knownScriptMapping)
|
||||
{
|
||||
if (key == null)
|
||||
@ -66,7 +47,7 @@ namespace NBXplorer{
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
TrackedSource = trackedSource;
|
||||
Key = key;
|
||||
if(knownScriptMapping != null)
|
||||
if (knownScriptMapping != null)
|
||||
KnownKeyPathMapping = knownScriptMapping;
|
||||
if (receivedCoins != null)
|
||||
ReceivedCoins.AddRange(receivedCoins);
|
||||
@ -83,7 +64,8 @@ namespace NBXplorer{
|
||||
throw new ArgumentNullException(nameof(trackedSource));
|
||||
if (key.IsPruned)
|
||||
{
|
||||
throw new ArgumentException("The key should not be pruned", nameof(key)); }
|
||||
throw new ArgumentException("The key should not be pruned", nameof(key));
|
||||
}
|
||||
Key = key;
|
||||
TrackedSource = trackedSource;
|
||||
Transaction = transaction;
|
||||
@ -109,7 +91,7 @@ namespace NBXplorer{
|
||||
}
|
||||
|
||||
public Dictionary<Script, KeyPath> KnownKeyPathMapping { get; } = new Dictionary<Script, KeyPath>();
|
||||
public HashSet<Coin> ReceivedCoins { get; } = new HashSet<Coin>(CoinOutpointEqualityComparer.Instance);
|
||||
public HashSet<ICoin> ReceivedCoins { get; } = new HashSet<ICoin>(CoinOutpointEqualityComparer.Instance);
|
||||
public HashSet<OutPoint> SpentOutpoints { get; } = new HashSet<OutPoint>();
|
||||
|
||||
public Transaction Transaction
|
||||
@ -137,19 +119,25 @@ namespace NBXplorer{
|
||||
return this.ReceivedCoins
|
||||
.Select(o => (Index: (int)o.Outpoint.N,
|
||||
Output: o,
|
||||
KeyPath: KnownKeyPathMapping.TryGet(o.ScriptPubKey)))
|
||||
.Where(o => o.KeyPath != null || o.Output.ScriptPubKey == (TrackedSource as IDestination)?.ScriptPubKey)
|
||||
KeyPath: KnownKeyPathMapping.TryGet(o.TxOut.ScriptPubKey)))
|
||||
.Where(o => o.KeyPath != null || o.Output.TxOut.ScriptPubKey == (TrackedSource as IDestination)?.ScriptPubKey)
|
||||
.Select(o => new MatchedOutput()
|
||||
{
|
||||
Index = o.Index,
|
||||
Value = o.Output.Amount,
|
||||
KeyPath = o.KeyPath,
|
||||
ScriptPubKey = o.Output.ScriptPubKey
|
||||
ScriptPubKey = o.Output.TxOut.ScriptPubKey
|
||||
});
|
||||
}
|
||||
|
||||
public virtual ITrackedTransactionSerializable CreateBitcoinSerializable()
|
||||
{
|
||||
return new TransactionMatchData(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class TrackedTransactionKey {
|
||||
public class TrackedTransactionKey
|
||||
{
|
||||
public uint256 TxId { get; }
|
||||
public uint256 BlockHash { get; }
|
||||
|
||||
|
||||
@ -193,15 +193,7 @@ namespace NBXplorer
|
||||
}
|
||||
stream.ReadWrite(ref _FirstSeenTickCount);
|
||||
}
|
||||
public TrackedTransaction ToTrackedTransaction(TrackedSource trackedSource)
|
||||
{
|
||||
var trackedTransaction = Key.IsPruned
|
||||
? new TrackedTransaction(Key, trackedSource, GetCoins(), KnownKeyPathMapping)
|
||||
: new TrackedTransaction(Key, trackedSource, Transaction, KnownKeyPathMapping);
|
||||
trackedTransaction.Inserted = TickCount == 0 ? NBitcoin.Utils.UnixTimeToDateTime(0) : new DateTimeOffset((long)TickCount, TimeSpan.Zero);
|
||||
trackedTransaction.FirstSeen = FirstSeenTickCount == 0 ? NBitcoin.Utils.UnixTimeToDateTime(0) : new DateTimeOffset((long)FirstSeenTickCount, TimeSpan.Zero);
|
||||
return trackedTransaction;
|
||||
}
|
||||
|
||||
public virtual IEnumerable<Coin> GetCoins()
|
||||
{
|
||||
foreach (var coinData in _CoinsData)
|
||||
@ -285,8 +277,8 @@ namespace NBXplorer
|
||||
TrackedTransactionKey Key { get; }
|
||||
IEnumerable<Coin> GetCoins();
|
||||
long FirstSeenTickCount { get; set; }
|
||||
long TickCount { get; }
|
||||
long TickCount { get; }
|
||||
Transaction Transaction { get; }
|
||||
Dictionary<Script, KeyPath> KnownKeyPathMapping { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,17 +7,17 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace NBXplorer
|
||||
{
|
||||
internal class UTXOByOutpoint : IEnumerable<KeyValuePair<OutPoint, Coin>>
|
||||
internal class UTXOByOutpoint : IEnumerable<KeyValuePair<OutPoint, ICoin>>
|
||||
{
|
||||
Dictionary<OutPoint, Coin> _Inner;
|
||||
Dictionary<OutPoint, ICoin> _Inner;
|
||||
|
||||
public UTXOByOutpoint(UTXOByOutpoint other)
|
||||
{
|
||||
_Inner = new Dictionary<OutPoint, Coin>(other._Inner);
|
||||
_Inner = new Dictionary<OutPoint, ICoin>(other._Inner);
|
||||
}
|
||||
public UTXOByOutpoint()
|
||||
{
|
||||
_Inner = new Dictionary<OutPoint, Coin>();
|
||||
_Inner = new Dictionary<OutPoint, ICoin>();
|
||||
}
|
||||
|
||||
internal bool ContainsKey(OutPoint outpoint)
|
||||
@ -30,12 +30,12 @@ namespace NBXplorer
|
||||
return _Inner.Remove(prevOut);
|
||||
}
|
||||
|
||||
internal bool TryAdd(OutPoint outpoint, Coin coin)
|
||||
internal bool TryAdd(OutPoint outpoint, ICoin coin)
|
||||
{
|
||||
return _Inner.TryAdd(outpoint, coin);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<OutPoint, Coin>> GetEnumerator()
|
||||
public IEnumerator<KeyValuePair<OutPoint, ICoin>> GetEnumerator()
|
||||
{
|
||||
return _Inner.GetEnumerator();
|
||||
}
|
||||
|
||||
@ -11,12 +11,15 @@ services:
|
||||
NBXPLORER_NETWORK: ${NBITCOIN_NETWORK:-regtest}
|
||||
NBXPLORER_BIND: 0.0.0.0:32838
|
||||
NBXPLORER_NOAUTH: 1
|
||||
NBXPLORER_CHAINS: "btc"
|
||||
NBXPLORER_CHAINS: "btc,lbtc"
|
||||
NBXPLORER_BTCRPCURL: http://bitcoind:43782/
|
||||
NBXPLORER_BTCNODEENDPOINT: bitcoind:39388
|
||||
NBXPLORER_LBTCRPCURL: http://elementsd-liquid:43783/
|
||||
NBXPLORER_LBTCNODEENDPOINT: elementsd-liquid:39389
|
||||
volumes:
|
||||
- "nbxplorer_datadir:/datadir"
|
||||
- "bitcoin_datadir:/root/.bitcoin"
|
||||
- "elementsd_liquid_datadir:/root/.elements"
|
||||
links:
|
||||
- bitcoind
|
||||
|
||||
@ -31,13 +34,45 @@ services:
|
||||
rpcbind=0.0.0.0:43782
|
||||
port=39388
|
||||
whitelist=0.0.0.0/0
|
||||
rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6
|
||||
expose:
|
||||
- "43782"
|
||||
- "39388"
|
||||
ports:
|
||||
- "43782:43782"
|
||||
- "39388:39388"
|
||||
volumes:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
|
||||
elementsd-liquid:
|
||||
restart: always
|
||||
container_name: btcpayserver_elementsd_liquid
|
||||
image: btcpayserver/elements:0.18.1.1
|
||||
environment:
|
||||
ELEMENTS_CHAIN: elementsregtest
|
||||
ELEMENTS_EXTRA_ARGS: |
|
||||
mainchainrpcport=43782
|
||||
mainchainrpchost=bitcoind
|
||||
mainchainrpcuser=liquid
|
||||
mainchainrpcpassword=liquid
|
||||
rpcport=19332
|
||||
rpcbind=0.0.0.0:19332
|
||||
rpcauth=liquid:c8bf1a8961d97f224cb21224aaa8235d$$402f4a8907683d057b8c58a42940b6e54d1638322a42986ae28ebb844e603ae6
|
||||
port=19444
|
||||
whitelist=0.0.0.0/0
|
||||
validatepegin=0
|
||||
initialfreecoins=210000000000000
|
||||
con_dyna_deploy_start=99999999999
|
||||
expose:
|
||||
- "19332"
|
||||
- "19444"
|
||||
ports:
|
||||
- "19332:19332"
|
||||
- "19444:19444"
|
||||
volumes:
|
||||
- "elementsd_liquid_datadir:/data"
|
||||
|
||||
volumes:
|
||||
nbxplorer_datadir:
|
||||
bitcoin_datadir:
|
||||
bitcoin_datadir:
|
||||
elementsd_liquid_datadir:
|
||||
|
||||
34
docs/API.md
34
docs/API.md
@ -35,6 +35,7 @@ NBXplorer does not index the whole blockchain, rather, it listens transactions a
|
||||
* [Manual pruning](#pruning)
|
||||
* [Generate a wallet](#wallet)
|
||||
* [Health check](#health)
|
||||
* [Liquid integration](#liquid)
|
||||
|
||||
## <a name="configuration"></a>Configuration
|
||||
|
||||
@ -206,6 +207,8 @@ Returns:
|
||||
}
|
||||
```
|
||||
|
||||
Note for liquid, `balanceChange` is an array of [AssetMoney](#liquid).
|
||||
|
||||
## <a name="address-transactions"></a>Query transactions associated to a specific address
|
||||
|
||||
Query all transactions of a tracked address. (Only work if you called the Track operation on this specific address)
|
||||
@ -317,6 +320,7 @@ Returns:
|
||||
"total": 210000000
|
||||
}
|
||||
```
|
||||
Note for liquid, the values are array of [AssetMoney](#liquid).
|
||||
|
||||
## <a name="gettransaction"></a>Get a transaction
|
||||
|
||||
@ -1121,3 +1125,33 @@ A endpoint that can be used without the need for [authentication](#auth) which w
|
||||
HTTP GET /health
|
||||
|
||||
It will output the state for each nodes in JSON, whose format might change in the future.
|
||||
|
||||
## <a name="liquid"></a>Liquid integration
|
||||
|
||||
NBXplorer supports liquid, the API is the same as all the other coins, except for the following:
|
||||
|
||||
* All references to `value` which normally contains an integer of the amount of the altcoin will instead output a JSON Object of type `AssetMoney`.
|
||||
* [When listing the transaction of a derivation scheme](#transactions), the `balanceChange` elements is instead a `JSON array of AssetMoney`.
|
||||
* [Get Balance](#balance) returns values as `JSON array of AssetMoney`.
|
||||
* [Get a new unused address](#unused) returns a confidential address. (See note below)
|
||||
* [Create Partially Signed Bitcoin Transaction](#psbt) is not supported.
|
||||
* [Update Partially Signed Bitcoin Transaction](#updatepsbt) is not supported
|
||||
* [Scan UTXO Set](#scanUtxoSet) is not supported.
|
||||
* Any sort of recovery is not supported.
|
||||
|
||||
The `AssetMoney` JSON format is:
|
||||
|
||||
```json
|
||||
{
|
||||
"assetId": "abc",
|
||||
"value": 123
|
||||
}
|
||||
```
|
||||
|
||||
The blinding key of the confidential address is derived directly from the `derivationScheme`.
|
||||
If the `scriptPubKey` `0/2` is generated, the blinding private key used by NBXplorer is the SHA256 of the scriptPubKey at `0/2/0`.
|
||||
|
||||
In order to send in and out of liquid, we advise you to rely on the RPC command line interface of the liquid deamon.
|
||||
For doing this you need to [Generate a wallet](#wallet) with `importAddressToRPC` and `savePrivateKeys` set to `true`.
|
||||
|
||||
Be careful to not expose your NBXplorer server on internet, your private keys can be [retrieved trivially](#getmetadata).
|
||||
1
elements-cli.ps1
Normal file
1
elements-cli.ps1
Normal file
@ -0,0 +1 @@
|
||||
docker exec -ti btcpayserver_elementsd_liquid elements-cli -rpcuser=liquid -rpcpassword=liquid -rpcport=19332 $args
|
||||
Loading…
Reference in New Issue
Block a user