Exemple #1
0
        public async Task <OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest,
                                                            CancellationToken cancellation = default(CancellationToken))
        {
            try
            {
                var result = await _eclairClient.Open(openChannelRequest.NodeInfo.NodeId,
                                                      openChannelRequest.ChannelAmount.Satoshi
                                                      , null,
                                                      Convert.ToInt64(openChannelRequest.FeeRate.SatoshiPerByte), null, cancellation);

                if (result.Contains("created channel", StringComparison.OrdinalIgnoreCase))
                {
                    var channelId = result.Replace("created channel", "").Trim();
                    var channel   = await _eclairClient.Channel(channelId, cancellation);

                    switch (channel.State)
                    {
                    case "WAIT_FOR_OPEN_CHANNEL":
                    case "WAIT_FOR_ACCEPT_CHANNEL":
                    case "WAIT_FOR_FUNDING_CREATED":
                    case "WAIT_FOR_FUNDING_SIGNED":
                    case "WAIT_FOR_FUNDING_LOCKED":
                    case "WAIT_FOR_FUNDING_CONFIRMED":
                        return(new OpenChannelResponse(OpenChannelResult.NeedMoreConf));
                    }
                }

                if (result.Contains("couldn't publish funding tx", StringComparison.OrdinalIgnoreCase))
                {
                    return(new OpenChannelResponse(OpenChannelResult.CannotAffordFunding));
                }

                return(new OpenChannelResponse(OpenChannelResult.Ok));
            }
            catch (Exception e) when(e.Message.Contains("not connected", StringComparison.OrdinalIgnoreCase) || e.Message.Contains("no connection to peer", StringComparison.OrdinalIgnoreCase) || e.Message.Contains("not found", StringComparison.OrdinalIgnoreCase))
            {
                return(new OpenChannelResponse(OpenChannelResult.PeerNotConnected));
            }
            catch (Exception e) when(e.Message.Contains("insufficient funds", StringComparison.OrdinalIgnoreCase))
            {
                return(new OpenChannelResponse(OpenChannelResult.CannotAffordFunding));
            }
        }
        public async Task <OpenChannelResponse> OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation)
        {
            var result = await _ptarmiganClient.OpenChannel(openChannelRequest.NodeInfo.NodeId,
                                                            openChannelRequest.ChannelAmount.Satoshi, 0,
                                                            Convert.ToInt64(openChannelRequest.FeeRate.FeePerK));

            if (result.Error != null)
            {
                if (result.Error.Message.Contains("not connected"))
                {
                    return(new OpenChannelResponse(OpenChannelResult.PeerNotConnected));
                }

                if (result.Error.Message.Contains("channel already opened"))
                {
                    return(new OpenChannelResponse(OpenChannelResult.NeedMoreConf));
                }

                return(new OpenChannelResponse(OpenChannelResult.CannotAffordFunding));
            }

            var info = await _ptarmiganClient.GetInfo(cancellation);

            var node = info.Result.Peers.Find(peer => peer.NodeId == openChannelRequest.NodeInfo.NodeId.ToString());

            if (node == null)
            {
                return(new OpenChannelResponse(OpenChannelResult.CannotAffordFunding));
            }

            if (node.Status == "none")
            {
                return(new OpenChannelResponse(OpenChannelResult.AlreadyExists));
            }

            if (node.Status == "establishing")
            {
                return(new OpenChannelResponse(OpenChannelResult.NeedMoreConf));
            }

            return(new OpenChannelResponse(OpenChannelResult.Ok));
        }
