Skip to content

Commit

Permalink
feat: improve idempotent-proxy-canister; add examples/eth-canister-lite
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jul 8, 2024
1 parent 308536d commit 66c186b
Show file tree
Hide file tree
Showing 27 changed files with 519 additions and 67 deletions.
21 changes: 20 additions & 1 deletion Cargo.lock

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

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ members = [
"src/idempotent-proxy-server",
"src/idempotent-proxy-types",
"examples/eth-canister",
"examples/eth-canister-lite",
]
resolver = "2"

[profile.release]
debug = false
lto = true
strip = true
opt-level = 's'

[workspace.package]
version = "0.5.4"
edition = "2021"
Expand Down Expand Up @@ -37,6 +44,7 @@ reqwest = { version = "0.12", features = [
"http2",
], default-features = false }
dotenvy = "0.15"
futures = "0.3"
log = "0.4"
structured-logger = "1"
http = "1"
Expand Down
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ build-linux:
build-wasm:
@cargo build --release --target wasm32-unknown-unknown --package idempotent-proxy-canister
@cargo build --release --target wasm32-unknown-unknown --package eth-canister
@cargo build --release --target wasm32-unknown-unknown --package eth-canister-lite

# cargo install candid-extractor
build-did:
@candid-extractor target/wasm32-unknown-unknown/release/idempotent_proxy_canister.wasm > src/idempotent-proxy-canister/idempotent-proxy-canister.did
@candid-extractor target/wasm32-unknown-unknown/release/eth_canister.wasm > examples/eth-canister/eth-canister.did
@candid-extractor target/wasm32-unknown-unknown/release/eth_canister_lite.wasm > examples/eth-canister-lite/eth-canister-lite.did
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ When multiple requests with the same idempotency key arrive within a specific ti

This service can be used to proxy [HTTPS outcalls](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works) for [ICP canisters](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/overview/introduction), enabling integration with any Web2 http service.

![Idempotent Proxy](./idempotent-proxy.png)
![Idempotent Proxy](./idempotent-proxy.webp)

