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        };
683
684        self.update("wallet_create_canister")
685            .with_arg(In { cycles, settings })
686            .build()
687            .map(|result: (Result<CreateResult, String>,)| (result.0,))
688    }
689
690    /// Create a canister through the wallet, using the 128-bit API.
691    pub fn wallet_create_canister128(
692        &self,
693        cycles: u128,
694        controllers: Option<Vec<Principal>>,
695        compute_allocation: Option<ComputeAllocation>,
696        memory_allocation: Option<MemoryAllocation>,
697        freezing_threshold: Option<FreezingThreshold>,
698    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
699        #[derive(CandidType)]
700        struct In {
701            cycles: u128,
702            settings: CanisterSettings,
703        }
704
705        let settings = CanisterSettings {
706            controllers,
707            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
708            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
709            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
710            reserved_cycles_limit: None,
711            wasm_memory_limit: None,
712            wasm_memory_threshold: None,
713            log_visibility: None,
714            log_memory_limit: None,
715            environment_variables: None,
716        };
717
718        self.update("wallet_create_canister128")
719            .with_arg(In { cycles, settings })
720            .build()
721            .map(|result: (Result<CreateResult, String>,)| (result.0,))
722    }
723
724    /// Create a canister through the wallet.
725    ///
726    /// This method does not have a `reserved_cycles_limit` parameter,
727    /// as the wallet does not support the setting.  If you need to create a canister
728    /// with a `reserved_cycles_limit` set, use the management canister.
729    ///
730    /// This method does not have a `wasm_memory_limit` or `log_visibility` parameter,
731    /// as the wallet does not support the setting.  If you need to create a canister
732    /// with a `wasm_memory_limit` or `log_visibility` set, use the management canister.
733    pub async fn wallet_create_canister(
734        &self,
735        cycles: u128,
736        controllers: Option<Vec<Principal>>,
737        compute_allocation: Option<ComputeAllocation>,
738        memory_allocation: Option<MemoryAllocation>,
739        freezing_threshold: Option<FreezingThreshold>,
740    ) -> Result<CreateResult, AgentError> {
741        if self.version_supports_u128_cycles() {
742            self.wallet_create_canister128(
743                cycles,
744                controllers,
745                compute_allocation,
746                memory_allocation,
747                freezing_threshold,
748            )
749            .call_and_wait()
750            .await?
751        } else {
752            let cycles = u64::try_from(cycles).map_err(|_| {
753                AgentError::WalletUpgradeRequired(
754                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
755                )
756            })?;
757            if self.version_supports_multiple_controllers() {
758                self.wallet_create_canister64_v2(
759                    cycles,
760                    controllers,
761                    compute_allocation,
762                    memory_allocation,
763                    freezing_threshold,
764                )
765                .call_and_wait()
766                .await?
767            } else {
768                let controller: Option<Principal> = match &controllers {
769                    Some(c) if c.len() == 1 => {
770                        let first: Option<&Principal> = c.first();
771                        let first: Principal = *first.unwrap();
772                        Ok(Some(first))
773                    }
774                    Some(_) => Err(AgentError::WalletUpgradeRequired(
775                        "The installed wallet does not support multiple controllers.".to_string(),
776                    )),
777                    None => Ok(None),
778                }?;
779                self.wallet_create_canister64_v1(
780                    cycles,
781                    controller,
782                    compute_allocation,
783                    memory_allocation,
784                    freezing_threshold,
785                )
786                .call_and_wait()
787                .await?
788            }
789        }
790        .0
791        .map_err(AgentError::WalletError)
792    }
793
794    /// Create a wallet canister with the single-controller 64-bit API.
795    pub fn wallet_create_wallet64_v1(
796        &self,
797        cycles: u64,
798        controller: Option<Principal>,
799        compute_allocation: Option<ComputeAllocation>,
800        memory_allocation: Option<MemoryAllocation>,
801        freezing_threshold: Option<FreezingThreshold>,
802    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
803        #[derive(CandidType)]
804        struct In {
805            cycles: u64,
806            settings: CanisterSettingsV1,
807        }
808
809        let settings = CanisterSettingsV1 {
810            controller,
811            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
812            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
813            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
814        };
815
816        self.update("wallet_create_wallet")
817            .with_arg(In { cycles, settings })
818            .build()
819            .map(|result: (Result<CreateResult, String>,)| (result.0,))
820    }
821
822    /// Create a wallet canister with the multi-controller 64-bit API.
823    pub fn wallet_create_wallet64_v2(
824        &self,
825        cycles: u64,
826        controllers: Option<Vec<Principal>>,
827        compute_allocation: Option<ComputeAllocation>,
828        memory_allocation: Option<MemoryAllocation>,
829        freezing_threshold: Option<FreezingThreshold>,
830    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
831        #[derive(CandidType)]
832        struct In {
833            cycles: u64,
834            settings: CanisterSettings,
835        }
836
837        let settings = CanisterSettings {
838            controllers,
839            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
840            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
841            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
842            reserved_cycles_limit: None,
843            wasm_memory_limit: None,
844            wasm_memory_threshold: None,
845            log_visibility: None,
846            log_memory_limit: None,
847            environment_variables: None,
848        };
849
850        self.update("wallet_create_wallet")
851            .with_arg(In { cycles, settings })
852            .build()
853            .map(|result: (Result<CreateResult, String>,)| (result.0,))
854    }
855
856    /// Create a wallet canister with the 128-bit API.
857    pub fn wallet_create_wallet128(
858        &self,
859        cycles: u128,
860        controllers: Option<Vec<Principal>>,
861        compute_allocation: Option<ComputeAllocation>,
862        memory_allocation: Option<MemoryAllocation>,
863        freezing_threshold: Option<FreezingThreshold>,
864    ) -> impl 'agent + AsyncCall<Value = (Result<CreateResult, String>,)> {
865        #[derive(CandidType)]
866        struct In {
867            cycles: u128,
868            settings: CanisterSettings,
869        }
870
871        let settings = CanisterSettings {
872            controllers,
873            compute_allocation: compute_allocation.map(u8::from).map(Nat::from),
874            memory_allocation: memory_allocation.map(u64::from).map(Nat::from),
875            freezing_threshold: freezing_threshold.map(u64::from).map(Nat::from),
876            reserved_cycles_limit: None,
877            wasm_memory_limit: None,
878            wasm_memory_threshold: None,
879            log_visibility: None,
880            log_memory_limit: None,
881            environment_variables: None,
882        };
883
884        self.update("wallet_create_wallet128")
885            .with_arg(In { cycles, settings })
886            .build()
887            .map(|result: (Result<CreateResult, String>,)| (result.0,))
888    }
889
890    /// Create a wallet canister.
891    pub async fn wallet_create_wallet(
892        &self,
893        cycles: u128,
894        controllers: Option<Vec<Principal>>,
895        compute_allocation: Option<ComputeAllocation>,
896        memory_allocation: Option<MemoryAllocation>,
897        freezing_threshold: Option<FreezingThreshold>,
898    ) -> Result<CreateResult, AgentError> {
899        if self.version_supports_u128_cycles() {
900            self.wallet_create_wallet128(
901                cycles,
902                controllers,
903                compute_allocation,
904                memory_allocation,
905                freezing_threshold,
906            )
907            .call_and_wait()
908            .await?
909        } else {
910            let cycles = u64::try_from(cycles).map_err(|_| {
911                AgentError::WalletUpgradeRequired(
912                    "The installed wallet does not support cycle counts >2^64-1.".to_string(),
913                )
914            })?;
915            if self.version_supports_multiple_controllers() {
916                self.wallet_create_wallet64_v2(
917                    cycles,
918                    controllers,
919                    compute_allocation,
920                    memory_allocation,
921                    freezing_threshold,
922                )
923                .call_and_wait()
924                .await?
925            } else {
926                let controller: Option<Principal> = match &controllers {
927                    Some(c) if c.len() == 1 => Ok(Some(c[0])),
928                    Some(_) => Err(AgentError::WalletUpgradeRequired(
929                        "The installed wallet does not support multiple controllers.".to_string(),
930                    )),
931                    None => Ok(None),
932                }?;
933                self.wallet_create_wallet64_v1(
934                    cycles,
935                    controller,
936                    compute_allocation,
937                    memory_allocation,
938                    freezing_threshold,
939                )
940                .call_and_wait()
941                .await?
942            }
943        }
944        .0
945        .map_err(AgentError::WalletError)
946    }
947
948    /// Store the wallet WASM inside the wallet canister.
949    /// This is needed to enable `wallet_create_wallet`
950    pub fn wallet_store_wallet_wasm(
951        &self,
952        wasm_module: Vec<u8>,
953    ) -> impl 'agent + AsyncCall<Value = ()> {
954        #[derive(CandidType, Deserialize)]
955        struct In {
956            #[serde(with = "serde_bytes")]
957            wasm_module: Vec<u8>,
958        }
959        self.update("wallet_store_wallet_wasm")
960            .with_arg(In { wasm_module })
961            .build()
962    }
963
964    /// Add a principal to the address book.
965    pub fn add_address(&self, address: AddressEntry) -> impl 'agent + AsyncCall<Value = ()> {
966        self.update("add_address").with_arg(address).build()
967    }
968
969    /// List the entries in the address book.
970    pub fn list_addresses(&self) -> impl 'agent + SyncCall<Value = (Vec<AddressEntry>,)> {
971        self.query("list_addresses").build()
972    }
973
974    /// Remove a principal from the address book.
975    pub fn remove_address(&self, principal: Principal) -> impl 'agent + AsyncCall<Value = ()> {
976        self.update("remove_address").with_arg(principal).build()
977    }
978
979    /// Get a list of all transaction events this wallet remembers, using the 64-bit API. Fails if any events are 128-bit.
980    pub fn get_events64(
981        &self,
982        from: Option<u32>,
983        to: Option<u32>,
984    ) -> impl 'agent + SyncCall<Value = (Vec<Event<u64>>,)> {
985        #[derive(CandidType)]
986        struct In {
987            from: Option<u32>,
988            to: Option<u32>,
989        }
990
991        let arg = if from.is_none() && to.is_none() {
992            None
993        } else {
994            Some(In { from, to })
995        };
996
997        self.query("get_events").with_arg(arg).build()
998    }
999
1000    /// Get a list of all transaction events this wallet remembers, using the 128-bit API.
1001    pub fn get_events128(
1002        &self,
1003        from: Option<u32>,
1004        to: Option<u32>,
1005    ) -> impl 'agent + SyncCall<Value = (Vec<Event>,)> {
1006        #[derive(CandidType)]
1007        struct In {
1008            from: Option<u32>,
1009            to: Option<u32>,
1010        }
1011        let arg = if from.is_none() && to.is_none() {
1012            None
1013        } else {
1014            Some(In { from, to })
1015        };
1016        self.query("get_events128").with_arg(arg).build()
1017    }
1018
1019    /// Get a list of all transaction events this wallet remembers.
1020    pub async fn get_events(
1021        &self,
1022        from: Option<u32>,
1023        to: Option<u32>,
1024    ) -> Result<Vec<Event>, AgentError> {
1025        if self.version_supports_u128_cycles() {
1026            self.get_events128(from, to)
1027                .call()
1028                .await
1029                .map(|(events,)| events)
1030        } else {
1031            self.get_events64(from, to)
1032                .call()
1033                .await
1034                .map(|(events,)| events.into_iter().map(Event::into).collect())
1035        }
1036    }
1037
1038    /// Forward a call to another canister, including an amount of cycles
1039    /// from the wallet, using the 64-bit API.
1040    pub fn call64<'canister, Out, M: Into<String>>(
1041        &'canister self,
1042        destination: Principal,
1043        method_name: M,
1044        arg: Argument,
1045        amount: u64,
1046    ) -> CallForwarder<'agent, 'canister, Out>
1047    where
1048        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1049    {
1050        CallForwarder {
1051            wallet: self,
1052            destination,
1053            method_name: method_name.into(),
1054            amount: amount.into(),
1055            arg,
1056            phantom_out: std::marker::PhantomData,
1057            u128: false,
1058        }
1059    }
1060
1061    /// Forward a call to another canister, including an amount of cycles
1062    /// from the wallet, using the 128-bit API.
1063    pub fn call128<'canister, Out, M: Into<String>>(
1064        &'canister self,
1065        destination: Principal,
1066        method_name: M,
1067        arg: Argument,
1068        amount: u128,
1069    ) -> CallForwarder<'agent, 'canister, Out>
1070    where
1071        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1072    {
1073        CallForwarder {
1074            wallet: self,
1075            destination,
1076            method_name: method_name.into(),
1077            amount,
1078            arg,
1079            phantom_out: std::marker::PhantomData,
1080            u128: true,
1081        }
1082    }
1083
1084    /// Forward a call to another canister, including an amount of cycles
1085    /// from the wallet.
1086    pub fn call<'canister, Out, M: Into<String>>(
1087        &'canister self,
1088        destination: Principal,
1089        method_name: M,
1090        arg: Argument,
1091        amount: u128,
1092    ) -> CallForwarder<'agent, 'canister, Out>
1093    where
1094        Out: for<'de> ArgumentDecoder<'de> + Send + Sync,
1095    {
1096        CallForwarder {
1097            wallet: self,
1098            destination,
1099            method_name: method_name.into(),
1100            amount,
1101            arg,
1102            phantom_out: std::marker::PhantomData,
1103            u128: self.version_supports_u128_cycles(),
1104        }
1105    }
1106
1107    /// Gets the managed canisters the wallet knows about.
1108    pub fn list_managed_canisters(
1109        &self,
1110        from: Option<u32>,
1111        to: Option<u32>,
1112    ) -> impl 'agent + SyncCall<Value = (Vec<ManagedCanisterInfo>, u32)> {
1113        #[derive(CandidType)]
1114        struct In {
1115            from: Option<u32>,
1116            to: Option<u32>,
1117        }
1118        self.query("list_managed_canisters")
1119            .with_arg((In { from, to },))
1120            .build()
1121    }
1122
1123    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 64-bit API.
1124    pub fn get_managed_canister_events64(
1125        &self,
1126        canister: Principal,
1127        from: Option<u32>,
1128        to: Option<u32>,
1129    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent<u64>>>,)> {
1130        #[derive(CandidType)]
1131        struct In {
1132            canister: Principal,
1133            from: Option<u32>,
1134            to: Option<u32>,
1135        }
1136        self.query("get_managed_canister_events")
1137            .with_arg((In { canister, from, to },))
1138            .build()
1139    }
1140
1141    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister, using the 128-bit API.
1142    pub fn get_managed_canister_events128(
1143        &self,
1144        canister: Principal,
1145        from: Option<u32>,
1146        to: Option<u32>,
1147    ) -> impl 'agent + SyncCall<Value = (Option<Vec<ManagedCanisterEvent>>,)> {
1148        #[derive(CandidType)]
1149        struct In {
1150            canister: Principal,
1151            from: Option<u32>,
1152            to: Option<u32>,
1153        }
1154        self.query("get_managed_canister_events128")
1155            .with_arg((In { canister, from, to },))
1156            .build()
1157    }
1158
1159    /// Gets the [`ManagedCanisterEvent`]s for a particular canister, if the wallet knows about that canister
1160    pub async fn get_managed_canister_events(
1161        &self,
1162        canister: Principal,
1163        from: Option<u32>,
1164        to: Option<u32>,
1165    ) -> Result<Option<Vec<ManagedCanisterEvent>>, AgentError> {
1166        if self.version_supports_u128_cycles() {
1167            self.get_managed_canister_events128(canister, from, to)
1168                .call()
1169                .await
1170                .map(|(events,)| events)
1171        } else {
1172            self.get_managed_canister_events64(canister, from, to)
1173                .call()
1174                .await
1175                .map(|(events,)| {
1176                    events
1177                        .map(|events| events.into_iter().map(ManagedCanisterEvent::into).collect())
1178                })
1179        }
1180    }
1181
1182    /// Gets whether the wallet version supports initializing a canister with multiple controllers (introduced in 0.2.0).
1183    pub fn version_supports_multiple_controllers(&self) -> bool {
1184        static CONTROLLERS: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.2.0").unwrap());
1185        CONTROLLERS.matches(&self.version)
1186    }
1187
1188    /// Gets whether the wallet version supports 128-bit cycle counts (introduced in 0.3.0).
1189    pub fn version_supports_u128_cycles(&self) -> bool {
1190        static U128_CYCLES: Lazy<VersionReq> = Lazy::new(|| VersionReq::parse(">=0.3.0").unwrap());
1191        U128_CYCLES.matches(&self.version)
1192    }
1193}