Skip to content

Commit

Permalink
Merge pull request #21 from Doy-lee/doyle-backup-providers
Browse files Browse the repository at this point in the history
Support backup providers in `ethyl::Provider`
  • Loading branch information
darcys22 authored May 15, 2024
2 parents 1a35a6a + be110ea commit b4893cd
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 86 deletions.
56 changes: 46 additions & 10 deletions include/ethyl/provider.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

using namespace std::literals;

namespace ethyl
{
struct ReadCallData {
std::string contractAddress;
std::string data;
Expand All @@ -30,16 +32,26 @@ struct FeeData {
: gasPrice(_gasPrice), maxFeePerGas(_maxFeePerGas), maxPriorityFeePerGas(_maxPriorityFeePerGas) {}
};

class Provider {
std::string clientName;
struct Client {
std::string name;
cpr::Url url;
cpr::Session session;
std::mutex mutex;
public:
Provider(std::string name, std::string url);
~Provider();
};

void connectToNetwork();
struct Provider {
/** Add a RPC backend for interacting with the Ethereum network.
*
* The provider does not ensure that no duplicates are added to the list.
*
* @param name A label for the type of client being added. This information
* is stored only for the user to identify the client in the list of
* clients in a given provider.
*
* @returns True if the client was added successfully. False if the `url`
* was not set.
*/
bool addClient(std::string name, std::string url);

bool connectToNetwork();
void disconnectFromNetwork();

uint64_t getTransactionCount(std::string_view address, std::string_view blockTag);
Expand Down Expand Up @@ -72,7 +84,31 @@ class Provider {
uint64_t getLatestHeight();
FeeData getFeeData();

/// List of clients for interacting with the Ethereum network via RPC
/// The order of the clients dictates the order in which a request is
/// attempted.
std::vector<Client> clients;

/// How long the provider is to attempt a connection to the client when
/// sending a request to it. If no value is set, the default connect timeout
/// of CURL is used which is currently 300 seconds.
std::optional<std::chrono::milliseconds> connectTimeout;

private:
cpr::Response makeJsonRpcRequest(std::string_view method, const nlohmann::json& params);
/**
* @param timeout Set a timeout for the provider to connect to current
* client it's attempting before returning a timeout failure. If this is not
* set,the timeout is the default timeout value of CURL which is 300
* seconds.
*
* If you have multiple clients it may be desired to set this value such
* that the provider can quickly evaluate the backup clients in its list on
* failure.
*/
cpr::Response makeJsonRpcRequest(std::string_view method,
const nlohmann::json& params,
std::optional<std::chrono::milliseconds> timeout);
cpr::Session session;
std::mutex mutex;
};

}; // namespace ethyl
16 changes: 4 additions & 12 deletions include/ethyl/signer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@

typedef struct secp256k1_context_struct secp256k1_context; // forward decl

namespace ethyl
{
class Signer {
private:
secp256k1_context* ctx;
std::shared_ptr<Provider> provider;

uint64_t maxPriorityFeePerGas = 0;
uint64_t maxFeePerGas = 0;
uint64_t gasPrice = 0;

public:
Signer();
Signer(const std::shared_ptr<Provider>& client);
~Signer();

// Returns <Pubkey, Seckey>
Expand All @@ -34,20 +34,12 @@ class Signer {
std::vector<unsigned char> sign(std::string_view hash, std::span<const unsigned char> seckey);

// Client usage methods
bool hasProvider() const { return static_cast<bool>(provider); }
std::shared_ptr<Provider> getProvider() { return provider; }


std::vector<unsigned char> signMessage(std::string_view message, std::span<const unsigned char> seckey);
std::string signTransaction(Transaction& tx, std::span<const unsigned char> seckey);

void populateTransaction(Transaction& tx, std::string sender_address);
std::string sendTransaction(Transaction& tx, std::span<const unsigned char> seckey);


private:
void initContext();

// END
Provider provider;
};

}; // namespace ethyl
100 changes: 63 additions & 37 deletions src/provider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,72 @@

#include <gmpxx.h>


Provider::Provider(std::string name, std::string url)
: clientName{std::move(name)}, url{std::move(url)} {
// Initialize client
}

Provider::~Provider() {
// Cleanup client
namespace ethyl
{
bool Provider::addClient(std::string name, std::string url) {
bool result = url.size();
if (result) {
clients.emplace_back(Client{std::move(name), std::move(url)});
}
return result;
}

void Provider::connectToNetwork() {
bool Provider::connectToNetwork() {
// Here we can verify connection by calling some simple JSON RPC method like `net_version`
auto response = makeJsonRpcRequest("net_version", cpr::Body("{}"));
if (response.status_code == 200) {
auto response = makeJsonRpcRequest("net_version", cpr::Body("{}"), connectTimeout);
bool result = response.status_code == 200;
if (result) {
std::cout << "Connected to the Ethereum network.\n";
} else {
std::cout << "Failed to connect to the Ethereum network.\n";
}
return result;
}

void Provider::disconnectFromNetwork() {
// Code to disconnect from Ethereum network
std::cout << "Disconnected from the Ethereum network.\n";
}

cpr::Response Provider::makeJsonRpcRequest(std::string_view method, const nlohmann::json& params) {
if (url.str() == "")
throw std::runtime_error("No URL provided to provider");
cpr::Response Provider::makeJsonRpcRequest(std::string_view method,
const nlohmann::json& params,
std::optional<std::chrono::milliseconds> timeout) {
if (clients.empty()) {
throw std::runtime_error(
"No clients were set for the provider. Ensure that a client was "
"added to the provider before issuing a request.");
}

nlohmann::json bodyJson;
bodyJson["jsonrpc"] = "2.0";
bodyJson["method"] = method;
bodyJson["params"] = params;
bodyJson["id"] = 1;

bodyJson["method"] = method;
bodyJson["params"] = params;
bodyJson["id"] = 1;
cpr::Body body(bodyJson.dump());

cpr::Response result = {};

std::lock_guard lock{mutex};
session.SetUrl(url);
session.SetBody(body);
session.SetHeader({{"Content-Type", "application/json"}});
return session.Post();
if (timeout) {
session.SetOption(cpr::ConnectTimeout(*timeout));
}

// NOTE: Try the request across clients one-by-one until we succeed.
for (const Client& client : clients) {
session.SetUrl(client.url);
result = session.Post();

// TODO(doyle): It is worth it in the future to give stats on which
// client failed and return it to the caller so that they can have some
// mitigation strategy when a client is frequently failing.
if (result.error.code == cpr::ErrorCode::OK) {
break;
}
}

return result;
}

nlohmann::json Provider::callReadFunctionJSON(const ReadCallData& callData, std::string_view blockNumber) {
Expand All @@ -67,7 +92,7 @@ nlohmann::json Provider::callReadFunctionJSON(const ReadCallData& callData, std:
params[0]["to"] = callData.contractAddress;
params[0]["data"] = callData.data;
params[1] = blockNumber; // use the provided block number or default to "latest"
cpr::Response response = makeJsonRpcRequest("eth_call", params);
cpr::Response response = makeJsonRpcRequest("eth_call", params, connectTimeout);

if (response.status_code == 200) {
nlohmann::json responseJson = nlohmann::json::parse(response.text);
Expand Down Expand Up @@ -102,7 +127,7 @@ std::string Provider::callReadFunction(const ReadCallData& callData, uint64_t bl
uint32_t Provider::getNetworkChainId() {
// Make the request takes no params
nlohmann::json params = nlohmann::json::array();
cpr::Response response = makeJsonRpcRequest("net_version", params);
cpr::Response response = makeJsonRpcRequest("net_version", params, connectTimeout);

if (response.status_code == 200) {
// Parse the response
Expand All @@ -126,7 +151,7 @@ uint32_t Provider::getNetworkChainId() {

std::string Provider::evm_snapshot() {
nlohmann::json params = nlohmann::json::array();
cpr::Response response = makeJsonRpcRequest("evm_snapshot", params);
cpr::Response response = makeJsonRpcRequest("evm_snapshot", params, connectTimeout);

if (response.status_code == 200) {
nlohmann::json responseJson = nlohmann::json::parse(response.text);
Expand All @@ -142,7 +167,7 @@ bool Provider::evm_revert(std::string_view snapshotId) {
nlohmann::json params = nlohmann::json::array();
params.push_back(snapshotId);

cpr::Response response = makeJsonRpcRequest("evm_revert", params);
cpr::Response response = makeJsonRpcRequest("evm_revert", params, connectTimeout);

if (response.status_code == 200) {
nlohmann::json responseJson = nlohmann::json::parse(response.text);
Expand All @@ -156,7 +181,7 @@ uint64_t Provider::evm_increaseTime(std::chrono::seconds seconds) {
nlohmann::json params = nlohmann::json::array();
params.push_back(seconds.count());

cpr::Response response = makeJsonRpcRequest("evm_increaseTime", params);
cpr::Response response = makeJsonRpcRequest("evm_increaseTime", params, connectTimeout);
if (response.status_code != 200) {
throw std::runtime_error("Unable to set time");
}
Expand All @@ -170,7 +195,7 @@ uint64_t Provider::evm_increaseTime(std::chrono::seconds seconds) {
} else if (responseJson["result"].is_null()) {
throw std::runtime_error("Null result in response");
}
response = makeJsonRpcRequest("evm_mine", nlohmann::json::array());
response = makeJsonRpcRequest("evm_mine", nlohmann::json::array(), connectTimeout);
if (response.status_code != 200) {
throw std::runtime_error("Unable to set time");
}
Expand All @@ -195,7 +220,7 @@ uint64_t Provider::getTransactionCount(std::string_view address, std::string_vie
params.push_back(blockTag);

// Make the request
cpr::Response response = makeJsonRpcRequest("eth_getTransactionCount", params);
cpr::Response response = makeJsonRpcRequest("eth_getTransactionCount", params, connectTimeout);

if (response.status_code == 200) {
// Parse the response
Expand Down Expand Up @@ -223,7 +248,7 @@ std::optional<nlohmann::json> Provider::getTransactionByHash(std::string_view tr
params.push_back(transactionHash);

// Make the request
cpr::Response response = makeJsonRpcRequest("eth_getTransactionByHash", params);
cpr::Response response = makeJsonRpcRequest("eth_getTransactionByHash", params, connectTimeout);

if (response.status_code == 200) {
// Parse the response
Expand All @@ -248,7 +273,7 @@ std::optional<nlohmann::json> Provider::getTransactionReceipt(std::string_view t
params.push_back(transactionHash);

// Make the request
cpr::Response response = makeJsonRpcRequest("eth_getTransactionReceipt", params);
cpr::Response response = makeJsonRpcRequest("eth_getTransactionReceipt", params, connectTimeout);

if (response.status_code == 200) {
// Parse the response
Expand Down Expand Up @@ -279,7 +304,7 @@ std::vector<LogEntry> Provider::getLogs(uint64_t fromBlock, uint64_t toBlock, st
params.push_back(params_data);

// Make the RPC call
cpr::Response response = makeJsonRpcRequest("eth_getLogs", params);
cpr::Response response = makeJsonRpcRequest("eth_getLogs", params, connectTimeout);

if (response.status_code == 200) {
// Parse the response
Expand Down Expand Up @@ -330,7 +355,7 @@ std::string Provider::getContractStorageRoot(std::string_view address, std::stri
params.push_back(storage_keys);
params.push_back(blockNumber);

auto response = makeJsonRpcRequest("eth_getProof", params);
auto response = makeJsonRpcRequest("eth_getProof", params, connectTimeout);
if(response.status_code != 200)
throw std::runtime_error("Failed to call eth_getProof: " + response.text + " params: " + params.dump());

Expand Down Expand Up @@ -379,7 +404,7 @@ std::string Provider::sendUncheckedTransaction(const Transaction& signedTx) {
nlohmann::json params = nlohmann::json::array();
params.push_back(signedTx.serialized());

auto response = makeJsonRpcRequest("eth_sendRawTransaction", params);
auto response = makeJsonRpcRequest("eth_sendRawTransaction", params, connectTimeout);
if (response.status_code == 200) {
nlohmann::json responseJson = nlohmann::json::parse(response.text);

Expand Down Expand Up @@ -445,7 +470,7 @@ std::string Provider::getBalance(std::string_view address) {
params.push_back(address);
params.push_back("latest");

auto response = makeJsonRpcRequest("eth_getBalance", params);
auto response = makeJsonRpcRequest("eth_getBalance", params, connectTimeout);
if (response.status_code == 200) {
nlohmann::json responseJson = nlohmann::json::parse(response.text);

Expand All @@ -468,7 +493,7 @@ std::string Provider::getContractDeployedInLatestBlock() {
nlohmann::json params = nlohmann::json::array();
params.push_back("latest");
params.push_back(true);
auto blockResponse = makeJsonRpcRequest("eth_getBlockByNumber", params);
auto blockResponse = makeJsonRpcRequest("eth_getBlockByNumber", params, connectTimeout);
if (blockResponse.status_code != 200)
throw std::runtime_error("Failed to get the latest block");
nlohmann::json blockJson = nlohmann::json::parse(blockResponse.text);
Expand All @@ -485,7 +510,7 @@ std::string Provider::getContractDeployedInLatestBlock() {

uint64_t Provider::getLatestHeight() {
nlohmann::json params = nlohmann::json::array();
auto blockResponse = makeJsonRpcRequest("eth_blockNumber", params);
auto blockResponse = makeJsonRpcRequest("eth_blockNumber", params, connectTimeout);
if (blockResponse.status_code != 200) {
throw std::runtime_error("Failed to get the latest height");
}
Expand All @@ -499,7 +524,7 @@ FeeData Provider::getFeeData() {
nlohmann::json params = nlohmann::json::array();
params.push_back("latest");
params.push_back(true);
auto blockResponse = makeJsonRpcRequest("eth_getBlockByNumber", params);
auto blockResponse = makeJsonRpcRequest("eth_getBlockByNumber", params, connectTimeout);
if (blockResponse.status_code != 200) {
throw std::runtime_error("Failed to call get block by number for latest block for baseFeePerGas");
}
Expand All @@ -508,7 +533,7 @@ FeeData Provider::getFeeData() {

// Get gas price
params = nlohmann::json::array();
auto gasPriceResponse = makeJsonRpcRequest("eth_gasPrice", params);
auto gasPriceResponse = makeJsonRpcRequest("eth_gasPrice", params, connectTimeout);
if (gasPriceResponse.status_code != 200) {
throw std::runtime_error("Failed to get gas price");
}
Expand All @@ -521,3 +546,4 @@ FeeData Provider::getFeeData() {

return FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
}
}; // namespace ethyl
Loading

0 comments on commit b4893cd

Please sign in to comment.