Exemple #3
0
        async Task <OpenChannelResponse> ILightningClient.OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation)
        {
retry:
            try
            {
                await FundChannelAsync(openChannelRequest, cancellation);
            }
            catch (LightningRPCException ex) when(ex.Code == CLightningErrorCode.STILL_SYNCING_BITCOIN)
            {
                await Task.Delay(1000, cancellation);

                goto retry;
            }
            catch (LightningRPCException ex) when(ex.Code == CLightningErrorCode.CANNOT_AFFORD)
            {
                return(new OpenChannelResponse(OpenChannelResult.CannotAffordFunding));
            }
            catch (LightningRPCException ex) when(ex.Code == CLightningErrorCode.CONNECT_ALL_ADDRESSES_FAILED ||
                                                  ex.Code == CLightningErrorCode.CONNECT_NO_KNOWN_ADDRESS ||
                                                  ex.Message == "Peer not connected" ||
                                                  ex.Message == "Unknown peer" ||
                                                  ex.Message == "Unable to connect, no address known for peer")
            {
                return(new OpenChannelResponse(OpenChannelResult.PeerNotConnected));
            }
            catch (LightningRPCException ex) when(ex.Message.Contains("CHANNELD_AWAITING_LOCKIN"))
            {
                return(new OpenChannelResponse(OpenChannelResult.NeedMoreConf));
            }
            catch (LightningRPCException ex) when(
                ex.Message.Contains("CHANNELD_NORMAL") ||
                ex.Message.Contains("CHANNELD_SHUTTING_DOWN") ||
                ex.Message.Contains("CLOSINGD_SIGEXCHANGE") ||
                ex.Message.Contains("CLOSINGD_COMPLETE") ||
                ex.Message.Contains("AWAITING_UNILATERAL") ||
                ex.Message.Contains("FUNDING_SPEND_SEEN") ||
                ex.Message.Contains("ONCHAIN"))
            {
                return(new OpenChannelResponse(OpenChannelResult.AlreadyExists));
            }
            return(new OpenChannelResponse(OpenChannelResult.Ok));
        }
        public IActionResult OpenChannel([FromBody] OpenChannelRequest request)
        {
            var height = Services.BlockExplorerService.GetCurrentHeight();
            BobServerChannelNegotiation session = CreateBobServerChannelNegotiation(request.CycleStart);
            var cycle = session.GetCycle();

            if (!cycle.IsInPhase(CyclePhase.TumblerChannelEstablishment, height))
            {
                return(BadRequest("incorrect-phase"));
            }
            var fee = Services.FeeService.GetFeeRate();

            try
            {
                session.ReceiveBobEscrowInformation(request);
                if (!Repository.MarkUsedNonce(request.CycleStart, request.Nonce))
                {
                    return(BadRequest("nonce-already-used"));
                }
                var txOut = session.BuildEscrowTxOut();
                var tx    = Services.WalletService.FundTransaction(txOut, fee);
                if (tx == null)
                {
                    return(BadRequest("tumbler-insufficient-funds"));
                }

                var escrowTumblerLabel = $"Cycle {session.GetCycle().Start} Tumbler Escrow";
                Services.BlockExplorerService.Track(escrowTumblerLabel, txOut.ScriptPubKey);
                Services.BroadcastService.Broadcast(escrowTumblerLabel, tx);
                var promiseServerSession = session.SetSignedTransaction(tx);
                Repository.Save(cycle.Start, promiseServerSession);

                var redeem   = Services.WalletService.GenerateAddress($"Cycle {cycle.Start} Tumbler Redeem");
                var redeemTx = promiseServerSession.CreateRedeemTransaction(fee, redeem.ScriptPubKey);
                Services.TrustedBroadcastService.Broadcast($"Cycle {session.GetCycle().Start} Tumbler Redeem (locked until: {redeemTx.Transaction.LockTime})", redeemTx);
                return(Json(promiseServerSession.EscrowedCoin));
            }
            catch (PuzzleException)
            {
                return(BadRequest("incorrect-voucher"));
            }
        }
