Skip to content

Commit 0671b60

Browse files
authored
refactor!: layer for cycles accounting (#7)
Refactor the trait `CyclesChargingPolicy` to make it easier to configure cycles accounting when building a service as shown in the examples of #6 . **BREAKING CHANGE**: The trait `CyclesChargingPolicy` was changed as follows: 1. The method `cycles_to_charge` that only computed the amount of cycles to charge was removed. 2. A new method `charge_cycles` that does the actual charging was introduced as well as several implementations such as `ChargeMyself` or `ChargeCaller` that can be used to achieve the same functionalty as before.
1 parent 9a6cdc2 commit 0671b60

File tree

3 files changed

+153
-42
lines changed

3 files changed

+153
-42
lines changed

canhttp/src/cycles/mod.rs

Lines changed: 151 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,130 @@
1+
//! Middleware to handle cycles accounting.
2+
//!
3+
//! Issuing HTTPs outcalls requires cycles, and this layer takes care of the following:
4+
//! 1. Estimate the number of cycles required.
5+
//! 2. Decide how the canister should charge for those cycles.
6+
//! 3. Do the actual charging.
7+
//!
8+
//! # Examples
9+
//!
10+
//! To let the canister pay for HTTPs outcalls with its own cycle:
11+
//! ```rust
12+
//! use canhttp::{cycles::{ChargeMyself, CyclesAccountingServiceBuilder}, Client};
13+
//! use tower::{Service, ServiceBuilder, ServiceExt, BoxError};
14+
//!
15+
//! # #[tokio::main]
16+
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
17+
//! let mut service = ServiceBuilder::new()
18+
//! .cycles_accounting(34, ChargeMyself::default())
19+
//! .service(Client::new_with_box_error());
20+
//!
21+
//! let _ = service.ready().await.unwrap();
22+
//!
23+
//! # Ok(())
24+
//! # }
25+
//! ```
26+
//!
27+
//! To charge the caller of the canister for the whole cost of the HTTPs outcall with an additional fixed fee of 1M cycles:
28+
//! ```rust
29+
//! use canhttp::{cycles::{ChargeCaller, CyclesAccountingServiceBuilder}, Client};
30+
//! use tower::{Service, ServiceBuilder, ServiceExt, BoxError};
31+
//!
32+
//! # #[tokio::main]
33+
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
34+
//! let mut service = ServiceBuilder::new()
35+
//! .cycles_accounting(34, ChargeCaller::new(|_request, cost| cost + 1_000_000))
36+
//! .service(Client::new_with_box_error());
37+
//!
38+
//! let _ = service.ready().await.unwrap();
39+
//!
40+
//! # Ok(())
41+
//! # }
42+
//! ```
143
#[cfg(test)]
244
mod tests;
345

446
use crate::client::IcHttpRequestWithCycles;
5-
use crate::convert::Convert;
47+
use crate::convert::{Convert, ConvertRequestLayer};
48+
use crate::ConvertServiceBuilder;
649
use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument;
50+
use std::convert::Infallible;
751
use thiserror::Error;
52+
use tower::ServiceBuilder;
53+
use tower_layer::Stack;
854

9-
/// Estimate the amount of cycles to charge for a single HTTPs outcall.
55+
/// Charge cycles to pay for a single HTTPs outcall.
1056
pub trait CyclesChargingPolicy {
11-
/// Determine the amount of cycles to charge the caller.
12-
///
13-
/// If the value is `0`, no cycles will be charged, meaning that the canister using that library will
14-
/// pay for HTTPs outcalls with its own cycles. Otherwise, the returned amount of cycles will be transferred
15-
/// from the caller to the canister's cycles balance to pay (in part or fully) for the HTTPs outcall.
16-
fn cycles_to_charge(
57+
/// Type returned in case of a charging error.
58+
type Error;
59+
60+
/// Charge cycles and return the charged amount.
61+
fn charge_cycles(
62+
&self,
63+
request: &CanisterHttpRequestArgument,
64+
request_cycles_cost: u128,
65+
) -> Result<u128, Self::Error>;
66+
}
67+
68+
/// The canister using that policy will pay for HTTPs outcalls with its own cycles.
69+
#[derive(Default, Clone)]
70+
pub struct ChargeMyself {}
71+
72+
impl CyclesChargingPolicy for ChargeMyself {
73+
type Error = Infallible;
74+
75+
fn charge_cycles(
1776
&self,
1877
_request: &CanisterHttpRequestArgument,
19-
_attached_cycles: u128,
20-
) -> u128 {
21-
0
78+
_request_cycles_cost: u128,
79+
) -> Result<u128, Self::Error> {
80+
// no-op,
81+
Ok(0)
82+
}
83+
}
84+
85+
/// Cycles will be transferred from the caller of the canister using that library to pay for HTTPs outcalls.
86+
#[derive(Clone)]
87+
pub struct ChargeCaller<F> {
88+
cycles_to_charge: F,
89+
}
90+
91+
impl<F> ChargeCaller<F>
92+
where
93+
F: Fn(&CanisterHttpRequestArgument, u128) -> u128,
94+
{
95+
/// Create a new instance of [`ChargeCaller`].
96+
pub fn new(cycles_to_charge: F) -> Self {
97+
ChargeCaller { cycles_to_charge }
98+
}
99+
}
100+
101+
impl<F> CyclesChargingPolicy for ChargeCaller<F>
102+
where
103+
F: Fn(&CanisterHttpRequestArgument, u128) -> u128,
104+
{
105+
type Error = ChargeCallerError;
106+
107+
fn charge_cycles(
108+
&self,
109+
request: &CanisterHttpRequestArgument,
110+
request_cycles_cost: u128,
111+
) -> Result<u128, Self::Error> {
112+
let cycles_to_charge = (self.cycles_to_charge)(request, request_cycles_cost);
113+
if cycles_to_charge > 0 {
114+
let cycles_available = ic_cdk::api::call::msg_cycles_available128();
115+
if cycles_available < cycles_to_charge {
116+
return Err(ChargeCallerError::InsufficientCyclesError {
117+
expected: cycles_to_charge,
118+
received: cycles_available,
119+
});
120+
}
121+
let cycles_received = ic_cdk::api::call::msg_cycles_accept128(cycles_to_charge);
122+
assert_eq!(
123+
cycles_received, cycles_to_charge,
124+
"Expected to receive {cycles_to_charge}, but got {cycles_received}"
125+
);
126+
}
127+
Ok(cycles_to_charge)
22128
}
23129
}
24130

@@ -95,9 +201,9 @@ impl CyclesCostEstimator {
95201
}
96202
}
97203

98-
/// Error return by the [`CyclesAccounting`] middleware.
204+
/// Error returned by the [`CyclesAccounting`] middleware.
99205
#[derive(Error, Clone, Debug, PartialEq, Eq)]
100-
pub enum CyclesAccountingError {
206+
pub enum ChargeCallerError {
101207
/// Error returned when the caller should be charged but did not attach sufficiently many cycles.
102208
#[error("insufficient cycles (expected {expected:?}, received {received:?})")]
103209
InsufficientCyclesError {
@@ -111,53 +217,61 @@ pub enum CyclesAccountingError {
111217
/// A middleware to handle cycles accounting, i.e. verify if sufficiently many cycles are available in a request.
112218
/// How cycles are estimated is given by `CyclesEstimator`
113219
#[derive(Clone, Debug)]
114-
pub struct CyclesAccounting<Charging> {
220+
pub struct CyclesAccounting<ChargingPolicy> {
115221
cycles_cost_estimator: CyclesCostEstimator,
116-
charging_policy: Charging,
222+
charging_policy: ChargingPolicy,
117223
}
118224

119-
impl<Charging> CyclesAccounting<Charging> {
225+
impl<ChargingPolicy> CyclesAccounting<ChargingPolicy> {
120226
/// Create a new middleware given the cycles estimator.
121-
pub fn new(num_nodes_in_subnet: u32, charging_policy: Charging) -> Self {
227+
pub fn new(num_nodes_in_subnet: u32, charging_policy: ChargingPolicy) -> Self {
122228
Self {
123229
cycles_cost_estimator: CyclesCostEstimator::new(num_nodes_in_subnet),
124230
charging_policy,
125231
}
126232
}
127233
}
128234

129-
impl<CyclesEstimator> Convert<CanisterHttpRequestArgument> for CyclesAccounting<CyclesEstimator>
235+
impl<ChargingPolicy> Convert<CanisterHttpRequestArgument> for CyclesAccounting<ChargingPolicy>
130236
where
131-
CyclesEstimator: CyclesChargingPolicy,
237+
ChargingPolicy: CyclesChargingPolicy,
132238
{
133239
type Output = IcHttpRequestWithCycles;
134-
type Error = CyclesAccountingError;
240+
type Error = ChargingPolicy::Error;
135241

136242
fn try_convert(
137243
&mut self,
138244
request: CanisterHttpRequestArgument,
139245
) -> Result<Self::Output, Self::Error> {
140246
let cycles_to_attach = self.cycles_cost_estimator.cost_of_http_request(&request);
141-
let cycles_to_charge = self
142-
.charging_policy
143-
.cycles_to_charge(&request, cycles_to_attach);
144-
if cycles_to_charge > 0 {
145-
let cycles_available = ic_cdk::api::call::msg_cycles_available128();
146-
if cycles_available < cycles_to_charge {
147-
return Err(CyclesAccountingError::InsufficientCyclesError {
148-
expected: cycles_to_charge,
149-
received: cycles_available,
150-
});
151-
}
152-
let cycles_received = ic_cdk::api::call::msg_cycles_accept128(cycles_to_charge);
153-
assert_eq!(
154-
cycles_received, cycles_to_charge,
155-
"Expected to receive {cycles_to_charge}, but got {cycles_received}"
156-
);
157-
}
247+
self.charging_policy
248+
.charge_cycles(&request, cycles_to_attach)?;
158249
Ok(IcHttpRequestWithCycles {
159250
request,
160251
cycles: cycles_to_attach,
161252
})
162253
}
163254
}
255+
256+
/// Extension trait that adds methods to [`tower::ServiceBuilder`] for adding middleware
257+
/// related to cycles accounting
258+
pub trait CyclesAccountingServiceBuilder<L> {
259+
/// Add cycles accounting.
260+
///
261+
/// See the [module docs](crate::cycles) for examples.
262+
fn cycles_accounting<C>(
263+
self,
264+
num_nodes_in_subnet: u32,
265+
charging: C,
266+
) -> ServiceBuilder<Stack<ConvertRequestLayer<CyclesAccounting<C>>, L>>;
267+
}
268+
269+
impl<L> CyclesAccountingServiceBuilder<L> for ServiceBuilder<L> {
270+
fn cycles_accounting<C>(
271+
self,
272+
num_nodes_in_subnet: u32,
273+
charging: C,
274+
) -> ServiceBuilder<Stack<ConvertRequestLayer<CyclesAccounting<C>>, L>> {
275+
self.convert_request(CyclesAccounting::new(num_nodes_in_subnet, charging))
276+
}
277+
}

canhttp/src/cycles/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::CyclesCostEstimator;
1+
use crate::cycles::CyclesCostEstimator;
22
use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument;
33

44
#[test]

canhttp/src/lib.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ pub use client::{
1010
TransformContextRequestExtension,
1111
};
1212
pub use convert::ConvertServiceBuilder;
13-
pub use cycles::{
14-
CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, CyclesCostEstimator,
15-
};
1613

1714
mod client;
1815
pub mod convert;
19-
mod cycles;
16+
pub mod cycles;
2017
#[cfg(feature = "http")]
2118
pub mod http;
2219
#[cfg(feature = "multi")]

0 commit comments

Comments
 (0)