Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ solana-program = "2.2.0"
solana-pubkey = "2.2.0"
solana-signature = "2.2.0"
solana-transaction = { version = "2.2.0", features = ["bincode"] }
strum = { version = "0.27.0", features = ["derive"] }
thiserror = "2.0.11"
tokio = "1.43.0"
url = "2.5"

# TODO XC-297: Currently, the solana-* crates have a dependency on wasm-bindgen
# when they are built for wasm32-unknown-unknown target. For this reason, we
Expand Down
8 changes: 4 additions & 4 deletions canister/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ name = "sol_rpc_canister"
path = "src/main.rs"

[dependencies]
candid = {workspace = true}
ic-cdk = {workspace = true}
sol_rpc_types = {path = "../libs/types"}
candid = { workspace = true }
ic-cdk = { workspace = true }
sol_rpc_types = { path = "../libs/types" }

[dev-dependencies]
candid_parser = {workspace = true}
candid_parser = { workspace = true }
47 changes: 42 additions & 5 deletions canister/sol_rpc_canister.did
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
type DummyRequest = record {input : text};
type DummyResponse = record {output : text};

type Provider = record {
providerId : ProviderId;
cluster : SolanaCluster;
access : RpcAccess;
alias : opt RpcService;
};
type ProviderId = text;
type SolanaCluster = variant {
Mainnet;
Devnet;
Testnet;
};
type RpcAccess = variant {
Authenticated : record {
auth : RpcAuth;
publicUrl : opt text;
};
Unauthenticated : record {
publicUrl : text;
};
};
type RpcAuth = variant {
BearerToken : record { url : text };
UrlParameter : record { urlPattern : text };
};
type RpcService = variant {
Provider : ProviderId;
// TODO: Custom : RpcApi;
SolMainnet : SolMainnetService;
SolDevnet : SolDevnetService;
};
type SolMainnetService = variant {
Alchemy;
Ankr;
PublicNode;
};
type SolDevnetService = variant {
Alchemy;
Ankr;
};
service : {
greet: (DummyRequest) -> (DummyResponse) query;
}
getProviders : () -> (vec Provider) query;
};
1 change: 1 addition & 0 deletions canister/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub const API_KEY_REPLACE_STRING: &str = "{API_KEY}";
3 changes: 2 additions & 1 deletion canister/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@

pub mod constants;
pub mod providers;
13 changes: 7 additions & 6 deletions canister/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use sol_rpc_types::{DummyRequest, DummyResponse};
use candid::candid_method;
use ic_cdk::query;
use sol_rpc_canister::providers::PROVIDERS;

