diff --git a/BTCPayServer.Tests/EclairTester.cs b/BTCPayServer.Tests/EclairTester.cs new file mode 100644 index 000000000..6454f3230 --- /dev/null +++ b/BTCPayServer.Tests/EclairTester.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using BTCPayServer.Eclair; + +namespace BTCPayServer.Tests +{ + public class EclairTester + { + ServerTester parent; + public EclairTester(ServerTester parent, string environmentName, string defaultRPC, string defaultHost) + { + this.parent = parent; + RPC = new EclairRPCClient(new Uri(parent.GetEnvironment(environmentName, defaultRPC)), parent.Network); + P2PHost = parent.GetEnvironment(environmentName + "_HOST", defaultHost); + } + + public EclairRPCClient RPC { get; } + public string P2PHost { get; } + + NodeInfo _NodeInfo; + public async Task GetNodeInfoAsync() + { + if (_NodeInfo != null) + return _NodeInfo; + var info = await RPC.GetInfoAsync(); + _NodeInfo = new NodeInfo(info.NodeId, P2PHost, info.Port); + return _NodeInfo; + } + + public NodeInfo GetNodeInfo() + { + return GetNodeInfoAsync().GetAwaiter().GetResult(); + } + } +} diff --git a/BTCPayServer.Tests/ServerTester.cs b/BTCPayServer.Tests/ServerTester.cs index 6f8994655..2956275ee 100644 --- a/BTCPayServer.Tests/ServerTester.cs +++ b/BTCPayServer.Tests/ServerTester.cs @@ -1,4 +1,5 @@ using BTCPayServer.Controllers; +using System.Linq; using BTCPayServer.Models.AccountViewModels; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,6 +17,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Threading; +using BTCPayServer.Eclair; namespace BTCPayServer.Tests { @@ -56,9 +58,42 @@ namespace BTCPayServer.Tests PayTester.Port = int.Parse(GetEnvironment("TESTS_PORT", Utils.FreeTcpPort().ToString())); PayTester.HostName = GetEnvironment("TESTS_HOSTNAME", "127.0.0.1"); PayTester.Start(); + + MerchantEclair = new EclairTester(this, "TEST_ECLAIR1", "http://127.0.0.1:30992/", "eclair1"); + CustomerEclair = new EclairTester(this, "TEST_ECLAIR2", "http://127.0.0.1:30993/", "eclair2"); } - private string GetEnvironment(string variable, string defaultValue) + + /// + /// This will setup a channel going from customer to merchant + /// + public void PrepareLightning() + { + PrepareLightningAsync().GetAwaiter().GetResult(); + } + + public async Task PrepareLightningAsync() + { + // Activate segwit + var blockCount = ExplorerNode.GetBlockCountAsync(); + // Fetch node info, but that in cache + var merchant = MerchantEclair.GetNodeInfoAsync(); + var customer = CustomerEclair.GetNodeInfoAsync(); + var channels = CustomerEclair.RPC.ChannelsAsync(); + var connect = CustomerEclair.RPC.ConnectAsync(merchant.Result); + await Task.WhenAll(blockCount, merchant, customer, channels, connect); + + // Mine until segwit is activated + if (blockCount.Result <= 432) + { + ExplorerNode.Generate(433 - blockCount.Result); + } + } + + public EclairTester MerchantEclair { get; set; } + public EclairTester CustomerEclair { get; set; } + + internal string GetEnvironment(string variable, string defaultValue) { var var = Environment.GetEnvironmentVariable(variable); return String.IsNullOrEmpty(var) ? defaultValue : var; diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 0dc74735b..0f1571467 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -22,6 +22,7 @@ using BTCPayServer.Data; using Microsoft.EntityFrameworkCore; using BTCPayServer.Services.Rates; using Microsoft.Extensions.Caching.Memory; +using BTCPayServer.Eclair; namespace BTCPayServer.Tests { @@ -115,6 +116,24 @@ namespace BTCPayServer.Tests } } + [Fact] + public void CanUseLightMoney() + { + var light = LightMoney.MilliSatoshis(1); + Assert.Equal("0.00000000001", light.ToString()); + } + + [Fact] + public void CanSendLightningPayment() + { + + using (var tester = ServerTester.Create()) + { + tester.Start(); + tester.PrepareLightning(); + } + } + [Fact] public void CanUseServerInitiatedPairingCode() { diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 048c3dcd0..5651869bf 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -1,5 +1,8 @@ version: "3" +# Run `docker-compose up dev` for bootstrapping your development environment +# Doing so will expose eclair API, NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run, +# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment. services: tests: @@ -17,9 +20,24 @@ services: - "80" links: - nbxplorer + - eclair1 + - eclair2 extra_hosts: - "tests:127.0.0.1" + # The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services + dev: + image: nicolasdorier/docker-bitcoin:0.15.0.1 + environment: + BITCOIN_EXTRA_ARGS: | + regtest=1 + connect=bitcoind:39388 + links: + - bitcoind + - nbxplorer + - eclair1 + - eclair2 + nbxplorer: image: nicolasdorier/nbxplorer:1.0.0.29 ports: @@ -39,28 +57,73 @@ services: - bitcoind - postgres - eclair: - image: nicolasdorier/docker-bitcoin:0.15.0.1 + eclair1: + image: acinq/eclair:latest + environment: + JAVA_OPTS: > + -Xmx512m + -Declair.printToConsole + -Declair.bitcoind.host=bitcoind + -Declair.bitcoind.rpcport=43782 + -Declair.bitcoind.rpcuser=ceiwHEbqWI83 + -Declair.bitcoind.rpcpassword=DwubwWsoo3 + -Declair.bitcoind.zmq=tcp://bitcoind:29000 + -Declair.chain=regtest + -Declair.api.binding-ip=0.0.0.0 + links: + - bitcoind + ports: + - "30992:8080" # api port + expose: + - "9735" # server port + - "8080" # api port + + eclair2: + image: acinq/eclair:latest + environment: + JAVA_OPTS: > + -Xmx512m + -Declair.printToConsole + -Declair.bitcoind.host=bitcoind + -Declair.bitcoind.rpcport=43782 + -Declair.bitcoind.rpcuser=ceiwHEbqWI83 + -Declair.bitcoind.rpcpassword=DwubwWsoo3 + -Declair.bitcoind.zmq=tcp://bitcoind:29000 + -Declair.chain=regtest + -Declair.api.binding-ip=0.0.0.0 + links: + - bitcoind + ports: + - "30993:8080" # api port + expose: + - "9735" # server port + - "8080" # api port bitcoind: container_name: btcpayserver_dev_bitcoind image: nicolasdorier/docker-bitcoin:0.15.0.1 - ports: - - "43782:43782" - - "39388:39388" environment: BITCOIN_EXTRA_ARGS: | rpcuser=ceiwHEbqWI83 rpcpassword=DwubwWsoo3 regtest=1 + server=1 rpcport=43782 port=39388 whitelist=0.0.0.0/0 + zmqpubrawblock=tcp://0.0.0.0:29000 + zmqpubrawtx=tcp://0.0.0.0:29000 + txindex=1 + ports: + - "43782:43782" # RPC expose: - - "43782" - - "39388" + - "43782" # RPC + - "39388" # P2P + - "29000" # zmq postgres: image: postgres:9.6.5 ports: - "39372:5432" + expose: + - "5432" diff --git a/BTCPayServer/Eclair/AllChannelResponse.cs b/BTCPayServer/Eclair/AllChannelResponse.cs new file mode 100644 index 000000000..c166cbc7e --- /dev/null +++ b/BTCPayServer/Eclair/AllChannelResponse.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Eclair +{ + public class AllChannelResponse + { + public string ShortChannelId { get; set; } + public string NodeId1 { get; set; } + public string NodeId2 { get; set; } + } +} diff --git a/BTCPayServer/Eclair/ChannelResponse.cs b/BTCPayServer/Eclair/ChannelResponse.cs new file mode 100644 index 000000000..94971a028 --- /dev/null +++ b/BTCPayServer/Eclair/ChannelResponse.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Eclair +{ + public class ChannelResponse + { + + public string NodeId { get; set; } + public string ChannelId { get; set; } + public string State { get; set; } + } + public static class ChannelStates + { + public const string WAIT_FOR_FUNDING_CONFIRMED = "WAIT_FOR_FUNDING_CONFIRMED"; + + public const string NORMAL = "NORMAL"; + } +} diff --git a/BTCPayServer/Eclair/EclairRPCClient.cs b/BTCPayServer/Eclair/EclairRPCClient.cs new file mode 100644 index 000000000..be34fb673 --- /dev/null +++ b/BTCPayServer/Eclair/EclairRPCClient.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using NBitcoin; +using NBitcoin.JsonConverters; +using NBitcoin.RPC; + +namespace BTCPayServer.Eclair +{ + public class EclairRPCClient + { + public EclairRPCClient(Uri address, Network network) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + if (network == null) + throw new ArgumentNullException(nameof(network)); + Address = address; + Network = network; + } + + public Network Network { get; private set; } + + + public GetInfoResponse GetInfo() + { + return GetInfoAsync().GetAwaiter().GetResult(); + } + + public Task GetInfoAsync() + { + return SendCommandAsync(new RPCRequest("getinfo", new object[] { })); + } + + public async Task SendCommandAsync(RPCRequest request, bool throwIfRPCError = true) + { + var response = await SendCommandAsync(request, throwIfRPCError); + return Serializer.ToObject(response.ResultString, Network); + } + + public async Task SendCommandAsync(RPCRequest request, bool throwIfRPCError = true) + { + RPCResponse response = null; + HttpWebRequest webRequest = response == null ? CreateWebRequest() : null; + if (response == null) + { + var writer = new StringWriter(); + request.WriteJSON(writer); + writer.Flush(); + var json = writer.ToString(); + var bytes = Encoding.UTF8.GetBytes(json); +#if !(PORTABLE || NETCORE) + webRequest.ContentLength = bytes.Length; +#endif + var dataStream = await webRequest.GetRequestStreamAsync().ConfigureAwait(false); + await dataStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await dataStream.FlushAsync().ConfigureAwait(false); + dataStream.Dispose(); + } + WebResponse webResponse = null; + WebResponse errorResponse = null; + try + { + webResponse = response == null ? await webRequest.GetResponseAsync().ConfigureAwait(false) : null; + response = response ?? RPCResponse.Load(await ToMemoryStreamAsync(webResponse.GetResponseStream()).ConfigureAwait(false)); + + if (throwIfRPCError) + response.ThrowIfError(); + } + catch (WebException ex) + { + if (ex.Response == null || ex.Response.ContentLength == 0 || + !ex.Response.ContentType.Equals("application/json", StringComparison.Ordinal)) + throw; + errorResponse = ex.Response; + response = RPCResponse.Load(await ToMemoryStreamAsync(errorResponse.GetResponseStream()).ConfigureAwait(false)); + if (throwIfRPCError) + response.ThrowIfError(); + } + finally + { + if (errorResponse != null) + { + errorResponse.Dispose(); + errorResponse = null; + } + if (webResponse != null) + { + webResponse.Dispose(); + webResponse = null; + } + } + return response; + } + + public AllChannelResponse[] AllChannels() + { + return AllChannelsAsync().GetAwaiter().GetResult(); + } + + public async Task AllChannelsAsync() + { + return await SendCommandAsync(new RPCRequest("allchannels", new object[] { })).ConfigureAwait(false); + } + + public string[] Channels() + { + return ChannelsAsync().GetAwaiter().GetResult(); + } + + public async Task ChannelsAsync() + { + return await SendCommandAsync(new RPCRequest("channels", new object[] { })).ConfigureAwait(false); + } + + public void Close(string channelId) + { + CloseAsync(channelId).GetAwaiter().GetResult(); + } + + public async Task CloseAsync(string channelId) + { + if (channelId == null) + throw new ArgumentNullException(nameof(channelId)); + try + { + await SendCommandAsync(new RPCRequest("close", new object[] { channelId })).ConfigureAwait(false); + } + catch (RPCException ex) when (ex.Message == "closing already in progress") + { + + } + } + + public ChannelResponse Channel(string channelId) + { + return ChannelAsync(channelId).GetAwaiter().GetResult(); + } + + public async Task ChannelAsync(string channelId) + { + if (channelId == null) + throw new ArgumentNullException(nameof(channelId)); + return await SendCommandAsync(new RPCRequest("channel", new object[] { channelId })).ConfigureAwait(false); + } + + public string[] AllNodes() + { + return AllNodesAsync().GetAwaiter().GetResult(); + } + + public async Task AllNodesAsync() + { + return await SendCommandAsync(new RPCRequest("allnodes", new object[] { })).ConfigureAwait(false); + } + + public Uri Address { get; private set; } + + private HttpWebRequest CreateWebRequest() + { + var webRequest = (HttpWebRequest)WebRequest.Create(Address.AbsoluteUri); + webRequest.ContentType = "application/json"; + webRequest.Method = "POST"; + return webRequest; + } + + + private async Task ToMemoryStreamAsync(Stream stream) + { + MemoryStream ms = new MemoryStream(); + await stream.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; + return ms; + } + + public string Open(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null) + { + return OpenAsync(node, fundingSatoshi, pushAmount).GetAwaiter().GetResult(); + } + + public string Connect(NodeInfo node) + { + return ConnectAsync(node).GetAwaiter().GetResult(); + } + + public async Task ConnectAsync(NodeInfo node) + { + if (node == null) + throw new ArgumentNullException(nameof(node)); + return (await SendCommandAsync(new RPCRequest("connect", new object[] { node.NodeId, node.Host, node.Port })).ConfigureAwait(false)).ResultString; + } + + public string Receive(LightMoney amount, string description = null) + { + return ReceiveAsync(amount, description).GetAwaiter().GetResult(); + } + + public async Task ReceiveAsync(LightMoney amount, string description = null) + { + if (amount == null) + throw new ArgumentNullException(nameof(amount)); + List args = new List(); + args.Add(amount.MilliSatoshi); + if(description != null) + { + args.Add(description); + } + return (await SendCommandAsync(new RPCRequest("receive", args.ToArray())).ConfigureAwait(false)).ResultString; + } + + public async Task OpenAsync(NodeInfo node, Money fundingSatoshi, LightMoney pushAmount = null) + { + if (fundingSatoshi == null) + throw new ArgumentNullException(nameof(fundingSatoshi)); + if (node == null) + throw new ArgumentNullException(nameof(node)); + pushAmount = pushAmount ?? LightMoney.Zero; + + var result = await SendCommandAsync(new RPCRequest("open", new object[] { node.NodeId, node.Host, node.Port, fundingSatoshi.Satoshi, pushAmount.MilliSatoshi })); + + return result.ResultString; + } + + + } +} diff --git a/BTCPayServer/Eclair/GetInfoResponse.cs b/BTCPayServer/Eclair/GetInfoResponse.cs new file mode 100644 index 000000000..7b73c6335 --- /dev/null +++ b/BTCPayServer/Eclair/GetInfoResponse.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NBitcoin; +using Newtonsoft.Json; + +namespace BTCPayServer.Eclair +{ + public class GetInfoResponse + { + public string NodeId { get; set; } + public string Alias { get; set; } + public int Port { get; set; } + public uint256 ChainHash { get; set; } + public int BlockHeight { get; set; } + } +} diff --git a/BTCPayServer/Eclair/LightMoney.cs b/BTCPayServer/Eclair/LightMoney.cs new file mode 100644 index 000000000..e19ede84e --- /dev/null +++ b/BTCPayServer/Eclair/LightMoney.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Eclair +{ + public enum LightMoneyUnit : ulong + { + BTC = 100000000000, + MilliBTC = 100000000, + Bit = 100000, + Satoshi = 1000, + MilliSatoshi = 1 + } + + public class LightMoney : IComparable, IComparable, IEquatable + { + + + // for decimal.TryParse. None of the NumberStyles' composed values is useful for bitcoin style + private const NumberStyles BitcoinStyle = + NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite + | NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint; + + + /// + /// Parse a bitcoin amount (Culture Invariant) + /// + /// + /// + /// + public static bool TryParse(string bitcoin, out LightMoney nRet) + { + nRet = null; + + decimal value; + if (!decimal.TryParse(bitcoin, BitcoinStyle, CultureInfo.InvariantCulture, out value)) + { + return false; + } + + try + { + nRet = new LightMoney(value, LightMoneyUnit.BTC); + return true; + } + catch (OverflowException) + { + return false; + } + } + + /// + /// Parse a bitcoin amount (Culture Invariant) + /// + /// + /// + public static LightMoney Parse(string bitcoin) + { + LightMoney result; + if (TryParse(bitcoin, out result)) + { + return result; + } + throw new FormatException("Impossible to parse the string in a bitcoin amount"); + } + + long _MilliSatoshis; + public long MilliSatoshi + { + get + { + return _MilliSatoshis; + } + // used as a central point where long.MinValue checking can be enforced + private set + { + CheckLongMinValue(value); + _MilliSatoshis = value; + } + } + + /// + /// Get absolute value of the instance + /// + /// + public LightMoney Abs() + { + var a = this; + if (a < LightMoney.Zero) + a = -a; + return a; + } + + public LightMoney(int satoshis) + { + MilliSatoshi = satoshis; + } + + public LightMoney(uint satoshis) + { + MilliSatoshi = satoshis; + } + + public LightMoney(long satoshis) + { + MilliSatoshi = satoshis; + } + + public LightMoney(ulong satoshis) + { + // overflow check. + // ulong.MaxValue is greater than long.MaxValue + checked + { + MilliSatoshi = (long)satoshis; + } + } + + public LightMoney(decimal amount, LightMoneyUnit unit) + { + // sanity check. Only valid units are allowed + CheckMoneyUnit(unit, "unit"); + checked + { + var satoshi = amount * (long)unit; + MilliSatoshi = (long)satoshi; + } + } + + + /// + /// Split the Money in parts without loss + /// + /// The number of parts (must be more than 0) + /// The splitted money + public IEnumerable Split(int parts) + { + if (parts <= 0) + throw new ArgumentOutOfRangeException("Parts should be more than 0", "parts"); + long remain; + long result = DivRem(_MilliSatoshis, parts, out remain); + + for (int i = 0; i < parts; i++) + { + yield return LightMoney.Satoshis(result + (remain > 0 ? 1 : 0)); + remain--; + } + } + + private static long DivRem(long a, long b, out long result) + { + result = a % b; + return a / b; + } + + public static LightMoney FromUnit(decimal amount, LightMoneyUnit unit) + { + return new LightMoney(amount, unit); + } + + /// + /// Convert Money to decimal (same as ToDecimal) + /// + /// + /// + public decimal ToUnit(LightMoneyUnit unit) + { + CheckMoneyUnit(unit, "unit"); + // overflow safe because (long / int) always fit in decimal + // decimal operations are checked by default + return (decimal)MilliSatoshi / (int)unit; + } + /// + /// Convert Money to decimal (same as ToUnit) + /// + /// + /// + public decimal ToDecimal(LightMoneyUnit unit) + { + return ToUnit(unit); + } + + public static LightMoney Coins(decimal coins) + { + // overflow safe. + // decimal operations are checked by default + return new LightMoney(coins * (ulong)LightMoneyUnit.BTC, LightMoneyUnit.MilliBTC); + } + + public static LightMoney Bits(decimal bits) + { + // overflow safe. + // decimal operations are checked by default + return new LightMoney(bits * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC); + } + + public static LightMoney Cents(decimal cents) + { + // overflow safe. + // decimal operations are checked by default + return new LightMoney(cents * (ulong)LightMoneyUnit.Bit, LightMoneyUnit.MilliBTC); + } + + public static LightMoney Satoshis(decimal sats) + { + return new LightMoney(sats * (ulong)LightMoneyUnit.Satoshi, LightMoneyUnit.MilliBTC); + } + + public static LightMoney Satoshis(ulong sats) + { + return new LightMoney(sats); + } + + public static LightMoney Satoshis(long sats) + { + return new LightMoney(sats); + } + + public static LightMoney MilliSatoshis(long msats) + { + return new LightMoney(msats); + } + + public static LightMoney MilliSatoshis(ulong msats) + { + return new LightMoney(msats); + } + + #region IEquatable Members + + public bool Equals(LightMoney other) + { + if (other == null) + return false; + return _MilliSatoshis.Equals(other._MilliSatoshis); + } + + public int CompareTo(LightMoney other) + { + if (other == null) + return 1; + return _MilliSatoshis.CompareTo(other._MilliSatoshis); + } + + #endregion + + #region IComparable Members + + public int CompareTo(object obj) + { + if (obj == null) + return 1; + LightMoney m = obj as LightMoney; + if (m != null) + return _MilliSatoshis.CompareTo(m._MilliSatoshis); +#if !(PORTABLE || NETCORE) + return _MilliSatoshis.CompareTo(obj); +#else + return _Satoshis.CompareTo((long)obj); +#endif + } + + #endregion + + public static LightMoney operator -(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return new LightMoney(checked(left._MilliSatoshis - right._MilliSatoshis)); + } + public static LightMoney operator -(LightMoney left) + { + if (left == null) + throw new ArgumentNullException("left"); + return new LightMoney(checked(-left._MilliSatoshis)); + } + public static LightMoney operator +(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return new LightMoney(checked(left._MilliSatoshis + right._MilliSatoshis)); + } + public static LightMoney operator *(int left, LightMoney right) + { + if (right == null) + throw new ArgumentNullException("right"); + return LightMoney.Satoshis(checked(left * right._MilliSatoshis)); + } + + public static LightMoney operator *(LightMoney right, int left) + { + if (right == null) + throw new ArgumentNullException("right"); + return LightMoney.Satoshis(checked(right._MilliSatoshis * left)); + } + public static LightMoney operator *(long left, LightMoney right) + { + if (right == null) + throw new ArgumentNullException("right"); + return LightMoney.Satoshis(checked(left * right._MilliSatoshis)); + } + public static LightMoney operator *(LightMoney right, long left) + { + if (right == null) + throw new ArgumentNullException("right"); + return LightMoney.Satoshis(checked(left * right._MilliSatoshis)); + } + + public static LightMoney operator /(LightMoney left, long right) + { + if (left == null) + throw new ArgumentNullException("left"); + return new LightMoney(checked(left._MilliSatoshis / right)); + } + + public static bool operator <(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return left._MilliSatoshis < right._MilliSatoshis; + } + public static bool operator >(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return left._MilliSatoshis > right._MilliSatoshis; + } + public static bool operator <=(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return left._MilliSatoshis <= right._MilliSatoshis; + } + public static bool operator >=(LightMoney left, LightMoney right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + return left._MilliSatoshis >= right._MilliSatoshis; + } + + public static implicit operator LightMoney(long value) + { + return new LightMoney(value); + } + public static implicit operator LightMoney(int value) + { + return new LightMoney(value); + } + + public static implicit operator LightMoney(uint value) + { + return new LightMoney(value); + } + + public static implicit operator LightMoney(ulong value) + { + return new LightMoney(checked((long)value)); + } + + public static implicit operator long(LightMoney value) + { + return value.MilliSatoshi; + } + + public static implicit operator ulong(LightMoney value) + { + return checked((ulong)value.MilliSatoshi); + } + + public static implicit operator LightMoney(string value) + { + return LightMoney.Parse(value); + } + + public override bool Equals(object obj) + { + LightMoney item = obj as LightMoney; + if (item == null) + return false; + return _MilliSatoshis.Equals(item._MilliSatoshis); + } + public static bool operator ==(LightMoney a, LightMoney b) + { + if (Object.ReferenceEquals(a, b)) + return true; + if (((object)a == null) || ((object)b == null)) + return false; + return a._MilliSatoshis == b._MilliSatoshis; + } + + public static bool operator !=(LightMoney a, LightMoney b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return _MilliSatoshis.GetHashCode(); + } + + + /// + /// Returns a culture invariant string representation of Bitcoin amount + /// + /// + public override string ToString() + { + return ToString(false, false); + } + + /// + /// Returns a culture invariant string representation of Bitcoin amount + /// + /// True if show + for a positive amount + /// True if trim excess zeroes + /// + public string ToString(bool fplus, bool trimExcessZero = true) + { + var fmt = string.Format("{{0:{0}{1}B}}", + (fplus ? "+" : null), + (trimExcessZero ? "2" : "11")); + return string.Format(BitcoinFormatter.Formatter, fmt, _MilliSatoshis); + } + + + static LightMoney _Zero = new LightMoney(0); + public static LightMoney Zero + { + get + { + return _Zero; + } + } + + internal class BitcoinFormatter : IFormatProvider, ICustomFormatter + { + public static readonly BitcoinFormatter Formatter = new BitcoinFormatter(); + + public object GetFormat(Type formatType) + { + return formatType == typeof(ICustomFormatter) ? this : null; + } + + public string Format(string format, object arg, IFormatProvider formatProvider) + { + if (!this.Equals(formatProvider)) + { + return null; + } + var i = 0; + var plus = format[i] == '+'; + if (plus) + i++; + int decPos = 0; + if (int.TryParse(format.Substring(i, 1), out decPos)) + { + i++; + } + var unit = format[i]; + var unitToUseInCalc = LightMoneyUnit.BTC; + switch (unit) + { + case 'B': + unitToUseInCalc = LightMoneyUnit.BTC; + break; + } + var val = Convert.ToDecimal(arg) / (long)unitToUseInCalc; + var zeros = new string('0', decPos); + var rest = new string('#', 11 - decPos); + var fmt = plus && val > 0 ? "+" : string.Empty; + + fmt += "{0:0" + (decPos > 0 ? "." + zeros + rest : string.Empty) + "}"; + return string.Format(CultureInfo.InvariantCulture, fmt, val); + } + } + + /// + /// Tell if amount is almost equal to this instance + /// + /// + /// more or less amount + /// true if equals, else false + public bool Almost(LightMoney amount, LightMoney dust) + { + if (amount == null) + throw new ArgumentNullException("amount"); + if (dust == null) + throw new ArgumentNullException("dust"); + return (amount - this).Abs() <= dust; + } + + /// + /// Tell if amount is almost equal to this instance + /// + /// + /// error margin (between 0 and 1) + /// true if equals, else false + public bool Almost(LightMoney amount, decimal margin) + { + if (amount == null) + throw new ArgumentNullException("amount"); + if (margin < 0.0m || margin > 1.0m) + throw new ArgumentOutOfRangeException("margin", "margin should be between 0 and 1"); + var dust = LightMoney.Satoshis((decimal)this.MilliSatoshi * margin); + return Almost(amount, dust); + } + + public static LightMoney Min(LightMoney a, LightMoney b) + { + if (a == null) + throw new ArgumentNullException("a"); + if (b == null) + throw new ArgumentNullException("b"); + if (a <= b) + return a; + return b; + } + + public static LightMoney Max(LightMoney a, LightMoney b) + { + if (a == null) + throw new ArgumentNullException("a"); + if (b == null) + throw new ArgumentNullException("b"); + if (a >= b) + return a; + return b; + } + + private static void CheckLongMinValue(long value) + { + if (value == long.MinValue) + throw new OverflowException("satoshis amount should be greater than long.MinValue"); + } + + private static void CheckMoneyUnit(LightMoneyUnit value, string paramName) + { + var typeOfMoneyUnit = typeof(LightMoneyUnit); + if (!Enum.IsDefined(typeOfMoneyUnit, value)) + { + throw new ArgumentException("Invalid value for MoneyUnit", paramName); + } + } + + #region IComparable Members + + int IComparable.CompareTo(object obj) + { + return this.CompareTo(obj); + } + + #endregion + } +} diff --git a/BTCPayServer/Eclair/NodeInfo.cs b/BTCPayServer/Eclair/NodeInfo.cs new file mode 100644 index 000000000..71c17383f --- /dev/null +++ b/BTCPayServer/Eclair/NodeInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Eclair +{ + public class NodeInfo + { + public NodeInfo(string nodeId, string host, int port) + { + if (host == null) + throw new ArgumentNullException(nameof(host)); + if (nodeId == null) + throw new ArgumentNullException(nameof(nodeId)); + Port = port; + Host = host; + NodeId = nodeId; + } + public string NodeId { get; private set; } + public string Host { get; private set; } + public int Port { get; private set; } + } +}