Skip to content

Commit 08a795d

Browse files
committed
Add CyclesWalletRuntime
1 parent 5395674 commit 08a795d

File tree

5 files changed

+159
-0
lines changed

5 files changed

+159
-0
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ num-traits = "0.2.19"
2828
pin-project = "1.1.10"
2929
pocket-ic = "9.0.2"
3030
proptest = "1.6.0"
31+
regex-lite = "0.1.8"
3132
serde = "1.0"
33+
serde_bytes = "0.11.19"
3234
serde_json = "1.0"
3335
sha2 = "0.10.8"
3436
strum = { version = "0.27.1", features = ["derive"] }

ic-canister-runtime/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"]
1111
repository.workspace = true
1212
documentation = "https://docs.rs/ic-canister-runtime"
1313

14+
[features]
15+
wallet = ["dep:regex-lite", "dep:serde_bytes"]
16+
1417
[dependencies]
1518
async-trait = { workspace = true }
1619
candid = { workspace = true }
1720
ic-cdk = { workspace = true }
1821
ic-error-types = { workspace = true }
22+
regex-lite = { workspace = true, optional = true }
1923
serde = { workspace = true }
24+
serde_bytes = { workspace = true, optional = true }
2025
thiserror = { workspace = true }
2126

2227
[dev-dependencies]

ic-canister-runtime/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ use ic_cdk::call::{Call, CallFailed, CandidDecodeFailed};
1212
use ic_error_types::RejectCode;
1313
use serde::de::DeserializeOwned;
1414
use thiserror::Error;
15+
#[cfg(feature = "wallet")]
16+
pub use wallet::CyclesWalletRuntime;
17+
18+
#[cfg(feature = "wallet")]
19+
mod wallet;
1520

1621
/// Abstract the canister runtime so that code making requests to canisters can be reused:
1722
/// * in production using [`ic_cdk`],

ic-canister-runtime/src/wallet.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use crate::{IcError, Runtime};
2+
use async_trait::async_trait;
3+
use candid::{decode_one, encode_args, utils::ArgumentEncoder, CandidType, Deserialize, Principal};
4+
use ic_cdk::management_canister::CanisterId;
5+
use ic_error_types::RejectCode;
6+
use regex_lite::Regex;
7+
use serde::de::DeserializeOwned;
8+
9+
/// Runtime wrapping another [`Runtime`] instance, where update calls are routed through a
10+
/// [cycles wallet](https://github.com/dfinity/cycles-wallet) to attach cycles to them.
11+
pub struct CyclesWalletRuntime<R> {
12+
runtime: R,
13+
cycles_wallet_canister_id: Principal,
14+
}
15+
16+
impl<R> CyclesWalletRuntime<R> {
17+
/// Create a new [`CyclesWalletRuntime`] wrapping the given [`Runtime`] by routing update calls
18+
/// through the given cycles wallet to attach cycles.
19+
pub fn new(runtime: R, cycles_wallet_canister_id: Principal) -> Self {
20+
CyclesWalletRuntime {
21+
runtime,
22+
cycles_wallet_canister_id,
23+
}
24+
}
25+
}
26+
27+
#[async_trait]
28+
impl<R: Runtime + Send + Sync> Runtime for CyclesWalletRuntime<R> {
29+
async fn update_call<In, Out>(
30+
&self,
31+
id: Principal,
32+
method: &str,
33+
args: In,
34+
cycles: u128,
35+
) -> Result<Out, IcError>
36+
where
37+
In: ArgumentEncoder + Send,
38+
Out: CandidType + DeserializeOwned,
39+
{
40+
self.runtime
41+
.update_call::<(WalletCall128Args,), Result<WalletCall128Result, String>>(
42+
self.cycles_wallet_canister_id,
43+
"wallet_call128",
44+
(WalletCall128Args::new(id, method, args, cycles),),
45+
0,
46+
)
47+
.await
48+
.and_then(decode_cycles_wallet_response)
49+
}
50+
51+
async fn query_call<In, Out>(
52+
&self,
53+
id: Principal,
54+
method: &str,
55+
args: In,
56+
) -> Result<Out, IcError>
57+
where
58+
In: ArgumentEncoder + Send,
59+
Out: CandidType + DeserializeOwned,
60+
{
61+
self.runtime.query_call(id, method, args).await
62+
}
63+
}
64+
65+
// Argument to the cycles wallet canister `wallet_call128` method.
66+
#[derive(CandidType, Deserialize)]
67+
struct WalletCall128Args {
68+
canister: Principal,
69+
method_name: String,
70+
#[serde(with = "serde_bytes")]
71+
args: Vec<u8>,
72+
cycles: u128,
73+
}
74+
75+
impl WalletCall128Args {
76+
pub fn new<In: ArgumentEncoder>(
77+
canister_id: CanisterId,
78+
method: impl ToString,
79+
args: In,
80+
cycles: u128,
81+
) -> Self {
82+
Self {
83+
canister: canister_id,
84+
method_name: method.to_string(),
85+
args: encode_args(args).unwrap_or_else(panic_when_encode_fails),
86+
cycles,
87+
}
88+
}
89+
}
90+
91+
// Return type of the cycles wallet canister `wallet_call128` method.
92+
#[derive(CandidType, Deserialize)]
93+
struct WalletCall128Result {
94+
#[serde(with = "serde_bytes", rename = "return")]
95+
pub bytes: Vec<u8>,
96+
}
97+
98+
// The cycles wallet canister formats the rejection code and error message from the target
99+
// canister into a single string. Extract them back from the formatted string.
100+
fn decode_cycles_wallet_response<Out>(
101+
result: Result<WalletCall128Result, String>,
102+
) -> Result<Out, IcError>
103+
where
104+
Out: CandidType + DeserializeOwned,
105+
{
106+
match result {
107+
Ok(WalletCall128Result { bytes }) => {
108+
decode_one(&bytes).map_err(|e| IcError::CandidDecodeFailed {
109+
message: format!(
110+
"failed to decode canister response as {}: {}",
111+
std::any::type_name::<Out>(),
112+
e
113+
),
114+
})
115+
}
116+
Err(message) => {
117+
match Regex::new(r"^An error happened during the call: (\d+): (.*)$")
118+
.unwrap()
119+
.captures(&message)
120+
{
121+
Some(captures) => {
122+
let (_, [code, message]) = captures.extract();
123+
Err(IcError::CallRejected {
124+
code: code.parse::<u64>().unwrap().try_into().unwrap(),
125+
message: message.to_string(),
126+
})
127+
}
128+
None => Err(IcError::CallRejected {
129+
code: RejectCode::SysFatal,
130+
message: message.to_string(),
131+
}),
132+
}
133+
}
134+
}
135+
}
136+
137+
fn panic_when_encode_fails(err: candid::error::Error) -> Vec<u8> {
138+
panic!("failed to encode args: {err}")
139+
}

0 commit comments

Comments
 (0)