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) ]
244mod tests;
345
446use crate :: client:: IcHttpRequestWithCycles ;
5- use crate :: convert:: Convert ;
47+ use crate :: convert:: { Convert , ConvertRequestLayer } ;
48+ use crate :: ConvertServiceBuilder ;
649use ic_cdk:: api:: management_canister:: http_request:: CanisterHttpRequestArgument ;
50+ use std:: convert:: Infallible ;
751use 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.
1056pub 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 >
130236where
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+ }
0 commit comments