Exemple #5
0
        public async Task <OpenChannelResponse> OpenChannel(OpenChannelRequest req, CancellationToken cancellation = default)
        {
            OpenChannelResult result;

            try
            {
                await _client.OpenChannel(req.NodeInfo, req.ChannelAmount, req.FeeRate, cancellation);

                result = OpenChannelResult.Ok;
            }
            catch (LNbankClient.LNbankApiException ex)
            {
                switch (ex.ErrorCode)
                {
                case "channel-already-exists":
                    result = OpenChannelResult.AlreadyExists;
                    break;

                case "cannot-afford-funding":
                    result = OpenChannelResult.CannotAffordFunding;
                    break;

                case "need-more-confirmations":
                    result = OpenChannelResult.NeedMoreConf;
                    break;

                case "peer-not-connected":
                    result = OpenChannelResult.PeerNotConnected;
                    break;

                default:
                    throw new NotSupportedException("Unknown OpenChannelResult");
                }
            }

            return(new OpenChannelResponse(result));
        }
Exemple #6
0
        public ActionResult <ulong> OpenChannel(string cryptoCode, [FromBody] OpenChannelRequest o)
        {
            var n       = _networkProvider.GetByCryptoCode(cryptoCode.ToLowerInvariant());
            var peerMan = _peerManagerProvider.TryGetPeerManager(n);
            var ps      = peerMan.GetPeerNodeIds(_pool);

            if (ps.All(p => p != o.TheirNetworkKey))
            {
                return(BadRequest($"Unknown peer {o.TheirNetworkKey}. Make sure you are already connected to the peer."));
            }
            var chanMan     = peerMan.ChannelManager;
            var maybeConfig = o.OverrideConfig;
            var userId      = RandomUtils.GetUInt64();

            try
            {
                if (maybeConfig is null)
                {
                    chanMan.CreateChannel(o.TheirNetworkKey, o.ChannelValueSatoshis, o.PushMSat,
                                          userId);
                }
                else
                {
                    var v = maybeConfig.Value;
                    chanMan.CreateChannel(o.TheirNetworkKey, o.ChannelValueSatoshis, o.PushMSat,
                                          userId, in v);
                }
                peerMan.ProcessEvents();
            }
            catch (FFIException ex)
            {
                return(BadRequest(ex.Message));
            }

            return(Ok(userId));
        }
 /// <summary>
 /// Returns Id for the opened channel
 /// </summary>
 /// <param name="request"></param>
 /// <returns></returns>
 public Task <ulong> OpenChannelAsync(OpenChannelRequest request)
 {
     return(RequestAsync <ulong>($"/v1/channel/{cryptoCode}", HttpMethod.Post, request));
 }
Exemple #8
0
        /// <inheritdoc />
        public async Task <ScriptCoin> OpenChannelAsync(OpenChannelRequest request)
        {
            ScriptCoin result = await this.serverAddress.AppendPathSegment("api/v1/tumblers/0/channels/").PostJsonAsync(request).ReceiveJson <ScriptCoin>();

            return(result);
        }
Exemple #9
0
 Task <OpenChannelResponse> ILightningClient.OpenChannel(OpenChannelRequest openChannelRequest, CancellationToken cancellation)
 {
     throw new NotSupportedException();
 }
