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