## Features
- Reverse proxy with built-in idempotency support
Expand All @@ -20,16 +20,18 @@ This service can be used to proxy [HTTPS outcalls](https://internetcomputer.org/
- Response headers filtering
- Access control using Secp256k1 and Ed25519
- Deployable with Docker or Cloudflare Worker
- On-chain Idempotent Proxy service on the ICP

## Packages

| Package | Description |
| :----------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- |
| [idempotent-proxy-server](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-server) | Idempotent Proxy implemented in Rust. |
| [idempotent-proxy-cf-worker](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-cf-worker) | Idempotent Proxy implemented as Cloudflare Worker. |
| [idempotent-proxy-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-canister) | Make `idempotent-proxy-server` or `idempotent-proxy-cf-worker` as a canister. |
| [idempotent-proxy-types](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-types) | Idempotent Proxy types in Rust. Should not be used in ICP canister! |
| [examples/eth-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/examples/eth-canister) | A ICP canister integration with Ethereum JSON-RPC API. |
| Package | Description |
| :----------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- |
| [idempotent-proxy-server](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-server) | Idempotent Proxy implemented in Rust. |
| [idempotent-proxy-cf-worker](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-cf-worker) | Idempotent Proxy implemented as Cloudflare Worker. |
| [idempotent-proxy-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-canister) | A ICP canister Make Idempotent Proxy service on-chain. |
| [idempotent-proxy-types](https://github.com/ldclabs/idempotent-proxy/tree/main/src/idempotent-proxy-types) | Idempotent Proxy types in Rust. Should not be used in ICP canister! |
| [examples/eth-canister](https://github.com/ldclabs/idempotent-proxy/tree/main/examples/eth-canister) | A ICP canister integration with Ethereum JSON-RPC API. |
| [examples/eth-canister-lite](https://github.com/ldclabs/idempotent-proxy/tree/main/examples/eth-canister-lite) | A ICP canister integration with Ethereum JSON-RPC API through idempotent-proxy-canister |

## Who's using?

Expand All @@ -39,11 +41,20 @@ If you plan to use this project and have any questions, feel free to open an iss

## Usage

### ICP Canister Integration
### On-chain Idempotent Proxy

Online `eth-canister`: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=hpudd-yqaaa-aaaap-ahnbq-cai
The `idempotent-proxy-canister` is an ICP smart contract that can connect to 1 to N Idempotent Proxy services deployed by `idempotent-proxy-server` or `idempotent-proxy-cf-worker`. It provides on-chain HTTPS outcalls with idempotency for other smart contracts.

Go to the [examples/eth-canister](./examples/eth-canister) directory for more information.
![Idempotent Proxy Canister](./idempotent-proxy-canister.webp)

Go to the [idempotent-proxy-canister](./src/idempotent-proxy-canister) directory for more information.

**Online Demo**: https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=hpudd-yqaaa-aaaap-ahnbq-cai

### ICP Canister Integration Examples

- [examples/eth-canister-lite](./examples/eth-canister-lite): A ICP canister integration with Ethereum JSON-RPC API through idempotent-proxy-canister.
- [examples/eth-canister](./examples/eth-canister): A ICP canister integration with Ethereum JSON-RPC API.

### Run proxy in development mode

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"eth-canister": {
"idempotent-proxy-canister": {
"ic": "hpudd-yqaaa-aaaap-ahnbq-cai"
}
}
25 changes: 25 additions & 0 deletions examples/eth-canister-lite/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "eth-canister-lite"
version = "0.1.0"
edition = "2021"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
async-trait = "0.1"
bytes = "1.6"
candid = "0.10"
ic-cdk = "0.14"
ic-cdk-timers = "0.8"
ic-stable-structures = "0.6"
# getrandom = { version = "0.2", features = ["custom"] }
base64 = { workspace = true }
ciborium = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_bytes = { workspace = true }
sha3 = { workspace = true }
25 changes: 25 additions & 0 deletions examples/eth-canister-lite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Example: `eth-canister-lite`

## Running the project locally

If you want to test your project locally, you can use the following commands:

```bash
cd examples/eth-canister
# Starts the replica, running in the background
dfx start --background

# deploy the canister
dfx deploy eth-canister-lite

dfx canister call eth-canister-lite eth_chain_id '()'

dfx canister call eth-canister-lite get_best_block '()'
```

`idempotent-proxy-cf-worker` does not enable `proxy-authorization`, so it can be accessed.

## License
Copyright © 2024 [LDC Labs](https://github.com/ldclabs).

`ldclabs/idempotent-proxy` is licensed under the MIT License. See [LICENSE](../../LICENSE-MIT) for the full license text.
17 changes: 17 additions & 0 deletions examples/eth-canister-lite/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"canisters": {
"eth-canister-lite": {
"candid": "eth-canister-lite.did",
"package": "eth-canister-lite",
"type": "rust"
}
},
"defaults": {
"build": {
"args": "",
"packtool": ""
}
},
"output_env_file": ".env",
"version": 1
}
2 changes: 2 additions & 0 deletions examples/eth-canister-lite/eth-canister-lite.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type Result = variant { Ok : text; Err : text };
service : { eth_chain_id : () -> (Result); get_best_block : () -> (Result) }
154 changes: 154 additions & 0 deletions examples/eth-canister-lite/src/jsonrpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use candid::Principal;
use ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::{to_vec, Value};

pub static APP_AGENT: &str = concat!(
"Mozilla/5.0 eth-canister ",
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION"),
);

pub struct EthereumRPC {
pub provider: String, // provider url or `URL_` constant defined in the Idempotent Proxy
pub proxy: Principal, // idempotent-proxy-canister id
pub api_token: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct RPCRequest<'a> {
jsonrpc: &'a str,
method: &'a str,
params: &'a [Value],
id: u64,
}

#[derive(Debug, Deserialize)]
pub struct RPCResponse<T> {
result: Option<T>,
error: Option<Value>,
}

impl EthereumRPC {
pub async fn eth_chain_id(&self, idempotency_key: String) -> Result<String, String> {
self.call("parallel_call_one_ok", idempotency_key, "eth_chainId", &[])
.await
}

pub async fn get_best_block(&self, idempotency_key: String) -> Result<Value, String> {
self.call(
"parallel_call_all_ok",
idempotency_key,
"eth_getBlockByNumber",
&["latest".into(), false.into()],
)
.await
}

pub async fn send_raw_transaction(
&self,
idempotency_key: String,
raw_tx: String,
) -> Result<String, String> {
self.call(
"proxy_http_request",
idempotency_key,
"eth_sendTransaction",
&[raw_tx.into()],
)
.await
}

// you can add more methods here

pub async fn call<T: DeserializeOwned>(
&self,
proxy_method: &str, // "proxy_http_request" | "parallel_call_one_ok" | "parallel_call_all_ok"
idempotency_key: String,
method: &str,
params: &[Value],
) -> Result<T, String> {
let input = RPCRequest {
jsonrpc: "2.0",
method,
params,
id: 1,
};
let input = to_vec(&input).map_err(|err| err.to_string())?;
let data = self.proxy(proxy_method, idempotency_key, input).await?;

let output: RPCResponse<T> =
serde_json::from_slice(&data).map_err(|err| err.to_string())?;

if let Some(error) = output.error {
return Err(serde_json::to_string(&error).map_err(|err| err.to_string())?);
}

match output.result {
Some(result) => Ok(result),
None => serde_json::from_value(Value::Null).map_err(|err| err.to_string()),
}
}

async fn proxy(
&self,
proxy_method: &str,
idempotency_key: String,
body: Vec<u8>,
) -> Result<Vec<u8>, String> {
let mut request_headers = vec![
HttpHeader {
name: "content-type".to_string(),
value: "application/json".to_string(),
},
HttpHeader {
name: "user-agent".to_string(),
value: APP_AGENT.to_string(),
},
HttpHeader {
name: "idempotency-key".to_string(),
value: idempotency_key.clone(),
},
];

if let Some(api_token) = &self.api_token {
request_headers.push(HttpHeader {
name: "authorization".to_string(),
value: api_token.clone(),
});
}

let request = CanisterHttpRequestArgument {
url: self.provider.clone(),
max_response_bytes: None, //optional for request
method: HttpMethod::POST,
headers: request_headers,
body: Some(body),
transform: None,
};

let (res,): (HttpResponse,) = ic_cdk::call(self.proxy, proxy_method, (request,))
.await
.map_err(|(code, msg)| {
format!(
"failed to call {} on {:?}, code: {}, message: {}",
proxy_method, &self.proxy, code as u32, msg
)
})?;

if res.status >= 200u64 && res.status < 300u64 {
Ok(res.body)
} else {
Err(format!(
"failed to request provider: {}, idempotency-key: {}, status: {}, body: {}",
self.provider,
idempotency_key,
res.status,
String::from_utf8(res.body).unwrap_or_default(),
))
}
}
}
Loading

0 comments on commit 66c186b

Please sign in to comment.