Exemple #10
0
        public async Task <uint160.MutableUint160> BeginOpenChannel(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] OpenChannelRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            var height = Services.BlockExplorerService.GetCurrentHeight();

            if (Repository.IsUsed(request.CycleStart, request.Nonce))
            {
                throw new ActionResultException(BadRequest("duplicate-query"));
            }
            var cycle = GetCycle(request.CycleStart);

            if (!cycle.IsInPhase(CyclePhase.TumblerChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }
            var fee = await Services.FeeService.GetFeeRateAsync();

            try
            {
                if (!Parameters.VoucherKey.PublicKey.Verify(request.Signature, NBitcoin.Utils.ToBytes((uint)request.CycleStart, true), request.Nonce))
                {
                    throw new ActionResultException(BadRequest("incorrect-voucher"));
                }
                if (!Repository.MarkUsedNonce(request.CycleStart, request.Nonce))
                {
                    throw new ActionResultException(BadRequest("duplicate-query"));
                }

                var escrowKey = new Key();

                var escrow = new EscrowScriptPubKeyParameters();
                escrow.LockTime  = cycle.GetTumblerLockTime();
                escrow.Receiver  = request.EscrowKey;
                escrow.Initiator = escrowKey.PubKey;
                var channelId = new uint160(RandomUtils.GetBytes(20));
                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Asked to open channel");
                var txOut = new TxOut(Parameters.Denomination, escrow.ToScript().WitHash.ScriptPubKey.Hash);

                var unused = Services.WalletService.FundTransactionAsync(txOut, fee)
                             .ContinueWith(async(Task <Transaction> task) =>
                {
                    try
                    {
                        var tx          = await task.ConfigureAwait(false);
                        var correlation = new CorrelationId(channelId);
                        Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, tx.GetHash(), correlation);

                        //Logging/Broadcast per funding TX one time
                        if (Repository.MarkUsedNonce(cycle.Start, new uint160(tx.GetHash().ToBytes().Take(20).ToArray())))
                        {
                            var bobCount = Parameters.CountEscrows(tx, Client.Identity.Bob);
                            Logs.Tumbler.LogInformation($"Cycle {cycle.Start} channel created {tx.GetHash()} with {bobCount} users");
                            await Services.BroadcastService.BroadcastAsync(tx).ConfigureAwait(false);
                        }

                        await Services.BlockExplorerService.TrackAsync(txOut.ScriptPubKey).ConfigureAwait(false);
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, txOut.ScriptPubKey, correlation);
                        var coin    = tx.Outputs.AsCoins().First(o => o.ScriptPubKey == txOut.ScriptPubKey && o.TxOut.Value == txOut.Value);
                        var session = new PromiseServerSession(Parameters.CreatePromiseParamaters());
                        var redeem  = await Services.WalletService.GenerateAddressAsync().ConfigureAwait(false);
                        session.ConfigureEscrowedCoin(channelId, coin.ToScriptCoin(escrow.ToScript()), escrowKey, redeem.ScriptPubKey);
                        var redeemTx = session.CreateRedeemTransaction(fee);
                        Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerRedeem, correlation, redeemTx);
                        Repository.Save(cycle.Start, session);
                        Tracker.AddressCreated(cycle.Start, TransactionType.TumblerRedeem, redeem.ScriptPubKey, correlation);
                    }
                    catch (Exception ex)
                    {
                        Logs.Tumbler.LogCritical(new EventId(), ex, "Error during escrow transaction callback");
                    }
                });
                return(channelId.AsBitcoinSerializable());
            }
            catch (NotEnoughFundsException ex)
            {
                Logs.Tumbler.LogInformation(ex.Message);
                throw new ActionResultException(BadRequest("tumbler-insufficient-funds"));
            }
        }
Exemple #11
0
 public ScriptCoin OpenChannel(OpenChannelRequest request)
 {
     return(OpenChannelAsync(request).GetAwaiter().GetResult());
 }
