ic_utils/interfaces/
wallet.rs

1//! The canister interface for the [cycles wallet] canister.
2//!
3//! [cycles wallet]: https://github.com/dfinity/cycles-wallet
4
5use std::{
6    future::{Future, IntoFuture},
7    ops::Deref,
8};
9
10use crate::{
11    call::{AsyncCall, CallFuture, SyncCall},
12    canister::Argument,
13    interfaces::management_canister::{
14        attributes::{ComputeAllocation, FreezingThreshold, MemoryAllocation},
15        builders::CanisterSettings,
16    },
17    Canister,
18};
19use async_trait::async_trait;
20use candid::{decode_args, utils::ArgumentDecoder, CandidType, Deserialize, Nat};
21use ic_agent::{agent::CallResponse, export::Principal, Agent, AgentError};
22use once_cell::sync::Lazy;
23use semver::{Version, VersionReq};
24
25const REPLICA_ERROR_NO_SUCH_QUERY_METHOD: &str = "has no query method 'wallet_api_version'";
26const IC_REF_ERROR_NO_SUCH_QUERY_METHOD: &str = "query method does not exist";
27
28/// An interface for forwarding a canister method call through the wallet canister via `wallet_canister_call`.
29#[derive(Debug)]
30pub struct CallForwarder<'agent, 'canister, Out>
31where
32    Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
33{
34    wallet: &'canister WalletCanister<'agent>,
35    destination: Principal,
36    method_name: String,
37    amount: u128,
38    u128: bool,
39    arg: Argument,
40    phantom_out: std::marker::PhantomData<Out>,
41}
42
43/// A canister's settings. Similar to the canister settings struct from [`management_canister`](super::management_canister),
44/// but the management canister may evolve to have more settings without the wallet canister evolving to recognize them.
45#[derive(Debug, Clone, CandidType, Deserialize)]
46pub struct CanisterSettingsV1 {
47    /// The set of canister controllers. Controllers can update the canister via the management canister.
48    pub controller: Option<Principal>,
49    /// The allocation percentage (between 0 and 100 inclusive) for *guaranteed* compute capacity.
50    pub compute_allocation: Option<Nat>,
51    /// The allocation, in bytes (up to 256 TiB) that the canister is allowed to use for storage.
52    pub memory_allocation: Option<Nat>,
53    /// The IC will freeze a canister protectively if it will likely run out of cycles before this amount of time, in seconds (up to `u64::MAX`), has passed.
54    pub freezing_threshold: Option<Nat>,
55}
56
57impl<'agent: 'canister, 'canister, Out> CallForwarder<'agent, 'canister, Out>
58where
59    Out: for<'de> ArgumentDecoder<'de> + Send + Sync + 'agent,
60{
61    /// Set the argument with candid argument. Can be called at most once.
62    pub fn with_arg<Argument>(mut self, arg: Argument) -> Self
63    where
64        Argument: CandidType + Sync + Send,
65    {
66        self.arg.set_idl_arg(arg);
67        self
68    }
69    /// Set the argument with multiple arguments as tuple. Can be called at most once.
70    pub fn with_args(mut self, tuple: impl candid::utils::ArgumentEncoder) -> Self {
71        if self.arg.0.is_some() {
72            panic!("argument is being set more than once");
73        }
74        self.arg = Argument::from_candid(tuple);
75        self
76    }
77
78    /// Set the argument with raw argument bytes. Can be called at most once.
79    pub fn with_arg_raw(mut self, arg: Vec<u8>) -> Self {
80        self.arg.set_raw_arg(arg);
81        self
82    }
83
84    /// Creates an [`AsyncCall`] implementation that, when called, will forward the specified canister call.
85    pub fn build(self) -> Result<impl 'agent + AsyncCall<Value = Out>, AgentError> {
86        #[derive(CandidType, Deserialize)]
87        struct In<TCycles> {
88            canister: Principal,
89            method_name: String,
90            #[serde(with = "serde_bytes")]
91            args: Vec<u8>,
92            cycles: TCycles,
93        }
94        Ok(if self.u128 {
95            self.wallet.update("wallet_call128").with_arg(In {
96                canister: self.destination,
97                method_name: self.method_name,
98                args: self.arg.serialize()?,
99                cycles: self.amount,
100            })
101        } else {
102            self.wallet.update("wallet_call").with_arg(In {
103                canister: self.destination,
104                method_name: self.method_name,
105                args: self.arg.serialize()?,
106                cycles: u64::try_from(self.amount).map_err(|_| {
107                    AgentError::WalletUpgradeRequired(
108                        "The installed wallet does not support cycle counts >2^64-1".to_string(),
109                    )
110                })?,
111            })
112        }
113        .build()
114        .and_then(|(result,): (Result<CallResult, String>,)| async move {
115            let result = result.map_err(AgentError::WalletCallFailed)?;
116            decode_args::<Out>(result.r#return.as_slice())
117                .map_err(|e| AgentError::CandidError(Box::new(e)))
118        }))
119    }
120
121    /// Calls the forwarded canister call on the wallet canister. Equivalent to `.build().call()`.
122    pub fn call(self) -> impl Future<Output = Result<CallResponse<Out>, AgentError>> + 'agent {
123        let call = self.build();
124        async { call?.call().await }
125    }
126
127    /// Calls the forwarded canister call on the wallet canister, and waits for the result. Equivalent to `.build().call_and_wait()`.
128    pub fn call_and_wait(self) -> impl Future<Output = Result<Out, AgentError>> + 'agent {
129        let call = self.build();
130        async { call?.call_and_wait().await }
131    }
132}
133
134#[cfg_attr(target_family = "wasm", async_trait(?Send))]
135#[cfg_attr(not(target_family = "wasm"), async_trait)]
136impl<'agent: 'canister, 'canister, Out> AsyncCall for CallForwarder<'agent, 'canister, Out>
137where
138    Out: for<'de> ArgumentDecoder<'de> + Send + Sync + 'agent,
139{
140    type Value = Out;
141
142    async fn call(self) -> Result<CallResponse<Out>, AgentError> {
143        self.call().await
144    }
145
146    async fn call_and_wait(self) -> Result<Out, AgentError> {
147        self.call_and_wait().await
148    }
149}
150
151impl<'agent: 'canister, 'canister, Out> IntoFuture for CallForwarder<'agent, 'canister, Out>
152where
153    Out: for<'de> ArgumentDecoder<'de> + Send + Sync + 'agent,
154{
155    type IntoFuture = CallFuture<'agent, Out>;
156    type Output = Result<Out, AgentError>;
157    fn into_future(self) -> Self::IntoFuture {
158        Box::pin(self.call_and_wait())
159    }
160}
161
162/// A wallet canister interface, for the standard wallet provided by DFINITY.
163/// This interface implements most methods conveniently for the user.
164#[derive(Debug, Clone)]
165pub struct WalletCanister<'agent> {
166    canister: Canister<'agent>,
167    version: Version,
168}
169
170impl<'agent> Deref for WalletCanister<'agent> {
171    type Target = Canister<'agent>;
172    fn deref(&self) -> &Self::Target {
173        &self.canister
174    }
175}
176
177/// The possible kinds of events that can be stored in an [`Event`].
178#[derive(CandidType, Debug, Deserialize)]
179pub enum EventKind<TCycles = u128> {
180    /// Cycles were sent to a canister.
181    CyclesSent {
182        /// The canister the cycles were sent to.
183        to: Principal,
184        /// The number of cycles that were initially sent.
185        amount: TCycles,
186        /// The number of cycles that were refunded by the canister.
187        refund: TCycles,
188    },
189    /// Cycles were received from a canister.
190    CyclesReceived {
191        /// The canister that sent the cycles.
192        from: Principal,
193        /// The number of cycles received.
194        amount: TCycles,
195        /// The memo provided with the payment.
196        memo: Option<String>,
197    },
198    /// A known principal was added to the address book.
199    AddressAdded {
200        /// The principal that was added.
201        id: Principal,
202        /// The friendly name of the principal, if any.
203        name: Option<String>,
204        /// The significance of this principal to the wallet.
205        role: Role,
206    },
207    /// A principal was removed from the address book.
208    AddressRemoved {
209        /// The principal that was removed.
210        id: Principal,
211    },
212    /// A canister was created.
213    CanisterCreated {
214        /// The canister that was created.
215        canister: Principal,
216        /// The initial cycles balance that the canister was created with.
217        cycles: TCycles,
218    },
219    /// A call was forwarded to the canister.
220    CanisterCalled {
221        /// The canister that was called.
222        canister: Principal,
223        /// The name of the canister method that was called.
224        method_name: String,
225        /// The number of cycles that were supplied with the call.
226        cycles: TCycles,
227    },
228}
229
230impl From<EventKind<u64>> for EventKind {
231    fn from(kind: EventKind<u64>) -> Self {
232        use EventKind::*;
233        match kind {
234            AddressAdded { id, name, role } => AddressAdded { id, name, role },
235            AddressRemoved { id } => AddressRemoved { id },
236            CanisterCalled {
237                canister,
238                cycles,
239                method_name,
240            } => CanisterCalled {
241                canister,
242                cycles: cycles.into(),
243                method_name,
244            },
245            CanisterCreated { canister, cycles } => CanisterCreated {
246                canister,
247                cycles: cycles.into(),
248            },
249            CyclesReceived { amount, from, memo } => CyclesReceived {
250                amount: amount.into(),
251                from,
252                memo,
253            },
254            CyclesSent { amount, refund, to } => CyclesSent {
255                amount: amount.into(),
256                refund: refund.into(),
257                to,
258            },
259        }
260    }
261}
262
263/// A transaction event tracked by the wallet's history feature.
264#[derive(CandidType, Debug, Deserialize)]
265pub struct Event<TCycles = u128> {
266    /// An ID uniquely identifying this event.
267    pub id: u32,
268    /// The Unix timestamp that this event occurred at.
269    pub timestamp: u64,
270    /// The kind of event that occurred.
271    pub kind: EventKind<TCycles>,
272}
273
274impl From<Event<u64>> for Event {
275    fn from(
276        Event {
277            id,
278            timestamp,
279            kind,
280        }: Event<u64>,
281    ) -> Self {
282        Self {
283            id,
284            timestamp,
285            kind: kind.into(),
286        }
287    }
288}
289
290/// The significance of a principal in the wallet's address book.
291#[derive(CandidType, Debug, Deserialize)]
292pub enum Role {
293    /// The principal has no particular significance, and is only there to be assigned a friendly name or be mentioned in the event log.
294    Contact,
295    /// The principal is a custodian of the wallet, and can therefore access the wallet, create canisters, and send and receive cycles.
296    Custodian,
297    /// The principal is a controller of the wallet, and can therefore access any wallet function or action.
298    Controller,
299}
300
301/// The kind of principal that a particular principal is.
302#[derive(CandidType, Debug, Deserialize)]
303pub enum Kind {
304    /// The kind of principal is unknown, such as the anonymous principal `2vxsx-fae`.
305    Unknown,
306    /// The principal belongs to an external user.
307    User,
308    /// The principal belongs to an IC canister.
309    Canister,
310}
311
312/// An entry in the address book.
313#[derive(CandidType, Debug, Deserialize)]
314pub struct AddressEntry {
315    /// The principal being identified.
316    pub id: Principal,
317    /// The friendly name for this principal, if one exists.
318    pub name: Option<String>,
319    /// The kind of principal it is.
320    pub kind: Kind,
321    /// The significance of this principal to the wallet canister.
322    pub role: Role,
323}
324
325/// A canister that the wallet is responsible for managing.
326#[derive(CandidType, Debug, Deserialize)]
327pub struct ManagedCanisterInfo {
328    /// The principal ID of the canister.
329    pub id: Principal,
330    /// The friendly name of the canister, if one has been set.
331    pub name: Option<String>,
332    /// The Unix timestamp that the canister was created at.
333    pub created_at: u64,
334}
335
336/// The possible kinds of events that can be stored in a [`ManagedCanisterEvent`].
337#[derive(CandidType, Debug, Deserialize)]
338pub enum ManagedCanisterEventKind<TCycles = u128> {
339    /// Cycles were sent to the canister.
340    CyclesSent {
341        /// The number of cycles that were sent.
342        amount: TCycles,
343        /// The number of cycles that were refunded.
344        refund: TCycles,
345    },
346    /// A function call was forwarded to the canister.
347    Called {
348        /// The name of the function that was called.
349        method_name: String,
350        /// The number of cycles that were provided along with the call.
351        cycles: TCycles,
352    },
353    /// The canister was created.
354    Created {
355        /// The number of cycles the canister was created with.
356        cycles: TCycles,
357    },
358}
359
360impl From<ManagedCanisterEventKind<u64>> for ManagedCanisterEventKind {
361    fn from(event: ManagedCanisterEventKind<u64>) -> Self {
362        use ManagedCanisterEventKind::*;
363        match event {
364            Called {
365                cycles,
366                method_name,
367            } => Called {
368                cycles: cycles.into(),
369                method_name,
370            },
371            Created { cycles } => Created {
372                cycles: cycles.into(),
373            },
374            CyclesSent { amount, refund } => CyclesSent {
375                amount: amount.into(),
376                refund: refund.into(),
377            },
378        }
379    }
380}
381
382/// A transaction event related to a [`ManagedCanisterInfo`].
383#[derive(CandidType, Deserialize, Debug)]
384pub struct ManagedCanisterEvent<TCycles = u128> {
385    /// The event ID.
386    pub id: u32,
387    /// The Unix timestamp the event occurred at.
388    pub timestamp: u64,
389    /// The kind of event that occurred.
390    pub kind: ManagedCanisterEventKind<TCycles>,
391}
392
393impl From<ManagedCanisterEvent<u64>> for ManagedCanisterEvent {
394    fn from(
395        ManagedCanisterEvent {
396            id,
397            timestamp,
398            kind,
399        }: ManagedCanisterEvent<u64>,
400    ) -> Self {
401        Self {
402            id,
403            timestamp,
404            kind: kind.into(),
405        }
406    }
407}
408
409/// The result of a balance request.
410#[derive(Debug, Copy, Clone, CandidType, Deserialize)]
411pub struct BalanceResult<TCycles = u128> {
412    /// The balance of the wallet, in cycles.
413    pub amount: TCycles,
414}
415
416/// The result of a canister creation request.
417#[derive(Debug, Copy, Clone, CandidType, Deserialize)]
418pub struct CreateResult {
419    /// The principal ID of the newly created (empty) canister.
420    pub canister_id: Principal,
421}
422
423/// The result of a call forwarding request.
424#[derive(Debug, Clone, CandidType, Deserialize)]
425pub struct CallResult {
426    /// The encoded return value blob of the canister method.
427    #[serde(with = "serde_bytes")]
428    pub r#return: Vec<u8>,
429}
430
431impl<'agent> WalletCanister<'agent> {
432    /// Create an instance of a `WalletCanister` interface pointing to the given Canister ID. Fails if it cannot learn the wallet's version.
433    pub async fn create(
434        agent: &'agent Agent,
435        canister_id: Principal,
436    ) -> Result<WalletCanister<'agent>, AgentError> {
437        let canister = Canister::builder()
438            .with_agent(agent)
439            .with_canister_id(canister_id)
440            .build()
441            .unwrap();
442        Self::from_canister(canister).await
443    }
444
445    /// Create a `WalletCanister` interface from an existing canister object. Fails if it cannot learn the wallet's version.
446    pub async fn from_canister(
447        canister: Canister<'agent>,
448    ) -> Result<WalletCanister<'agent>, AgentError> {
449        static DEFAULT_VERSION: Lazy<Version> = Lazy::new(|| Version::parse("0.1.0").unwrap());
450        let version: Result<(String,), _> =
451            canister.query("wallet_api_version").build().call().await;
452        let version = match version {
453            Err(AgentError::UncertifiedReject(replica_error))
454                if replica_error
455                    .reject_message
456                    .contains(REPLICA_ERROR_NO_SUCH_QUERY_METHOD)
457                    || replica_error
458                        .reject_message
459                        .contains(IC_REF_ERROR_NO_SUCH_QUERY_METHOD) =>
460            {
461                DEFAULT_VERSION.clone()
462            }
463            version => Version::parse(&version?.0).unwrap_or_else(|_| DEFAULT_VERSION.clone()),
464        };
465        Ok(Self { canister, version })
466    }
467
468    /// Create a `WalletCanister` interface from an existing canister object and a known wallet version.
469    ///
470    /// This interface's methods may raise errors if the provided version is newer than the wallet's actual supported version.
471    pub fn from_canister_with_version(canister: Canister<'agent>, version: Version) -> Self {
472        Self { canister, version }
473    }
474}
475
476impl<'agent> WalletCanister<'agent> {
477    /// Re-fetch the API version string of the wallet.
478    pub fn fetch_wallet_api_version(&self) -> impl 'agent + SyncCall<Value = (Option<String>,)> {
479        self.query("wallet_api_version").build()
480    }
481
482    /// Get the (cached) API version of the wallet.
483    pub fn wallet_api_version(&self) -> &Version {
484        &self.version
485    }
486
487    /// Get the friendly name of the wallet (if one exists).
488    pub fn name(&self) -> impl 'agent + SyncCall<Value = (Option<String>,)> {
489        self.query("name").build()
490    }
491
492    /// Set the friendly name of the wallet.
493    pub fn set_name(&self, name: String) -> impl 'agent + AsyncCall<Value = ()> {
494        self.update("set_name").with_arg(name).build()
495    }
496
497    /// Get the current controller's principal ID.
498    pub fn get_controllers(&self) -> impl 'agent + SyncCall<Value = (Vec<Principal>,)> {
499        self.query("get_controllers").build()
500    }
501
502    /// Transfer controller to another principal ID.
503    pub fn add_controller(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
504        self.update("add_controller").with_arg(principal).build()
505    }
506
507    /// Remove a user as a wallet controller.
508    pub fn remove_controller(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
509        self.update("remove_controller").with_arg(principal).build()
510    }
511
512    /// Get the list of custodians.
513    pub fn get_custodians(&self) -> impl 'agent + SyncCall<Value = (Vec<Principal>,)> {
514        self.query("get_custodians").build()
515    }
516
517    /// Authorize a new custodian.
518    pub fn authorize(&self, custodian: Principal) -> impl 'agent + AsyncCall<Value = ()> {
519        self.update("authorize").with_arg(custodian).build()
520    }
521
522    /// Deauthorize a custodian.
523    pub fn deauthorize(&self, custodian: Principal) -> impl 'agent + AsyncCall<Value = ()> {
524        self.update("deauthorize").with_arg(custodian).build()
525    }
526
527    /// Get the balance with the 64-bit API.
528    pub fn wallet_balance64(&self) -> impl 'agent + SyncCall<Value = (BalanceResult<u64>,)> {
529        self.query("wallet_balance").build()
530    }
531
532    /// Get the balance with the 128-bit API.
533    pub fn wallet_balance128(&self) -> impl 'agent + SyncCall<Value = (BalanceResult,)> {
534        self.query("wallet_balance128").build()
535    }
536
537    /// Get the balance.
538    pub async fn wallet_balance(&self) -> Result<BalanceResult, AgentError> {
539        if self.version_supports_u128_cycles() {
540            self.wallet_balance128().call().await.map(|(r,)| r)
541        } else {
542            self.wallet_balance64()
543                .call()
544                .await
545                .map(|(r,)| BalanceResult {
546                    amount: r.amount.into(),
547                })
548        }
549    }
550
551    /// Send cycles to another canister using the 64-bit API.
552    pub fn wallet_send64(
553        &self,
554        destination: Principal,
555        amount: u64,
556    ) -> impl 'agent + AsyncCall<Value = (Result<(), String>,)> {
557        #[derive(CandidType)]
558        struct In {
559            canister: Principal,
560            amount: u64,
561        }
562
563        self.update("wallet_send")
564            .with_arg(In {
565                canister: destination,
566                amount,
567            })
568            .build()
569    }
570
571    /// Send cycles to another canister using the 128-bit API.
572    pub fn wallet_send128<'canister: 'agent>(
573        &'canister self,
574        destination: Principal,
575        amount: u128,
576    ) -> impl 'agent + AsyncCall<Value = (Result<(), String>,)> {
577        #[derive(CandidType)]
578        struct In {
579            canister: Principal,
580            amount: u128,
581        }
582
583        self.update("wallet_send128")
584            .with_arg(In {
585                canister: destination,
586                amount,
587            })
588            .build()
589    }
590
591    /// Send cycles to another canister.
592    pub async fn wallet_send(
593        &self,
594        destination: Principal,
595        amount: u128,
596    ) -> Result<(), AgentError> {
597        if self.version_supports_u128_cycles() {
598            self.wallet_send128(destination, amount)
599                .call_and_wait()
600                .await?
601        } else {
602            let amount = u64::try_from(amount).map_err(|_| {
603                AgentError::WalletUpgradeRequired(
604                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
605                )
606            })?;
607            self.wallet_send64(destination, amount)
608                .call_and_wait()
609                .await?
610        }
611        .0
612        .map_err(AgentError::WalletError)
613    }
614
615    /// A function for sending cycles to, so that a memo can be passed along with them.
616    pub fn wallet_receive(&self, memo: Option<String>) -> impl 'agent + AsyncCall<Value = ((),)> {
617        #[derive(CandidType)]
618        struct In {
619            memo: Option<String>,
620        }
621        self.update("wallet_receive")
622            .with_arg(memo.map(|memo| In { memo: Some(memo) }))
623            .build()
624    }
625
626    /// Create a canister through the wallet, using the single-controller 64-bit API.
627    pub fn wallet_create_canister64_v1(
628        &self,
629        cycles: u64,
630        controller: Option<Principal>,
631        compute_allocation: Option<ComputeAllocation>,
632        memory_allocation: Option<MemoryAllocation>,
633        freezing_threshold: Option<FreezingThreshold>,
634    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
635        #[derive(CandidType)]
636        struct In {
637            cycles: u64,
638            settings: CanisterSettingsV1,
639        }
640
641        let settings = CanisterSettingsV1 {
642            controller,
643            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
644            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
645            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
646        };
647
648        self.update("wallet_create_canister")
649            .with_arg(In { cycles, settings })
650            .build()
651            .map(|result: (Result<CreateResult, String>,)| (result.0,))
652    }
653
654    /// Create a canister through the wallet, using the multi-controller 64-bit API.
655    pub fn wallet_create_canister64_v2(
656        &self,
657        cycles: u64,
658        controllers: Option<Vec<Principal>>,
659        compute_allocation: Option<ComputeAllocation>,
660        memory_allocation: Option<MemoryAllocation>,
661        freezing_threshold: Option<FreezingThreshold>,
662    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
663        #[derive(CandidType)]
664        struct In {
665            cycles: u64,
666            settings: CanisterSettings,
667        }
668
669        let settings = CanisterSettings {
670            controllers,
671            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
672            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
673            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
674            reserved_cycles_limit: None,
675            wasm_memory_limit: None,
676            wasm_memory_threshold: None,
677            log_visibility: None,
678        };
679
680        self.update("wallet_create_canister")
681            .with_arg(In { cycles, settings })
682            .build()
683            .map(|result: (Result<CreateResult, String>,)| (result.0,))
684    }
685
686    /// Create a canister through the wallet, using the 128-bit API.
687    pub fn wallet_create_canister128(
688        &self,
689        cycles: u128,
690        controllers: Option<Vec<Principal>>,
691        compute_allocation: Option<ComputeAllocation>,
692        memory_allocation: Option<MemoryAllocation>,
693        freezing_threshold: Option<FreezingThreshold>,
694    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
695        #[derive(CandidType)]
696        struct In {
697            cycles: u128,
698            settings: CanisterSettings,
699        }
700
701        let settings = CanisterSettings {
702            controllers,
703            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
704            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
705            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
706            reserved_cycles_limit: None,
707            wasm_memory_limit: None,
708            wasm_memory_threshold: None,
709            log_visibility: None,
710        };
711
712        self.update("wallet_create_canister128")
713            .with_arg(In { cycles, settings })
714            .build()
715            .map(|result: (Result<CreateResult, String>,)| (result.0,))
716    }
717
718    /// Create a canister through the wallet.
719    ///
720    /// This method does not have a `reserved_cycles_limit` parameter,
721    /// as the wallet does not support the setting.  If you need to create a canister
722    /// with a `reserved_cycles_limit` set, use the management canister.
723    ///
724    /// This method does not have a `wasm_memory_limit` or `log_visibility` parameter,
725    /// as the wallet does not support the setting.  If you need to create a canister
726    /// with a `wasm_memory_limit` or `log_visibility` set, use the management canister.
727    pub async fn wallet_create_canister(
728        &self,
729        cycles: u128,
730        controllers: Option<Vec<Principal>>,
731        compute_allocation: Option<ComputeAllocation>,
732        memory_allocation: Option<MemoryAllocation>,
733        freezing_threshold: Option<FreezingThreshold>,
734    ) -> Result<CreateResult, AgentError> {
735        if self.version_supports_u128_cycles() {
736            self.wallet_create_canister128(
737                cycles,
738                controllers,
739                compute_allocation,
740                memory_allocation,
741                freezing_threshold,
742            )
743            .call_and_wait()
744            .await?
745        } else {
746            let cycles = u64::try_from(cycles).map_err(|_| {
747                AgentError::WalletUpgradeRequired(
748                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
749                )
750            })?;
751            if self.version_supports_multiple_controllers() {
752                self.wallet_create_canister64_v2(
753                    cycles,
754                    controllers,
755                    compute_allocation,
756                    memory_allocation,
757                    freezing_threshold,
758                )
759                .call_and_wait()
760                .await?
761            } else {
762                let controller: Option<Principal> = match &controllers {
763                    Some(c) if c.len() == 1 => {
764                        let first: Option<&Principal> = c.first();
765                        let first: Principal = *first.unwrap();
766                        Ok(Some(first))
767                    }
768                    Some(_) => Err(AgentError::WalletUpgradeRequired(
769                        "The installed wallet does not support multiple controllers.".to_string(),
770                    )),
771                    None => Ok(None),
772                }?;
773                self.wallet_create_canister64_v1(
774                    cycles,
775                    controller,
776                    compute_allocation,
777                    memory_allocation,
778                    freezing_threshold,
779                )
780                .call_and_wait()
781                .await?
782            }
783        }
784        .0
785        .map_err(AgentError::WalletError)
786    }
787
788    /// Create a wallet canister with the single-controller 64-bit API.
789    pub fn wallet_create_wallet64_v1(
790        &self,
791        cycles: u64,
792        controller: Option<Principal>,
793        compute_allocation: Option<ComputeAllocation>,
794        memory_allocation: Option<MemoryAllocation>,
795        freezing_threshold: Option<FreezingThreshold>,
796    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
797        #[derive(CandidType)]
798        struct In {
799            cycles: u64,
800            settings: CanisterSettingsV1,
801        }
802
803        let settings = CanisterSettingsV1 {
804            controller,
805            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
806            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
807            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
808        };
809
810        self.update("wallet_create_wallet")
811            .with_arg(In { cycles, settings })
812            .build()
813            .map(|result: (Result<CreateResult, String>,)| (result.0,))
814    }
815
816    /// Create a wallet canister with the multi-controller 64-bit API.
817    pub fn wallet_create_wallet64_v2(
818        &self,
819        cycles: u64,
820        controllers: Option<Vec<Principal>>,
821        compute_allocation: Option<ComputeAllocation>,
822        memory_allocation: Option<MemoryAllocation>,
823        freezing_threshold: Option<FreezingThreshold>,
824    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
825        #[derive(CandidType)]
826        struct In {
827            cycles: u64,
828            settings: CanisterSettings,
829        }
830
831        let settings = CanisterSettings {
832            controllers,
833            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
834            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
835            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
836            reserved_cycles_limit: None,
837            wasm_memory_limit: None,
838            wasm_memory_threshold: None,
839            log_visibility: None,
840        };
841
842        self.update("wallet_create_wallet")
843            .with_arg(In { cycles, settings })
844            .build()
845            .map(|result: (Result<CreateResult, String>,)| (result.0,))
846    }
847
848    /// Create a wallet canister with the 128-bit API.
849    pub fn wallet_create_wallet128(
850        &self,
851        cycles: u128,
852        controllers: Option<Vec<Principal>>,
853        compute_allocation: Option<ComputeAllocation>,
854        memory_allocation: Option<MemoryAllocation>,
855        freezing_threshold: Option<FreezingThreshold>,
856    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
857        #[derive(CandidType)]
858        struct In {
859            cycles: u128,
860            settings: CanisterSettings,
861        }
862
863        let settings = CanisterSettings {
864            controllers,
865            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
866            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
867            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
868            reserved_cycles_limit: None,
869            wasm_memory_limit: None,
870            wasm_memory_threshold: None,
871            log_visibility: None,
872        };
873
874        self.update("wallet_create_wallet128")
875            .with_arg(In { cycles, settings })
876            .build()
877            .map(|result: (Result<CreateResult, String>,)| (result.0,))
878    }
879
880    /// Create a wallet canister.
881    pub async fn wallet_create_wallet(
882        &self,
883        cycles: u128,
884        controllers: Option<Vec<Principal>>,
885        compute_allocation: Option<ComputeAllocation>,
886        memory_allocation: Option<MemoryAllocation>,
887        freezing_threshold: Option<FreezingThreshold>,
888    ) -> Result<CreateResult, AgentError> {
889        if self.version_supports_u128_cycles() {
890            self.wallet_create_wallet128(
891                cycles,
892                controllers,
893                compute_allocation,
894                memory_allocation,
895                freezing_threshold,
896            )
897            .call_and_wait()
898            .await?
899        } else {
900            let cycles = u64::try_from(cycles).map_err(|_| {
901                AgentError::WalletUpgradeRequired(
902                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
903                )
904            })?;
905            if self.version_supports_multiple_controllers() {
906                self.wallet_create_wallet64_v2(
907                    cycles,
908                    controllers,
909                    compute_allocation,
910                    memory_allocation,
911                    freezing_threshold,
912                )
913                .call_and_wait()
914                .await?
915            } else {
916                let controller: Option<Principal> = match &controllers {
917                    Some(c) if c.len() == 1 => Ok(Some(c[0])),
918                    Some(_) => Err(AgentError::WalletUpgradeRequired(
919                        "The installed wallet does not support multiple controllers.".to_string(),
920                    )),
921                    None => Ok(None),
922                }?;
923                self.wallet_create_wallet64_v1(
924                    cycles,
925                    controller,
926                    compute_allocation,
927                    memory_allocation,
928                    freezing_threshold,
929                )
930                .call_and_wait()
931                .await?
932            }
933        }
934        .0
935        .map_err(AgentError::WalletError)
936    }
937
938    /// Store the wallet WASM inside the wallet canister.
939    /// This is needed to enable `wallet_create_wallet`
940    pub fn wallet_store_wallet_wasm(
941        &self,
942        wasm_module: Vec<u8>,
943    ) -> impl 'agent + AsyncCall<Value = ()> {
944        #[derive(CandidType, Deserialize)]
945        struct In {
946            #[serde(with = "serde_bytes")]
947            wasm_module: Vec<u8>,
948        }
949        self.update("wallet_store_wallet_wasm")
950            .with_arg(In { wasm_module })
951            .build()
952    }
953
954    /// Add a principal to the address book.
955    pub fn add_address(&self, address: AddressEntry) -> impl 'agent + AsyncCall<Value = ()> {
956        self.update("add_address").with_arg(address).build()
957    }
958
959    /// List the entries in the address book.
960    pub fn list_addresses(&self) -> impl 'agent + SyncCall<Value = (Vec<AddressEntry>,)> {
961        self.query("list_addresses").build()
962    }
963
964    /// Remove a principal from the address book.
965    pub fn remove_address(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
966        self.update("remove_address").with_arg(principal).build()
967    }
968
969    /// Get a list of all transaction events this wallet remembers, using the 64-bit API. Fails if any events are 128-bit.
970    pub fn get_events64(
971        &self,
972        from: Option<u32>,
973        to: Option<u32>,
974    ) -> impl 'agent + SyncCall<Value = (Vec<Event<u64>>,)> {
975        #[derive(CandidType)]
976        struct In {
977            from: Option<u32>,
978            to: Option<u32>,
979        }
980
981        let arg = if from.is_none() && to.is_none() {
982            None
983        } else {
984            Some(In { from, to })
985        };
986
987        self.query("get_events").with_arg(arg).build()
988    }
989
990    /// Get a list of all transaction events this wallet remembers, using the 128-bit API.
991    pub fn get_events128(
992        &self,
993        from: Option<u32>,
994        to: Option<u32>,
995    ) -> impl 'agent + SyncCall<Value = (Vec<Event>,)> {
996        #[derive(CandidType)]
997        struct In {
998            from: Option<u32>,
999            to: Option<u32>,
1000        }
1001        let arg = if from.is_none() && to.is_none() {
1002            None
1003        } else {
1004            Some(In { from, to })
1005        };
1006        self.query("get_events128").with_arg(arg).build()
1007    }
1008
1009    /// Get a list of all transaction events this wallet remembers.
1010    pub async fn get_events(
1011        &self,
1012        from: Option<u32>,
1013        to: Option<u32>,
1014    ) -> Result<Vec<Event>, AgentError> {
1015        if self.version_supports_u128_cycles() {
1016            self.get_events128(from, to)
1017                .call()
1018                .await
1019                .map(|(events,)| events)
1020        } else {
1021            self.get_events64(from, to)
1022                .call()
1023                .await
1024                .map(|(events,)| events.into_iter().map(Event::into).collect())
1025        }
1026    }
1027
1028    /// Forward a call to another canister, including an amount of cycles
1029    /// from the wallet, using the 64-bit API.
1030    pub fn call64<'canister, Out, M: Into<String>>(
1031        &'canister self,
1032        destination: Principal,
1033        method_name: M,
1034        arg: Argument,
1035        amount: u64,
1036    ) -> CallForwarder<'agent, 'canister, Out>
1037    where
1038        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1039    {
1040        CallForwarder {
1041            wallet: self,
1042            destination,
1043            method_name: method_name.into(),
1044            amount: amount.into(),
1045            arg,
1046            phantom_out: std::marker::PhantomData,
1047            u128: false,
1048        }
1049    }
1050
1051    /// Forward a call to another canister, including an amount of cycles
1052    /// from the wallet, using the 128-bit API.
1053    pub fn call128<'canister, Out, M: Into<String>>(
1054        &'canister self,
1055        destination: Principal,
1056        method_name: M,
1057        arg: Argument,
1058        amount: u128,
1059    ) -> CallForwarder<'agent, 'canister, Out>
1060    where
1061        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1062    {
1063        CallForwarder {
1064            wallet: self,
1065            destination,
1066            method_name: method_name.into(),
1067            amount,
1068            arg,
1069            phantom_out: std::marker::PhantomData,
1070            u128: true,
1071        }
1072    }
1073
1074    /// Forward a call to another canister, including an amount of cycles
1075    /// from the wallet.
1076    pub fn call<'canister, Out, M: Into<String>>(
1077        &'canister self,
1078        destination: Principal,
1079        method_name: M,
1080        arg: Argument,
1081        amount: u128,
1082    ) -> CallForwarder<'agent, 'canister, Out>
1083    where
1084        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1085    {
1086        CallForwarder {
1087            wallet: self,
1088            destination,
1089            method_name: method_name.into(),
1090            amount,
1091            arg,
1092            phantom_out: std::marker::PhantomData,
1093            u128: self.version_supports_u128_cycles(),
1094        }
1095    }
1096
1097    /// Gets the managed canisters the wallet knows about.
1098    pub fn list_managed_canisters(
1099        &self,
1100        from: Option<u32>,
1101        to: Option<u32>,
1102    ) -> impl 'agent + SyncCall<Value = (Vec<ManagedCanisterInfo>, u32)> {
1103        #[derive(CandidType)]
1104        struct In {
1105            from: Option<u32>,
1106            to: Option<u32>,
1107        }
1108        self.query("list_managed_canisters")
1109            .with_arg((In { from, to },))
1110            .build()
1111    }
1112
1113    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 64-bit API.
1114    pub fn get_managed_canister_events64(
1115        &self,
1116        canister: Principal,
1117        from: Option<u32>,
1118        to: Option<u32>,
1119    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent<u64>>>,)> {
1120        #[derive(CandidType)]
1121        struct In {
1122            canister: Principal,
1123            from: Option<u32>,
1124            to: Option<u32>,
1125        }
1126        self.query("get_managed_canister_events")
1127            .with_arg((In { canister, from, to },))
1128            .build()
1129    }
1130
1131    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 128-bit API.
1132    pub fn get_managed_canister_events128(
1133        &self,
1134        canister: Principal,
1135        from: Option<u32>,
1136        to: Option<u32>,
1137    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent>>,)> {
1138        #[derive(CandidType)]
1139        struct In {
1140            canister: Principal,
1141            from: Option<u32>,
1142            to: Option<u32>,
1143        }
1144        self.query("get_managed_canister_events128")
1145            .with_arg((In { canister, from, to },))
1146            .build()
1147    }
1148
1149    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister
1150    pub async fn get_managed_canister_events(
1151        &self,
1152        canister: Principal,
1153        from: Option<u32>,
1154        to: Option<u32>,
1155    ) -> Result<Option<Vec<ManagedCanisterEvent>>, AgentError> {
1156        if self.version_supports_u128_cycles() {
1157            self.get_managed_canister_events128(canister, from, to)
1158                .call()
1159                .await
1160                .map(|(events,)| events)
1161        } else {
1162            self.get_managed_canister_events64(canister, from, to)
1163                .call()
1164                .await
1165                .map(|(events,)| {
1166                    events
1167                        .map(|events| events.into_iter().map(ManagedCanisterEvent::into).collect())
1168                })
1169        }
1170    }
1171
1172    /// Gets whether the wallet version supports initializing a canister with multiple controllers (introduced in 0.2.0).
1173    pub fn version_supports_multiple_controllers(&self) -> bool {
1174        static CONTROLLERS: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.2.0").unwrap());
1175        CONTROLLERS.matches(&self.version)
1176    }
1177
1178    /// Gets whether the wallet version supports 128-bit cycle counts (introduced in 0.3.0).
1179    pub fn version_supports_u128_cycles(&self) -> bool {
1180        static U128_CYCLES: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.3.0").unwrap());
1181        U128_CYCLES.matches(&self.version)
1182    }
1183}