Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT: Option to print BTC transactions #176

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/GWallet.Backend/GWallet.Backend.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
<Compile Include="UtxoCoin\UtxoCoinMinerFee.fs" />
<Compile Include="UtxoCoin\TransactionTypes.fs" />
<Compile Include="UtxoCoin\UtxoCoinAccount.fs" />
<Compile Include="UtxoCoin\BtcTransactionPrinting.fs" />
<Compile Include="Ether\EtherExceptions.fs" />
<Compile Include="Ether\EtherMinerFee.fs" />
<Compile Include="Ether\TransactionMetadata.fs" />
Expand Down
5 changes: 5 additions & 0 deletions src/GWallet.Backend/Server.fs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ module ServerRegistry =
// they create exception when being queried for advanced ones (e.g. latest block)
server.ServerInfo.NetworkPath.Contains "blockscout" ||

// Blockstream electrum servers doesn't support sending verbose transactions, we use this functionality
// for getting confirmations of a transaction, this causes geewallet to crash. See:
// https://github.com/Blockstream/electrs/pull/36
server.ServerInfo.NetworkPath.Contains "blockstream" ||

// there was a mistake when adding this server to geewallet's JSON: it was added in the ETC currency instead of ETH
(currency = Currency.ETC && server.ServerInfo.NetworkPath.Contains "ethrpc.mewapi.io")

Expand Down
166 changes: 166 additions & 0 deletions src/GWallet.Backend/UtxoCoin/BtcTransactionPrinting.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
namespace GWallet.Backend.UtxoCoin

open System
open System.Net

open GWallet.Backend
open GWallet.Backend.FSharpUtil
open GWallet.Backend.FSharpUtil.UwpHacks


module BtcTransactionPrinting =
type private ProcessedHistoryEntry =
{
Date: DateTime
Outputs: NBitcoin.TxOutList
}

type private EntryWithSharedAddress =
{
Date: DateTime
SharedOutputs: seq<NBitcoin.TxOut>
}

let GetBitcoinPriceForDate (date: DateTime) : Async<Result<decimal, Exception>> =
async {
try
let baseUrl =
let dateFormated = date.ToString("dd-MM-yyyy")
SPrintF1 "https://api.coingecko.com/api/v3/coins/bitcoin/history?date=%s&localization=false" dateFormated
let uri = Uri baseUrl
use webClient = new WebClient()
let task = webClient.DownloadStringTaskAsync uri
let! result = Async.AwaitTask task
let json = Newtonsoft.Json.Linq.JObject.Parse result
return Ok(json.["market_data"].["current_price"].["usd"].ToObject<decimal>())
with
| ex ->
return Error ex
}

