Skip to content

Commit fcf1f0b

Browse files
committed
feat(evm_icp_bridge): bridge API
1 parent 3d4a729 commit fcf1f0b

File tree

8 files changed

+596
-69
lines changed

8 files changed

+596
-69
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/evm_icp_bridge/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ ic-dummy-getrandom-for-wasm = { workspace = true }
2828
ic_auth_types = { workspace = true }
2929
ic_cose_types = { workspace = true }
3030
ic-secp256k1 = { workspace = true }
31+
icrc-ledger-types = { workspace = true }
3132
lazy_static = { workspace = true }
3233
once_cell = { workspace = true }
34+
num-traits = { workspace = true }
3335
url = { workspace = true }
3436
serde = { workspace = true }
3537
serde_bytes = { workspace = true }

src/evm_icp_bridge/src/api.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
use candid::Principal;
2-
use ic_cose_types::ANONYMOUS;
3-
4-
use crate::{store, store::StateInfo};
1+
use crate::{helper::msg_caller, store};
52

63
#[ic_cdk::query]
7-
fn info() -> Result<StateInfo, String> {
4+
fn info() -> Result<store::StateInfo, String> {
85
Ok(store::state::info())
96
}
107

@@ -15,11 +12,13 @@ fn evm_address() -> Result<String, String> {
1512
Ok(addr.to_string())
1613
}
1714

18-
fn msg_caller() -> Result<Principal, String> {
19-
let caller = ic_cdk::api::msg_caller();
20-
if caller == ANONYMOUS {
21-
Err("anonymous user is not allowed".to_string())
22-
} else {
23-
Ok(caller)
24-
}
15+
#[ic_cdk::update]
16+
async fn bridge(
17+
from_chain: String,
18+
to_chain: String,
19+
icp_amount: u128,
20+
) -> Result<store::BridgeTx, String> {
21+
let caller = msg_caller()?;
22+
let now_ms = ic_cdk::api::time() / 1_000_000;
23+
store::state::bridge(from_chain, to_chain, icp_amount, caller, now_ms).await
2524
}

src/evm_icp_bridge/src/api_admin.rs

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ async fn admin_add_evm_contract(
99
chain_name: String,
1010
chain_id: u64,
1111
address: String,
12-
decimals: u8,
1312
) -> Result<(), String> {
14-
let address = check_admin_add_evm_contract(&chain_name, chain_id, &address, decimals)?;
13+
let address = check_admin_add_evm_contract(&chain_name, chain_id, &address)?;
1514
let cli = store::state::evm_client(&chain_name);
1615
let now_ms = ic_cdk::api::time() / 1_000_000;
17-
let (cid, gas_price, block_number, symbol, dec) = futures::future::try_join5(
16+
let (cid, gas_price, block_number, symbol, decimals) = futures::future::try_join5(
1817
cli.chain_id(now_ms),
1918
cli.gas_price(now_ms),
20-
cli.finalized_block_number(now_ms),
19+
cli.block_number(now_ms),
2120
cli.erc20_symbol(now_ms, &address),
2221
cli.erc20_decimals(now_ms, &address),
2322
)
@@ -29,12 +28,6 @@ async fn admin_add_evm_contract(
2928
cid, chain_id
3029
));
3130
}
32-
if decimals != dec {
33-
return Err(format!(
34-
"decimals mismatch, got {}, expected {}",
35-
dec, decimals
36-
));
37-
}
3831

3932
store::state::with_mut(|s| {
4033
if s.token_symbol != symbol {
@@ -46,8 +39,13 @@ async fn admin_add_evm_contract(
4639

4740
s.evm_token_contracts
4841
.insert(chain_name.clone(), (address, decimals, chain_id));
49-
s.evm_finalized_block
50-
.insert(chain_name, (block_number, gas_price));
42+
s.evm_finalized_block.insert(
43+
chain_name,
44+
(
45+
block_number.saturating_sub(cli.max_confirmations),
46+
gas_price,
47+
),
48+
);
5149
Ok(())
5250
})
5351
}
@@ -57,17 +55,15 @@ fn validate_admin_add_evm_contract(
5755
chain_name: String,
5856
chain_id: u64,
5957
address: String,
60-
decimals: u8,
6158
) -> Result<String, String> {
62-
check_admin_add_evm_contract(&chain_name, chain_id, &address, decimals)?;
63-
pretty_format(&(chain_name, chain_id, address, decimals))
59+
check_admin_add_evm_contract(&chain_name, chain_id, &address)?;
60+
pretty_format(&(chain_name, chain_id, address))
6461
}
6562

6663
fn check_admin_add_evm_contract(
6764
chain_name: &str,
6865
chain_id: u64,
6966
address: &str,
70-
decimals: u8,
7167
) -> Result<Address, String> {
7268
if chain_name.trim().to_ascii_uppercase() != chain_name
7369
|| chain_name.is_empty()
@@ -80,13 +76,6 @@ fn check_admin_add_evm_contract(
8076
.map_err(|err| format!("invalid address: {err:?}"))?;
8177

8278
store::state::with(|s| {
83-
if decimals < s.token_decimals {
84-
return Err(format!(
85-
"decimals must be >= {}, got {}",
86-
s.token_decimals, decimals
87-
));
88-
}
89-
9079
if s.evm_token_contracts.contains_key(chain_name) {
9180
return Err("chain_id already exists".to_string());
9281
}
@@ -103,23 +92,32 @@ fn check_admin_add_evm_contract(
10392
}
10493

10594
#[ic_cdk::update(guard = "is_controller")]
106-
fn admin_set_evm_providers(chain_name: String, providers: Vec<String>) -> Result<(), String> {
95+
fn admin_set_evm_providers(
96+
chain_name: String,
97+
max_confirmations: u64,
98+
providers: Vec<String>,
99+
) -> Result<(), String> {
107100
for url in &providers {
108101
let v = Url::parse(url).map_err(|err| format!("invalid url {url}, error: {err}"))?;
109102
if v.scheme() != "https" {
110103
return Err(format!("url scheme must be https, got: {url}"));
111104
}
112105
}
106+
if max_confirmations < 2 {
107+
return Err("max_confirmations must be at least 2".to_string());
108+
}
113109

114110
store::state::with_mut(|s| {
115-
s.evm_providers.insert(chain_name, providers);
111+
s.evm_providers
112+
.insert(chain_name, (max_confirmations, providers));
116113
Ok(())
117114
})
118115
}
119116

120117
#[ic_cdk::update]
121118
fn validate_admin_set_evm_providers(
122-
chain_name: u64,
119+
chain_name: String,
120+
max_confirmations: u64,
123121
providers: Vec<String>,
124122
) -> Result<String, String> {
125123
for url in &providers {
@@ -128,7 +126,15 @@ fn validate_admin_set_evm_providers(
128126
return Err(format!("url scheme must be https, got: {url}"));
129127
}
130128
}
131-
pretty_format(&(chain_name, providers))
129+
if max_confirmations < 2 {
130+
return Err("max_confirmations must be at least 2".to_string());
131+
}
132+
pretty_format(&(chain_name, max_confirmations, providers))
133+
}
134+
135+
#[ic_cdk::update(guard = "is_controller")]
136+
async fn admin_update_evm_gas_price() -> Result<(), String> {
137+
unimplemented!()
132138
}
133139

134140
fn is_controller() -> Result<(), String> {

src/evm_icp_bridge/src/evm.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub static APP_AGENT: &str = concat!(
1818

1919
pub struct EvmClient {
2020
pub providers: Vec<String>,
21+
pub max_confirmations: u64,
2122
pub api_token: Option<String>,
2223
}
2324

@@ -35,6 +36,7 @@ pub struct RPCResponse<T> {
3536
error: Option<Value>,
3637
}
3738

39+
// https://www.quicknode.com/docs/ethereum
3840
impl EvmClient {
3941
pub async fn chain_id(&self, now_ms: u64) -> Result<u64, String> {
4042
let res: String = self
@@ -50,16 +52,15 @@ impl EvmClient {
5052
hex_to_u128(&res)
5153
}
5254

53-
pub async fn finalized_block_number(&self, now_ms: u64) -> Result<u64, String> {
55+
pub async fn block_number(&self, now_ms: u64) -> Result<u64, String> {
5456
let res: String = self
5557
.call(
5658
format!("eth_blockNumber-{}", now_ms),
5759
"eth_blockNumber",
5860
&[],
5961
)
6062
.await?;
61-
let num = hex_to_u64(&res)?;
62-
Ok(num.saturating_sub(42)) // consider a block finalized if it is 42 blocks deep
63+
hex_to_u64(&res)
6364
}
6465

6566
pub async fn get_transaction_count(
@@ -92,7 +93,7 @@ impl EvmClient {
9293
&self,
9394
now_ms: u64,
9495
tx_hash: &TxHash,
95-
) -> Result<TransactionReceipt, String> {
96+
) -> Result<Option<TransactionReceipt>, String> {
9697
self.call(
9798
format!("eth_getTransactionReceipt-{}", now_ms),
9899
"eth_getTransactionReceipt",
@@ -103,11 +104,11 @@ impl EvmClient {
103104

104105
pub async fn send_raw_transaction(
105106
&self,
106-
idempotency_key: String,
107+
now_ms: u64,
107108
signed_tx: String,
108-
) -> Result<TxHash, String> {
109+
) -> Result<Value, String> {
109110
self.call(
110-
idempotency_key,
111+
format!("eth_sendRawTransaction-{}", now_ms),
111112
"eth_sendRawTransaction",
112113
&[signed_tx.into()],
113114
)
@@ -323,3 +324,10 @@ fn decode_abi_uint(bytes: &[u8]) -> Result<U256, String> {
323324
}
324325
Ok(U256::from_be_slice(bytes))
325326
}
327+
328+
fn decode_abi_address(bytes: &[u8]) -> Result<Address, String> {
329+
if bytes.len() != 32 {
330+
return Err("abi address result must be 32 bytes".to_string());
331+
}
332+
Address::try_from(&bytes[12..32]).map_err(|err| err.to_string())
333+
}

src/evm_icp_bridge/src/helper.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use candid::{utils::ArgumentEncoder, Principal};
2+
3+
const ANONYMOUS: Principal = Principal::anonymous();
4+
pub fn msg_caller() -> Result<Principal, String> {
5+
let caller = ic_cdk::api::msg_caller();
6+
if caller == ANONYMOUS {
7+
Err("anonymous user is not allowed".to_string())
8+
} else {
9+
Ok(caller)
10+
}
11+
}
12+
13+
pub fn convert_amount(
14+
src_amount: u128,
15+
src_decimals: u8,
16+
target_decimals: u8,
17+
) -> Result<u128, String> {
18+
if src_decimals == target_decimals {
19+
Ok(src_amount)
20+
} else if src_decimals < target_decimals {
21+
let factor = 10u128
22+
.checked_pow((target_decimals - src_decimals) as u32)
23+
.ok_or_else(|| "exponent too large".to_string())?;
24+
src_amount
25+
.checked_mul(factor)
26+
.ok_or_else(|| "multiplication overflow".to_string())
27+
} else {
28+
let factor = 10u128
29+
.checked_pow((src_decimals - target_decimals) as u32)
30+
.ok_or_else(|| "exponent too large".to_string())?;
31+
Ok(src_amount / factor)
32+
}
33+
}
34+
35+
pub async fn call<In, Out>(
36+
id: Principal,
37+
method: &str,
38+
args: In,
39+
cycles: u128,
40+
) -> Result<Out, String>
41+
where
42+
In: ArgumentEncoder + Send,
43+
Out: candid::CandidType + for<'a> candid::Deserialize<'a>,
44+
{
45+
let res = ic_cdk::call::Call::bounded_wait(id, method)
46+
.with_args(&args)
47+
.with_cycles(cycles)
48+
.await
49+
.map_err(|err| format!("failed to call {} on {:?}, error: {:?}", method, &id, err))?;
50+
res.candid().map_err(|err| {
51+
format!(
52+
"failed to decode response from {} on {:?}, error: {:?}",
53+
method, &id, err
54+
)
55+
})
56+
}

src/evm_icp_bridge/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ mod api_http;
44
mod api_init;
55
mod ecdsa;
66
mod evm;
7+
mod helper;
78
mod store;
89

910
use api_init::CanisterArgs;
10-
use store::StateInfo;
1111

1212
ic_cdk::export_candid!();

0 commit comments

Comments
 (0)