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