Skip to content

Instantly share code, notes, and snippets.

@knocte
Created June 9, 2020 06:31
Show Gist options
  • Select an option

  • Save knocte/d30ca67e3b7fc32def1a85dbf43564d4 to your computer and use it in GitHub Desktop.

Select an option

Save knocte/d30ca67e3b7fc32def1a85dbf43564d4 to your computer and use it in GitHub Desktop.

Revisions

  1. knocte created this gist Jun 9, 2020.
    918 changes: 918 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,918 @@
    From 3caa604be0918f8b264984451348735f30faab2b Mon Sep 17 00:00:00 2001
    From: "Andres G. Aragoneses" <[email protected]>
    Date: Mon, 8 Jun 2020 00:50:05 +0800
    Subject: [PATCH 1/3] Backend,Frontend.Console: improve B's API so that F.C.'s
    doesn't need refs

    Improve the API design of GWallet.Backend project so that
    GWallet.Frontend.Console doesn't need to reference directly
    some sub-dependencies of GWallet.Backend, such as NBitcoin
    and DotNetLightning.
    ---
    src/GWallet.Backend/GWallet.Backend.fsproj | 1 +
    src/GWallet.Backend/PublicKey.fs | 11 +
    .../UtxoCoin/Lightning/Lightning.fs | 265 +++++++++++++-----
    .../UtxoCoin/Lightning/SerializedChannel.fs | 21 +-
    .../UtxoCoin/UtxoCoinAccount.fs | 12 +-
    .../GWallet.Frontend.Console.fsproj | 12 -
    src/GWallet.Frontend.Console/Program.fs | 76 ++---
    .../UserInteraction.fs | 16 +-
    src/GWallet.Frontend.Console/packages.config | 2 -
    9 files changed, 256 insertions(+), 160 deletions(-)
    create mode 100644 src/GWallet.Backend/PublicKey.fs

    diff --git a/src/GWallet.Backend/GWallet.Backend.fsproj b/src/GWallet.Backend/GWallet.Backend.fsproj
    index 678a79fc..c841a96c 100644
    --- a/src/GWallet.Backend/GWallet.Backend.fsproj
    +++ b/src/GWallet.Backend/GWallet.Backend.fsproj
    @@ -61,6 +61,7 @@ <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.mic
    <Compile Include="Config.fs" />
    <Compile Include="Networking.fs" />
    <Compile Include="JsonRpcTcpClient.fs" />
    + <Compile Include="PublicKey.fs" />
    <Compile Include="IBlockchainFeeInfo.fs" />
    <Compile Include="TransferAmount.fs" />
    <Compile Include="Infrastructure.fs" />
    diff --git a/src/GWallet.Backend/PublicKey.fs b/src/GWallet.Backend/PublicKey.fs
    new file mode 100644
    index 00000000..74842efa
    --- /dev/null
    +++ b/src/GWallet.Backend/PublicKey.fs
    @@ -0,0 +1,11 @@
    +
    +namespace GWallet.Backend
    +
    +type PublicKey(pubKey: string, currency: Currency) =
    + do
    + if currency = Currency.BTC || currency = Currency.LTC then
    + NBitcoin.PubKey pubKey |> ignore
    +
    + override __.ToString() =
    + pubKey
    +
    diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/Lightning.fs b/src/GWallet.Backend/UtxoCoin/Lightning/Lightning.fs
    index ee1a23ee..5f4d2785 100644
    --- a/src/GWallet.Backend/UtxoCoin/Lightning/Lightning.fs
    +++ b/src/GWallet.Backend/UtxoCoin/Lightning/Lightning.fs
    @@ -25,6 +25,45 @@ open FSharp.Core


    module Lightning =
    +
    + type Connection =
    + {
    + Client: TcpClient
    +
    + // ideally we would mark the members below as 'internal' only, but: https://stackoverflow.com/q/62274013/544947
    + Init: Init
    + Peer: Peer
    + }
    +
    + type IChannelToBeOpened =
    + abstract member ConfirmationsRequired: uint32 with get
    +
    + type FullChannel =
    + {
    + // ideally we would mark all the members as 'internal' only, but: https://stackoverflow.com/q/62274013/544947
    + InboundChannel: AcceptChannel
    + OutboundChannel: Channel
    + Peer: Peer
    + }
    + interface IChannelToBeOpened with
    + member self.ConfirmationsRequired
    + with get(): uint32 =
    + self.InboundChannel.MinimumDepth.Value
    +
    + type PotentialChannel internal (channelKeysSeed, temporaryChannelId) =
    + member val internal KeysSeed = channelKeysSeed with get
    + member val internal TemporaryId = temporaryChannelId with get
    +
    + type ChannelCreationDetails =
    + {
    + Client: TcpClient
    + Password: Ref<string>
    +
    + // ideally we would mark the members below as 'internal' only, but: https://stackoverflow.com/q/62274013/544947
    + ChannelInfo: PotentialChannel
    + FullChannel: FullChannel
    + }
    +
    let private hex = DataEncoders.HexEncoder()

    let GetIndexOfDestinationInOutputSeq (dest: IDestination) (outputs: seq<IndexedTxOut>): TxOutIndex =
    @@ -56,7 +95,7 @@ module Lightning =
    During: string
    }

    - type LNError =
    + type internal LNInternalError =
    | DNLError of ErrorMessage
    | ConnectError of seq<SocketException>
    | StringError of StringErrorInner
    @@ -94,10 +133,13 @@ module Lightning =
    else
    "Error: peer disconnected for unknown reason"

    + type LNError internal (error: LNInternalError) =
    + member val internal Inner = error with get
    + member val Message = error.Message with get

    let internal ReadExactAsync (stream: NetworkStream)
    (numberBytesToRead: int)
    - : Async<Result<array<byte>, LNError>> =
    + : Async<Result<array<byte>, LNInternalError>> =
    let buf: array<byte> = Array.zeroCreate numberBytesToRead
    let rec read buf totalBytesRead = async {
    let! bytesRead =
    @@ -117,7 +159,7 @@ module Lightning =
    }
    read buf 0

    - let ReadAsync (keyRepo: DefaultKeyRepository) (peer: Peer) (stream: NetworkStream): Async<Result<PeerCommand, LNError>> =
    + let internal ReadAsync (keyRepo: DefaultKeyRepository) (peer: Peer) (stream: NetworkStream): Async<Result<PeerCommand, LNInternalError>> =
    match peer.ChannelEncryptor.GetNoiseStep() with
    | ActTwo ->
    async {
    @@ -216,8 +258,12 @@ module Lightning =
    let channelConfig = CreateChannelConfig account
    Channel.CreateCurried channelConfig

    - type ChannelEnvironment = { Account: UtxoCoin.NormalUtxoAccount; NodeIdForResponder: NodeId; KeyRepo: DefaultKeyRepository }
    - type Connection = { Init: Init; Peer: Peer; Client: TcpClient }
    + type ChannelEnvironment internal (account: UtxoCoin.NormalUtxoAccount,
    + nodeIdForResponder: NodeId,
    + keyRepo: DefaultKeyRepository) =
    + member val internal Account = account with get
    + member val internal NodeIdForResponder = nodeIdForResponder with get
    + member val internal KeyRepo = keyRepo with get

    let internal Send (msg: ILightningMsg) (peer: Peer) (stream: NetworkStream): Async<Peer> =
    async {
    @@ -242,10 +288,12 @@ module Lightning =
    context
    Infrastructure.ReportWarningMessage msg

    - let ConnectAndHandshake ({ Account = _; NodeIdForResponder = nodeIdForResponder; KeyRepo = keyRepo }: ChannelEnvironment)
    + let ConnectAndHandshake (channelEnv: ChannelEnvironment)
    (channelCounterpartyIP: IPEndPoint)
    : Async<Result<Connection, LNError>> =
    async {
    + let nodeIdForResponder = channelEnv.NodeIdForResponder
    + let keyRepo = channelEnv.KeyRepo
    let responderId = channelCounterpartyIP :> EndPoint |> PeerId
    let initialPeer = Peer.CreateOutbound(responderId, nodeIdForResponder, keyRepo.NodeSecret.PrivateKey)
    let act1, peerEncryptor = PeerChannelEncryptor.getActOne initialPeer.ChannelEncryptor
    @@ -264,7 +312,7 @@ module Lightning =
    return Error <| ConnectError socketExceptions
    }
    match connectRes with
    - | Error err -> return Error err
    + | Error err -> return Error <| LNError err
    | Ok () ->
    let stream = client.GetStream()
    do! stream.WriteAsync(act1, 0, act1.Length) |> Async.AwaitTask
    @@ -275,8 +323,8 @@ module Lightning =
    match act2Res with
    | Error (PeerDisconnected abruptly) ->
    ReportDisconnection channelCounterpartyIP nodeIdForResponder abruptly "receiving act 2"
    - return Error (PeerDisconnected abruptly)
    - | Error err -> return Error err
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error err -> return Error <| LNError err
    | Ok act2 ->
    let actThree, receivedAct2Peer =
    match Peer.executeCommand sentAct1Peer act2 with
    @@ -300,21 +348,22 @@ module Lightning =
    | Error (PeerDisconnected abruptly) ->
    let context = SPrintF1 "receiving init message while connecting, our init: %A" plainInit
    ReportDisconnection channelCounterpartyIP nodeIdForResponder abruptly context
    - return Error (PeerDisconnected abruptly)
    - | Error err -> return Error err
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error err -> return Error <| LNError err
    | Ok init ->
    return
    match Peer.executeCommand sentInitPeer init with
    | Ok (ReceivedInit (newInit, _) as evt::[]) ->
    let peer = Peer.applyEvent sentInitPeer evt
    - Ok { Init = newInit; Peer = peer; Client = client }
    + Ok <| { Init = newInit; Peer = peer; Client = client }
    | Ok _ ->
    failwith "not one good ReceivedInit event"
    | Error peerError ->
    failwith <| SPrintF1 "couldn't parse init: %s" peerError.Message
    }

    - let rec ReadUntilChannelMessage (keyRepo: DefaultKeyRepository, peer: Peer, stream: NetworkStream): Async<Result<Peer * IChannelMsg, LNError>> =
    + let rec internal ReadUntilChannelMessage (keyRepo: DefaultKeyRepository, peer: Peer, stream: NetworkStream)
    + : Async<Result<Peer * IChannelMsg, LNInternalError>> =
    async {
    let! channelMsgRes = ReadAsync keyRepo peer stream
    match channelMsgRes with
    @@ -356,14 +405,18 @@ module Lightning =
    DefaultFinalScriptPubKey = account |> CreatePayoutScript
    }

    - let GetAcceptChannel ({ Account = account; NodeIdForResponder = nodeIdForResponder; KeyRepo = keyRepo }: ChannelEnvironment)
    - ({ Init = receivedInit; Peer = receivedInitPeer; Client = client }: Connection)
    + let GetAcceptChannel (potentialChannel: PotentialChannel)
    + (channelEnv: ChannelEnvironment)
    + (connection: Connection)
    (channelCapacity: TransferAmount)
    (metadata: TransactionMetadata)
    (password: unit -> string)
    (balance: decimal)
    - (temporaryChannelId: ChannelId)
    - : Async<Result<AcceptChannel * Channel * Peer, LNError>> =
    + : Async<Result<FullChannel, LNError>> =
    + let client = connection.Client
    + let receivedInit = connection.Init
    + let receivedInitPeer = connection.Peer
    + let account = channelEnv.Account
    let fundingTxProvider (dest: IDestination, amount: Money, _: FeeRatePerKw) =
    let transferAmount = TransferAmount (amount.ToDecimal MoneyUnit.BTC, balance, Currency.BTC)
    Debug.Assert (
    @@ -378,6 +431,8 @@ module Lightning =
    (fundingTransaction |> FinalizedTx, GetIndexOfDestinationInOutputSeq dest outputs) |> Ok

    let fundingAmount = Money (channelCapacity.ValueToSend, MoneyUnit.BTC)
    + let nodeIdForResponder = channelEnv.NodeIdForResponder
    + let keyRepo = channelEnv.KeyRepo
    let channelKeys, localParams = GetLocalParams true fundingAmount nodeIdForResponder account keyRepo

    async {
    @@ -386,7 +441,7 @@ module Lightning =
    let initFunder =
    {
    InputInitFunder.PushMSat = LNMoney.MilliSatoshis 0L
    - TemporaryChannelId = temporaryChannelId
    + TemporaryChannelId = potentialChannel.TemporaryId
    FundingSatoshis = fundingAmount
    InitFeeRatePerKw = feeEstimator.GetEstSatPer1000Weight <| ConfirmationTarget.Normal
    FundingTxFeeRatePerKw = feeEstimator.GetEstSatPer1000Weight <| ConfirmationTarget.Normal
    @@ -420,29 +475,33 @@ module Lightning =
    | Error (PeerDisconnected abruptly) ->
    let context = SPrintF1 "receiving accept_channel, our open_channel == %A" openChanMsg
    ReportDisconnection channelCounterpartyIP nodeIdForResponder abruptly context
    - return Error (PeerDisconnected abruptly)
    - | Error errorMsg -> return Error errorMsg
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error err -> return Error <| LNError err
    | Ok (receivedOpenChanReplyPeer, chanMsg) ->
    match chanMsg with
    | :? AcceptChannel as acceptChannel ->
    - return Ok (acceptChannel, sentOpenChan, receivedOpenChanReplyPeer)
    + return Ok ({
    + InboundChannel = acceptChannel
    + OutboundChannel = sentOpenChan
    + Peer = receivedOpenChanReplyPeer
    + })
    | _ ->
    - return Error <| StringError {
    + return Error <| LNError (StringError {
    Msg = SPrintF1 "channel message is not accept channel: %s" (chanMsg.GetType().Name)
    During = "waiting for accept_channel"
    - }
    + })
    | Ok evtList ->
    return failwith <| SPrintF1 "event was not a single NewOutboundChannelStarted, it was: %A" evtList
    | Error channelError ->
    - return Error <| DNLChannelError channelError
    + return Error <| LNError (DNLChannelError channelError)
    }

    - let ContinueFromAcceptChannel (keyRepo: DefaultKeyRepository)
    - (acceptChannel: AcceptChannel)
    - (sentOpenChan: Channel)
    - (stream: NetworkStream)
    - (receivedOpenChanReplyPeer: Peer)
    - : Async<Result<string * Channel, LNError>> =
    + let internal ContinueFromAcceptChannel (keyRepo: DefaultKeyRepository)
    + (acceptChannel: AcceptChannel)
    + (sentOpenChan: Channel)
    + (stream: NetworkStream)
    + (receivedOpenChanReplyPeer: Peer)
    + : Async<Result<string * Channel, LNInternalError>> =
    async {
    match Channel.executeCommand sentOpenChan (ApplyAcceptChannel acceptChannel) with
    | Ok (ChannelEvent.WeAcceptedAcceptChannel(fundingCreated, _) as evt::[]) ->
    @@ -458,9 +517,9 @@ module Lightning =
    | Error (PeerDisconnected abruptly) ->
    let context = SPrintF1 "receiving funding_created, their accept_channel == %A" acceptChannel
    ReportDisconnection channelCounterpartyIP nodeIdForResponder abruptly context
    - return Error (PeerDisconnected abruptly)
    - | Error errorMsg ->
    - return Error errorMsg
    + return Error <| PeerDisconnected abruptly
    + | Error error ->
    + return Error error
    | Ok (_, chanMsg) ->
    match chanMsg with
    | :? FundingSigned as fundingSigned ->
    @@ -503,7 +562,7 @@ module Lightning =
    return Error <| StringError innerError
    }

    - let GetSeedAndRepo (random: Random): uint256 * DefaultKeyRepository * ChannelId =
    + let private GetSeedAndRepo (random: Random): uint256 * DefaultKeyRepository * ChannelId =
    let channelKeysSeedBytes = Array.zeroCreate 32
    random.NextBytes channelKeysSeedBytes
    let channelKeysSeed = uint256 channelKeysSeedBytes
    @@ -516,6 +575,12 @@ module Lightning =
    |> ChannelId
    channelKeysSeed, keyRepo, temporaryChannelId

    + let GenerateNewPotentialChannelDetails account (channelCounterpartyPubKey: PublicKey) (random: Random) =
    + let channelKeysSeed, keyRepo, temporaryChannelId = GetSeedAndRepo random
    + let pubKey = NBitcoin.PubKey (channelCounterpartyPubKey.ToString())
    + let channelEnv = ChannelEnvironment(account, NodeId pubKey, keyRepo)
    + PotentialChannel(channelKeysSeed, temporaryChannelId), channelEnv
    +
    let GetNewChannelFilename(): string =
    SerializedChannel.ChannelFilePrefix
    // this offset is the approximate time this feature was added (making filenames shorter)
    @@ -523,21 +588,28 @@ module Lightning =
    + SerializedChannel.ChannelFileEnding

    let ContinueFromAcceptChannelAndSave (account: UtxoCoin.NormalUtxoAccount)
    - (channelKeysSeed: uint256)
    (channelCounterpartyIP: IPEndPoint)
    - (acceptChannel: AcceptChannel)
    - (chan: Channel)
    - (stream: NetworkStream)
    - (peer: Peer)
    + (channelDetails: ChannelCreationDetails)
    : Async<Result<string, LNError>> = // TxId of Funding Transaction is returned
    async {
    - let keyRepo = SerializedChannel.UIntToKeyRepo channelKeysSeed
    - let! res = ContinueFromAcceptChannel keyRepo acceptChannel chan stream peer
    + let stream = channelDetails.Client.GetStream()
    + let keyRepo = SerializedChannel.UIntToKeyRepo channelDetails.ChannelInfo.KeysSeed
    + let! res =
    + ContinueFromAcceptChannel keyRepo
    + channelDetails.FullChannel.InboundChannel
    + channelDetails.FullChannel.OutboundChannel
    + stream
    + channelDetails.FullChannel.Peer
    match res with
    - | Error errorMsg -> return Error errorMsg
    + | Error error -> return Error <| LNError error
    | Ok (fundingTxId, receivedFundingSignedChan) ->
    let fileName = GetNewChannelFilename()
    - SerializedChannel.Save account receivedFundingSignedChan channelKeysSeed channelCounterpartyIP acceptChannel.MinimumDepth fileName
    + SerializedChannel.Save account
    + receivedFundingSignedChan
    + channelDetails.ChannelInfo.KeysSeed
    + channelCounterpartyIP
    + channelDetails.FullChannel.InboundChannel.MinimumDepth
    + fileName
    Infrastructure.LogDebug <| SPrintF1 "Channel saved to %s" fileName
    return Ok fundingTxId
    }
    @@ -568,8 +640,8 @@ module Lightning =
    }

    type NotReadyReason = {
    - CurrentConfirmations: BlockHeightOffset32
    - NeededConfirmations: BlockHeightOffset32
    + CurrentConfirmations: uint32
    + NeededConfirmations: uint32
    }

    type internal ChannelMessageOrDeepEnough =
    @@ -601,8 +673,8 @@ module Lightning =
    }
    else
    NotReady {
    - CurrentConfirmations = details.ConfirmationsCount
    - NeededConfirmations = details.SerializedChannel.MinSafeDepth
    + CurrentConfirmations = details.ConfirmationsCount.Value
    + NeededConfirmations = details.SerializedChannel.MinSafeDepth.Value
    }

    let internal GetFundingLockedMsg (channel: Channel) (channelCommand: ChannelCommand): Channel * FundingLocked =
    @@ -699,7 +771,7 @@ module Lightning =
    let! channelCommand = channelCommandAction
    let keyRepo = SerializedChannel.UIntToKeyRepo channelKeysSeed
    let channelEnvironment: ChannelEnvironment =
    - { Account = details.Account; NodeIdForResponder = notReestablishedChannel.RemoteNodeId; KeyRepo = keyRepo }
    + ChannelEnvironment(details.Account, notReestablishedChannel.RemoteNodeId, keyRepo)
    let! connectionRes = ConnectAndHandshake channelEnvironment channelCounterpartyIP
    match connectionRes with
    | Error err -> return Error err
    @@ -726,8 +798,8 @@ module Lightning =
    match msgRes with
    | Error (PeerDisconnected abruptly) ->
    ReportDisconnection channelCounterpartyIP nodeIdForResponder abruptly "receiving channel_reestablish or funding_locked"
    - return Error (PeerDisconnected abruptly)
    - | Error errorMsg -> return Error errorMsg
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error error -> return Error <| LNError error
    | Ok (receivedChannelReestablishPeer, chanMsg) ->
    let! fundingLockedRes =
    match chanMsg with
    @@ -762,7 +834,7 @@ module Lightning =
    return Error <| StringError { Msg = msg; During = "reception of reply to channel_reestablish" }
    }
    match fundingLockedRes with
    - | Error errorMsg -> return Error errorMsg
    + | Error error -> return Error <| LNError error
    | Ok fundingLocked ->
    match Channel.executeCommand channelWithFundingLockedSent (ApplyFundingLocked fundingLocked) with
    | Ok ((ChannelEvent.BothFundingLocked _) as evt::[]) ->
    @@ -776,13 +848,19 @@ module Lightning =
    connection.Client.Dispose()
    return Ok (UsableChannel txIdHex)
    | Error channelError ->
    - return Error <| DNLChannelError channelError
    + return Error <| LNError (DNLChannelError channelError)
    | Ok (evt::[]) ->
    let msg = SPrintF1 "expected event BothFundingLocked, is %s" (evt.GetType().Name)
    - return Error <| StringError { Msg = msg; During = "application of funding_locked" }
    + return Error <| LNError (StringError {
    + Msg = msg
    + During = "application of funding_locked"
    + })
    | Ok _ ->
    let msg = "expected only one event"
    - return Error <| StringError { Msg = msg; During = "application of funding_locked" }
    + return Error <| LNError (StringError {
    + Msg = msg
    + During = "application of funding_locked"
    + })
    }

    let AcceptTheirChannel (random: Random)
    @@ -805,24 +883,29 @@ module Lightning =

    let! act1Res = ReadExactAsync stream bolt08ActOneLength
    match act1Res with
    - | Error err -> return Error err
    + | Error err -> return Error <| LNError err
    | Ok act1 ->
    let act1Result = PeerChannelEncryptor.processActOneWithKey act1 ourNodeSecret initialPeer.ChannelEncryptor
    match act1Result with
    | Error err ->
    - return Error <| StringError { Msg = SPrintF1 "error from DNL: %A" err; During = "processing of their act1" }
    + return Error <| LNError(StringError {
    + Msg = SPrintF1 "error from DNL: %A" err
    + During = "processing of their act1"
    + })
    | Ok (act2, pce) ->
    let act2, peerWithSentAct2 =
    act2, { initialPeer with ChannelEncryptor = pce }
    do! stream.WriteAsync(act2, 0, act2.Length) |> Async.AwaitTask
    let! act3Res = ReadExactAsync stream bolt08ActThreeLength
    match act3Res with
    - | Error err -> return Error err
    + | Error err -> return Error <| LNError err
    | Ok act3 ->
    let act3Result = PeerChannelEncryptor.processActThree act3 peerWithSentAct2.ChannelEncryptor
    match act3Result with
    | Error err ->
    - return Error <| StringError { Msg = SPrintF1 "error from DNL: %A" err; During = "processing of their act3" }
    + return Error <| LNError(StringError {
    + Msg = SPrintF1 "error from DNL: %A" err; During = "processing of their act3"
    + })
    | Ok (remoteNodeId, pce) ->
    let receivedAct3Peer = { peerWithSentAct2 with ChannelEncryptor = pce }
    Infrastructure.LogDebug "Receiving init..."
    @@ -830,24 +913,26 @@ module Lightning =
    match initRes with
    | Error (PeerDisconnected abruptly) ->
    ReportDisconnection channelCounterpartyIP remoteNodeId abruptly "receiving init message while accepting"
    - return Error (PeerDisconnected abruptly)
    - | Error err -> return Error err
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error err -> return Error <| LNError err
    | Ok init ->
    match Peer.executeCommand receivedAct3Peer init with
    | Error peerError ->
    - return Error <| StringError { Msg = SPrintF1 "couldn't parse init: %s" peerError.Message; During = "receiving init" }
    + return Error <| LNError(StringError {
    + Msg = SPrintF1 "couldn't parse init: %s" peerError.Message
    + During = "receiving init"
    + })
    | Ok (ReceivedInit (newInit, _) as evt::[]) ->
    let peer = Peer.applyEvent receivedAct3Peer evt
    - let connection: Connection =
    - { Init = newInit; Peer = peer; Client = client }
    + let connection = { Init = newInit; Peer = peer; Client = client }
    let! sentInitPeer = Send plainInit connection.Peer stream
    Infrastructure.LogDebug "Receiving open_channel..."
    let! msgRes = ReadUntilChannelMessage (keyRepo, sentInitPeer, stream)
    match msgRes with
    | Error (PeerDisconnected abruptly) ->
    ReportDisconnection channelCounterpartyIP remoteNodeId abruptly "receiving open_channel"
    - return Error (PeerDisconnected abruptly)
    - | Error errorMsg -> return Error errorMsg
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error error -> return Error <| LNError error
    | Ok (receivedOpenChanPeer, chanMsg) ->
    match chanMsg with
    | :? OpenChannel as openChannel ->
    @@ -895,8 +980,8 @@ module Lightning =
    | Error (PeerDisconnected abruptly) ->
    let context = SPrintF2 "receiving funding_created, their open_channel == %A, our accept_channel == %A" openChannel acceptChannel
    ReportDisconnection channelCounterpartyIP remoteNodeId abruptly context
    - return Error (PeerDisconnected abruptly)
    - | Error errorMsg -> return Error errorMsg
    + return Error <| LNError (PeerDisconnected abruptly)
    + | Error error -> return Error <| LNError error
    | Ok (receivedFundingCreatedPeer, chanMsg) ->
    match chanMsg with
    | :? FundingCreated as fundingCreated ->
    @@ -918,25 +1003,51 @@ module Lightning =

    return Ok ()
    | Ok evtList ->
    - return Error <| StringError { Msg = SPrintF1 "event was not a single WeAcceptedFundingCreated, it was: %A" evtList; During = "application of their funding_created message" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "event was not a single WeAcceptedFundingCreated, it was: %A" evtList
    + During = "application of their funding_created message"
    + })
    | Error channelError ->
    - return Error <| StringError { Msg = SPrintF1 "could not apply funding_created: %s" channelError.Message; During = "application of their funding_created message" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "could not apply funding_created: %s" channelError.Message
    + During = "application of their funding_created message"
    + })

    | _ ->
    - return Error <| StringError { Msg = SPrintF1 "channel message is not funding_created: %s" (chanMsg.GetType().Name); During = "reception of answer to accept_channel" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "channel message is not funding_created: %s" (chanMsg.GetType().Name)
    + During = "reception of answer to accept_channel"
    + })

    | Ok evtList ->
    - return Error <| StringError { Msg = SPrintF1 "event list was not a single WeAcceptedOpenChannel, it was: %A" evtList; During = "generation of an accept_channel message" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "event list was not a single WeAcceptedOpenChannel, it was: %A" evtList
    + During = "generation of an accept_channel message"
    + })
    | Error err ->
    - return Error <| StringError { Msg = SPrintF1 "error from DNL: %A" err; During = "generation of an accept_channel message" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "error from DNL: %A" err
    + During = "generation of an accept_channel message"
    + })

    | Ok evtList ->
    - return Error <| StringError { Msg = SPrintF1 "event was not a single NewInboundChannelStarted, it was: %A" evtList; During = "execution of CreateChannel command" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "event was not a single NewInboundChannelStarted, it was: %A" evtList
    + During = "execution of CreateChannel command"
    + })
    | Error channelError ->
    - return Error <| StringError { Msg = SPrintF1 "could not execute channel command: %s" channelError.Message; During = "execution of CreateChannel command" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "could not execute channel command: %s" channelError.Message
    + During = "execution of CreateChannel command"
    + })
    | _ ->
    - return Error <| StringError { Msg = SPrintF1 "channel message is not open_channel: %s" (chanMsg.GetType().Name); During = "reception of open_channel" }
    + return Error <| LNError (StringError {
    + Msg = SPrintF1 "channel message is not open_channel: %s" (chanMsg.GetType().Name)
    + During = "reception of open_channel"
    + })
    | Ok _ ->
    - return Error <| StringError { Msg = "not one good ReceivedInit event"; During = "reception of init message" }
    -
    + return Error <| LNError(StringError {
    + Msg = "not one good ReceivedInit event"
    + During = "reception of init message"
    + })
    }
    diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/SerializedChannel.fs b/src/GWallet.Backend/UtxoCoin/Lightning/SerializedChannel.fs
    index a5c33a44..7de57e89 100644
    --- a/src/GWallet.Backend/UtxoCoin/Lightning/SerializedChannel.fs
    +++ b/src/GWallet.Backend/UtxoCoin/Lightning/SerializedChannel.fs
    @@ -144,15 +144,6 @@ type SerializedChannel = {
    settings.Converters.Add commitmentsConverter
    settings

    - static member ListSavedChannels (): seq<string * int> =
    - if SerializedChannel.LightningDir.Exists then
    - let files =
    - Directory.GetFiles
    - ((SerializedChannel.LightningDir.ToString()), SerializedChannel.ChannelFilePrefix + "*" + SerializedChannel.ChannelFileEnding)
    - files |> Seq.choose SerializedChannel.ExtractChannelNumber
    - else
    - Seq.empty
    -
    member this.SaveSerializedChannel (fileName: string) =
    let json = Marshalling.SerializeCustom(this, SerializedChannel.LightningSerializerSettings)
    let filePath = Path.Combine (SerializedChannel.LightningDir.FullName, fileName)
    @@ -197,3 +188,15 @@ type SerializedChannel = {
    (NodeId this.RemoteNodeId)
    with State = this.ChanState
    }
    +
    +module ChannelManager =
    +
    + let ListSavedChannels (): seq<string * int> =
    + if SerializedChannel.LightningDir.Exists then
    + let files =
    + Directory.GetFiles
    + ((SerializedChannel.LightningDir.ToString()),
    + SerializedChannel.ChannelFilePrefix + "*" + SerializedChannel.ChannelFileEnding)
    + files |> Seq.choose SerializedChannel.ExtractChannelNumber
    + else
    + Seq.empty
    diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
    index 0561906f..2385fcd0 100644
    --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
    +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
    @@ -229,8 +229,8 @@ module Account =
    return! EstimateFees newTxBuilder feeRate account newInputs tail
    }

    - let EstimateFeeForDestination (account: IUtxoAccount) (amount: TransferAmount) (destination: IDestination)
    - : Async<TransactionMetadata> = async {
    + let private EstimateFeeForDestination (account: IUtxoAccount) (amount: TransferAmount) (destination: IDestination)
    + : Async<TransactionMetadata> = async {
    let rec addInputsUntilAmount (utxos: List<UnspentTransactionOutputInfo>)
    soFarInSatoshis
    amount
    @@ -322,6 +322,14 @@ module Account =
    return raise <| InsufficientBalanceForFee None
    }

    + // move to lightning module?
    + let EstimateChannelOpeningFee (account: IUtxoAccount) (amount: TransferAmount) =
    + let witScriptIdLength = 32
    + // this dummy address is only used for fee estimation
    + let nullScriptId = NBitcoin.WitScriptId (Array.zeroCreate witScriptIdLength)
    + // TODO: pass currency argument in order to support Litecoin+Lightning here
    + let dummyAddr = NBitcoin.BitcoinWitScriptAddress (nullScriptId, Config.BitcoinNet)
    + EstimateFeeForDestination account amount dummyAddr

    let EstimateFee (account: IUtxoAccount) (amount: TransferAmount) (destination: string)
    : Async<TransactionMetadata> =
    diff --git a/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj b/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
    index c3a18875..a23bfa58 100644
    --- a/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
    +++ b/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
    @@ -94,9 +94,6 @@ <?xml version="1.0" encoding="utf-8"?>
    <Reference Include="Microsoft.Extensions.Logging.Abstractions">
    <HintPath>..\..\packages\Microsoft.Extensions.Logging.Abstractions.1.0.0\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
    </Reference>
    - <Reference Include="NBitcoin">
    - <HintPath>..\..\packages\NBitcoin.5.0.13\lib\net461\NBitcoin.dll</HintPath>
    - </Reference>
    <Reference Include="System.Net.Http" />
    <Reference Include="BouncyCastle.Crypto">
    <HintPath>..\..\packages\Portable.BouncyCastle.1.8.6\lib\net40\BouncyCastle.Crypto.dll</HintPath>
    @@ -104,15 +101,6 @@ <?xml version="1.0" encoding="utf-8"?>
    <Reference Include="System.Numerics.Vectors">
    <HintPath>..\..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll</HintPath>
    </Reference>
    - <Reference Include="DotNetLightning.Core">
    - <HintPath>..\..\packages\DotNetLightning.1.1.2-date20200527-1047-git-42c7cb9\lib\netstandard2.0\DotNetLightning.Core.dll</HintPath>
    - </Reference>
    - <Reference Include="InternalBech32Encoder">
    - <HintPath>..\..\packages\DotNetLightning.1.1.2-date20200527-1047-git-42c7cb9\lib\netstandard2.0\InternalBech32Encoder.dll</HintPath>
    - </Reference>
    - <Reference Include="ResultUtils">
    - <HintPath>..\..\packages\DotNetLightning.1.1.2-date20200527-1047-git-42c7cb9\lib\netstandard2.0\ResultUtils.dll</HintPath>
    - </Reference>
    <Reference Include="System.Runtime">
    <HintPath>..\..\packages\System.Runtime.4.1.0\lib\net462\System.Runtime.dll</HintPath>
    </Reference>
    diff --git a/src/GWallet.Frontend.Console/Program.fs b/src/GWallet.Frontend.Console/Program.fs
    index ffac8ff2..98fd7ad6 100644
    --- a/src/GWallet.Frontend.Console/Program.fs
    +++ b/src/GWallet.Frontend.Console/Program.fs
    @@ -6,11 +6,11 @@ open System.Text.RegularExpressions
    open System.Text

    open FSharp.Core
    -open NBitcoin

    open GWallet.Backend
    open GWallet.Backend.FSharpUtil
    open GWallet.Backend.UtxoCoin.Lightning
    +open GWallet.Backend.UtxoCoin.Lightning.Lightning
    open GWallet.Frontend.Console

    let random = Org.BouncyCastle.Security.SecureRandom () :> Random
    @@ -292,44 +292,25 @@ let WalletOptions(): unit =
    WipeWallet()
    | _ -> ()

    -type ChannelCreationDetails =
    - {
    - Seed: NBitcoin.uint256
    - AcceptChannel: DotNetLightning.Serialize.Msgs.AcceptChannel
    - Channel: DotNetLightning.Channel.Channel
    - Connection: Lightning.Connection
    - Password: Ref<string>
    - }
    -
    let AskChannelFee (account: UtxoCoin.NormalUtxoAccount)
    (channelCapacity: TransferAmount)
    (balance: decimal)
    (channelCounterpartyIP: System.Net.IPEndPoint)
    - (channelCounterpartyPubKey: NBitcoin.PubKey)
    + (channelCounterpartyPubKey: PublicKey)
    : Async<Option<ChannelCreationDetails>> =
    - let witScriptIdLength = 32
    - // this dummy address is only used for fee estimation
    - let nullScriptId = NBitcoin.WitScriptId (Array.zeroCreate witScriptIdLength)
    - let dummyAddr = NBitcoin.BitcoinWitScriptAddress (nullScriptId, Config.BitcoinNet)
    -
    Infrastructure.LogDebug "Calling EstimateFee..."
    let metadata =
    try
    - UtxoCoin.Account.EstimateFeeForDestination
    - account channelCapacity dummyAddr
    + UtxoCoin.Account.EstimateChannelOpeningFee
    + account channelCapacity
    |> Async.RunSynchronously
    with
    | InsufficientBalanceForFee _ ->
    failwith "Estimated fee is too high for the remaining balance, \
    use a different account or a different amount."

    - let channelKeysSeed, keyRepo, temporaryChannelId = Lightning.GetSeedAndRepo random
    - let channelEnvironment: Lightning.ChannelEnvironment =
    - {
    - Account = account
    - NodeIdForResponder = DotNetLightning.Utils.Primitives.NodeId channelCounterpartyPubKey
    - KeyRepo = keyRepo
    - }
    + let potentialChannel, channelEnvironment =
    + Lightning.GenerateNewPotentialChannelDetails account channelCounterpartyPubKey random
    async {
    let! connectionBeforeAcceptChannelRes =
    Lightning.ConnectAndHandshake channelEnvironment channelCounterpartyIP
    @@ -340,38 +321,36 @@ let AskChannelFee (account: UtxoCoin.NormalUtxoAccount)
    | Result.Ok connectionBeforeAcceptChannel ->
    let passwordRef = ref "DotNetLightning shouldn't ask for password until later when user has
    confirmed the funding transaction fee. So this is a placeholder."
    - let! acceptChannelRes =
    + let! maybeAcceptChannel =
    Lightning.GetAcceptChannel
    + potentialChannel
    channelEnvironment
    connectionBeforeAcceptChannel
    channelCapacity
    metadata
    (fun _ -> !passwordRef)
    balance
    - temporaryChannelId
    - match acceptChannelRes with
    + match maybeAcceptChannel with
    | Result.Error error ->
    Console.WriteLine error.Message
    return None
    - | Result.Ok (acceptChannel, chan, peer) ->
    + | Result.Ok fullChannel ->
    Presentation.ShowFee Currency.BTC metadata
    + let confsReq = (fullChannel :> IChannelToBeOpened).ConfirmationsRequired
    printfn
    "Opening a channel with this party will require %i confirmations (~%i minutes)"
    - acceptChannel.MinimumDepth.Value
    - (acceptChannel.MinimumDepth.Value * 10u)
    + confsReq
    + (confsReq * 10u)
    let accept = UserInteraction.AskYesNo "Do you accept?"

    return
    if accept then
    - let connectionWithNewPeer = { connectionBeforeAcceptChannel with Peer = peer }
    - Some
    - {
    - ChannelCreationDetails.Seed = channelKeysSeed
    - AcceptChannel = acceptChannel
    - Channel = chan
    - Connection = connectionWithNewPeer
    - Password = passwordRef
    - }
    + {
    + Client = connectionBeforeAcceptChannel.Client
    + Password = passwordRef
    + ChannelInfo = potentialChannel
    + FullChannel = fullChannel
    + } |> Some
    else
    connectionBeforeAcceptChannel.Client.Dispose()
    None
    @@ -425,10 +404,11 @@ let rec PerformOperation (numAccounts: int) =
    | Operations.Options ->
    WalletOptions()
    | Operations.OpenChannel ->
    + let currency = Currency.BTC
    let btcAccount = Account
    .GetAllActiveAccounts()
    .OfType<UtxoCoin.NormalUtxoAccount>()
    - .Single(fun account -> (account :> IAccount).Currency = Currency.BTC)
    + .Single(fun account -> (account :> IAccount).Currency = currency)
    let balance = Account.GetShowableBalance
    btcAccount ServerSelectionMode.Fast None
    |> Async.RunSynchronously
    @@ -436,7 +416,7 @@ let rec PerformOperation (numAccounts: int) =
    FSharpUtil.option {
    let! balance = OptionFromMaybeCachedBalance balance
    let! channelCapacity = UserInteraction.AskAmount btcAccount
    - let! ipEndpoint, pubKey = UserInteraction.AskChannelCounterpartyConnectionDetails()
    + let! ipEndpoint, pubKey = UserInteraction.AskChannelCounterpartyConnectionDetails currency
    Infrastructure.LogDebug "Getting channel fee..."
    let! channelCreationDetails =
    AskChannelFee
    @@ -461,14 +441,10 @@ let rec PerformOperation (numAccounts: int) =
    let txIdRes =
    Lightning.ContinueFromAcceptChannelAndSave
    btcAccount
    - details.Seed
    ipEndpoint
    - details.AcceptChannel
    - details.Channel
    - (details.Connection.Client.GetStream())
    - details.Connection.Peer
    + details
    |> Async.RunSynchronously
    - details.Connection.Client.Dispose()
    + details.Client.Dispose()
    Some txIdRes
    with
    | :? InvalidPassword ->
    @@ -534,7 +510,7 @@ let rec CheckArchivedAccountsAreEmpty(): bool =
    not (archivedAccountsInNeedOfAction.Any())

    let private NotReadyReasonToString (reason: Lightning.NotReadyReason): string =
    - sprintf "%i out of %i confirmations" reason.CurrentConfirmations.Value reason.NeededConfirmations.Value
    + sprintf "%i out of %i confirmations" reason.CurrentConfirmations reason.NeededConfirmations

    let private CheckChannelStatus (path: string, channelFileId: int): Async<seq<string>> =
    async {
    @@ -560,7 +536,7 @@ let private CheckChannelStatus (path: string, channelFileId: int): Async<seq<str

    let private CheckChannelStatuses(): Async<seq<string>> =
    async {
    - let jobs = SerializedChannel.ListSavedChannels () |> Seq.map CheckChannelStatus
    + let jobs = ChannelManager.ListSavedChannels () |> Seq.map CheckChannelStatus
    let! statuses = Async.Parallel jobs
    return Seq.collect id statuses
    }
    diff --git a/src/GWallet.Frontend.Console/UserInteraction.fs b/src/GWallet.Frontend.Console/UserInteraction.fs
    index ece21074..f5142589 100644
    --- a/src/GWallet.Frontend.Console/UserInteraction.fs
    +++ b/src/GWallet.Frontend.Console/UserInteraction.fs
    @@ -660,13 +660,13 @@ module UserInteraction =
    IPEndPoint(AskChannelCounterpartyIP(), AskChannelCounterpartyPort())

    // Throws FormatException
    - let private AskChannelCounterpartyPubKey(): NBitcoin.PubKey =
    + let private AskChannelCounterpartyPubKey (currency: Currency): PublicKey =
    Console.Write "Channel counterparty public key in hexadecimal notation: "
    - let pubkeyHex = Console.ReadLine().Trim()
    - NBitcoin.PubKey pubkeyHex
    + let pubKeyHex = Console.ReadLine().Trim()
    + PublicKey(pubKeyHex, currency)

    // Throws FormatException
    - let private AskChannelCounterpartyQRString(): IPEndPoint * NBitcoin.PubKey =
    + let private AskChannelCounterpartyQRString (currency: Currency): IPEndPoint * PublicKey =
    Console.Write "Channel counterparty QR connection string contents: "
    let connectionString = Console.ReadLine().Trim()
    let atIndex = connectionString.IndexOf "@"
    @@ -679,16 +679,16 @@ module UserInteraction =
    let ipString, portString = ipPortCombo.[..portSeparatorIndex - 1], ipPortCombo.[portSeparatorIndex + 1..]
    let ipAddress = IPAddress.Parse ipString
    let port: int = ParsePortString portString
    - IPEndPoint(ipAddress, port), NBitcoin.PubKey pubKeyHex
    + IPEndPoint(ipAddress, port), PublicKey(pubKeyHex, currency)

    - let AskChannelCounterpartyConnectionDetails(): Option<IPEndPoint * NBitcoin.PubKey> =
    + let AskChannelCounterpartyConnectionDetails (currency: Currency): Option<IPEndPoint * PublicKey> =
    let useQRString = AskYesNo "Do you want to supply the channel counterparty connection string as used embedded in QR codes?"
    try
    if useQRString then
    - Some <| AskChannelCounterpartyQRString()
    + Some <| AskChannelCounterpartyQRString currency
    else
    let ipEndpoint = AskChannelCounterpartyIPAndPort()
    - let pubKey = AskChannelCounterpartyPubKey()
    + let pubKey = AskChannelCounterpartyPubKey currency
    Some (ipEndpoint, pubKey)
    with
    | :? FormatException as e ->
    diff --git a/src/GWallet.Frontend.Console/packages.config b/src/GWallet.Frontend.Console/packages.config
    index 20738d4e..bf2f39b6 100644
    --- a/src/GWallet.Frontend.Console/packages.config
    +++ b/src/GWallet.Frontend.Console/packages.config
    @@ -1,10 +1,8 @@
    <?xml version="1.0" encoding="utf-8"?>
    <packages>
    <package id="BouncyCastle" version="1.8.5" targetFramework="net461" />
    - <package id="DotNetLightning" version="1.1.2-date20200527-1047-git-42c7cb9" targetFramework="net472" />
    <package id="FSharp.Core" version="4.7.0" targetFramework="net45" />
    <package id="Microsoft.Extensions.Logging.Abstractions" version="1.0.0" targetFramework="net461" />
    - <package id="NBitcoin" version="5.0.13" targetFramework="net461" />
    <package id="Newtonsoft.Json" version="11.0.2" targetFramework="net46" />
    <package id="Portable.BouncyCastle" version="1.8.6" targetFramework="net461" />
    <package id="System.Buffers" version="4.5.0" targetFramework="net461" />
    --
    2.21.0