Skip to main content

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 {
454                reject: replica_error,
455                ..
456            }) if replica_error
457                .reject_message
458                .contains(REPLICA_ERROR_NO_SUCH_QUERY_METHOD)
459                || replica_error
460                    .reject_message
461                    .contains(IC_REF_ERROR_NO_SUCH_QUERY_METHOD) =>
462            {
463                DEFAULT_VERSION.clone()
464            }
465            version => Version::parse(&version?.0).unwrap_or_else(|_| DEFAULT_VERSION.clone()),
466        };
467        Ok(Self { canister, version })
468    }
469
470    /// Create a `WalletCanister` interface from an existing canister object and a known wallet version.
471    ///
472    /// This interface's methods may raise errors if the provided version is newer than the wallet's actual supported version.
473    pub fn from_canister_with_version(canister: Canister<'agent>, version: Version) -> Self {
474        Self { canister, version }
475    }
476}
477
478impl<'agent> WalletCanister<'agent> {
479    /// Re-fetch the API version string of the wallet.
480    pub fn fetch_wallet_api_version(&self) -> impl 'agent + SyncCall<Value = (Option<String>,)> {
481        self.query("wallet_api_version").build()
482    }
483
484    /// Get the (cached) API version of the wallet.
485    pub fn wallet_api_version(&self) -> &Version {
486        &self.version
487    }
488
489    /// Get the friendly name of the wallet (if one exists).
490    pub fn name(&self) -> impl 'agent + SyncCall<Value = (Option<String>,)> {
491        self.query("name").build()
492    }
493
494    /// Set the friendly name of the wallet.
495    pub fn set_name(&self, name: String) -> impl 'agent + AsyncCall<Value = ()> {
496        self.update("set_name").with_arg(name).build()
497    }
498
499    /// Get the current controller's principal ID.
500    pub fn get_controllers(&self) -> impl 'agent + SyncCall<Value = (Vec<Principal>,)> {
501        self.query("get_controllers").build()
502    }
503
504    /// Transfer controller to another principal ID.
505    pub fn add_controller(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
506        self.update("add_controller").with_arg(principal).build()
507    }
508
509    /// Remove a user as a wallet controller.
510    pub fn remove_controller(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
511        self.update("remove_controller").with_arg(principal).build()
512    }
513
514    /// Get the list of custodians.
515    pub fn get_custodians(&self) -> impl 'agent + SyncCall<Value = (Vec<Principal>,)> {
516        self.query("get_custodians").build()
517    }
518
519    /// Authorize a new custodian.
520    pub fn authorize(&self, custodian: Principal) -> impl 'agent + AsyncCall<Value = ()> {
521        self.update("authorize").with_arg(custodian).build()
522    }
523
524    /// Deauthorize a custodian.
525    pub fn deauthorize(&self, custodian: Principal) -> impl 'agent + AsyncCall<Value = ()> {
526        self.update("deauthorize").with_arg(custodian).build()
527    }
528
529    /// Get the balance with the 64-bit API.
530    pub fn wallet_balance64(&self) -> impl 'agent + SyncCall<Value = (BalanceResult<u64>,)> {
531        self.query("wallet_balance").build()
532    }
533
534    /// Get the balance with the 128-bit API.
535    pub fn wallet_balance128(&self) -> impl 'agent + SyncCall<Value = (BalanceResult,)> {
536        self.query("wallet_balance128").build()
537    }
538
539    /// Get the balance.
540    pub async fn wallet_balance(&self) -> Result<BalanceResult, AgentError> {
541        if self.version_supports_u128_cycles() {
542            self.wallet_balance128().call().await.map(|(r,)| r)
543        } else {
544            self.wallet_balance64()
545                .call()
546                .await
547                .map(|(r,)| BalanceResult {
548                    amount: r.amount.into(),
549                })
550        }
551    }
552
553    /// Send cycles to another canister using the 64-bit API.
554    pub fn wallet_send64(
555        &self,
556        destination: Principal,
557        amount: u64,
558    ) -> impl 'agent + AsyncCall<Value = (Result<(), String>,)> {
559        #[derive(CandidType)]
560        struct In {
561            canister: Principal,
562            amount: u64,
563        }
564
565        self.update("wallet_send")
566            .with_arg(In {
567                canister: destination,
568                amount,
569            })
570            .build()
571    }
572
573    /// Send cycles to another canister using the 128-bit API.
574    pub fn wallet_send128<'canister: 'agent>(
575        &'canister self,
576        destination: Principal,
577        amount: u128,
578    ) -> impl 'agent + AsyncCall<Value = (Result<(), String>,)> {
579        #[derive(CandidType)]
580        struct In {
581            canister: Principal,
582            amount: u128,
583        }
584
585        self.update("wallet_send128")
586            .with_arg(In {
587                canister: destination,
588                amount,
589            })
590            .build()
591    }
592
593    /// Send cycles to another canister.
594    pub async fn wallet_send(
595        &self,
596        destination: Principal,
597        amount: u128,
598    ) -> Result<(), AgentError> {
599        if self.version_supports_u128_cycles() {
600            self.wallet_send128(destination, amount)
601                .call_and_wait()
602                .await?
603        } else {
604            let amount = u64::try_from(amount).map_err(|_| {
605                AgentError::WalletUpgradeRequired(
606                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
607                )
608            })?;
609            self.wallet_send64(destination, amount)
610                .call_and_wait()
611                .await?
612        }
613        .0
614        .map_err(AgentError::WalletError)
615    }
616
617    /// A function for sending cycles to, so that a memo can be passed along with them.
618    pub fn wallet_receive(&self, memo: Option<String>) -> impl 'agent + AsyncCall<Value = ((),)> {
619        #[derive(CandidType)]
620        struct In {
621            memo: Option<String>,
622        }
623        self.update("wallet_receive")
624            .with_arg(memo.map(|memo| In { memo: Some(memo) }))
625            .build()
626    }
627
628    /// Create a canister through the wallet, using the single-controller 64-bit API.
629    pub fn wallet_create_canister64_v1(
630        &self,
631        cycles: u64,
632        controller: Option<Principal>,
633        compute_allocation: Option<ComputeAllocation>,
634        memory_allocation: Option<MemoryAllocation>,
635        freezing_threshold: Option<FreezingThreshold>,
636    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
637        #[derive(CandidType)]
638        struct In {
639            cycles: u64,
640            settings: CanisterSettingsV1,
641        }
642
643        let settings = CanisterSettingsV1 {
644            controller,
645            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
646            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
647            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
648        };
649
650        self.update("wallet_create_canister")
651            .with_arg(In { cycles, settings })
652            .build()
653            .map(|result: (Result<CreateResult, String>,)| (result.0,))
654    }
655
656    /// Create a canister through the wallet, using the multi-controller 64-bit API.
657    pub fn wallet_create_canister64_v2(
658        &self,
659        cycles: u64,
660        controllers: Option<Vec<Principal>>,
661        compute_allocation: Option<ComputeAllocation>,
662        memory_allocation: Option<MemoryAllocation>,
663        freezing_threshold: Option<FreezingThreshold>,
664    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
665        #[derive(CandidType)]
666        struct In {
667            cycles: u64,
668            settings: CanisterSettings,
669        }
670
671        let settings = CanisterSettings {
672            controllers,
673            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
674            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
675            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
676            reserved_cycles_limit: None,
677            wasm_memory_limit: None,
678            wasm_memory_threshold: None,
679            log_visibility: None,
680            log_memory_limit: None,
681            environment_variables: None,
682            snapshot_visibility: None,
683        };
684
685        self.update("wallet_create_canister")
686            .with_arg(In { cycles, settings })
687            .build()
688            .map(|result: (Result<CreateResult, String>,)| (result.0,))
689    }
690
691    /// Create a canister through the wallet, using the 128-bit API.
692    pub fn wallet_create_canister128(
693        &self,
694        cycles: u128,
695        controllers: Option<Vec<Principal>>,
696        compute_allocation: Option<ComputeAllocation>,
697        memory_allocation: Option<MemoryAllocation>,
698        freezing_threshold: Option<FreezingThreshold>,
699    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
700        #[derive(CandidType)]
701        struct In {
702            cycles: u128,
703            settings: CanisterSettings,
704        }
705
706        let settings = CanisterSettings {
707            controllers,
708            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
709            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
710            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
711            reserved_cycles_limit: None,
712            wasm_memory_limit: None,
713            wasm_memory_threshold: None,
714            log_visibility: None,
715            log_memory_limit: None,
716            environment_variables: None,
717            snapshot_visibility: None,
718        };
719
720        self.update("wallet_create_canister128")
721            .with_arg(In { cycles, settings })
722            .build()
723            .map(|result: (Result<CreateResult, String>,)| (result.0,))
724    }
725
726    /// Create a canister through the wallet.
727    ///
728    /// This method does not have a `reserved_cycles_limit` parameter,
729    /// as the wallet does not support the setting.  If you need to create a canister
730    /// with a `reserved_cycles_limit` set, use the management canister.
731    ///
732    /// This method does not have a `wasm_memory_limit` or `log_visibility` parameter,
733    /// as the wallet does not support the setting.  If you need to create a canister
734    /// with a `wasm_memory_limit` or `log_visibility` set, use the management canister.
735    pub async fn wallet_create_canister(
736        &self,
737        cycles: u128,
738        controllers: Option<Vec<Principal>>,
739        compute_allocation: Option<ComputeAllocation>,
740        memory_allocation: Option<MemoryAllocation>,
741        freezing_threshold: Option<FreezingThreshold>,
742    ) -> Result<CreateResult, AgentError> {
743        if self.version_supports_u128_cycles() {
744            self.wallet_create_canister128(
745                cycles,
746                controllers,
747                compute_allocation,
748                memory_allocation,
749                freezing_threshold,
750            )
751            .call_and_wait()
752            .await?
753        } else {
754            let cycles = u64::try_from(cycles).map_err(|_| {
755                AgentError::WalletUpgradeRequired(
756                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
757                )
758            })?;
759            if self.version_supports_multiple_controllers() {
760                self.wallet_create_canister64_v2(
761                    cycles,
762                    controllers,
763                    compute_allocation,
764                    memory_allocation,
765                    freezing_threshold,
766                )
767                .call_and_wait()
768                .await?
769            } else {
770                let controller: Option<Principal> = match &controllers {
771                    Some(c) if c.len() == 1 => {
772                        let first: Option<&Principal> = c.first();
773                        let first: Principal = *first.unwrap();
774                        Ok(Some(first))
775                    }
776                    Some(_) => Err(AgentError::WalletUpgradeRequired(
777                        "The installed wallet does not support multiple controllers.".to_string(),
778                    )),
779                    None => Ok(None),
780                }?;
781                self.wallet_create_canister64_v1(
782                    cycles,
783                    controller,
784                    compute_allocation,
785                    memory_allocation,
786                    freezing_threshold,
787                )
788                .call_and_wait()
789                .await?
790            }
791        }
792        .0
793        .map_err(AgentError::WalletError)
794    }
795
796    /// Create a wallet canister with the single-controller 64-bit API.
797    pub fn wallet_create_wallet64_v1(
798        &self,
799        cycles: u64,
800        controller: Option<Principal>,
801        compute_allocation: Option<ComputeAllocation>,
802        memory_allocation: Option<MemoryAllocation>,
803        freezing_threshold: Option<FreezingThreshold>,
804    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
805        #[derive(CandidType)]
806        struct In {
807            cycles: u64,
808            settings: CanisterSettingsV1,
809        }
810
811        let settings = CanisterSettingsV1 {
812            controller,
813            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
814            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
815            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
816        };
817
818        self.update("wallet_create_wallet")
819            .with_arg(In { cycles, settings })
820            .build()
821            .map(|result: (Result<CreateResult, String>,)| (result.0,))
822    }
823
824    /// Create a wallet canister with the multi-controller 64-bit API.
825    pub fn wallet_create_wallet64_v2(
826        &self,
827        cycles: u64,
828        controllers: Option<Vec<Principal>>,
829        compute_allocation: Option<ComputeAllocation>,
830        memory_allocation: Option<MemoryAllocation>,
831        freezing_threshold: Option<FreezingThreshold>,
832    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
833        #[derive(CandidType)]
834        struct In {
835            cycles: u64,
836            settings: CanisterSettings,
837        }
838
839        let settings = CanisterSettings {
840            controllers,
841            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
842            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
843            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
844            reserved_cycles_limit: None,
845            wasm_memory_limit: None,
846            wasm_memory_threshold: None,
847            log_visibility: None,
848            log_memory_limit: None,
849            environment_variables: None,
850            snapshot_visibility: None,
851        };
852
853        self.update("wallet_create_wallet")
854            .with_arg(In { cycles, settings })
855            .build()
856            .map(|result: (Result<CreateResult, String>,)| (result.0,))
857    }
858
859    /// Create a wallet canister with the 128-bit API.
860    pub fn wallet_create_wallet128(
861        &self,
862        cycles: u128,
863        controllers: Option<Vec<Principal>>,
864        compute_allocation: Option<ComputeAllocation>,
865        memory_allocation: Option<MemoryAllocation>,
866        freezing_threshold: Option<FreezingThreshold>,
867    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
868        #[derive(CandidType)]
869        struct In {
870            cycles: u128,
871            settings: CanisterSettings,
872        }
873
874        let settings = CanisterSettings {
875            controllers,
876            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
877            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
878            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
879            reserved_cycles_limit: None,
880            wasm_memory_limit: None,
881            wasm_memory_threshold: None,
882            log_visibility: None,
883            log_memory_limit: None,
884            environment_variables: None,
885            snapshot_visibility: None,
886        };
887
888        self.update("wallet_create_wallet128")
889            .with_arg(In { cycles, settings })
890            .build()
891            .map(|result: (Result<CreateResult, String>,)| (result.0,))
892    }
893
894    /// Create a wallet canister.
895    pub async fn wallet_create_wallet(
896        &self,
897        cycles: u128,
898        controllers: Option<Vec<Principal>>,
899        compute_allocation: Option<ComputeAllocation>,
900        memory_allocation: Option<MemoryAllocation>,
901        freezing_threshold: Option<FreezingThreshold>,
902    ) -> Result<CreateResult, AgentError> {
903        if self.version_supports_u128_cycles() {
904            self.wallet_create_wallet128(
905                cycles,
906                controllers,
907                compute_allocation,
908                memory_allocation,
909                freezing_threshold,
910            )
911            .call_and_wait()
912            .await?
913        } else {
914            let cycles = u64::try_from(cycles).map_err(|_| {
915                AgentError::WalletUpgradeRequired(
916                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
917                )
918            })?;
919            if self.version_supports_multiple_controllers() {
920                self.wallet_create_wallet64_v2(
921                    cycles,
922                    controllers,
923                    compute_allocation,
924                    memory_allocation,
925                    freezing_threshold,
926                )
927                .call_and_wait()
928                .await?
929            } else {
930                let controller: Option<Principal> = match &controllers {
931                    Some(c) if c.len() == 1 => Ok(Some(c[0])),
932                    Some(_) => Err(AgentError::WalletUpgradeRequired(
933                        "The installed wallet does not support multiple controllers.".to_string(),
934                    )),
935                    None => Ok(None),
936                }?;
937                self.wallet_create_wallet64_v1(
938                    cycles,
939                    controller,
940                    compute_allocation,
941                    memory_allocation,
942                    freezing_threshold,
943                )
944                .call_and_wait()
945                .await?
946            }
947        }
948        .0
949        .map_err(AgentError::WalletError)
950    }
951
952    /// Store the wallet WASM inside the wallet canister.
953    /// This is needed to enable `wallet_create_wallet`
954    pub fn wallet_store_wallet_wasm(
955        &self,
956        wasm_module: Vec<u8>,
957    ) -> impl 'agent + AsyncCall<Value = ()> {
958        #[derive(CandidType, Deserialize)]
959        struct In {
960            #[serde(with = "serde_bytes")]
961            wasm_module: Vec<u8>,
962        }
963        self.update("wallet_store_wallet_wasm")
964            .with_arg(In { wasm_module })
965            .build()
966    }
967
968    /// Add a principal to the address book.
969    pub fn add_address(&self, address: AddressEntry) -> impl 'agent + AsyncCall<Value = ()> {
970        self.update("add_address").with_arg(address).build()
971    }
972
973    /// List the entries in the address book.
974    pub fn list_addresses(&self) -> impl 'agent + SyncCall<Value = (Vec<AddressEntry>,)> {
975        self.query("list_addresses").build()
976    }
977
978    /// Remove a principal from the address book.
979    pub fn remove_address(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
980        self.update("remove_address").with_arg(principal).build()
981    }
982
983    /// Get a list of all transaction events this wallet remembers, using the 64-bit API. Fails if any events are 128-bit.
984    pub fn get_events64(
985        &self,
986        from: Option<u32>,
987        to: Option<u32>,
988    ) -> impl 'agent + SyncCall<Value = (Vec<Event<u64>>,)> {
989        #[derive(CandidType)]
990        struct In {
991            from: Option<u32>,
992            to: Option<u32>,
993        }
994
995        let arg = if from.is_none() && to.is_none() {
996            None
997        } else {
998            Some(In { from, to })
999        };
1000
1001        self.query("get_events").with_arg(arg).build()
1002    }
1003
1004    /// Get a list of all transaction events this wallet remembers, using the 128-bit API.
1005    pub fn get_events128(
1006        &self,
1007        from: Option<u32>,
1008        to: Option<u32>,
1009    ) -> impl 'agent + SyncCall<Value = (Vec<Event>,)> {
1010        #[derive(CandidType)]
1011        struct In {
1012            from: Option<u32>,
1013            to: Option<u32>,
1014        }
1015        let arg = if from.is_none() && to.is_none() {
1016            None
1017        } else {
1018            Some(In { from, to })
1019        };
1020        self.query("get_events128").with_arg(arg).build()
1021    }
1022
1023    /// Get a list of all transaction events this wallet remembers.
1024    pub async fn get_events(
1025        &self,
1026        from: Option<u32>,
1027        to: Option<u32>,
1028    ) -> Result<Vec<Event>, AgentError> {
1029        if self.version_supports_u128_cycles() {
1030            self.get_events128(from, to)
1031                .call()
1032                .await
1033                .map(|(events,)| events)
1034        } else {
1035            self.get_events64(from, to)
1036                .call()
1037                .await
1038                .map(|(events,)| events.into_iter().map(Event::into).collect())
1039        }
1040    }
1041
1042    /// Forward a call to another canister, including an amount of cycles
1043    /// from the wallet, using the 64-bit API.
1044    pub fn call64<'canister, Out, M: Into<String>>(
1045        &'canister self,
1046        destination: Principal,
1047        method_name: M,
1048        arg: Argument,
1049        amount: u64,
1050    ) -> CallForwarder<'agent, 'canister, Out>
1051    where
1052        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1053    {
1054        CallForwarder {
1055            wallet: self,
1056            destination,
1057            method_name: method_name.into(),
1058            amount: amount.into(),
1059            arg,
1060            phantom_out: std::marker::PhantomData,
1061            u128: false,
1062        }
1063    }
1064
1065    /// Forward a call to another canister, including an amount of cycles
1066    /// from the wallet, using the 128-bit API.
1067    pub fn call128<'canister, Out, M: Into<String>>(
1068        &'canister self,
1069        destination: Principal,
1070        method_name: M,
1071        arg: Argument,
1072        amount: u128,
1073    ) -> CallForwarder<'agent, 'canister, Out>
1074    where
1075        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1076    {
1077        CallForwarder {
1078            wallet: self,
1079            destination,
1080            method_name: method_name.into(),
1081            amount,
1082            arg,
1083            phantom_out: std::marker::PhantomData,
1084            u128: true,
1085        }
1086    }
1087
1088    /// Forward a call to another canister, including an amount of cycles
1089    /// from the wallet.
1090    pub fn call<'canister, Out, M: Into<String>>(
1091        &'canister self,
1092        destination: Principal,
1093        method_name: M,
1094        arg: Argument,
1095        amount: u128,
1096    ) -> CallForwarder<'agent, 'canister, Out>
1097    where
1098        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1099    {
1100        CallForwarder {
1101            wallet: self,
1102            destination,
1103            method_name: method_name.into(),
1104            amount,
1105            arg,
1106            phantom_out: std::marker::PhantomData,
1107            u128: self.version_supports_u128_cycles(),
1108        }
1109    }
1110
1111    /// Gets the managed canisters the wallet knows about.
1112    pub fn list_managed_canisters(
1113        &self,
1114        from: Option<u32>,
1115        to: Option<u32>,
1116    ) -> impl 'agent + SyncCall<Value = (Vec<ManagedCanisterInfo>, u32)> {
1117        #[derive(CandidType)]
1118        struct In {
1119            from: Option<u32>,
1120            to: Option<u32>,
1121        }
1122        self.query("list_managed_canisters")
1123            .with_arg((In { from, to },))
1124            .build()
1125    }
1126
1127    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 64-bit API.
1128    pub fn get_managed_canister_events64(
1129        &self,
1130        canister: Principal,
1131        from: Option<u32>,
1132        to: Option<u32>,
1133    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent<u64>>>,)> {
1134        #[derive(CandidType)]
1135        struct In {
1136            canister: Principal,
1137            from: Option<u32>,
1138            to: Option<u32>,
1139        }
1140        self.query("get_managed_canister_events")
1141            .with_arg((In { canister, from, to },))
1142            .build()
1143    }
1144
1145    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 128-bit API.
1146    pub fn get_managed_canister_events128(
1147        &self,
1148        canister: Principal,
1149        from: Option<u32>,
1150        to: Option<u32>,
1151    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent>>,)> {
1152        #[derive(CandidType)]
1153        struct In {
1154            canister: Principal,
1155            from: Option<u32>,
1156            to: Option<u32>,
1157        }
1158        self.query("get_managed_canister_events128")
1159            .with_arg((In { canister, from, to },))
1160            .build()
1161    }
1162
1163    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister
1164    pub async fn get_managed_canister_events(
1165        &self,
1166        canister: Principal,
1167        from: Option<u32>,
1168        to: Option<u32>,
1169    ) -> Result<Option<Vec<ManagedCanisterEvent>>, AgentError> {
1170        if self.version_supports_u128_cycles() {
1171            self.get_managed_canister_events128(canister, from, to)
1172                .call()
1173                .await
1174                .map(|(events,)| events)
1175        } else {
1176            self.get_managed_canister_events64(canister, from, to)
1177                .call()
1178                .await
1179                .map(|(events,)| {
1180                    events
1181                        .map(|events| events.into_iter().map(ManagedCanisterEvent::into).collect())
1182                })
1183        }
1184    }
1185
1186    /// Gets whether the wallet version supports initializing a canister with multiple controllers (introduced in 0.2.0).
1187    pub fn version_supports_multiple_controllers(&self) -> bool {
1188        static CONTROLLERS: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.2.0").unwrap());
1189        CONTROLLERS.matches(&self.version)
1190    }
1191
1192    /// Gets whether the wallet version supports 128-bit cycle counts (introduced in 0.3.0).
1193    pub fn version_supports_u128_cycles(&self) -> bool {
1194        static U128_CYCLES: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.3.0").unwrap());
1195        U128_CYCLES.matches(&self.version)
1196    }
1197}