let PrintTransactions (maxTransactionsCountOption: uint32 option) (btcAddress: string) =
let maxTransactionsCount = defaultArg maxTransactionsCountOption UInt32.MaxValue
let address = NBitcoin.BitcoinAddress.Create(btcAddress, NBitcoin.Network.Main)
async {
let scriptHash = Account.GetElectrumScriptHashFromPublicAddress Currency.BTC btcAddress
let! history =
Server.Query
Currency.BTC
(QuerySettings.Default ServerSelectionMode.Fast)
(ElectrumClient.GetBlockchainScriptHashHistory scriptHash)
None

let sortedHistory = history |> List.sortByDescending (fun entry -> entry.Height)

let rec processHistory history (maxTransactionsToPrint: uint32) =
match history with
| nextEntry :: rest when maxTransactionsToPrint > 0u ->
async {
let! transaction =
Server.Query
Currency.BTC
(QuerySettings.Default ServerSelectionMode.Fast)
(ElectrumClient.GetBlockchainTransactionVerbose nextEntry.TxHash)
None
let transactionInfo = NBitcoin.Transaction.Parse(transaction.Hex, NBitcoin.Network.Main)

let incomingOutputs =
transactionInfo.Outputs
|> Seq.filter (fun output -> output.IsTo address)

if not (Seq.isEmpty incomingOutputs) then
let amount = incomingOutputs |> Seq.sumBy (fun txOut -> txOut.Value)
let dateTime =
let startOfUnixEpoch = DateTime(1970, 1, 1)
startOfUnixEpoch + TimeSpan.FromSeconds(float transaction.Time)
let! bitcoinPrice = GetBitcoinPriceForDate dateTime
Console.WriteLine(SPrintF1 "BTC amount: %A" amount)
match bitcoinPrice with
| Ok price ->
let bitcoinAmount = amount.ToDecimal(NBitcoin.MoneyUnit.BTC)
Console.WriteLine(SPrintF1 "~USD amount: %s" ((bitcoinAmount * price).ToString("F2")))
| Error exn ->
Console.WriteLine("Could not get bitcoin price for the date. An error has occured:\n" + exn.ToString())
Console.WriteLine(SPrintF1 "date: %A UTC" dateTime)
Console.WriteLine()
let! otherEntries = processHistory rest (maxTransactionsToPrint - 1u)
return { Date = dateTime; Outputs = transactionInfo.Outputs } :: otherEntries
else
return! processHistory rest maxTransactionsToPrint
}
| _ -> async { return [] }

let! processedEntries = processHistory sortedHistory maxTransactionsCount

if maxTransactionsCountOption.IsSome then
let getAddress (output: NBitcoin.TxOut) =
output.ScriptPubKey.GetDestinationAddress NBitcoin.Network.Main

let allAddressesExceptOurs =
processedEntries
|> Seq.collect(
fun entry ->
entry.Outputs
|> Seq.map getAddress)
|> Seq.filter (fun each -> each <> address)
|> Seq.distinct
|> Seq.cache

let sharedAddresses =
allAddressesExceptOurs
|> Seq.filter (
fun addr ->
processedEntries
|> Seq.forall(
fun entry ->
entry.Outputs
|> Seq.exists (fun output -> output.IsTo addr) ) )
|> Seq.cache

let entriesWithSharedAddresses =
processedEntries
|> Seq.choose (
fun entry ->
let sharedOutputs =
entry.Outputs
|> Seq.filter (
fun output ->
sharedAddresses |> Seq.exists (fun addr -> output.IsTo addr))
if sharedOutputs |> Seq.isEmpty then
None
else
let outputsToOurAddress = entry.Outputs |> Seq.filter (fun output -> output.IsTo address)
Some {
SharedOutputs = sharedOutputs |> Seq.append outputsToOurAddress
Date = entry.Date
} )
|> Seq.cache

if (entriesWithSharedAddresses |> Seq.length) >= 2 then
Console.WriteLine(SPrintF1 "Transactions with outputs shared with %A:\n" address)
for entry in entriesWithSharedAddresses do
let totalAmount =
entry.SharedOutputs
|> Seq.sumBy (fun txOut -> txOut.Value)
let sharedOutputs =
entry.SharedOutputs
|> Seq.groupBy getAddress
let currentSharedAddresses =
sharedOutputs
|> Seq.map fst

Console.WriteLine(SPrintF1 "Transaction with outputs to %s" (String.Join(", ", currentSharedAddresses)))
Console.WriteLine(SPrintF1 "Date: %A UTC" entry.Date)
Console.WriteLine(SPrintF1 "Total BTC: %A" totalAmount)
for addr, outputs in sharedOutputs do
let amount = outputs |> Seq.sumBy (fun txOut -> txOut.Value)
let percentage =
amount.ToDecimal(NBitcoin.MoneyUnit.BTC) / totalAmount.ToDecimal(NBitcoin.MoneyUnit.BTC) * 100.0m
Console.WriteLine(SPrintF3 "Sent %A BTC to %A (%s%%)" amount addr (percentage.ToString("F2")))
Console.WriteLine()

Console.WriteLine("End of results")
Console.ReadKey() |> ignore
return 0
}
|> Async.RunSynchronously
12 changes: 12 additions & 0 deletions src/GWallet.Backend/UtxoCoin/ElectrumClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,24 @@ module ElectrumClient =
return unspentListResult.Result
}

