From 6b9aa05dfef80a2f89686688e6b80ecc58aa50da Mon Sep 17 00:00:00 2001 From: jmhickman Date: Fri, 20 May 2022 12:41:01 -0500 Subject: [PATCH] * Added a check for empty bytecodes on contract preparation, indicating a possible issue with bytecode import * Significant refactoring of logging to support better record output * Experimenting with Async transactions at the user level. May lead to `__Async` versions of the primary Eth calls. This would allow groups of transactions to be submitted without waiting for a pending transaction to be mined before the next is sent. This is NOT transaction bundling; that requires a contract. --- Contracts.fs | 28 +++++-- Logger.fs | 190 +++++++++++++++++++++++++++++++++++++++++++----- Logging.fs | 34 +-------- RPCFunctions.fs | 55 +++++++------- Types/Types.fs | 85 +++++++++++----------- 5 files changed, 265 insertions(+), 127 deletions(-) diff --git a/Contracts.fs b/Contracts.fs index f636714..318b30c 100644 --- a/Contracts.fs +++ b/Contracts.fs @@ -469,23 +469,34 @@ module ContractFunctions = /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// Basic check to ensure there is actually bytecode, in case the file used to provide it was malformed. + let checkForBytecode (bytecode: RawContractBytecode) (abi: ABI) = + let (RawContractBytecode s) = bytecode + match s.Length with + | x when x = 0 -> EmptyBytecodeError |> Error + | _ -> abi |> Ok + + /// /// Generates Web3Error if the abi can't be parsed, implying interacting with the contract after deployment will /// fail. Otherwise, passes along the JsonValue. /// - let private canABIBeParsed abi = - let (ABI _abi) = abi - match JsonValue.TryParse(_abi) with - | Some v -> v |> Ok - | None -> ContractParseFailure "Json was incorrectly formatted or otherwise failed to parse" |> Error + let private canABIBeParsed (pipe: Result) = + pipe + |> Result.bind( fun abi -> + let (ABI _abi) = abi + match JsonValue.TryParse(_abi) with + | Some v -> v |> Ok + | None -> ContractParseFailure "Json was incorrectly formatted or otherwise failed to parse" |> Error ) /// /// Adapted version of canABIBeParsed for different pipeline - let private pipeCanABIBeParsed abi (pipe: Result) = + let private pipeCanABIBeParsed (abi: ABI) (pipe: Result) = pipe |> Result.bind(fun p -> - canABIBeParsed abi + canABIBeParsed (abi |> Ok)// this is kinda stupid but whatever |> Result.bind(fun b -> (p, b) |> Ok )) @@ -555,7 +566,7 @@ module ContractFunctions = /// or '0x4' (rinkeby). See `Types.fs` for a set of pre-defined network aliases. /// * `abi`: An `ABI` associated with the deployed contract. /// - let public loadDeployedContract env address chainId abi = + let public loadDeployedContract env address chainId (abi: ABI) = address |> wrapEthAddress |> pipeCanABIBeParsed abi @@ -584,6 +595,7 @@ module ContractFunctions = /// let public prepareUndeployedContract env bytecode (constructorArguments: EVMDatatype list option) chainId abi = abi + |> checkForBytecode bytecode |> canABIBeParsed |> checkForHashCollisions env |> buildConstructorHash env diff --git a/Logger.fs b/Logger.fs index d3839e1..acdecad 100644 --- a/Logger.fs +++ b/Logger.fs @@ -1,23 +1,177 @@ namespace web3.fs +open web3.fs + module Logger = open System /// /// Just color bindings. - let setColor color = + let internal setColor color = match color with | Blue -> Console.ForegroundColor <- ConsoleColor.Blue + | DarkBlue -> Console.ForegroundColor <- ConsoleColor.DarkBlue | Yellow -> Console.ForegroundColor <- ConsoleColor.Yellow | Green -> Console.ForegroundColor <- ConsoleColor.Green | Red -> Console.ForegroundColor <- ConsoleColor.Red + | Gold -> Console.ForegroundColor <- ConsoleColor.DarkYellow /// /// Revert the color. - let revertColor () =Console.ResetColor() + let internal revertColor () = Console.ResetColor() + + + let printAndRevert (color: WConsoleColor) message = + setColor color + printf $"{message}" + revertColor () + + + /// + /// Formatted print of a simple response value, typically an RPC response + let prettySimple (simple: string) = + printAndRevert Blue $"{simple}\n" + + + + /// + /// Formatted print of a TransactionReceipt + let prettyTransactionReceipt (receipt: TransactionReceipt) = + let toOrContract = + if receipt.contractAddress.IsSome then + $"Contract deployed to: {receipt.contractAddress.Value}\n" + else $"To: {receipt.toAddr.Value}\n" + + let logs = + if receipt.logs.Length > 0 then + receipt.logs + |> List.toArray + |> Array.map(fun s -> "\t" + s) + |> fun a -> String.Join("\n", a) + else "" + + if receipt.status = "0x0" then + printAndRevert Red "Failed transaction" + else printAndRevert Green "Successful Transaction\n" + printAndRevert Blue $"Transaction hash: {receipt.transactionHash}\n" + printAndRevert DarkBlue $"From: {receipt.from}\n" + printAndRevert Blue toOrContract + printAndRevert Gold $"Gas used: {receipt.gasUsed |> hexToBigIntP}\n" + printAndRevert Gold $"Effective gas price: {receipt.effectiveGasPrice |> hexToBigIntP}\n" + printAndRevert Gold $"Cumulative gas used: {receipt.cumulativeGasUsed |> hexToBigIntP}\n" + printfn $"Block hash: {receipt.blockHash}" + printfn $"Block number: {receipt.blockNumber}" + printfn $"Index of transaction in block: {receipt.transactionIndex}" + printfn $"Type: {receipt.tType}" + printAndRevert Blue "Logs:\n" + printfn $"{logs}" + + + /// + /// Formatted print of Transaction records + let prettyTransaction (txn: MinedTransaction) = + let accessList = + if txn.accessList.Length > 0 then + txn.accessList + |> List.toArray + |> Array.map(fun s -> "\t" + s) + |> fun a -> String.Join("\n", a) + else "" + + printAndRevert Blue $"Transaction hash: {txn.hash}\n" + printAndRevert DarkBlue $"From: {txn.from}\n" + printAndRevert Blue $"To: {txn.toAddr}\n" + printAndRevert Blue "Nonce: " + printfn $"{txn.nonce}" + printAndRevert Blue "Input: " + printfn $"{txn.input}" + printAndRevert Gold $"Value: {txn.value |> hexToBigIntP}\n" + printAndRevert Gold "Gas: " + printfn $"{txn.gas |> hexToBigIntP}" + printAndRevert Gold "Gas price: " + printfn $"{txn.gasPrice |> hexToBigIntP}" + printAndRevert Gold "Max fee per gas: " + printfn $"{txn.maxFeePerGas |> hexToBigIntP}" + printAndRevert Gold "Max priority fee per gas: " + printfn $"{txn.maxPriorityFeePerGas |> hexToBigIntP}" + printfn "Access list: " + printfn $"{accessList}" + printfn $"Block hash: {txn.blockHash}" + printfn $"Block number: {txn.blockNumber |> hexToBigIntP}" + printfn $"Chain ID: {txn.chainId}" + printfn $"R: {txn.r}" + printfn $"S: {txn.s}" + printfn $"V: {txn.v}" + printfn $"Index in block: {txn.transactionIndex}" + printfn $"Transaction type: {txn.tType}" + + + /// + /// Formatted print of a Block record + let prettyBlock (block: EthBlock) = + let sealFields = + if block.sealFields.Length > 0 then + block.sealFields + |> List.toArray + |> Array.map(fun s -> "\t" + s) + |> fun a -> String.Join("\n", a) + else "" + + let uncles = + if block.uncles.Length > 0 then + block.uncles + |> List.toArray + |> Array.map(fun s -> "\t" + s) + |> fun a -> String.Join("\n", a) + else "" + + let transactions = + if block.transactions.Length > 0 then + block.transactions + |> List.toArray + |> Array.map(fun s -> "\t" + s) + |> fun a -> String.Join("\n", a) + else "" + + + printAndRevert Blue $"Ethereum Block: {block.number}\n" + printfn $"Block author: {block.author}" + printAndRevert Blue "Miner: " + printfn $"{block.miner}" + printAndRevert Gold "Base fee per gas: " + printfn $"{block.baseFeePerGas |> hexToBigIntP}" + printAndRevert Gold "Gas limit: " + printfn $"{block.gasLimit |> hexToBigIntP}" + printAndRevert Gold "Gas used: " + printfn $"{block.gasUsed |> hexToBigIntP}" + printfn $"Difficulty: {block.difficulty}" + printfn $"Extra data: {block.extraData}" + printfn $"Parent hash: {block.parentHash}" + printfn $"Receipts root: {block.receiptsRoot}" + printfn $"Seal fields: {sealFields}" + printfn $"Uncles hash: {block.sha3Uncles}" + printAndRevert Blue "Block size: " + printfn $"{block.size}" + printf $"Stateroot: {block.stateRoot}\n" + printAndRevert Blue "Timestamp: " + printfn $"{block.timestamp}" + printfn $"Total difficulty: {block.totalDifficulty}" + printfn $"Transactions root: {block.transactionsRoot}" + printfn $"Uncles: {uncles}" + printAndRevert Blue "Transactions:\n" + printfn $"{transactions}" + + let prettyCallResult (callResult: EVMDatatype list) = + callResult + |> List.fold (fun acc s -> + let stripped = s.ToString().Replace(QUOTE, EMPTY) + $"{acc}\t{stripped}\n") "" + |> fun p -> + printAndRevert Blue "Call response\n" + printfn $"{p}" /// /// Emits console messages with color and glyphs based on the incoming message. I'm not certain these locks @@ -29,30 +183,26 @@ module Logger = let! msg = mbox.Receive() let logType, message = msg match logType with - | Info -> - lock locker (fun _ -> - setColor Blue - printf "[*] " - revertColor () - printfn $"{message}") | Warn -> lock locker (fun _ -> - setColor Yellow - printf "[-] " - revertColor () - printfn $"{message}") - | Success -> - lock locker (fun _ -> - setColor Green - printf "[+] " - revertColor () + printAndRevert Yellow "[-] " printfn $"{message}") | Failure -> lock locker (fun _ -> - setColor Red - printf "[!] " - revertColor () + printAndRevert Red "[!] " printfn $"{message}") + | Success -> + lock locker (fun _ -> + printAndRevert Green "[+] Success\n" + match message with + | Block ethBlock -> prettyBlock ethBlock + | Transaction mTransaction -> prettyTransaction mTransaction + | TransactionReceiptResult receipt -> prettyTransactionReceipt receipt + | CallResult evmDatatypes -> prettyCallResult evmDatatypes + | SimpleValue s -> prettySimple s + | Library s -> prettySimple s + | Empty -> () + | TransactionHash _ -> () ) do! receiver () } diff --git a/Logging.fs b/Logging.fs index aa8ec83..be8d2de 100644 --- a/Logging.fs +++ b/Logging.fs @@ -5,24 +5,13 @@ open web3.fs.Types [] module Logging = - + open Logger /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Logging /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// Processes CallResponse data to remove quotes. Can't use `trimParameter` because that assumes simple strings - /// with no "words" like "byte" or whatever. - /// - let private prepareCallResult callResult = - callResult - |> List.fold (fun acc s -> - let stripped = s.ToString().Replace(QUOTE, EMPTY) - $"{acc}{stripped}\n") "" - - /// /// Binds and starts the transaction monitor if a transaction hash was emitted from `makeEthTxn`. Intended to be /// placed in a transaction pipeline to provide realtime logging of transaction completion. @@ -33,30 +22,15 @@ module Logging = | Error e -> e |> Error - /// - /// Handles the emission of information to the console - let private logCallResponse (logger: Logger) callResponse = - match callResponse with - | SimpleValue s -> logger.Post (Success, $"Value: {s}") - | Block ethBlock -> logger.Post (Info, $"Ethereum block:\n{ethBlock}") - | TransactionHash _ -> () // Handled by the monitor - | TransactionReceiptResult rpcTransactionResponse -> logger.Post (Info, $"Transaction receipt:\n{rpcTransactionResponse}") - | Transaction mTransaction -> logger.Post (Info, $"Transaction:\n{mTransaction}") - | CallResult callResult -> - logger.Post (Success, $"Call result:\n{prepareCallResult callResult}") - | Library s -> logger.Post (Info, s) - | Empty -> () // Do nothing - - /// /// Forwards CallResponses, logs errors to the console let private logCallResponsesOrWeb3Errors (logger: Logger) (pipeResult: Result) = match pipeResult with - | Ok o -> logCallResponse logger o + | Ok o -> logger.Post (Success, o) | Error e -> match e with - | PayableFunctionZeroValueWarning w -> logger.Post (Warn, w) - | e -> logger.Post (Failure, $"{e}") + | PayableFunctionZeroValueWarning w -> logger.Post (Warn, $"{w}" |> Library) + | e -> logger.Post (Failure, $"{e}" |> Library) /// diff --git a/RPCFunctions.fs b/RPCFunctions.fs index d61c160..d854b20 100644 --- a/RPCFunctions.fs +++ b/RPCFunctions.fs @@ -268,9 +268,9 @@ module RPCFunctions = |> env.log Emit |> unwrapSimpleValue |> fun chain -> - if not(chain = chainId ) then - WrongChainInSignerError |> Error - else () |> Ok + if chain = chainId then + () |> Ok + else WrongChainInSignerError |> Error /// @@ -379,7 +379,7 @@ module RPCFunctions = |> stringAndTrim |> EthTransactionHash |> fun ethHash -> - env.log Log (Library $"Monitoring transaction {ethHash}" |> Ok ) + env.log Log (Library $"Monitoring transaction {ethHash}\n" |> Ok ) |> fun _ -> ethHash |> Ok) |> monitorTransaction env.monitor @@ -444,6 +444,7 @@ module RPCFunctions = |> monitorTransaction env.monitor + /// /// Estimate the amount of gas units required for the given transaction to complete. Essentially the same as /// `makeEthTxn` but with a different underlying call. A static value argument of "0" is supplied. @@ -466,27 +467,27 @@ module RPCFunctions = /// This function is for the sending of Ether between EOAs. Use `makeEthTxn` with the `Receive` function indicator /// to send to contracts. ENS names are supported for this function. /// - let public sendValue env chainId destination value = + let public sendValue env chainId destination value = async { let _dest = handleENSName env chainId destination - - {dummyTransaction with - utoAddr = _dest - ufrom = env.constants.walletAddress - uvalue = value |> bigintToHex |> prepend0x - uchainId = chainId} - |> validateRPCParams - |> Result.bind - (fun _params -> - {method = EthMethod.SendTransaction - paramList = _params - blockHeight = LATEST} - |> env.connection) - |> Result.bind (fun r -> - unpackRoot r - |> stringAndTrim - |> EthTransactionHash - |> fun ethHash -> - env.log Log (Library $"Monitoring transaction {ethHash}" |> Ok ) - |> fun _ -> ethHash - |> Ok) - |> monitorTransaction env.monitor \ No newline at end of file + return + { dummyTransaction with + utoAddr = _dest + ufrom = env.constants.walletAddress + uvalue = value |> bigintToHex |> prepend0x + uchainId = chainId} + |> validateRPCParams + |> Result.bind + (fun _params -> + {method = EthMethod.SendTransaction + paramList = _params + blockHeight = LATEST} + |> env.connection) + |> Result.bind (fun r -> + unpackRoot r + |> stringAndTrim + |> EthTransactionHash + |> fun ethHash -> + env.log Log (Library $"Monitoring transaction {ethHash}" |> Ok ) + |> fun _ -> ethHash + |> Ok) + |> monitorTransaction env.monitor } \ No newline at end of file diff --git a/Types/Types.fs b/Types/Types.fs index 1496ba9..1d0ceb9 100644 --- a/Types/Types.fs +++ b/Types/Types.fs @@ -32,36 +32,6 @@ module Types = type ChainId = string - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - //// Logging types - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - - /// Color bindings - type ConsoleColor = - | Blue - | Yellow - | Green - | Red - - - /// Signal to indicate logging behavior - type LogType = - | Info - | Warn - | Success - | Failure - - - /// Simple logging message type for MailboxProcessor - type LogMessage = LogType * string - - - /// A MailboxProcessor-based logger - type Logger = MailboxProcessor - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //// Type Provider parser setup /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -370,7 +340,7 @@ module Types = cumulativeGasUsed: string effectiveGasPrice: string from: EthAddress - gasUsed: string + gasUsed: string logs: string list logsBloom: string status: string @@ -401,15 +371,15 @@ module Types = /// Record representing a block on the Ethereum blockchain type EthBlock = { author: string - baseFeePerGas: string + baseFeePerGas: string // gold difficulty: string extraData: string - gasLimit: string - gasUsed: string - hash: EthTransactionHash + gasLimit: string // gold + gasUsed: string // gold + hash: EthTransactionHash // blue logsBloom: string - miner: EthAddress - number: string + miner: EthAddress // blue + number: string //blue parentHash: string receiptsRoot: string sealFields: string list @@ -418,7 +388,7 @@ module Types = stateRoot: string timestamp: string totalDifficulty: string - transactions: string list + transactions: string list // ?? transactionsRoot: string uncles: string list } @@ -456,17 +426,17 @@ module Types = blockHash: string blockNumber: string chainId: string - from: EthAddress + from: EthAddress gas: string gasPrice: string - hash: EthTransactionHash + hash: EthTransactionHash input: string maxFeePerGas: string maxPriorityFeePerGas: string nonce: string r: string s: string - toAddr: EthAddress + toAddr: EthAddress transactionIndex: string tType: string v: string @@ -508,6 +478,7 @@ module Types = | ContractABIContainsHashCollisionsError | EthCallIntoNonCallPipelineError | RPCNullResponse + | EmptyBytecodeError | ConstructorArgumentsToEmptyConstructorError | ConstructorArgumentsMissingError | ArgumentsToEmptyFunctionSignatureError @@ -783,4 +754,34 @@ module Types = monitor: Monitor constants: ContractConstants digest: Keccak - log : LogSignal -> Result -> CallResponses } \ No newline at end of file + log : LogSignal -> Result -> CallResponses } + + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //// Logging types + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + /// Color bindings + type WConsoleColor = + | Blue + | DarkBlue + | Yellow + | Green + | Red + | Gold + + + /// Signal to indicate logging behavior + type LogType = + | Warn + | Success + | Failure + + + /// Simple logging message type for MailboxProcessor + type LogMessage = LogType * CallResponses + + + /// A MailboxProcessor-based logger + type Logger = MailboxProcessor \ No newline at end of file