Created
June 9, 2020 06:31
-
-
Save knocte/d30ca67e3b7fc32def1a85dbf43564d4 to your computer and use it in GitHub Desktop.
Revisions
-
knocte created this gist
Jun 9, 2020 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal 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