#[ic_cdk::query]
fn greet(request: DummyRequest) -> DummyResponse {
DummyResponse {
output: format!("Hello, {}!", request.input),
}
#[query(name = "getProviders")]
#[candid_method(query, rename = "getProviders")]
fn get_providers() -> Vec<sol_rpc_types::Provider> {
PROVIDERS.with(|providers| providers.clone().into_iter().collect())
}

fn main() {}
Expand Down
77 changes: 77 additions & 0 deletions canister/src/providers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#[cfg(test)]
mod tests;

use sol_rpc_types::{Provider, ProviderId, RpcAccess, RpcAuth};
use sol_rpc_types::{RpcService, SolDevnetService, SolMainnetService, SolanaCluster};
use std::collections::HashMap;

thread_local! {
pub static PROVIDERS: [Provider; 5] = [
Provider {
provider_id: "alchemy-mainnet".to_string(),
cluster: SolanaCluster::Mainnet,
access: RpcAccess::Authenticated {
auth: RpcAuth::BearerToken {
url: "https://solana-mainnet.g.alchemy.com/v2".to_string(),
},
public_url: Some("https://solana-mainnet.g.alchemy.com/v2/demo".to_string()),
},
alias: Some(RpcService::SolMainnet(SolMainnetService::Alchemy)),
},
Provider {
provider_id: "alchemy-devnet".to_string(),
cluster: SolanaCluster::Devnet,
access: RpcAccess::Authenticated {
auth: RpcAuth::BearerToken {
url: "https://solana-devnet.g.alchemy.com/v2".to_string(),
},
public_url: Some("https://solana-devnet.g.alchemy.com/v2/demo".to_string()),
},
alias: Some(RpcService::SolDevnet(SolDevnetService::Alchemy)),
},
Provider {
provider_id: "ankr-mainnet".to_string(),
cluster: SolanaCluster::Mainnet,
access: RpcAccess::Authenticated {
auth: RpcAuth::UrlParameter {
url_pattern: "https://rpc.ankr.com/solana/{API_KEY}".to_string(),
},
public_url: None,
},
alias: Some(RpcService::SolMainnet(SolMainnetService::Ankr)),
},
Provider {
provider_id: "ankr-devnet".to_string(),
cluster: SolanaCluster::Devnet,
access: RpcAccess::Authenticated {
auth: RpcAuth::UrlParameter {
url_pattern: "https://rpc.ankr.com/solana_devnet/{API_KEY}".to_string(),
},
public_url: Some("https://rpc.ankr.com/solana_devnet/".to_string()),
},
alias: Some(RpcService::SolDevnet(SolDevnetService::Ankr)),
},
Provider {
provider_id: "publicnode-mainnet".to_string(),
cluster: SolanaCluster::Mainnet,
access: RpcAccess::Unauthenticated {
public_url: "https://solana-rpc.publicnode.com".to_string(),
},
alias: Some(RpcService::SolMainnet(SolMainnetService::PublicNode)),
},
];

pub static PROVIDER_MAP: HashMap<ProviderId, Provider> = PROVIDERS.with(|providers| {
providers
.iter()
.map(|provider| (provider.provider_id.clone(), provider.clone()))
.collect()
});

pub static SERVICE_PROVIDER_MAP: HashMap<RpcService, ProviderId> = PROVIDERS.with(|providers| {
providers
.iter()
.filter_map(|provider| Some((provider.alias.clone()?, provider.provider_id.clone())))
.collect()
});
}
74 changes: 74 additions & 0 deletions canister/src/providers/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use super::{PROVIDERS, SERVICE_PROVIDER_MAP};
use crate::constants::API_KEY_REPLACE_STRING;
use sol_rpc_types::{Provider, RpcAccess, RpcAuth};
use std::collections::{HashMap, HashSet};

#[test]
fn test_rpc_provider_url_patterns() {
PROVIDERS.with(|providers| {
for provider in providers {
fn assert_not_url_pattern(url: &str, provider: &Provider) {
assert!(
!url.contains(API_KEY_REPLACE_STRING),
"Unexpected API key in URL for provider: {}",
provider.provider_id
)
}
fn assert_url_pattern(url: &str, provider: &Provider) {
assert!(
url.contains(API_KEY_REPLACE_STRING),
"Missing API key in URL pattern for provider: {}",
provider.provider_id
)
}
match &provider.access {
RpcAccess::Authenticated { auth, public_url } => {
match auth {
RpcAuth::BearerToken { url } => assert_not_url_pattern(url, provider),
RpcAuth::UrlParameter { url_pattern } => {
assert_url_pattern(url_pattern, provider)
}
}
if let Some(public_url) = public_url {
assert_not_url_pattern(public_url, provider);
}
}
RpcAccess::Unauthenticated { public_url } => {
assert_not_url_pattern(public_url, provider);
}
}
}
})
}

#[test]
fn test_no_duplicate_service_providers() {
SERVICE_PROVIDER_MAP.with(|map| {
assert_eq!(
map.len(),
map.keys().collect::<HashSet<_>>().len(),
"Duplicate service in mapping"
);
assert_eq!(
map.len(),
map.values().collect::<HashSet<_>>().len(),
"Duplicate provider in mapping"
);
})
}

#[test]
fn test_service_provider_coverage() {
SERVICE_PROVIDER_MAP.with(|map| {
let inverse_map: HashMap<_, _> = map.iter().map(|(k, v)| (v, k)).collect();
PROVIDERS.with(|providers| {
for provider in providers {
assert!(
inverse_map.contains_key(&provider.provider_id),
"Missing service mapping for provider with ID: {}",
provider.provider_id,
);
}
})
})
}
50 changes: 42 additions & 8 deletions integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use candid::utils::ArgumentEncoder;
use candid::{decode_args, encode_args, CandidType, Encode, Principal};
use ic_cdk::api::call::RejectionCode;
use pocket_ic::management_canister::{CanisterId, CanisterSettings};
use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder, WasmResult};
use pocket_ic::{nonblocking::PocketIc, PocketIcBuilder, UserError, WasmResult};
use serde::de::DeserializeOwned;
use sol_rpc_client::{Runtime, SolRpcClient};
use std::path::PathBuf;
Expand Down Expand Up @@ -84,7 +84,7 @@ pub struct PocketIcRuntime<'a> {

#[async_trait]
impl<'a> Runtime for PocketIcRuntime<'a> {
async fn call<In, Out>(
async fn update_call<In, Out>(
&self,
id: Principal,
method: &str,
Expand All @@ -95,12 +95,46 @@ impl<'a> Runtime for PocketIcRuntime<'a> {
In: ArgumentEncoder + Send + 'static,
Out: CandidType + DeserializeOwned + 'static,
{
let args_raw = encode_args(args).expect("Failed to encode arguments.");
match self
.env
.update_call(id, self.caller, method, args_raw)
.await
{
PocketIcRuntime::decode_call_result(
self.env
.update_call(id, self.caller, method, PocketIcRuntime::encode_args(args))
.await,
)
}

async fn query_call<In, Out>(
&self,
id: Principal,
method: &str,
args: In,
) -> Result<Out, (RejectionCode, String)>
where
In: ArgumentEncoder + Send + 'static,
Out: CandidType + DeserializeOwned + 'static,
{
PocketIcRuntime::decode_call_result(
self.env
.query_call(id, self.caller, method, PocketIcRuntime::encode_args(args))
.await,
)
}
}

impl PocketIcRuntime<'_> {
fn encode_args<In>(args: In) -> Vec<u8>
where
In: ArgumentEncoder,
{
encode_args(args).expect("Failed to encode arguments.")
}

fn decode_call_result<Out>(
result: Result<WasmResult, UserError>,
) -> Result<Out, (RejectionCode, String)>
where
Out: CandidType + DeserializeOwned + 'static,
{
match result {
Ok(WasmResult::Reply(bytes)) => decode_args(&bytes).map(|(res,)| res).map_err(|e| {
(
RejectionCode::CanisterError,
Expand Down
Loading