Exemple #12
0
        private void SubmitButtonOnClick(object sender, EventArgs e)
        {
            ClearErrorData();
            if (typeof(TRequestMessage) == typeof(ConnectPeerRequest))
            {
                var fullAddress = (string)Ws.Cells[_fieldToRow["addr"], EndColumn].Value2;
                if (fullAddress == null)
                {
                    return;
                }
                var addressParts = fullAddress.Split('@');

                string pubkey;
                string host;
                switch (addressParts.Length)
                {
                case 0:
                    return;

                case 2:
                    pubkey = addressParts[0];
                    host   = addressParts[1];
                    break;

                default:
                    Utilities.DisplayError(ErrorData, "Error", "Invalid address, must be pubkey@ip:host");
                    return;
                }

                var  permanent = Ws.Cells[_fieldToRow["perm"], EndColumn].Value2;
                bool perm      = permanent == null || (bool)permanent;

                var address = new LightningAddress {
                    Host = host, Pubkey = pubkey
                };
                var request = new ConnectPeerRequest {
                    Addr = address, Perm = perm
                };
                try
                {
                    _lApp.LndClient.ConnectPeer(request);
                    _lApp.Refresh(SheetNames.Peers);
                    ClearForm();
                }
                catch (RpcException rpcException)
                {
                    DisplayError(rpcException);
                }
            }
            else if (typeof(TRequestMessage) == typeof(SendCoinsRequest))
            {
                var request = new SendCoinsRequest
                {
                    Addr   = Ws.Cells[_fieldToRow["addr"], EndColumn].Value2,
                    Amount = (long)Ws.Cells[_fieldToRow["amount"], EndColumn].Value2
                };
                var satPerByte = Ws.Cells[_fieldToRow["sat_per_byte"], EndColumn].Value2;
                if (satPerByte == null)
                {
                    satPerByte = 0;
                }
                if (satPerByte > 0)
                {
                    request.SatPerByte = satPerByte;
                }

                var targetConf = Ws.Cells[_fieldToRow["target_conf"], EndColumn].Value2;
                if (targetConf == null)
                {
                    targetConf = 0;
                }
                if (targetConf > 0)
                {
                    request.TargetConf = targetConf;
                }

                try
                {
                    _lApp.LndClient.SendCoins(request);
                    _lApp.Refresh(SheetNames.Transactions);
                    ClearForm();
                }
                catch (RpcException rpcException)
                {
                    DisplayError(rpcException);
                }
            }
            else if (typeof(TRequestMessage) == typeof(OpenChannelRequest))
            {
                var localFundingAmount = long.Parse(GetValue("local_funding_amount"));
                var minConfs           = int.Parse(GetValue("min_confs"));
                var minHtlcMsat        = long.Parse(GetValue("min_htlc_msat"));
                var nodePubKeyString   = GetValue("node_pubkey");
                var isPrivate          = true;
                if (bool.TryParse(GetValue("private"), out var result))
                {
                    isPrivate = result;
                }
                var pushSat        = long.Parse(GetValue("push_sat"));
                var remoteCsvDelay = uint.Parse(GetValue("remote_csv_delay"));
                var satPerByte     = long.Parse(GetValue("sat_per_byte"));
                var targetConf     = int.Parse(GetValue("target_conf"));
                var request        = new OpenChannelRequest
                {
                    LocalFundingAmount = localFundingAmount,
                    MinConfs           = minConfs,
                    MinHtlcMsat        = minHtlcMsat,
                    NodePubkeyString   = nodePubKeyString,
                    Private            = isPrivate,
                    PushSat            = pushSat
                };
                if (remoteCsvDelay > 0)
                {
                    request.RemoteCsvDelay = remoteCsvDelay;
                }
                if (satPerByte > 0)
                {
                    request.SatPerByte = satPerByte;
                }
                if (targetConf > 0)
                {
                    request.TargetConf = targetConf;
                }

                try
                {
                    _lApp.LndClient.OpenChannel(request);
                    _lApp.Refresh(SheetNames.Channels);
                    ClearForm();
                }
                catch (RpcException rpcException)
                {
                    DisplayError(rpcException);
                }
            }
            else
            {
                var request   = new TRequestMessage();
                var rowNumber = _dataStartRow;
                foreach (var field in Fields)
                {
                    Range dataCell = Ws.Cells[rowNumber, EndColumn];
                    var   value    = dataCell.Value2;
                    if (!string.IsNullOrWhiteSpace(value?.ToString()))
                    {
                        field.Accessor.SetValue(request, dataCell.Value2);
                    }
                }

                _query(request);
            }
        }
        public ScriptCoinModel OpenChannel(
            [ModelBinder(BinderType = typeof(TumblerParametersModelBinder))]
            ClassicTumblerParameters tumblerId,
            [FromBody] OpenChannelRequest request)
        {
            if (tumblerId == null)
            {
                throw new ArgumentNullException("tumblerId");
            }
            var height = Services.BlockExplorerService.GetCurrentHeight();
            var cycle  = GetCycle(request.CycleStart);

            if (!cycle.IsInPhase(CyclePhase.TumblerChannelEstablishment, height))
            {
                throw new ActionResultException(BadRequest("invalid-phase"));
            }
            var fee = Services.FeeService.GetFeeRate();

            try
            {
                if (!Parameters.VoucherKey.Verify(request.Signature, NBitcoin.Utils.ToBytes((uint)request.CycleStart, true), request.Nonce))
                {
                    throw new ActionResultException(BadRequest("incorrect-voucher"));
                }
                if (!Repository.MarkUsedNonce(request.CycleStart, request.Nonce))
                {
                    throw new ActionResultException(BadRequest("nonce-already-used"));
                }

                var escrowKey = new Key();

                var escrow = new EscrowScriptPubKeyParameters();
                escrow.LockTime  = cycle.GetTumblerLockTime();
                escrow.Receiver  = request.EscrowKey;
                escrow.Initiator = escrowKey.PubKey;

                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Asked to open channel");
                var txOut              = new TxOut(Parameters.Denomination, escrow.ToScript().Hash);
                var tx                 = Services.WalletService.FundTransaction(txOut, fee);
                var correlation        = escrow.GetCorrelation();
                var escrowTumblerLabel = $"Cycle {cycle.Start} Tumbler Escrow";
                Services.BlockExplorerService.Track(txOut.ScriptPubKey);

                Tracker.AddressCreated(cycle.Start, TransactionType.TumblerEscrow, txOut.ScriptPubKey, correlation);
                Tracker.TransactionCreated(cycle.Start, TransactionType.TumblerEscrow, tx.GetHash(), correlation);
                Logs.Tumbler.LogInformation($"Cycle {cycle.Start} Channel created " + tx.GetHash());

                var coin = tx.Outputs.AsCoins().First(o => o.ScriptPubKey == txOut.ScriptPubKey && o.TxOut.Value == txOut.Value);

                var session = new PromiseServerSession(Parameters.CreatePromiseParamaters());
                var redeem  = Services.WalletService.GenerateAddress();
                session.ConfigureEscrowedCoin(coin.ToScriptCoin(escrow.ToScript()), escrowKey, redeem.ScriptPubKey);
                Repository.Save(cycle.Start, session);

                Services.BroadcastService.Broadcast(tx);

                var redeemTx = session.CreateRedeemTransaction(fee);
                Tracker.AddressCreated(cycle.Start, TransactionType.TumblerRedeem, redeem.ScriptPubKey, correlation);
                Services.TrustedBroadcastService.Broadcast(cycle.Start, TransactionType.TumblerRedeem, correlation, redeemTx);
                return(new ScriptCoinModel(session.EscrowedCoin));
            }
            catch (PuzzleException)
            {
                throw new ActionResultException(BadRequest("incorrect-voucher"));
            }
            catch (NotEnoughFundsException ex)
            {
                Logs.Tumbler.LogInformation(ex.Message);
                throw new ActionResultException(BadRequest("tumbler-insufficient-funds"));
            }
        }
 public uint160 BeginOpenChannel(OpenChannelRequest request)
 {
     return(BeginOpenChannelAsync(request).GetAwaiter().GetResult());
 }
