From ecdfaa92ec8418253822807bc6c215708aa64156 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 2 May 2024 20:07:26 -0300 Subject: [PATCH] Use oxenc; replace rlpvalue; c++20 modernizations and various cleanups - Replace rlpvalue with oxenc (which now has an rlp serializer) - Use oxenc for hex conversions and hex literals - Switch compilation to C++20 - Switch to std::string_views or std::spans in various places instead of const lvalue strings or specific vector/array types. - Change Provider::sendTransaction and similar to not use `std::async` because they were just waiting for the async to return (which blocks the current thread anyway, and so might as well just happen in the current thread), and abstracted the get-and-wait code into a single function. - removed some unused utils functions - external -- brought in `system_or_submodule` from other oxen packages to have cmake check for system libs before building/using submodules for nlohman, secp256k1, oxen-encoding. - Turn off `-Wshadow` because, under GCC, it produces warnings for things it shouldn't, like constructor arguments that initialize members, and lambdas that capture a copy of the same name. Clang's `-Wshadow` is less stupid about this, but as this also causes a metric ton of warnings for dependent code (like oxend) just turn it off for now. --- .gitmodules | 6 +- CMakeLists.txt | 14 +-- README.md | 4 - cmake/CompilerWarnings.cmake | 2 - cmake/StandardSettings.cmake | 20 ++-- cmake/system_or_submodule.cmake | 21 ++++ external/CMakeLists.txt | 81 +++++-------- external/cpr | 2 +- external/oxen-encoding | 1 + external/rlpvalue | 1 - include/ethyl/provider.hpp | 30 ++--- include/ethyl/signer.hpp | 27 +++-- include/ethyl/transaction.hpp | 16 ++- include/ethyl/utils.hpp | 66 +++++++++-- src/provider.cpp | 185 +++++++++++++----------------- src/signer.cpp | 22 ++-- src/transaction.cpp | 89 +++++++-------- src/utils.cpp | 194 ++++++-------------------------- test/CMakeLists.txt | 22 ++-- test/src/basic.cpp | 6 +- test/src/ethereum_client.cpp | 20 ++-- 21 files changed, 349 insertions(+), 480 deletions(-) create mode 100644 cmake/system_or_submodule.cmake create mode 160000 external/oxen-encoding delete mode 160000 external/rlpvalue diff --git a/.gitmodules b/.gitmodules index f049680..2316439 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,12 +4,12 @@ [submodule "external/cpr"] path = external/cpr url = https://github.com/libcpr/cpr.git -[submodule "external/rlpvalue"] - path = external/rlpvalue - url = https://github.com/bloq/rlpvalue.git [submodule "external/secp256k1"] path = external/secp256k1 url = https://github.com/bitcoin-core/secp256k1.git [submodule "external/json"] path = external/json url = https://github.com/nlohmann/json.git +[submodule "external/oxen-encoding"] + path = external/oxen-encoding + url = https://github.com/oxen-io/oxen-encoding.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 823af93..771c585 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # project( - "ethyl" + ethyl VERSION 0.1.0 LANGUAGES C CXX ) @@ -21,12 +21,6 @@ endif() # # Set project options # -if(${PROJECT_NAME}_IS_TOPLEVEL_PROJECT) - set(${PROJECT_NAME}_WARNINGS_AS_ERRORS ON CACHE BOOL "" FORCE) -else() - set(${PROJECT_NAME}_ENABLE_UNIT_TESTING FALSE CACHE BOOL "" FORCE) -endif() - include(cmake/StandardSettings.cmake) include(cmake/StaticAnalyzers.cmake) include(cmake/Utils.cmake) @@ -61,7 +55,6 @@ add_subdirectory(external) add_library( ${PROJECT_NAME} - STATIC ${headers} ${sources} ) @@ -92,7 +85,7 @@ message(STATUS "Added all header and implementation files.\n") include(cmake/CompilerWarnings.cmake) set_project_warnings(${PROJECT_NAME}) -verbose_message("Applied compiler warnings. Using standard ${CMAKE_CXX_STANDARD}.\n") +verbose_message("Applied compiler warnings.\n") # # Model project dependencies @@ -105,9 +98,9 @@ target_link_libraries( cpr::cpr secp256k1 nlohmann_json::nlohmann_json + oxenc::oxenc PRIVATE cncrypto - rlpvalue gmp gmpxx ) @@ -123,7 +116,6 @@ target_include_directories( PUBLIC $ $ - ${CMAKE_CURRENT_SOURCE_DIR}/src ) message(STATUS "Finished setting up include directories.") diff --git a/README.md b/README.md index 791d6eb..0d626b5 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@ cmake --build build --parallel --verbose pacman -Syuu # Terminal may prompt to restart before proceeding pacman -S git base-devel libargp-devel cmake gcc libcurl-devel gmp-devel autoconf automake libtool -# MSYS packages libargp with a .dll suffix which breaks rlpvalue's autotool -# script so we patch it up -ln -s /usr/lib/libargp.dll.a /usr/lib/libargp.a - # Build cmake -B build -S . cmake --build build --parallel --verbose diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index b64bffd..7a62601 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -42,8 +42,6 @@ function(set_project_warnings project_name) set(CLANG_WARNINGS -Wall -Wextra # reasonable and standard - -Wshadow # warn the user if a variable declaration shadows one from a - # parent context -Wnon-virtual-dtor # warn the user if a class with virtual functions has a # non-virtual destructor. This helps catch hard to # track down memory errors diff --git a/cmake/StandardSettings.cmake b/cmake/StandardSettings.cmake index e27d9cb..24b6bed 100644 --- a/cmake/StandardSettings.cmake +++ b/cmake/StandardSettings.cmake @@ -2,7 +2,14 @@ # Compiler options # -option(${PROJECT_NAME}_WARNINGS_AS_ERRORS "Treat compiler warnings as errors." OFF) +option(${PROJECT_NAME}_WARNINGS_AS_ERRORS "Treat compiler warnings as errors." ${${PROJECT_NAME}_IS_TOPLEVEL_PROJECT}) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +option(BUILD_SHARED_LIBS "Build libraries as shared libraries" ON) Include(FetchContent) @@ -11,7 +18,7 @@ Include(FetchContent) # # Currently supporting: Catch2. -option(${PROJECT_NAME}_ENABLE_UNIT_TESTING "Enable unit tests for the projects (from the `test` subfolder)." ON) +option(${PROJECT_NAME}_ENABLE_UNIT_TESTING "Enable unit tests for the projects (from the `test` subfolder)." ${${PROJECT_NAME}_IS_TOPLEVEL_PROJECT}) # # Crypto Library @@ -32,14 +39,7 @@ option(${PROJECT_NAME}_ENABLE_CPPCHECK "Enable static analysis with Cppcheck." O option(${PROJECT_NAME}_VERBOSE_OUTPUT "Enable verbose output, allowing for a better understanding of each step taken." ON) -# Export all symbols when building a shared library -if(BUILD_SHARED_LIBS) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF) - set(CMAKE_CXX_VISIBILITY_PRESET hidden) - set(CMAKE_VISIBILITY_INLINES_HIDDEN 1) -endif() - -option(${PROJECT_NAME}_ENABLE_LTO "Enable Interprocedural Optimization, aka Link Time Optimization (LTO)." OFF) +option(${PROJECT_NAME}_ENABLE_LTO "Enable Interprocedural Optimization, aka Link Time Optimization (LTO)." ON) if(${PROJECT_NAME}_ENABLE_LTO) include(CheckIPOSupported) check_ipo_supported(RESULT result OUTPUT output) diff --git a/cmake/system_or_submodule.cmake b/cmake/system_or_submodule.cmake new file mode 100644 index 0000000..d8b9afd --- /dev/null +++ b/cmake/system_or_submodule.cmake @@ -0,0 +1,21 @@ +macro(system_or_submodule BIGNAME smallname pkgconf subdir) + option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) + if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES) + pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET) + endif() + if(${BIGNAME}_FOUND) + add_library(${smallname} INTERFACE) + if(NOT TARGET PkgConfig::${BIGNAME} AND CMAKE_VERSION VERSION_LESS "3.21") + # Work around cmake bug 22180 (PkgConfig::THING not set if no flags needed) + else() + target_link_libraries(${smallname} INTERFACE PkgConfig::${BIGNAME}) + endif() + message(STATUS "Found system ${smallname} ${${BIGNAME}_VERSION}") + else() + message(STATUS "using ${smallname} submodule") + add_subdirectory(${subdir} EXCLUDE_FROM_ALL) + endif() + if(NOT TARGET ${smallname}::${smallname}) + add_library(${smallname}::${smallname} ALIAS ${smallname}) + endif() +endmacro() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index a575594..e69eaa1 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -1,7 +1,18 @@ + +include(../cmake/system_or_submodule.cmake) +find_package(PkgConfig REQUIRED) + + +# Force shared libs off for any libraries we build inside here so that if we build a shared lib we +# don't end up with a libspdlog.so or whatever that would need to be distributed alongside the +# libquic.so +set(BUILD_SHARED_LIBS OFF) + + # # CPR # -if(NOT TARGET cpr) +if(NOT TARGET cpr::cpr) set(CPR_USE_SYSTEM_CURL ON CACHE BOOL "") add_subdirectory(cpr) endif() @@ -9,16 +20,14 @@ endif() # # SECP256k1 # -if(NOT TARGET secp256k1) - set(SECP256K1_ENABLE_MODULE_RECOVERY ON CACHE BOOL "" FORCE) - set(SECP256K1_VALGRIND OFF CACHE BOOL "" FORCE) - set(SECP256K1_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE) - set(SECP256K1_BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF CACHE BOOL "" FORCE) - set(SECP256K1_BUILD_CTIME_TESTS OFF CACHE BOOL "" FORCE) - set(SECP256K1_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - add_subdirectory(secp256k1) -endif() +set(SECP256K1_ENABLE_MODULE_RECOVERY ON CACHE BOOL "" FORCE) +set(SECP256K1_VALGRIND OFF CACHE BOOL "" FORCE) +set(SECP256K1_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE) +set(SECP256K1_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF CACHE BOOL "" FORCE) +set(SECP256K1_BUILD_CTIME_TESTS OFF CACHE BOOL "" FORCE) +set(SECP256K1_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +system_or_submodule(SECP256K1 secp256k1 libsecp256k1 secp256k1) # # Catch2 @@ -28,53 +37,21 @@ if(NOT TARGET Catch2) endif() # -# rlpvalue is built using autotools +# oxen-encoding # -include(ExternalProject) -set(RLP_DIR ${CMAKE_CURRENT_SOURCE_DIR}/rlpvalue) -set(RLP_BIN ${CMAKE_CURRENT_BINARY_DIR}/librlpvalue) -set(RLP_STATIC_LIB ${RLP_BIN}/lib/librlpvalue.a) -set(RLP_INCLUDES ${RLP_BIN}/include) -file(MAKE_DIRECTORY ${RLP_INCLUDES}) - -# NOTE: rlpvalue includes a sub-project, univalue which it configures from the -# top level script. Passing in the `--prefix` at the top-level does not -# propagate down to the bottom level causing `make install` to install to the -# system level prefix. -# -# We override this by specifying prefix at the last step ensuring that both -# `rlpvalue` and `univalue` are installed to the same prefix. -ExternalProject_Add( - librlpvalue - PREFIX ${RLP_BIN} - SOURCE_DIR ${RLP_DIR} - DOWNLOAD_COMMAND cd ${RLP_DIR} && git clean -dfX - CONFIGURE_COMMAND bash -c "CFLAGS='-fPIC' CXXFLAGS='-fPIC' ${RLP_DIR}/autogen.sh" && - bash -c "CFLAGS='-fPIC' CXXFLAGS='-fPIC' ${RLP_DIR}/configure --srcdir=${RLP_DIR} --disable-shared --enable-static=yes" - PATCH_COMMAND git checkout -- src/InfInt.h && - git apply --ignore-whitespace ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/rlpvalue-001-infint-missing-limits-h.patch - BUILD_COMMAND make - INSTALL_COMMAND make install prefix=${RLP_BIN} - BUILD_BYPRODUCTS ${RLP_STATIC_LIB} ${RLP_BIN}/lib/libunivalue.a -) -add_library (rlpvalue STATIC IMPORTED GLOBAL) -add_dependencies (rlpvalue librlpvalue) -set_target_properties(rlpvalue PROPERTIES IMPORTED_LOCATION ${RLP_STATIC_LIB}) -set_target_properties(rlpvalue PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${RLP_INCLUDES}) +system_or_submodule(OXENC oxenc liboxenc>=1.1.0 oxen-encoding) # # nlohmann_json # -if(NOT TARGET nlohmann_json) - set(JSON_BuildTests OFF CACHE INTERNAL "") - set(JSON_MultipleHeaders ON CACHE BOOL "") # Allows multi-header nlohmann use - add_subdirectory(json EXCLUDE_FROM_ALL) -endif() +set(JSON_MultipleHeaders ON CACHE BOOL "") # Allows multi-header nlohmann use +system_or_submodule(NLOHMANN nlohmann_json nlohmann_json>=3.7.0 json) # # GMP # -find_library(gmp gmp) -if(NOT gmp) - message(FATAL_ERROR "gmp not found") -endif() + +pkg_check_modules(GMP gmp IMPORTED_TARGET REQUIRED) +add_library(gmp INTERFACE) +target_link_libraries(gmp INTERFACE PkgConfig::GMP) +message(STATUS "Found gmp ${GMP_VERSION}") diff --git a/external/cpr b/external/cpr index 2533463..3b15fa8 160000 --- a/external/cpr +++ b/external/cpr @@ -1 +1 @@ -Subproject commit 253346373575b383a56e2741b8250a6acf6fa50e +Subproject commit 3b15fa82ea74739b574d705fea44959b58142eb8 diff --git a/external/oxen-encoding b/external/oxen-encoding new file mode 160000 index 0000000..201c4ca --- /dev/null +++ b/external/oxen-encoding @@ -0,0 +1 @@ +Subproject commit 201c4ca86add82eaedf424c5f7c8fb756aa4fc17 diff --git a/external/rlpvalue b/external/rlpvalue deleted file mode 160000 index 1e58dce..0000000 --- a/external/rlpvalue +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1e58dcec547dd19da4c325344d6f363d533dabe0 diff --git a/include/ethyl/provider.hpp b/include/ethyl/provider.hpp index 7750bc9..2761099 100644 --- a/include/ethyl/provider.hpp +++ b/include/ethyl/provider.hpp @@ -13,6 +13,8 @@ #include "transaction.hpp" #include "logs.hpp" +using namespace std::literals; + struct ReadCallData { std::string contractAddress; std::string data; @@ -32,43 +34,43 @@ class Provider { cpr::Url url; cpr::Session session; public: - Provider(const std::string& name, const std::string& _url); + Provider(std::string name, std::string url); ~Provider(); void connectToNetwork(); void disconnectFromNetwork(); - uint64_t getTransactionCount(const std::string& address, const std::string& blockTag); + uint64_t getTransactionCount(std::string_view address, std::string_view blockTag); nlohmann::json callReadFunctionJSON(const ReadCallData& callData, std::string_view blockNumber = "latest"); std::string callReadFunction(const ReadCallData& callData, std::string_view blockNumber = "latest"); std::string callReadFunction(const ReadCallData& callData, uint64_t blockNumberInt); uint32_t getNetworkChainId(); std::string evm_snapshot(); - bool evm_revert(const std::string& snapshotId); + bool evm_revert(std::string_view snapshotId); uint64_t evm_increaseTime(std::chrono::seconds seconds); - std::optional getTransactionByHash(const std::string& transactionHash); - std::optional getTransactionReceipt(const std::string& transactionHash); - std::vector getLogs(uint64_t fromBlock, uint64_t toBlock, const std::string& address); - std::vector getLogs(uint64_t block, const std::string& address); - std::string getContractStorageRoot(const std::string& address, uint64_t blockNumberInt); - std::string getContractStorageRoot(const std::string& address, const std::string& blockNumber = "latest"); + std::optional getTransactionByHash(std::string_view transactionHash); + std::optional getTransactionReceipt(std::string_view transactionHash); + std::vector getLogs(uint64_t fromBlock, uint64_t toBlock, std::string_view address); + std::vector getLogs(uint64_t block, std::string_view address); + std::string getContractStorageRoot(std::string_view address, uint64_t blockNumberInt); + std::string getContractStorageRoot(std::string_view address, std::string_view blockNumber = "latest"); std::string sendTransaction(const Transaction& signedTx); std::string sendUncheckedTransaction(const Transaction& signedTx); - uint64_t waitForTransaction(const std::string& txHash, int64_t timeout = 320000); - bool transactionSuccessful(const std::string& txHash, int64_t timeout = 320000); - uint64_t gasUsed(const std::string& txHash, int64_t timeout = 320000); - std::string getBalance(const std::string& address); + uint64_t waitForTransaction(std::string_view txHash, std::chrono::milliseconds timeout = 320s); + bool transactionSuccessful(std::string_view txHash, std::chrono::milliseconds timeout = 320s); + uint64_t gasUsed(std::string_view txHash, std::chrono::milliseconds timeout = 320s); + std::string getBalance(std::string_view address); std::string getContractDeployedInLatestBlock(); uint64_t getLatestHeight(); FeeData getFeeData(); private: - cpr::Response makeJsonRpcRequest(const std::string& method, const nlohmann::json& params); + cpr::Response makeJsonRpcRequest(std::string_view method, const nlohmann::json& params); }; diff --git a/include/ethyl/signer.hpp b/include/ethyl/signer.hpp index 2fca531..d2e60f0 100644 --- a/include/ethyl/signer.hpp +++ b/include/ethyl/signer.hpp @@ -1,12 +1,15 @@ #pragma once -#include -#include -#include -#include +#include #include +#include +#include +#include +#include + +#include "provider.hpp" -#include "ethyl/provider.hpp" +typedef struct secp256k1_context_struct secp256k1_context; // forward decl class Signer { private: @@ -24,22 +27,22 @@ class Signer { // Returns std::pair, std::vector> generate_key_pair(); - std::array secretKeyToAddress(const std::vector& seckey); - std::string secretKeyToAddressString(const std::vector& seckey); + std::array secretKeyToAddress(std::span seckey); + std::string secretKeyToAddressString(std::span seckey); - std::vector sign(const std::array& hash, const std::vector& seckey); - std::vector sign(const std::string& hash, const std::vector& seckey); + std::vector sign(const std::array& hash, std::span seckey); + std::vector sign(std::string_view hash, std::span seckey); // Client usage methods bool hasProvider() const { return static_cast(provider); } std::shared_ptr getProvider() { return provider; } - std::vector signMessage(const std::string& message, const std::vector& seckey); - std::string signTransaction(Transaction& tx, const std::vector& seckey); + std::vector signMessage(std::string_view message, std::span seckey); + std::string signTransaction(Transaction& tx, std::span seckey); void populateTransaction(Transaction& tx, std::string sender_address); - std::string sendTransaction(Transaction& tx, const std::vector& seckey); + std::string sendTransaction(Transaction& tx, std::span seckey); private: diff --git a/include/ethyl/transaction.hpp b/include/ethyl/transaction.hpp index 22a8568..9e2afc1 100644 --- a/include/ethyl/transaction.hpp +++ b/include/ethyl/transaction.hpp @@ -4,8 +4,6 @@ #include #include -#include "ethyl/utils.hpp" - struct Signature { uint64_t signatureYParity = 0; std::vector signatureR = {}; @@ -13,15 +11,15 @@ struct Signature { bool isEmpty() const; - void fromHex(std::string hex_str); + void fromHex(std::string_view hex_str); }; class Transaction { public: - uint64_t chainId; - uint64_t nonce; - uint64_t maxPriorityFeePerGas; - uint64_t maxFeePerGas; + uint64_t chainId = 0; + uint64_t nonce = 0; + uint64_t maxPriorityFeePerGas = 0; + uint64_t maxFeePerGas = 0; std::string to; uint64_t value; uint64_t gasLimit; @@ -30,8 +28,8 @@ class Transaction { // Constructor // (toAddress, value, gasLimit, data) - Transaction(const std::string& _to , uint64_t _value , uint64_t _gasLimit = 21000, const std::string& _data = "") - : chainId(0), nonce(0), maxPriorityFeePerGas(0), maxFeePerGas(0), to(_to), value(_value), gasLimit(_gasLimit), data(_data) {} + Transaction(std::string to, uint64_t value, uint64_t gasLimit = 21000, std::string data = "") + : to{std::move(to)}, value{std::move(value)}, gasLimit{gasLimit}, data{std::move(data)} {} std::string serialized() const; std::string hash() const; diff --git a/include/ethyl/utils.hpp b/include/ethyl/utils.hpp index c29ee4c..f298d4c 100644 --- a/include/ethyl/utils.hpp +++ b/include/ethyl/utils.hpp @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include #include #include @@ -8,6 +10,9 @@ #include #include +#include +#include + namespace utils { @@ -38,23 +43,62 @@ namespace utils std::string_view trimPrefix(std::string_view src, std::string_view prefix); std::string_view trimLeadingZeros(std::string_view src); - std::vector fromHexString(std::string_view hexStr); - uint64_t fromHexStringToUint64(std::string_view hexStr); - std::array fromHexString32Byte(std::string_view hexStr); + using oxenc::basic_char; + template + std::vector fromHexString(std::string_view hexStr) { + hexStr = trimPrefix(hexStr, "0x"); - std::array hash(std::string in); + if (!oxenc::is_hex(hexStr)) + throw std::invalid_argument{"input string is not hex"}; - std::string getFunctionSignature(const std::string& function); + std::vector result; + result.reserve(oxenc::from_hex_size(hexStr.size())); + oxenc::from_hex(hexStr.begin(), hexStr.end(), std::back_inserter(result)); + return result; + } + extern template std::vector fromHexString(std::string_view); + extern template std::vector fromHexString(std::string_view); - std::string padToNBytes(const std::string& input, size_t byteCount, PaddingDirection direction = PaddingDirection::LEFT); - std::string padTo8Bytes(const std::string& input, PaddingDirection direction = PaddingDirection::LEFT); - std::string padTo32Bytes(const std::string& input, PaddingDirection direction = PaddingDirection::LEFT); + template + std::array fromHexString32Byte(std::string_view hexStr) { + hexStr = trimPrefix(hexStr, "0x"); - std::vector intToBytes(uint64_t num); + if (!oxenc::is_hex(hexStr) || hexStr.size() != 64) { + throw std::invalid_argument("Input string length should be 64 hex characters for 32 bytes"); + } + + std::array bytesArr; + oxenc::from_hex(hexStr.begin(), hexStr.end(), bytesArr.begin()); + + return bytesArr; + } + extern template std::array fromHexString32Byte(std::string_view); + extern template std::array fromHexString32Byte(std::string_view); - std::vector removeLeadingZeros(std::vector vec); - std::string generateRandomString(size_t length); + uint64_t fromHexStringToUint64(std::string_view hexStr); + + /// Parses an integer of some sort from a string, requiring that the entire string be consumed + /// during parsing. Return false if parsing failed, sets `value` and returns true if the entire + /// string was consumed. + template + bool parseInt(const std::string_view str, T& value, int base = 10) { + T tmp; + auto* strend = str.data() + str.size(); + auto [p, ec] = std::from_chars(str.data(), strend, tmp, base); + if (ec != std::errc() || p != strend) + return false; + value = tmp; + return true; + } + + std::array hash(std::string_view in); + + std::string getFunctionSignature(const std::string& function); + + std::vector intToBytes(uint64_t num); + + std::vector removeLeadingZeros(std::span vec); std::string trimAddress(const std::string& address); diff --git a/src/provider.cpp b/src/provider.cpp index 96cc62a..cc30228 100644 --- a/src/provider.cpp +++ b/src/provider.cpp @@ -1,9 +1,15 @@ // provider.cpp +#include #include #include -#include #include +#pragma GCC diagnostic push +#ifndef __clang__ +#pragma GCC diagnostic ignored "-Wuseless-cast" +#endif +#include +#pragma GCC diagnostic pop #include "ethyl/provider.hpp" #include "ethyl/utils.hpp" @@ -11,8 +17,8 @@ #include -Provider::Provider(const std::string& name, const std::string& _url) - : clientName(name), url(_url) { +Provider::Provider(std::string name, std::string url) + : clientName{std::move(name)}, url{std::move(url)} { // Initialize client } @@ -35,7 +41,7 @@ void Provider::disconnectFromNetwork() { std::cout << "Disconnected from the Ethereum network.\n"; } -cpr::Response Provider::makeJsonRpcRequest(const std::string& method, const nlohmann::json& params) { +cpr::Response Provider::makeJsonRpcRequest(std::string_view method, const nlohmann::json& params) { if (url.str() == "") throw std::runtime_error("No URL provided to provider"); nlohmann::json bodyJson; @@ -131,7 +137,7 @@ std::string Provider::evm_snapshot() { throw std::runtime_error("Unable to create snapshot"); } -bool Provider::evm_revert(const std::string& snapshotId) { +bool Provider::evm_revert(std::string_view snapshotId) { nlohmann::json params = nlohmann::json::array(); params.push_back(snapshotId); @@ -182,7 +188,7 @@ uint64_t Provider::evm_increaseTime(std::chrono::seconds seconds) { } -uint64_t Provider::getTransactionCount(const std::string& address, const std::string& blockTag) { +uint64_t Provider::getTransactionCount(std::string_view address, std::string_view blockTag) { nlohmann::json params = nlohmann::json::array(); params.push_back(address); params.push_back(blockTag); @@ -211,7 +217,7 @@ uint64_t Provider::getTransactionCount(const std::string& address, const std::st throw std::runtime_error("Unable to get transaction count"); } -std::optional Provider::getTransactionByHash(const std::string& transactionHash) { +std::optional Provider::getTransactionByHash(std::string_view transactionHash) { nlohmann::json params = nlohmann::json::array(); params.push_back(transactionHash); @@ -236,7 +242,7 @@ std::optional Provider::getTransactionByHash(const std::string& return std::nullopt; } -std::optional Provider::getTransactionReceipt(const std::string& transactionHash) { +std::optional Provider::getTransactionReceipt(std::string_view transactionHash) { nlohmann::json params = nlohmann::json::array(); params.push_back(transactionHash); @@ -261,7 +267,7 @@ std::optional Provider::getTransactionReceipt(const std::string& return std::nullopt; } -std::vector Provider::getLogs(uint64_t fromBlock, uint64_t toBlock, const std::string& address) { +std::vector Provider::getLogs(uint64_t fromBlock, uint64_t toBlock, std::string_view address) { std::vector logEntries; nlohmann::json params = nlohmann::json::array(); @@ -308,19 +314,15 @@ std::vector Provider::getLogs(uint64_t fromBlock, uint64_t toBlock, co return logEntries; } -std::vector Provider::getLogs(uint64_t block, const std::string& address) { +std::vector Provider::getLogs(uint64_t block, std::string_view address) { return getLogs(block, block, address); } -std::string Provider::getContractStorageRoot(const std::string& address, uint64_t blockNumberInt) { - std::stringstream stream; - stream << "0x" << std::hex << blockNumberInt; // Convert uint64_t to hex string - std::string blockNumberHex = stream.str(); - - return getContractStorageRoot(address, blockNumberHex); // Call the original function +std::string Provider::getContractStorageRoot(std::string_view address, uint64_t blockNumberInt) { + return getContractStorageRoot(address, "0x" + utils::decimalToHex(blockNumberInt)); } -std::string Provider::getContractStorageRoot(const std::string& address, const std::string& blockNumber) { +std::string Provider::getContractStorageRoot(std::string_view address, std::string_view blockNumber) { nlohmann::json params = nlohmann::json::array(); params.push_back(address); auto storage_keys = nlohmann::json::array(); @@ -342,28 +344,33 @@ std::string Provider::getContractStorageRoot(const std::string& address, const s throw std::runtime_error("No storage proof found in response"); } +// Calls `f()` which should return an optional repeatedly (sleeping for `call_interval` between +// each call) until it returns a non-nullopt, then returns it. Throws runtime_error on timeout. +template +static auto waitForResult(Func&& f, std::chrono::milliseconds timeout, const std::string& errmsg = "Transaction inclusion in a block timned out", std::chrono::milliseconds call_interval = 500ms) { + auto timeout_at = std::chrono::steady_clock::now() + timeout; + + auto val = f(); + while (!val.has_value() && std::chrono::steady_clock::now() < timeout_at) { + std::this_thread::sleep_for(call_interval); + val = f(); + } + + if (val.has_value()) + return std::move(*val); + + throw std::runtime_error{errmsg}; +} + // Create and send a raw transaction returns the hash but will also check that it got into the mempool std::string Provider::sendTransaction(const Transaction& signedTx) { std::string hash = sendUncheckedTransaction(signedTx); - std::future futureTx = std::async(std::launch::async, [&]() -> std::string { - std::vector timeouts = { 4000, 100, 1000 }; - - while(true) { - const auto maybe_tx = getTransactionByHash(hash); - if(maybe_tx) { + return waitForResult([&]() -> std::optional { + if (getTransactionByHash(hash)) return hash; - } - - if(timeouts.empty()) { - throw std::runtime_error("Transaction request timed out"); - } - std::this_thread::sleep_for(std::chrono::milliseconds(timeouts.back())); - timeouts.pop_back(); - } - }); - - return futureTx.get(); + return std::nullopt; + }, 5s, "Transaction request timed out"); } // Create and send a raw transaction returns the hash without checking if it succeeded in getting into the mempool @@ -386,85 +393,53 @@ std::string Provider::sendUncheckedTransaction(const Transaction& signedTx) { } } -uint64_t Provider::waitForTransaction(const std::string& txHash, int64_t timeout) { - std::future futureTx = std::async(std::launch::async, [&]() -> uint64_t { - auto start = std::chrono::steady_clock::now(); - while(true) { - const auto maybe_tx_json = getTransactionByHash(txHash); - - // If transaction is received, resolve the promise - if(maybe_tx_json && !(*maybe_tx_json)["blockNumber"].is_null()) { - // Parse the block number from the hex string - std::string blockNumberHex = (*maybe_tx_json)["blockNumber"]; - return utils::fromHexStringToUint64(blockNumberHex); - } +uint64_t Provider::waitForTransaction( + std::string_view txHash, std::chrono::milliseconds timeout) { + return waitForResult( + [&]() -> std::optional { + if (const auto maybe_tx_json = getTransactionByHash(txHash); + maybe_tx_json && !(*maybe_tx_json)["blockNumber"].is_null()) { - auto now = std::chrono::steady_clock::now(); - if(std::chrono::duration_cast(now - start).count() > timeout) { - throw std::runtime_error("Transaction inclusion in a block timed out"); - } - - // Wait for a while before next check - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - return futureTx.get(); + // Parse the block number from the hex string + auto blockNumberHex = (*maybe_tx_json)["blockNumber"].get(); + return utils::fromHexStringToUint64(blockNumberHex); + } + return std::nullopt; + }, + timeout); } -bool Provider::transactionSuccessful(const std::string& txHash, int64_t timeout) { - std::future futureTx = std::async(std::launch::async, [&]() -> uint64_t { - auto start = std::chrono::steady_clock::now(); - while(true) { - const auto maybe_tx_json = getTransactionReceipt(txHash); - - // If transaction is received, resolve the promise - if(maybe_tx_json && !(*maybe_tx_json)["status"].is_null()) { - // Parse the status from the hex string - std::string statusHex = (*maybe_tx_json)["status"]; - return static_cast(utils::fromHexStringToUint64(statusHex)); - } - - auto now = std::chrono::steady_clock::now(); - if(std::chrono::duration_cast(now - start).count() > timeout) { - throw std::runtime_error("Transaction inclusion in a block timed out"); - } +bool Provider::transactionSuccessful(std::string_view txHash, std::chrono::milliseconds timeout) { + return waitForResult( + [&]() -> std::optional { + if (const auto maybe_tx_json = getTransactionReceipt(txHash); + maybe_tx_json && !(*maybe_tx_json)["status"].is_null()) { - // Wait for a while before next check - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - return futureTx.get(); + // Parse the status from the hex string + auto statusHex = (*maybe_tx_json)["status"].get(); + return static_cast(utils::fromHexStringToUint64(statusHex)); + } + return std::nullopt; + }, + timeout); } -uint64_t Provider::gasUsed(const std::string& txHash, int64_t timeout) { - std::future futureTx = std::async(std::launch::async, [&]() -> uint64_t { - auto start = std::chrono::steady_clock::now(); - while(true) { - const auto maybe_tx_json = getTransactionReceipt(txHash); - - // If transaction is received, resolve the promise - if(maybe_tx_json && !(*maybe_tx_json)["gasUsed"].is_null()) { - // Parse the status from the hex string - std::string gasUsed = (*maybe_tx_json)["gasUsed"]; - return utils::fromHexStringToUint64(gasUsed); - } - - auto now = std::chrono::steady_clock::now(); - if(std::chrono::duration_cast(now - start).count() > timeout) { - throw std::runtime_error("Transaction inclusion in a block timed out"); - } +uint64_t Provider::gasUsed(std::string_view txHash, std::chrono::milliseconds timeout) { + return waitForResult( + [&]() -> std::optional { + if (const auto maybe_tx_json = getTransactionReceipt(txHash); + maybe_tx_json && !(*maybe_tx_json)["gasUsed"].is_null()) { - // Wait for a while before next check - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - }); - - return futureTx.get(); + // Parse the status from the hex string + auto gasUsed = (*maybe_tx_json)["gasUsed"].get(); + return utils::fromHexStringToUint64(gasUsed); + } + return std::nullopt; + }, + timeout); } -std::string Provider::getBalance(const std::string& address) { +std::string Provider::getBalance(std::string_view address) { nlohmann::json params = nlohmann::json::array(); params.push_back(address); params.push_back("latest"); @@ -484,7 +459,7 @@ std::string Provider::getBalance(const std::string& address) { return balance.get_str(); } else { - throw std::runtime_error("Failed to get balance for address " + address); + throw std::runtime_error("Failed to get balance for address " + std::string{address}); } } diff --git a/src/signer.cpp b/src/signer.cpp index 3bc01cd..daaf74a 100644 --- a/src/signer.cpp +++ b/src/signer.cpp @@ -1,9 +1,8 @@ #include "ethyl/signer.hpp" -#include #include #include -#include +#include #include "ethyl/ecdsa_util.h" #include "ethyl/utils.hpp" @@ -60,7 +59,7 @@ std::pair, std::vector> Signer::genera std::vector(compressed_pubkey, compressed_pubkey + sizeof(compressed_pubkey))}; } -std::array Signer::secretKeyToAddress(const std::vector& seckey) { +std::array Signer::secretKeyToAddress(std::span seckey) { std::string address; // Verify the private key. @@ -75,12 +74,13 @@ std::array Signer::secretKeyToAddress(const std::vector pub(65); + std::array pub; size_t pub_len = 65; secp256k1_ec_pubkey_serialize(ctx, pub.data(), &pub_len, &pubkey, SECP256K1_EC_UNCOMPRESSED); + std::string_view pub_string{reinterpret_cast(pub.data()), pub.size()}; // Skip the type byte. - std::string pub_string(pub.begin() + 1, pub.end()); + pub_string.remove_prefix(1); auto hashed_pub = utils::hash(pub_string); // The last 20 bytes of the Keccak-256 hash of the public key in hex is the address. @@ -89,7 +89,7 @@ std::array Signer::secretKeyToAddress(const std::vector& seckey) { +std::string Signer::secretKeyToAddressString(std::span seckey) { std::array address = secretKeyToAddress(seckey); std::string result = {}; result.reserve(2 + (address.max_size() * 2)); @@ -99,7 +99,7 @@ std::string Signer::secretKeyToAddressString(const std::vector& s } -std::vector Signer::sign(const std::array& hash, const std::vector& seckey) { +std::vector Signer::sign(const std::array& hash, std::span seckey) { secp256k1_ecdsa_recoverable_signature sig; unsigned char serialized_signature[64]; int recid; @@ -126,7 +126,7 @@ std::vector Signer::sign(const std::array& has return signature; } -std::vector Signer::sign(const std::string& hash, const std::vector& seckey) { +std::vector Signer::sign(std::string_view hash, std::span seckey) { return sign(utils::fromHexString32Byte(hash), seckey); } @@ -167,12 +167,12 @@ void Signer::populateTransaction(Transaction& tx, std::string sender_address) { } // Hash the message and sign -std::vector Signer::signMessage(const std::string& message, const std::vector& seckey) { +std::vector Signer::signMessage(std::string_view message, std::span seckey) { return sign(utils::hash(message), seckey); } // Hash the transaction and sign -std::string Signer::signTransaction(Transaction& txn, const std::vector& seckey) { +std::string Signer::signTransaction(Transaction& txn, std::span seckey) { const auto signature_hex = utils::toHexString(sign(txn.hash(), seckey)); txn.sig.fromHex(signature_hex); @@ -180,7 +180,7 @@ std::string Signer::signTransaction(Transaction& txn, const std::vector& seckey) { +std::string Signer::sendTransaction(Transaction& txn, std::span seckey) { const auto senders_address = secretKeyToAddressString(seckey); populateTransaction(txn, senders_address); diff --git a/src/transaction.cpp b/src/transaction.cpp index 738dfe8..992a482 100644 --- a/src/transaction.cpp +++ b/src/transaction.cpp @@ -1,50 +1,39 @@ #include "ethyl/transaction.hpp" #include "ethyl/utils.hpp" -#include -#include -#include - -// Optimized helper function to append data to the RLP array -// Declared inline to hint to the compiler that it should avoid function call overhead by integrating the function's body at each call site. -template -inline void appendDataToRLP(RLPValue& arr, const T& value, Converter convertFunction) { - RLPValue temp_val; - temp_val.assign(convertFunction(value)); - arr.push_back(temp_val); -} - +#include + +/** +* Returns the raw Bytes of the EIP-1559 transaction, in order. +* +* Format: `[chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, +* accessList, signatureYParity, signatureR, signatureS]` +* +* For an unsigned tx this method uses the empty Bytes values for the +* signature parameters `v`, `r` and `s` for encoding. +*/ std::string Transaction::serialized() const { - try { - RLPValue arr(RLPValue::VARR); - arr.setArray(); - - // Serialize transaction data using the optimized helper function - appendDataToRLP(arr, chainId, utils::intToBytes); - appendDataToRLP(arr, nonce, utils::intToBytes); - appendDataToRLP(arr, maxPriorityFeePerGas, utils::intToBytes); - appendDataToRLP(arr, maxFeePerGas, utils::intToBytes); - appendDataToRLP(arr, gasLimit, utils::intToBytes); - appendDataToRLP(arr, to, utils::fromHexString); - appendDataToRLP(arr, value, utils::intToBytes); - appendDataToRLP(arr, this->data, utils::fromHexString); // Explicitly use member variable - - // Handle the access list, which is empty in this implementation - RLPValue access_list(RLPValue::VARR); - access_list.setArray(); - arr.push_back(access_list); - - if (!sig.isEmpty()) { - appendDataToRLP(arr, sig.signatureYParity, utils::intToBytes); - appendDataToRLP(arr, sig.signatureR, utils::removeLeadingZeros); - appendDataToRLP(arr, sig.signatureS, utils::removeLeadingZeros); - } - - return "0x02" + utils::toHexString(arr.write()); - } catch (const std::exception& e) { - std::cerr << "Error serializing transaction: " << e.what() << std::endl; - throw; + using namespace oxenc; + + std::vector, std::vector>> arr; + arr.push_back(chainId); + arr.push_back(nonce); + arr.push_back(maxPriorityFeePerGas); + arr.push_back(maxFeePerGas); + arr.push_back(gasLimit); + arr.push_back(utils::fromHexString(to)); + arr.push_back(value); + arr.push_back(utils::fromHexString(data)); + + // Access list not going to use + arr.push_back(std::vector{}); + + if (!sig.isEmpty()) { + arr.push_back(sig.signatureYParity); + arr.push_back(sig.signatureR); + arr.push_back(sig.signatureS); } + return "0x02" + oxenc::to_hex(rlp_serialize(arr)); } std::string Transaction::hash() const { @@ -55,16 +44,16 @@ bool Signature::isEmpty() const { return signatureYParity == 0 && signatureR.empty() && signatureS.empty(); } -void Signature::fromHex(std::string hex_str) { - if (hex_str.size() >= 2 && hex_str[0] == '0' && hex_str[1] == 'x') { - hex_str = hex_str.substr(2); - } +void Signature::fromHex(std::string_view hex_str) { - if (hex_str.size() != 130) { + auto bytes = utils::fromHexString(hex_str); + if (bytes.size() != 65) { throw std::invalid_argument("Input string length should be 130 characters for 65 bytes"); } - signatureR = utils::fromHexString(hex_str.substr(0, 64)); - signatureS = utils::fromHexString(hex_str.substr(64, 64)); - signatureYParity = std::stoull(hex_str.substr(128, 2), nullptr, 16); + signatureR.resize(32); + signatureS.resize(32); + std::memcpy(signatureR.data(), bytes.data(), 32); + std::memcpy(signatureS.data(), bytes.data() + 32, 32); + signatureYParity = bytes[64]; } diff --git a/src/utils.cpp b/src/utils.cpp index de6241f..1ccebf6 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,123 +1,60 @@ #include "ethyl/utils.hpp" -#include +#include #include #include #include -#include + +#include +#include extern "C" { #include "crypto/keccak.h" } std::string utils::decimalToHex(uint64_t decimal) { - std::stringstream ss; - ss << std::hex << decimal; - return ss.str(); + char buf[20]; + auto [end, ec] = std::to_chars(std::begin(buf), std::end(buf), decimal, 16); + return {buf, ec == std::errc{} ? static_cast(end - buf) : 0}; } std::string_view utils::trimPrefix(std::string_view src, std::string_view prefix) { - std::string_view result = src; - if (result.size() >= prefix.size()) { - if (result.substr(0, prefix.size()) == prefix) { - result = result.substr(prefix.size(), result.size() - prefix.size()); - } - } - return result; + if (src.starts_with(prefix)) + return src.substr(prefix.size()); + return src; } std::string_view utils::trimLeadingZeros(std::string_view src) { - std::string_view result = src; - while (result.size() && result[0] == '0') { - result = result.substr(1, result.size() - 1); - } - return result; -} - -struct HexToU8Result { - bool success; - uint8_t u8; -}; - -static HexToU8Result hexToU8(char ch) { - HexToU8Result result = {}; - result.success = true; - - if (ch >= 'a' && ch <= 'f') - result.u8 = static_cast(ch - 'a' + 10); - else if (ch >= 'A' && ch <= 'F') - result.u8 = static_cast(ch - 'A' + 10); - else if (ch >= '0' && ch <= '9') - result.u8 = static_cast(ch - '0'); - else - result.success = false; - - return result; -} - -std::vector utils::fromHexString(std::string_view hexStr) { - hexStr = trimPrefix(hexStr, "0x"); - assert(hexStr.size() % 2 == 0); - - std::vector result; - result.reserve(hexStr.length() / 2); - for (size_t i = 0; i < hexStr.length(); i += 2) { - std::string_view byteString = hexStr.substr(i, 2); - HexToU8Result hi = hexToU8(byteString[0]); - HexToU8Result lo = hexToU8(byteString[1]); - unsigned char byte = static_cast(hi.u8 << 4 | lo.u8 << 0); - result.push_back(byte); - } - return result; + if (auto p = src.find_first_not_of('0'); p != src.npos) + return src.substr(p); + return src; } uint64_t utils::fromHexStringToUint64(std::string_view hexStr) { - std::string_view realHex = trimPrefix(hexStr, "0x"); - - // NOTE: Trim leading '0's - while (realHex.size() && realHex[0] == '0') { - realHex = realHex.substr(1, realHex.size() - 1); - } + uint64_t val; + if (parseInt(trimPrefix(hexStr, "0x"), val, 16)) + return val; - size_t maxHexSize = sizeof(uint64_t) * 2 /*hex chars per byte*/; - assert(realHex.size() <= maxHexSize); - - size_t size = std::min(maxHexSize, realHex.size()); - uint64_t result = 0; - for (size_t index = 0; index < size; index++) { - char ch = realHex[index]; - HexToU8Result hexResult = hexToU8(ch); - assert(hexResult.success); - result = (result << 4) | hexResult.u8; - } - return result; + throw std::invalid_argument{"failed to parse integer from hex input"}; } -std::array utils::fromHexString32Byte(std::string_view hexStr) { - std::vector bytesVec = fromHexString(hexStr); +template std::vector utils::fromHexString(std::string_view); +template std::vector utils::fromHexString(std::string_view); +template std::array utils::fromHexString32Byte(std::string_view); +template std::array utils::fromHexString32Byte(std::string_view); - if(bytesVec.size() != 32) { - throw std::invalid_argument("Input string length should be 64 characters for 32 bytes"); - } - - std::array bytesArr; - std::copy(bytesVec.begin(), bytesVec.end(), bytesArr.begin()); - - return bytesArr; -} - -std::array utils::hash(std::string in) { - std::vector bytes; +std::array utils::hash(std::string_view in) { + std::vector bytes; // Check for "0x" prefix and if exists, convert the hex to bytes - if(in.size() >= 2 && in[0] == '0' && in[1] == 'x') { - bytes = fromHexString(in); - in = std::string(bytes.begin(), bytes.end()); + if (in.starts_with("0x")) { + bytes = fromHexString(in); + in = {bytes.data(), bytes.size()}; } std::array hash; - keccak(reinterpret_cast(in.c_str()), in.size(), hash.data(), 32); + keccak(reinterpret_cast(in.data()), in.size(), hash.data(), 32); return hash; } @@ -132,85 +69,18 @@ std::string utils::getFunctionSignature(const std::string& function) { return "0x" + hashHex.substr(0, 8); } -std::string utils::padToNBytes(const std::string& input, size_t byteCount, utils::PaddingDirection direction) { - std::string output = input; - bool has0xPrefix = false; - - // Check if input starts with "0x" prefix - if (output.substr(0, 2) == "0x") { - has0xPrefix = true; - output = output.substr(2); // remove "0x" prefix for now - } - - // Calculate padding size based on byteCount * 2 (since each byte is represented by 2 hex characters) - const size_t targetHexStringSize = byteCount * 2; - const size_t startingSize = std::max(output.size(), static_cast(1)); // Size is atleast 1 element such that we handle when output.size == 0 - const size_t startingSizeRoundedUp = startingSize + (targetHexStringSize - 1); - const size_t nextMultiple = /*floor*/ (startingSizeRoundedUp / targetHexStringSize) * targetHexStringSize; - const size_t paddingSize = nextMultiple - output.size(); - - if (direction == PaddingDirection::LEFT) { - output.insert(0, paddingSize, '0'); - } else { - output.append(paddingSize, '0'); - } - - // If input started with "0x", add it back - if (has0xPrefix) { - output.insert(0, "0x"); - } - - return output; -} - -std::string utils::padTo32Bytes(const std::string& input, PaddingDirection direction) { - return padToNBytes(input, 32, direction); -} - -std::string utils::padTo8Bytes(const std::string& input, PaddingDirection direction) { - return padToNBytes(input, 8, direction); -} - std::vector utils::intToBytes(uint64_t num) { - if (num == 0) - return std::vector{}; - - std::stringstream stream; - stream << std::hex << num; - std::string hex = stream.str(); - if (hex.length() % 2) { hex = "0" + hex; } - - std::vector result(hex.length() / 2); - for (size_t i = 0; i < hex.length(); i += 2) { - std::string byteString = hex.substr(i, 2); - unsigned char byte = static_cast(std::stoi(byteString, nullptr, 16)); - result[i / 2] = byte; - } - - return result; + std::array buf; + oxenc::write_host_as_big(num, buf.data()); + return removeLeadingZeros(buf); } -std::vector utils::removeLeadingZeros(std::vector vec) { +std::vector utils::removeLeadingZeros(std::span vec) { auto it = vec.begin(); while(it != vec.end() && *it == 0) { ++it; } - vec.erase(vec.begin(), it); - return vec; -} - -std::string utils::generateRandomString(size_t length) { - srand(static_cast(time(nullptr))); // Seed the random number generator - const char charset[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - const int64_t max_index = sizeof(charset) - 1; - - std::string randomString; - - for (size_t i = 0; i < length; ++i) { - randomString += charset[static_cast(rand() % max_index)]; - } - - return randomString; + return {it, vec.end()}; } std::string utils::trimAddress(const std::string& address) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 808a4a7..4e86532 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -4,23 +4,27 @@ cmake_minimum_required(VERSION 3.15) # Project details # -verbose_message("Adding tests under ${CMAKE_PROJECT_NAME}tests...") +verbose_message("Adding tests under ${PROJECT_NAME}tests...") foreach(file ${test_sources}) - string(REGEX REPLACE "(.*/)([a-zA-Z0-9_ ]+)(\.cpp)" "\\2" test_name ${file}) + get_filename_component(test_name ${file} NAME_WE) add_executable(${test_name}_tests ${file}) - target_compile_features(${test_name}_tests PUBLIC cxx_std_17) - - - set(${CMAKE_PROJECT_NAME}_TEST_LIB ${CMAKE_PROJECT_NAME}) + set(${PROJECT_NAME}_TEST_LIB ${PROJECT_NAME}) target_link_libraries( ${test_name}_tests - PUBLIC + PRIVATE Catch2::Catch2WithMain - ${${CMAKE_PROJECT_NAME}_TEST_LIB} + ethyl + ${${PROJECT_NAME}_TEST_LIB} cncrypto + cpr::cpr + ) + target_include_directories( + ${test_name}_tests + PRIVATE + ${PROJECT_SOURCE_DIR}/src ) add_test( @@ -31,4 +35,4 @@ foreach(file ${test_sources}) ) endforeach() -verbose_message("Finished adding unit tests for ${CMAKE_PROJECT_NAME}.") +verbose_message("Finished adding unit tests for ${PROJECT_NAME}.") diff --git a/test/src/basic.cpp b/test/src/basic.cpp index d4053fc..7e80f6d 100644 --- a/test/src/basic.cpp +++ b/test/src/basic.cpp @@ -3,7 +3,9 @@ #include #pragma GCC diagnostic push +#ifndef __clang__ #pragma GCC diagnostic ignored "-Wduplicated-branches" +#endif #include #pragma GCC diagnostic pop @@ -11,10 +13,6 @@ #include "ethyl/ecdsa_util.h" -extern "C" { -#include "crypto/keccak.h" -} - #include #include diff --git a/test/src/ethereum_client.cpp b/test/src/ethereum_client.cpp index 6407e36..4f94bb1 100644 --- a/test/src/ethereum_client.cpp +++ b/test/src/ethereum_client.cpp @@ -1,5 +1,7 @@ #include +#include + #include "ethyl/provider.hpp" #include "ethyl/signer.hpp" #include "ethyl/utils.hpp" @@ -7,13 +9,13 @@ #include #include +using namespace oxenc::literals; + // Construct the client with the local RPC URL -inline constexpr std::string_view PRIVATE_KEY = "96a656cbd64281ea82257ca9978093b25117592287e4e07f5be660d1701f03e9"; +inline constexpr auto PRIVATE_KEY = "96a656cbd64281ea82257ca9978093b25117592287e4e07f5be660d1701f03e9"_hex_u; inline constexpr std::string_view ADDRESS = "0x2ccb8b65024e4aa9615a8e704dfb11be76674f1f"; inline constexpr std::string_view ANVIL_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; -inline constexpr std::string_view ANVIL_PRIVATE_KEY = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; -std::vector seckey = utils::fromHexString(std::string(PRIVATE_KEY)); -std::vector anvilseckey = utils::fromHexString(std::string(ANVIL_PRIVATE_KEY)); +inline constexpr auto ANVIL_PRIVATE_KEY = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"_hex_u; auto provider = std::make_shared("Client", std::string("127.0.0.1:8545")); Signer signer(provider); @@ -32,13 +34,13 @@ TEST_CASE( "HashTest", "[utils]" ) { TEST_CASE( "SigningTest", "[signer]" ) { std::string hash_hello_world = utils::toHexString(utils::hash("Hello World!\n")); - const auto signature_bytes = signer.signMessage("Hello World!", seckey); + const auto signature_bytes = signer.signMessage("Hello World!", PRIVATE_KEY); std::string signature_hex = utils::toHexString(signature_bytes); REQUIRE( signature_hex == "35f409302082e02b5126c82be93a3946d30e93722ce3ff87bdb01fc385fe312054f3fade7fab80dcabadabf96af75577327dfd064abd47a36543a475e04840e701" ); } TEST_CASE( "Get address from private key", "[signer]" ) { - std::string created_address = signer.secretKeyToAddressString(seckey); + std::string created_address = signer.secretKeyToAddressString(PRIVATE_KEY); REQUIRE( created_address == ADDRESS ); } @@ -80,7 +82,7 @@ TEST_CASE( "Signs an unsigned transaction correctly", "[transaction]" ) { Transaction tx("0xA6C077fd9283421C657EcEa8a9c1422cc6CEbc80", 1000000000000000000, 21000); tx.nonce = 1; tx.chainId = 1; - const auto signature_hex_string = signer.signTransaction(tx, seckey); + const auto signature_hex_string = signer.signTransaction(tx, PRIVATE_KEY); REQUIRE( signature_hex_string == "0x02f86a0101808082520894a6c077fd9283421c657ecea8a9c1422cc6cebc80880de0b6b3a764000080c080a084987299f8dd115333356ab03430ca8de593e03ba03d4ecd72daf15205119cf8a0216c9869da3497ae96dcb98713908af1a0abf866c12d51def821caf0374cccbb" ); } @@ -91,7 +93,7 @@ TEST_CASE( "Does a self transfer", "[transaction]" ) { const auto feeData = provider->getFeeData(); tx.maxFeePerGas = feeData.maxFeePerGas; tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas; - const auto signature_hex_string = signer.signTransaction(tx, anvilseckey); + const auto signature_hex_string = signer.signTransaction(tx, ANVIL_PRIVATE_KEY); const auto hash = provider->sendTransaction(tx); REQUIRE(hash != ""); REQUIRE(provider->transactionSuccessful(hash)); @@ -99,7 +101,7 @@ TEST_CASE( "Does a self transfer", "[transaction]" ) { TEST_CASE( "Does a self transfer on network using signer to populate", "[transaction]" ) { Transaction tx(std::string(ANVIL_ADDRESS), 100000000000000, 21000); - const auto hash = signer.sendTransaction(tx, anvilseckey); + const auto hash = signer.sendTransaction(tx, ANVIL_PRIVATE_KEY); REQUIRE(hash != ""); REQUIRE(provider->transactionSuccessful(hash)); }