let GetBlockchainScriptHashHistory scriptHash (stratumServer: Async<StratumClient>) = async {
let! stratumClient = stratumServer
let! history = stratumClient.BlockchainScriptHashHistory scriptHash
return history.Result
}

let GetBlockchainTransaction txHash (stratumServer: Async<StratumClient>) = async {
let! stratumClient = stratumServer
let! blockchainTransactionResult = stratumClient.BlockchainTransactionGet txHash
return blockchainTransactionResult.Result
}

let GetBlockchainTransactionVerbose (txHash: string) (stratumServer: Async<StratumClient>) = async {
let! stratumClient = stratumServer
let! blockchainTransactionResult = stratumClient.BlockchainTransactionGetVerbose txHash
return blockchainTransactionResult.Result
}

let EstimateFee (numBlocksTarget: int) (stratumServer: Async<StratumClient>): Async<decimal> = async {
let! stratumClient = stratumServer
let! estimateFeeResult = stratumClient.BlockchainEstimateFee numBlocksTarget
Expand Down
50 changes: 50 additions & 0 deletions src/GWallet.Backend/UtxoCoin/StratumClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,38 @@ type BlockchainScriptHashListUnspentResult =
Result: array<BlockchainScriptHashListUnspentInnerResult>
}

type BlockchainScriptHashHistoryInnerResult =
{
TxHash: string
Height: uint32
}

type BlockchainScriptHashHistoryResult =
{
Id: int
Result: List<BlockchainScriptHashHistoryInnerResult>
}

type BlockchainTransactionGetResult =
{
Id: int;
Result: string;
}

type BlockchainTransactionGetVerboseInnerResult =
{
Locktime: uint32
Confirmations: uint32
Hex: string
Time: uint32
}

type BlockchainTransactionGetVerboseResult =
{
Id: int
Result: BlockchainTransactionGetVerboseInnerResult
}

type BlockchainEstimateFeeResult =
{
Id: int;
Expand Down Expand Up @@ -246,6 +272,18 @@ type StratumClient (jsonRpcClient: JsonRpcTcpClient) =
return resObj
}

member self.BlockchainScriptHashHistory scriptHash: Async<BlockchainScriptHashHistoryResult> =
let obj = {
Id = 0
Method = "blockchain.scripthash.get_history"
Params = scriptHash :: List.Empty
}
let json = Serialize obj
async {
let! resObj,_ = self.Request<BlockchainScriptHashHistoryResult> json
return resObj
}

member self.BlockchainTransactionGet txHash: Async<BlockchainTransactionGetResult> =
let obj = {
Id = 0;
Expand All @@ -258,6 +296,18 @@ type StratumClient (jsonRpcClient: JsonRpcTcpClient) =
return resObj
}

member self.BlockchainTransactionGetVerbose (txHash: string): Async<BlockchainTransactionGetVerboseResult> =
let obj = {
Id = 0
Method = "blockchain.transaction.get"
Params = [txHash :> obj; true :> obj]
}
let json = Serialize obj
async {
let! resObj,_ = self.Request<BlockchainTransactionGetVerboseResult> json
return resObj
}

// NOTE: despite Electrum-X official docs claiming that this method is deprecated... it's not! go read the official
// non-shitcoin forked version of the docs: https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-estimatefee
member self.BlockchainEstimateFee (numBlocksTarget: int): Async<BlockchainEstimateFeeResult> =
Expand Down
6 changes: 6 additions & 0 deletions src/GWallet.Frontend.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -480,5 +480,11 @@ let main argv =
UpdateServersFile()
| 1 when argv.[0] = "--update-servers-stats" ->
UpdateServersStats()
| 2 | 3 when argv.[0] = "--print-transactions" ->
// --print-transactions <btcAddress> [maxTransactionsCount]
if argv.Length = 2 then
UtxoCoin.BtcTransactionPrinting.PrintTransactions None argv.[1]
else
UtxoCoin.BtcTransactionPrinting.PrintTransactions (Some <| uint32 argv.[2]) argv.[1]
| _ ->
failwith "Arguments not recognized"