diff --git a/API.md b/API.md index 1c345a97..f8ceb649 100644 --- a/API.md +++ b/API.md @@ -26,46 +26,28 @@ Confirm the authenticity of a message signed by an Ethereum private key. Check o Make a request to a Web2 Ethereum node using the caller's URL to an openly available JSON-RPC service, or the caller's URL (including an API key if necessary). No registered API key of the canister is used in this scenario. - request: (service_url: text, json_rpc_payload: text, max_response_bytes: nat64) -> (EthRpcResult); + request: (source: Source, json_rpc_payload: text, max_response_bytes: nat64) -> (EthRpcResult); -* `service_url`: The URL of the service, including any API key if required for access-protected services. +* `source`: Any of the following: + * `#Url : text` The URL of the service, including any API key if required for access-protected services. + * `#Chain : nat64` The relevant EVM network identifier ([reference list](https://chainlist.org/?testnets=true)). + * `#Provider : nat64` The ID of the provider to be used for this call. Call `get_providers` to view a full list of providers. * `json_rpc_payload`: The payload for the JSON-RPC request. View examples in the [Ethereum documentation](https://ethereum.org/en/developers/docs/apis/json-rpc/). * `max_response_bytes`: The expected maximum size of the response of the Web2 API server. This parameter determines the network response size that is charged for. Not specifying it or it being larger than required may lead to substantial extra cycles cost for the HTTPS outcalls mechanism as its (large) default value is used and charged for. * `EthRpcResult`: The response comprises the JSON-encoded result or error, see the corresponding type. -### `provider_request` - -Make a request to a Web2 Ethereum node using a registered provider for a JSON-RPC service. There is no need for the client to have any established relationship with the API service. - - provider_request: (provider_id: nat64, json_rpc_payload: text, max_response_bytes: nat64) -> (EthRpcResult); - -* `provider_id`: The id of the registered provider to be used for this call. This uniquely identifies a provider registered with the canister. -* `json_rpc_payload`: See `request`. -* `max_response_bytes`: See `request`. -* `EthRpcResult`: See `request`. - ### `request_cost` Calculate the cost of sending a request with the given input arguments. - request_cost: (service_url: text, json_rpc_payload: text, max_response_bytes: nat64) -> (nat) query; + request_cost: (source: Source, json_rpc_payload: text, max_response_bytes: nat64) -> (nat) query; -* `service_url`: The URL of the service, including any API key if required for access-protected services. +* `source`: See `request`. * `json_rpc_payload`: See `request`. * `max_response_bytes`: See `request`. -### `provider_request_cost` - -Calculate the cost of sending a request with the given input arguments. - - provider_request_cost: (provider_id: nat64, json_rpc_payload: text, max_response_bytes: nat64) -> (nat) query; - -* `provider_id`: The id of the registered provider to be used for this call. This uniquely identifies a provider registered with the canister. -* `json_rpc_payload`: See `request_cost`. -* `max_response_bytes`: See `request_cost`. - ### `get_providers` Returns a list of currently registered `RegisteredProvider` entries of the canister. diff --git a/README.md b/README.md index e22e3820..9ca35089 100644 --- a/README.md +++ b/README.md @@ -47,26 +47,27 @@ dfx start --background dfx deploy ic_eth # Call the `eth_gasPrice` JSON-RPC method -dfx canister call ic_eth request '("https://cloudflare-eth.com/v1/mainnet", "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}", 1000)' --wallet $(dfx identity get-wallet) --with-cycles 600000000 +dfx canister call ic_eth request '(variant {Url="https://cloudflare-eth.com/v1/mainnet"}, "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}", 1000)' --wallet $(dfx identity get-wallet) --with-cycles 600000000 ``` ## Examples +### Ethereum RPC (IC mainnet) +```bash +dfx canister call ic_eth --network ic --wallet $(dfx identity --network ic get-wallet) --with-cycles 600000000 request '(variant {Chain=0x1},"{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' +``` + ### Ethereum RPC (local replica) ```bash # Use a custom provider -dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 request '("https://cloudflare-eth.com","{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' -dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 request '("https://ethereum.publicnode.com","{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' +dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 request '(variant {Url="https://cloudflare-eth.com"},"{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' +dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 request '(variant {Url="https://ethereum.publicnode.com"},"{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' # Register your own provider dfx canister call ic_eth register_provider '(record { chain_id=1; base_url="https://cloudflare-eth.com"; credential_path="/v1/mainnet"; cycles_per_call=10; cycles_per_message_byte=1; })' -dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 provider_request '(0,"{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' -``` -### Ethereum RPC (IC mainnet) -```bash -dfx canister --network ic call ic_eth --wallet $(dfx identity --network ic get-wallet) --with-cycles 600000000 request '("https://cloudflare-eth.com","{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' -dfx canister --network ic call ic_eth --wallet $(dfx identity --network ic get-wallet) --with-cycles 600000000 request '("https://ethereum.publicnode.com","{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' +# Use a specific EVM chain +dfx canister call ic_eth --wallet $(dfx identity get-wallet) --with-cycles 600000000 request '(variant {Chain=0x1},"{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}",1000)' ``` ### Authorization (local replica) diff --git a/candid/ic_eth.did b/candid/ic_eth.did index c0124a45..c594da60 100644 --- a/candid/ic_eth.did +++ b/candid/ic_eth.did @@ -7,23 +7,35 @@ type EthRpcError = variant { ServiceUrlHostMissing; ProviderNotFound; NoPermission; + ProviderNotActive; }; -type RegisterProvider = record { +type ProviderView = record { base_url : text; + active : bool; + owner : principal; + provider_id : nat64; cycles_per_message_byte : nat64; chain_id : nat64; cycles_per_call : nat64; - credential_path : text; }; -type RegisteredProvider = record { +type RegisterProvider = record { base_url : text; - owner : principal; - provider_id : nat64; cycles_per_message_byte : nat64; chain_id : nat64; cycles_per_call : nat64; + credential_path : text; }; type Result = variant { Ok : vec nat8; Err : EthRpcError }; +type Result_1 = variant { Ok : nat; Err : EthRpcError }; +type Source = variant { Url : text; Chain : nat64; Provider : nat64 }; +type UpdateProvider = record { + base_url : opt text; + active : opt bool; + provider_id : nat64; + cycles_per_message_byte : opt nat64; + cycles_per_call : opt nat64; + credential_path : opt text; +}; service : { authorize : (principal, Auth) -> (); deauthorize : (principal, Auth) -> (); @@ -31,16 +43,14 @@ service : { get_nodes_in_subnet : () -> (nat32) query; get_open_rpc_access : () -> (bool) query; get_owed_cycles : (nat64) -> (nat) query; - get_providers : () -> (vec RegisteredProvider) query; - provider_request : (nat64, text, nat64) -> (Result); - provider_request_cost : (nat64, text, nat64) -> (opt nat) query; + get_providers : () -> (vec ProviderView) query; register_provider : (RegisterProvider) -> (nat64); - request : (text, text, nat64) -> (Result); - request_cost : (text, text, nat64) -> (nat) query; + request : (Source, text, nat64) -> (Result); + request_cost : (Source, text, nat64) -> (Result_1) query; set_nodes_in_subnet : (nat32) -> (); set_open_rpc_access : (bool) -> (); unregister_provider : (nat64) -> (); - update_provider_credential : (nat64, text) -> (); + update_provider : (UpdateProvider) -> (); verify_signature : (vec nat8, vec nat8, vec nat8) -> (bool) query; withdraw_owed_cycles : (nat64, principal) -> (); } \ No newline at end of file diff --git a/scripts/local b/scripts/local index 9e493d54..68cad185 100755 --- a/scripts/local +++ b/scripts/local @@ -8,8 +8,8 @@ dfx canister call ic_eth authorize "(principal \"$PRINCIPAL\", variant { Registe dfx canister call ic_eth register_provider '(record {base_url = "https://cloudflare-eth.com"; credential_path = "/v1/mainnet"; chain_id = 1; cycles_per_call = 1000; cycles_per_message_byte = 100})' -dfx canister call ic_eth request_cost '("https://cloudflare-eth.com", "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' -dfx canister call ic_eth request '("https://cloudflare-eth.com", "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' +dfx canister call ic_eth request_cost '(variant {Chain=1}, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' +dfx canister call ic_eth request '(variant {Chain=1}, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' -dfx canister call ic_eth provider_request_cost '(0, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' -dfx canister call ic_eth provider_request '(0, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' +dfx canister call ic_eth request_cost '(variant {Provider=0}, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' +dfx canister call ic_eth request '(variant {Provider=0}, "{ \"jsonrpc\": \"2.0\", \"method\": \"eth_getBlockByNumber\", \"params\": [\"0x2244\", true], \"id\": 1 }", 1000)' diff --git a/src/accounting.rs b/src/accounting.rs index 670b7941..f9c2a33b 100644 --- a/src/accounting.rs +++ b/src/accounting.rs @@ -1,9 +1,36 @@ use crate::*; -/// Calculate the baseline cost of sending a JSON-RPC request using HTTP outcalls. pub fn get_request_cost( + source: &ResolvedSource, + json_rpc_payload: &str, + max_response_bytes: u64, +) -> u128 { + let (http_cost, provider_cost) = + get_request_costs(source, json_rpc_payload, max_response_bytes); + http_cost + provider_cost +} + +pub fn get_request_costs( + source: &ResolvedSource, json_rpc_payload: &str, + max_response_bytes: u64, +) -> (u128, u128) { + match source { + ResolvedSource::Url(s) => ( + get_http_request_cost(s, json_rpc_payload, max_response_bytes), + 0, + ), + ResolvedSource::Provider(p) => ( + get_http_request_cost(&p.service_url(), json_rpc_payload, max_response_bytes), + get_provider_cost(p, json_rpc_payload), + ), + } +} + +/// Calculate the baseline cost of sending a JSON-RPC request using HTTP outcalls. +pub fn get_http_request_cost( service_url: &str, + json_rpc_payload: &str, max_response_bytes: u64, ) -> u128 { let nodes_in_subnet = METADATA.with(|m| m.borrow().get().nodes_in_subnet); @@ -17,7 +44,7 @@ pub fn get_request_cost( } /// Calculate the additional cost for calling a registered JSON-RPC provider. -pub fn get_provider_cost(json_rpc_payload: &str, provider: &Provider) -> u128 { +pub fn get_provider_cost(provider: &Provider, json_rpc_payload: &str) -> u128 { let nodes_in_subnet = METADATA.with(|m| m.borrow().get().nodes_in_subnet); let cost_per_node = provider.cycles_per_call as u128 + provider.cycles_per_message_byte as u128 * json_rpc_payload.len() as u128; @@ -33,15 +60,15 @@ fn test_request_cost() { }); let base_cost = get_request_cost( + &ResolvedSource::Url("https://cloudflare-eth.com".to_string()), "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}", - "https://cloudflare-eth.com", 1000, ); let s10 = "0123456789"; let base_cost_s10 = get_request_cost( + &ResolvedSource::Url("https://cloudflare-eth.com".to_string()), &("{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}".to_string() + s10), - "https://cloudflare-eth.com", 1000, ); assert_eq!( @@ -67,10 +94,11 @@ fn test_provider_cost() { cycles_owed: 0, cycles_per_call: 0, cycles_per_message_byte: 2, + active: true, }; let base_cost = get_provider_cost( - "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}", &provider, + "{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}", ); let provider_s10 = Provider { @@ -82,12 +110,13 @@ fn test_provider_cost() { cycles_owed: 0, cycles_per_call: 1000, cycles_per_message_byte: 2, + active: true, }; let s10 = "0123456789"; let base_cost_s10 = get_provider_cost( + &provider_s10, &("{\"jsonrpc\":\"2.0\",\"method\":\"eth_gasPrice\",\"params\":[],\"id\":1}".to_string() + s10), - &provider_s10, ); assert_eq!(base_cost + (10 * 2 + 1000) * 13, base_cost_s10) } diff --git a/src/constants.rs b/src/constants.rs index 9e674e4a..9ddf3e80 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -42,10 +42,3 @@ pub const INITIAL_SERVICE_HOSTS_ALLOWLIST: &[&str] = &[ // Principals allowed to send JSON-RPC requests. pub const DEFAULT_NODES_IN_SUBNET: u32 = 13; pub const DEFAULT_OPEN_RPC_ACCESS: bool = true; -pub const RPC_ALLOWLIST: &[&str] = &[]; -// Principals allowed to registry API keys. -pub const REGISTER_PROVIDER_ALLOWLIST: &[&str] = &[]; -// Principals that will not be charged cycles to send JSON-RPC requests. -pub const FREE_RPC_ALLOWLIST: &[&str] = &[]; -// Principals who have Admin authorization. -pub const AUTHORIZED_ADMIN: &[&str] = &[]; diff --git a/src/http.rs b/src/http.rs index b998e05b..81791663 100644 --- a/src/http.rs +++ b/src/http.rs @@ -17,6 +17,7 @@ pub async fn do_http_request( return Err(EthRpcError::NoPermission); } let cycles_available = ic_cdk::api::call::msg_cycles_available128(); + let cost = get_request_cost(&source, json_rpc_payload, max_response_bytes); let (service_url, provider) = match source { ResolvedSource::Url(url) => (url, None), ResolvedSource::Provider(provider) => (provider.service_url(), Some(provider)), @@ -31,11 +32,6 @@ pub async fn do_http_request( inc_metric!(request_err_host_not_allowed); return Err(EthRpcError::ServiceUrlHostNotAllowed); } - let request_cost = get_request_cost(json_rpc_payload, &service_url, max_response_bytes); - let provider_cost = provider - .as_ref() - .map_or(0, |provider| get_provider_cost(json_rpc_payload, provider)); - let cost = request_cost + provider_cost; if !is_authorized(Auth::FreeRpc) { if cycles_available < cost { return Err(EthRpcError::TooFewCycles(format!( @@ -44,7 +40,7 @@ pub async fn do_http_request( } ic_cdk::api::call::msg_cycles_accept128(cost); if let Some(mut provider) = provider { - provider.cycles_owed += provider_cost; + provider.cycles_owed += get_provider_cost(&provider, json_rpc_payload); PROVIDERS.with(|p| { // Error should not happen here as it was checked before. p.borrow_mut() diff --git a/src/main.rs b/src/main.rs index 90932f31..4013078c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,76 +18,43 @@ pub fn verify_signature(eth_address: Vec, message: Vec, signature: Vec Result, EthRpcError> { - do_http_request( - ResolvedSource::Url(service_url), - &json_rpc_payload, - max_response_bytes, - ) - .await -} - -#[update] -#[candid_method] -async fn provider_request( - provider_id: u64, - json_rpc_payload: String, - max_response_bytes: u64, -) -> Result, EthRpcError> { - let provider = PROVIDERS.with(|p| { - p.borrow() - .get(&provider_id) - .ok_or(EthRpcError::ProviderNotFound) - })?; - do_http_request( - ResolvedSource::Provider(provider), - &json_rpc_payload, - max_response_bytes, - ) - .await + do_http_request(source.resolve()?, &json_rpc_payload, max_response_bytes).await } #[query] #[candid_method(query)] -fn request_cost(service_url: String, json_rpc_payload: String, max_response_bytes: u64) -> u128 { - get_request_cost(&json_rpc_payload, &service_url, max_response_bytes) -} - -#[query] -#[candid_method(query)] -fn provider_request_cost( - provider_id: u64, +fn request_cost( + source: Source, json_rpc_payload: String, max_response_bytes: u64, -) -> Option { - let provider = PROVIDERS.with(|p| p.borrow().get(&provider_id))?; - let request_cost = get_request_cost( +) -> Result { + Ok(get_request_cost( + &source.resolve().unwrap(), &json_rpc_payload, - &provider.service_url(), max_response_bytes, - ); - let provider_cost = get_provider_cost(&json_rpc_payload, &provider); - Some(request_cost + provider_cost) + )) } #[query] #[candid_method(query)] -fn get_providers() -> Vec { +fn get_providers() -> Vec { PROVIDERS.with(|p| { p.borrow() .iter() - .map(|(_, e)| RegisteredProvider { + .map(|(_, e)| ProviderView { provider_id: e.provider_id, owner: e.owner, chain_id: e.chain_id, base_url: e.base_url, cycles_per_call: e.cycles_per_call, cycles_per_message_byte: e.cycles_per_message_byte, + active: e.active, }) - .collect::>() + .collect::>() }) } @@ -116,6 +83,7 @@ fn register_provider(provider: RegisterProvider) -> u64 { cycles_per_call: provider.cycles_per_call, cycles_per_message_byte: provider.cycles_per_message_byte, cycles_owed: 0, + active: true, }, ) }); @@ -124,15 +92,30 @@ fn register_provider(provider: RegisterProvider) -> u64 { #[update(guard = "require_register_provider")] #[candid_method] -fn update_provider_credential(provider_id: u64, credential_path: String) { - validate_credential_path(&credential_path); - PROVIDERS.with(|p| match p.borrow_mut().get(&provider_id) { +fn update_provider(update: UpdateProvider) { + PROVIDERS.with(|p| match p.borrow_mut().get(&update.provider_id) { Some(mut provider) => { if provider.owner != ic_cdk::caller() && !is_authorized(Auth::Admin) { ic_cdk::trap("Provider owner != caller"); } - provider.credential_path = credential_path; - p.borrow_mut().insert(provider_id, provider); + if let Some(url) = update.base_url { + validate_base_url(&url); + provider.base_url = url; + } + if let Some(path) = update.credential_path { + validate_credential_path(&path); + provider.credential_path = path; + } + if let Some(active) = update.active { + provider.active = active; + } + if let Some(cycles_per_call) = update.cycles_per_call { + provider.cycles_per_call = cycles_per_call; + } + if let Some(cycles_per_message_byte) = update.cycles_per_message_byte { + provider.cycles_per_message_byte = cycles_per_message_byte; + } + p.borrow_mut().insert(update.provider_id, provider); } None => ic_cdk::trap("Provider not found"), }); @@ -239,19 +222,10 @@ fn transform(args: TransformArgs) -> HttpResponse { #[ic_cdk::init] fn init() { initialize(); - METADATA.with(|m| { - let mut metadata = m.borrow().get().clone(); - metadata.nodes_in_subnet = DEFAULT_NODES_IN_SUBNET; - metadata.open_rpc_access = DEFAULT_OPEN_RPC_ACCESS; - m.borrow_mut().set(metadata).unwrap(); - }); } #[ic_cdk::post_upgrade] -fn post_upgrade() { - initialize(); - stable_authorize(ic_cdk::caller()); -} +fn post_upgrade() {} // #[query] // fn http_request(request: AssetHttpRequest) -> AssetHttpResponse { @@ -356,18 +330,14 @@ fn initialize() { SERVICE_HOSTS_ALLOWLIST .with(|a| (*a.borrow_mut()) = AllowlistSet::from_iter(INITIAL_SERVICE_HOSTS_ALLOWLIST)); - for principal in RPC_ALLOWLIST.iter() { - authorize(to_principal(principal), Auth::Rpc); - } - for principal in REGISTER_PROVIDER_ALLOWLIST.iter() { - authorize(to_principal(principal), Auth::RegisterProvider); - } - for principal in FREE_RPC_ALLOWLIST.iter() { - authorize(to_principal(principal), Auth::FreeRpc); - } - for principal in AUTHORIZED_ADMIN.iter() { - authorize(to_principal(principal), Auth::Admin); - } + stable_authorize(ic_cdk::caller()); + + METADATA.with(|m| { + let mut metadata = m.borrow().get().clone(); + metadata.nodes_in_subnet = DEFAULT_NODES_IN_SUBNET; + metadata.open_rpc_access = DEFAULT_OPEN_RPC_ACCESS; + m.borrow_mut().set(metadata).unwrap(); + }); } #[cfg(not(any(target_arch = "wasm32", test)))] diff --git a/src/types.rs b/src/types.rs index 13a903b0..e5de03aa 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,6 +5,42 @@ use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use crate::constants::STRING_STORABLE_MAX_SIZE; +use crate::PROVIDERS; + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum Source { + Url(String), + Provider(u64), + Chain(u64), +} + +impl Source { + pub fn resolve(self) -> Result { + Ok(match self { + Source::Url(name) => ResolvedSource::Url(name), + Source::Provider(id) => ResolvedSource::Provider({ + let p = PROVIDERS.with(|providers| { + providers + .borrow() + .get(&id) + .ok_or(EthRpcError::ProviderNotFound) + })?; + if !p.active { + Err(EthRpcError::ProviderNotActive)? + } else { + p + } + }), + Source::Chain(id) => ResolvedSource::Provider(PROVIDERS.with(|p| { + p.borrow() + .iter() + .find(|(_, p)| p.active && p.chain_id == id) + .map(|(_, p)| p) + .ok_or(EthRpcError::ProviderNotFound) + })?), + }) + } +} pub enum ResolvedSource { Url(String), @@ -76,13 +112,14 @@ impl BoundedStorable for PrincipalStorable { } #[derive(Debug, CandidType)] -pub struct RegisteredProvider { +pub struct ProviderView { pub provider_id: u64, pub owner: Principal, pub chain_id: u64, pub base_url: String, pub cycles_per_call: u64, pub cycles_per_message_byte: u64, + pub active: bool, } #[derive(Debug, CandidType, Deserialize)] @@ -94,6 +131,16 @@ pub struct RegisterProvider { pub cycles_per_message_byte: u64, } +#[derive(Debug, CandidType, Deserialize)] +pub struct UpdateProvider { + pub provider_id: u64, + pub base_url: Option, + pub credential_path: Option, + pub cycles_per_call: Option, + pub cycles_per_message_byte: Option, + pub active: Option, +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub struct Provider { pub provider_id: u64, @@ -104,6 +151,7 @@ pub struct Provider { pub cycles_per_call: u64, pub cycles_per_message_byte: u64, pub cycles_owed: u128, + pub active: bool, } impl Provider { @@ -143,6 +191,7 @@ pub enum EthRpcError { ServiceUrlHostMissing, ServiceUrlHostNotAllowed, ProviderNotFound, + ProviderNotActive, HttpRequestError { code: u32, message: String }, }