Exemple #15
0
        public void CanConvertJsonTypes()
        {
            var invoice =
                "lnbc1p0vhtzvpp5akajlfqdj6ek7eeh4kae6gc05fz9j99n8jadatqt4fmlwwxwx4zsnp4q2uqg2j52gxtxg5d0v928h5pll95ynsaek2csgfg26tvuzydgjrwgdqhdehjqer9wd3hy6tsw35k7msna3vtx";
            var paymentRequest = PaymentRequest.Parse(invoice);
            var resp           = new InvoiceResponse()
            {
                Invoice = paymentRequest.ResultValue
            };
            var j = JsonSerializer.Serialize(resp);

            JsonSerializer.Deserialize <InvoiceResponse>(j);
            var invoiceResponseRaw = "{\"invoice\":\"lnbc1p0vma42pp5t2v5ehyay3x9g8769gqkrhmdlqjq0kc8ksqfxu3xjw7s2y96jegqnp4q2uqg2j52gxtxg5d0v928h5pll95ynsaek2csgfg26tvuzydgjrwgdqhdehjqer9wd3hy6tsw35k7ms3xhenl\"}";

            JsonSerializer.Deserialize <InvoiceResponse>(invoiceResponseRaw);


            var conf = UserConfig.GetDefault();

            j = JsonSerializer.Serialize(conf);
            var v = JsonSerializer.Deserialize <UserConfig>(j);

            Assert.Equal(conf.ChannelOptions.AnnouncedChannel, v.ChannelOptions.AnnouncedChannel);

            var openChannelRequest = new OpenChannelRequest();

            j = JsonSerializer.Serialize(openChannelRequest);
            var conv = JsonSerializer.Deserialize <OpenChannelRequest>(j);

            Assert.Equal(openChannelRequest.OverrideConfig, conv.OverrideConfig);

            // with custom config
            openChannelRequest.OverrideConfig = UserConfig.GetDefault();
            j = JsonSerializer.Serialize(openChannelRequest);
            // Don't know why but we must specify option here.
            var opt = new JsonSerializerOptions();

            opt.Converters.Add(new NullableStructConverterFactory());
            conv = JsonSerializer.Deserialize <OpenChannelRequest>(j, opt);

            Assert.True(conv.OverrideConfig.HasValue);
            Assert.Equal(openChannelRequest.OverrideConfig.Value.ChannelOptions.AnnouncedChannel, conv.OverrideConfig.Value.ChannelOptions.AnnouncedChannel);
            j =
                "{\"TheirNetworkKey\":\"024a8b7fc86957537bb365cc0242255582d3d40a5532489f67e700a89bcac2f010\",\"ChannelValueSatoshis\":100000,\"PushMSat\":1000,\"OverrideConfig\":null}";
            openChannelRequest = JsonSerializer.Deserialize <OpenChannelRequest>(j, new JsonSerializerOptions()
            {
                Converters = { new HexPubKeyConverter() }
            });
            Assert.Equal(100000UL, openChannelRequest.ChannelValueSatoshis);
            Assert.Equal(1000UL, openChannelRequest.PushMSat);
            Assert.NotNull(openChannelRequest.TheirNetworkKey);

            // wallet info
            j =
                "{\"DerivationStrategy\":\"tpubDBte1PdX36pt167AFbKpHwFJqZAVVRuJSadZ49LdkX5JJbJCNDc8JQ7w5GdaDZcUXm2SutgwjRuufwq4q4soePD4fPKSZCUhqDDarKRCUen\",\"OnChainBalanceSatoshis\":0}";
            var networkProvider = new NRustLightningNetworkProvider(NetworkType.Regtest);
            var btcNetwork      = networkProvider.GetByCryptoCode("BTC");
            var walletInfo      = JsonSerializer.Deserialize <WalletInfo>(j, new JsonSerializerOptions {
                Converters = { new DerivationStrategyJsonConverter(btcNetwork.NbXplorerNetwork.DerivationStrategyFactory) }
            });

            // FeatureBit
            var featureBit = FeatureBit.TryParse("0b000000100100000100000000").ResultValue;
            var opts       = new JsonSerializerOptions()
            {
                Converters = { new FeatureBitJsonConverter() }
            };

            j = JsonSerializer.Serialize(featureBit, opts);
            Assert.Contains("prettyPrint", j);
            var featureBit2 = JsonSerializer.Deserialize <FeatureBit>(j, opts);

            Assert.Equal(featureBit, featureBit2);
        }