Add Liquid Support (#198)

This commit is contained in:
Andrew Camilleri 2019-12-01 12:53:30 +01:00 committed by Nicolas Dorier
parent 5ee8b32a56
commit 649e82a9f7
27 changed files with 1167 additions and 140 deletions

View 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
}
}

View 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);
}
}
}

View 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; }
}
}

View File

@ -89,7 +89,7 @@ namespace NBXplorer.Models
get;
set;
}
public Money BalanceChange
public IMoney BalanceChange
{
get;
set;

View File

@ -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];
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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
{

View File

@ -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>

View 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);
}
}
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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">

View File

@ -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);

View File

@ -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

View File

@ -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()
{

View 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);
}
}
}

View File

@ -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]

View 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);
}
}
}

View File

@ -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",

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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; }

View File

@ -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; }
}
}
}

View File

@ -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();
}

View File

@ -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:

View File

@ -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
View File

@ -0,0 +1 @@
docker exec -ti btcpayserver_elementsd_liquid elements-cli -rpcuser=liquid -rpcpassword=liquid -rpcport=19332 $args