Skip to main content

thruput_sdk/
signer.rs

1use thruput_xdr::types::ContractAddress;
2
3use crate::mintable::MintableType;
4use crate::{AuthTokenError, FaultString, xdr::*};
5use core::marker::PhantomData;
6
7/// Type-erased marker for [`Signed`] auth tokens. A `Signed<Opaque>` carries a
8/// valid auth token handle but hides the payload type. Use [`Signed::assume`] to
9/// reinterpret as a concrete `Signed<T>`, or [`AssumeTokens`] to destructure a
10/// `Vec<Signed<Opaque>>` into a typed tuple.
11#[derive(Debug)]
12pub struct Opaque(());
13
14#[derive(Debug)]
15pub struct Signed<T> {
16    handle: u32,
17    _phantom: PhantomData<T>,
18}
19
20impl<T> WriteXDR for Signed<T> {
21    #[inline(always)]
22    fn write_xdr<F, W: Writer<F>>(&self, _w: &mut W) -> WriteResult<()> {
23        Ok(())
24    }
25
26    #[inline(always)]
27    unsafe fn write_xdr_unchecked<F, W: WriterUnchecked<F>>(
28        &self,
29        _w: &mut W,
30    ) {
31    }
32}
33
34impl<'r, T> ReadXDR<'r> for Signed<T> {
35    #[inline(always)]
36    fn read_xdr<R: Reader<'r>>(_r: &mut R) -> ReadResult<Self> {
37        Ok(Self {
38            handle: u32::MAX, // sentinel — must be filled by ReceiveAuthHandles
39            _phantom: PhantomData,
40        })
41    }
42}
43
44impl<T> Signed<T> {
45    #[cfg(feature = "alloc")]
46    pub(crate) fn from_handle(handle: u32) -> Self {
47        Self {
48            handle,
49            _phantom: PhantomData,
50        }
51    }
52
53    /// An uninitialized `Signed` carrying the sentinel handle (`u32::MAX`).
54    ///
55    /// The handle must be filled via [`ReceiveAuthHandles`] before the value is
56    /// used; collecting or consuming it while uninitialized aborts. This is the
57    /// same value a freshly-decoded `Signed` holds before its handle arrives
58    /// out-of-band.
59    ///
60    /// Intentionally an explicit, `#[doc(hidden)]` constructor rather than a
61    /// `Default` impl: a sentinel must not be reachable via `#[derive(Default)]`
62    /// on a containing struct, which would silently leave an auth field
63    /// uninitialized.
64    #[doc(hidden)]
65    pub fn uninitialized() -> Self {
66        Self {
67            handle: u32::MAX,
68            _phantom: PhantomData,
69        }
70    }
71
72    pub fn handle(&self) -> u32 {
73        self.handle
74    }
75}
76
77impl Signed<Opaque> {
78    pub fn assume<T: MintableType>(self) -> Signed<T> {
79        Signed {
80            handle: self.handle,
81            _phantom: PhantomData,
82        }
83    }
84}
85
86#[cfg(feature = "alloc")]
87pub trait AssumeTokens: Sized {
88    /// Destructure the auth tokens returned by a called contract into a
89    /// typed tuple, checking the count.
90    ///
91    /// The callee decides how many tokens it returns, so a wrong count is
92    /// untrusted input, not a caller bug: it is reported as
93    /// [`CallingError::DecodingSuccess`](crate::exn::CallingError) — the same
94    /// cross-call error as a return-payload decode failure. Propagate it as a
95    /// typed abort with
96    /// [`or_re_throw`](crate::re_throw::OrReThrow::or_re_throw).
97    fn try_assume_tokens(
98        tokens: alloc::vec::Vec<Signed<Opaque>>,
99    ) -> Result<Self, crate::exn::CallingError>;
100
101    /// Throwing convenience form:
102    /// [`try_assume_tokens`](Self::try_assume_tokens) with a count mismatch
103    /// propagated as a typed abort.
104    #[cfg_attr(not(target_arch = "riscv32"), track_caller)]
105    fn assume_tokens(tokens: alloc::vec::Vec<Signed<Opaque>>) -> Self {
106        use crate::re_throw::OrReThrow;
107        Self::try_assume_tokens(tokens).or_re_throw()
108    }
109}
110
111/// Report a returned-token count mismatch as the cross-call decode error.
112#[cfg(feature = "alloc")]
113fn check_token_count(
114    got: usize,
115    expected: usize,
116) -> Result<(), crate::exn::CallingError> {
117    if got == expected {
118        Ok(())
119    } else {
120        Err(crate::exn::CallingError::DecodingSuccess(
121            FaultString::from_display(format_args!(
122                "expected {expected} auth tokens, got {got}"
123            )),
124        ))
125    }
126}
127
128#[cfg(feature = "alloc")]
129macro_rules! impl_assume_tokens {
130    () => {
131        impl AssumeTokens for () {
132            fn try_assume_tokens(
133                tokens: alloc::vec::Vec<Signed<Opaque>>,
134            ) -> Result<Self, crate::exn::CallingError> {
135                check_token_count(tokens.len(), 0)
136            }
137        }
138    };
139    ($($idx:tt : $T:ident),+) => {
140        impl<$($T: MintableType),+> AssumeTokens for ($(Signed<$T>,)+) {
141            fn try_assume_tokens(
142                tokens: alloc::vec::Vec<Signed<Opaque>>,
143            ) -> Result<Self, crate::exn::CallingError> {
144                const N: usize = impl_assume_tokens!(@count $($T),+);
145                check_token_count(tokens.len(), N)?;
146                let mut iter = tokens.into_iter();
147                Ok(($(impl_assume_tokens!(@take iter, $T),)+))
148            }
149        }
150    };
151    (@take $iter:ident, $T:ident) => {
152        $iter.next().unwrap().assume::<$T>()
153    };
154    (@count $head:ident $(, $tail:ident)*) => {
155        1 + impl_assume_tokens!(@count $($tail),*)
156    };
157    (@count) => { 0 };
158}
159
160#[cfg(feature = "alloc")]
161impl_assume_tokens!();
162#[cfg(feature = "alloc")]
163impl_assume_tokens!(0: A);
164#[cfg(feature = "alloc")]
165impl_assume_tokens!(0: A, 1: B);
166#[cfg(feature = "alloc")]
167impl_assume_tokens!(0: A, 1: B, 2: C);
168#[cfg(feature = "alloc")]
169impl_assume_tokens!(0: A, 1: B, 2: C, 3: D);
170#[cfg(feature = "alloc")]
171impl_assume_tokens!(0: A, 1: B, 2: C, 3: D, 4: E);
172#[cfg(feature = "alloc")]
173impl_assume_tokens!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F);
174#[cfg(feature = "alloc")]
175impl_assume_tokens!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G);
176#[cfg(feature = "alloc")]
177impl_assume_tokens!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H);
178#[cfg(feature = "alloc")]
179impl_assume_tokens!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I);
180#[cfg(feature = "alloc")]
181impl_assume_tokens!(
182    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J
183);
184#[cfg(feature = "alloc")]
185impl_assume_tokens!(
186    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K
187);
188#[cfg(feature = "alloc")]
189impl_assume_tokens!(
190    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L
191);
192#[cfg(feature = "alloc")]
193impl_assume_tokens!(
194    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
195    11: L, 12: M
196);
197#[cfg(feature = "alloc")]
198impl_assume_tokens!(
199    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
200    11: L, 12: M, 13: N
201);
202#[cfg(feature = "alloc")]
203impl_assume_tokens!(
204    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
205    11: L, 12: M, 13: N, 14: O
206);
207#[cfg(feature = "alloc")]
208impl_assume_tokens!(
209    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
210    11: L, 12: M, 13: N, 14: O, 15: P
211);
212#[cfg(feature = "alloc")]
213impl_assume_tokens!(
214    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
215    11: L, 12: M, 13: N, 14: O, 15: P, 16: Q
216);
217#[cfg(feature = "alloc")]
218impl_assume_tokens!(
219    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
220    11: L, 12: M, 13: N, 14: O, 15: P, 16: Q, 17: R
221);
222#[cfg(feature = "alloc")]
223impl_assume_tokens!(
224    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
225    11: L, 12: M, 13: N, 14: O, 15: P, 16: Q, 17: R, 18: S
226);
227#[cfg(feature = "alloc")]
228impl_assume_tokens!(
229    0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K,
230    11: L, 12: M, 13: N, 14: O, 15: P, 16: Q, 17: R, 18: S, 19: T
231);
232
233use crate::collect_auth_handles::{AuthHandleBuf, CollectAuthHandles};
234use crate::receive_auth_handles::ReceiveAuthHandles;
235
236impl<T> CollectAuthHandles for Signed<T> {
237    #[inline(always)]
238    fn collect_auth_handles(
239        &self,
240        out: &mut AuthHandleBuf,
241    ) -> Result<(), FaultString> {
242        debug_assert_ne!(
243            self.handle,
244            u32::MAX,
245            "Signed handle not initialized"
246        );
247        #[cfg(not(feature = "alloc"))]
248        match out.push(self.handle) {
249            Ok(()) => Ok(()),
250            Err(e) => Err(FaultString::from_display(e)),
251        }?;
252        #[cfg(feature = "alloc")]
253        out.push(self.handle);
254        Ok(())
255    }
256}
257
258impl<T> ReceiveAuthHandles for Signed<T> {
259    fn receive_auth_handles(
260        &mut self,
261        handles: &mut impl Iterator<Item = u32>,
262    ) -> Result<(), AuthTokenError> {
263        match handles.next() {
264            Some(handle) => {
265                self.handle = handle;
266                Ok(())
267            }
268            None => Err(AuthTokenError::ReceiveAuthHandlesFailure(
269                FaultString::from_display("missing auth handle for Signed"),
270            )),
271        }
272    }
273}
274
275/// A `Signed<Opaque>` is a token whose payload type is erased, so as a
276/// signed-token preimage `Opaque` is `None`. Its `INFO` type is
277/// unreachable in practice — `Opaque` has no XDR impls, so it cannot
278/// appear in a method signature outside `Signed<Opaque>`, whose spec node
279/// uses only `SIGNED_PREIMAGE`.
280impl thruput_contract_spec::ContractTypeRefSpec for Opaque {
281    const INFO: thruput_contract_spec::ContractTypeInfo<'static> =
282        thruput_contract_spec::ContractTypeInfo {
283            ty: thruput_contract_spec::view::ContractTypeView::Void,
284        };
285
286    const SIGNED_PREIMAGE: Option<
287        &'static thruput_contract_spec::view::ContractTypeView<'static>,
288    > = None;
289}
290
291/// A `Signed<T>` consumes no calldata bytes (the handle arrives out of
292/// band); its spec node is `SignedToken` carrying `T`'s type as the token
293/// preimage (`None` for `Signed<Opaque>`), so spec-driven encoders neither
294/// reserve calldata bytes for it nor lose the preimage shape.
295impl<T> thruput_contract_spec::ContractTypeRefSpec for Signed<T>
296where
297    T: thruput_contract_spec::ContractTypeRefSpec,
298{
299    const INFO: thruput_contract_spec::ContractTypeInfo<'static> =
300        thruput_contract_spec::ContractTypeInfo {
301            ty: thruput_contract_spec::view::ContractTypeView::SignedToken(
302                T::SIGNED_PREIMAGE,
303            ),
304        };
305
306    #[cfg_attr(target_arch = "riscv32", inline(never))]
307    #[cfg_attr(
308        target_arch = "riscv32",
309        unsafe(link_section = "thruputspecanchortextv0")
310    )]
311    fn __thruput_spec_anchor() -> usize {
312        thruput_contract_spec::mix_anchor_fn(
313            0,
314            <T as thruput_contract_spec::ContractTypeRefSpec>::__THRUPUT_SPEC_ANCHOR_ROOT,
315        )
316    }
317}
318
319#[cfg(feature = "alloc")]
320use crate::mintable::{AuthTokenPreimage, Mintable};
321
322impl<T: MintableType> MintableType for Signed<T> {
323    type Preimage =
324        crate::mintable::AuthTokenPreimage<<T as MintableType>::Preimage>;
325}
326
327#[cfg(feature = "alloc")]
328impl<T: Mintable> Mintable for Signed<T>
329where
330    T::Preimage: crate::xdr::WriteXDR + for<'r> crate::xdr::ReadXDR<'r>,
331    AuthTokenPreimage<T::Preimage>:
332        crate::xdr::WriteXDR + for<'r> crate::xdr::ReadXDR<'r>,
333{
334    fn mint_into_parent(
335        preimage: AuthTokenPreimage<<T as MintableType>::Preimage>,
336        children: &mut crate::mintable::ChildNodeBuf,
337    ) -> Self {
338        let node = crate::mintable::build_auth_token_node::<T>(preimage);
339        children.push(node).expect("too many auth token children");
340        // Placeholder handle; filled by ReceiveAuthHandles after minting.
341        Signed::uninitialized()
342    }
343}
344
345/// Mint an auth token whose payload is the preimage of `T`, returning a
346/// [`Signed<T>`] handle to it.
347///
348/// The token is valid until the block height `expiry` and may only be
349/// consumed by the contract named in `preimage.consumer`. Nested `Signed`
350/// fields inside the preimage are minted as child tokens of this one.
351///
352/// Panics past `kAuthTokenMaxChildNodes` (4) `Signed` fields at one level;
353/// containers do not open a level. Encoded `T` over `kAuthTokenMaxLen` (256
354/// bytes) throws `AuthTokenError::MintEncodeFailure` instead.
355///
356/// Aborts the transaction if the `MintAuthToken` syscall fails.
357#[cfg(feature = "alloc")]
358pub fn mint_auth_token<T: Mintable>(
359    expiry: thruput_xdr::types::BlockHeight,
360    preimage: crate::mintable::AuthTokenPreimage<
361        <T as MintableType>::Preimage,
362    >,
363) -> Signed<T>
364where
365    T::Preimage: crate::xdr::WriteXDR + for<'r> crate::xdr::ReadXDR<'r>,
366    AuthTokenPreimage<T::Preimage>:
367        crate::xdr::WriteXDR + for<'r> crate::xdr::ReadXDR<'r>,
368{
369    let root_node = crate::mintable::build_auth_token_node::<T>(preimage);
370    let buf = crate::mintable::encode_auth_token_node(&root_node);
371
372    mint_encoded_node(expiry, buf.as_ref())
373}
374
375/// Mint an auth token from an already-encoded [`AuthTokenNode`], returning a
376/// type-erased [`Signed<Opaque>`] handle.
377///
378/// This is the untyped counterpart of [`mint_auth_token`], for callers that
379/// build or forward token nodes without knowing the payload type (see
380/// [`crate::mintable::encode_auth_token_node`]). The token is valid until the
381/// block height `expiry`.
382///
383/// Aborts the transaction if the `MintAuthToken` syscall fails.
384///
385/// [`AuthTokenNode`]: crate::types::AuthTokenNode
386#[cfg(feature = "alloc")]
387pub fn mint_opaque_auth_token(
388    expiry: thruput_xdr::types::BlockHeight,
389    encoded_node: &[u8],
390) -> Signed<Opaque> {
391    mint_encoded_node(expiry, encoded_node)
392}
393
394/// Shared `MintAuthToken` syscall path for the typed and opaque mint fns.
395#[cfg(feature = "alloc")]
396fn mint_encoded_node<T>(
397    expiry: thruput_xdr::types::BlockHeight,
398    encoded_node: &[u8],
399) -> Signed<T> {
400    use crate::re_throw::OrSyscallThrow;
401    use crate::sys::SyscallNo;
402
403    let mut token_id: core::mem::MaybeUninit<u32> =
404        core::mem::MaybeUninit::uninit();
405
406    crate::sys::uninit_MintAuthToken(&expiry, encoded_node, &mut token_id)
407        .or_syscall_throw(SyscallNo::kMintAuthToken);
408
409    let handle = unsafe { token_id.assume_init() };
410    Signed::from_handle(handle)
411}
412
413impl<T> Signed<T>
414where
415    T: for<'a> ReadXDR<'a> + crate::ReceiveAuthHandles,
416{
417    /// Consume the auth token, returning its decoded payload and the
418    /// address of the contract that **minted** it.
419    ///
420    /// # Security
421    ///
422    /// Any contract can mint a token whose payload decodes as `T`.
423    /// Consuming proves only that the token was (a) minted by the returned
424    /// address and (b) destined for this contract. **You must check the
425    /// returned minter address against an address you trust** before acting
426    /// on the payload — skipping that check is an authorization bypass.
427    ///
428    /// # Aborts
429    ///
430    /// Aborts the transaction (rather than returning an error) if the
431    /// `ConsumeAuthToken` syscall fails — e.g. this contract is not the
432    /// token's consumer, the token is expired, or the handle was already
433    /// consumed — and throws [`AuthTokenError`] if
434    /// the payload fails to decode as `T` or the token's child-token count
435    /// does not match the `Signed` fields of `T`. Child handles are
436    /// assigned to `T`'s `Signed` fields in declaration order.
437    pub fn consume(self) -> (T, ContractAddress) {
438        debug_assert_ne!(
439            self.handle,
440            u32::MAX,
441            "Signed handle not initialized"
442        );
443
444        use crate::internal::MUBuf;
445        use crate::re_throw::OrSyscallThrow;
446        use crate::sys::SyscallNo;
447        use core::mem::MaybeUninit;
448
449        let mut payload_buf: MUBuf<500> = MUBuf::new();
450        let mut payload_written: MaybeUninit<
451            crate::types::RuntimeMemoryLength,
452        > = MaybeUninit::uninit();
453        let mut child_start: MaybeUninit<u32> = MaybeUninit::uninit();
454        let mut child_count: MaybeUninit<u32> = MaybeUninit::uninit();
455
456        let mut minter_address: MaybeUninit<ContractAddress> =
457            MaybeUninit::uninit();
458
459        crate::sys::uninit_ConsumeAuthToken(
460            self.handle,
461            payload_buf.spare_capacity_mut(),
462            &mut minter_address,
463            &mut payload_written,
464            &mut child_start,
465            &mut child_count,
466        )
467        .or_syscall_throw(SyscallNo::kConsumeAuthToken);
468
469        let written = unsafe { payload_written.assume_init() } as usize;
470        unsafe { payload_buf.set_len(written) };
471
472        let start = unsafe { child_start.assume_init() };
473        let count = unsafe { child_count.assume_init() };
474        let minter_address = unsafe { minter_address.assume_init() };
475
476        let mut value: T =
477            cxs::decode(payload_buf.as_ref()).unwrap_or_else(|e| {
478                crate::exn::library_throw(
479                    crate::AuthTokenError::ConsumeDecodeFailure(
480                        crate::exn::FaultString::from_display(e),
481                    ),
482                )
483            });
484
485        let end = start
486            .checked_add(count)
487            .expect("child handle range overflow");
488        let mut iter = start..end;
489        value
490            .receive_auth_handles(&mut iter)
491            .unwrap_or_else(|e| crate::exn::library_throw(e));
492
493        if iter.next().is_some() {
494            crate::exn::library_throw(
495                crate::AuthTokenError::ReceiveAuthHandlesFailure(
496                    FaultString::from_display(
497                        "unexpected auth handle remaining",
498                    ),
499                ),
500            )
501        }
502
503        (value, minter_address)
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    #[cfg(feature = "alloc")]
511    use alloc::vec;
512    use core::mem::MaybeUninit;
513
514    #[test]
515    fn signer_xdr_writes_zero_bytes() {
516        let signer: Signed<u64> = Signed::from_handle(42);
517        let mut buf: [MaybeUninit<u8>; 32] = [MaybeUninit::uninit(); 32];
518        let written = cxs::encode_into(&signer, &mut buf).unwrap();
519        assert_eq!(written, 0);
520    }
521
522    #[test]
523    fn signer_xdr_reads_sentinel() {
524        let decoded: Signed<u64> = cxs::decode(&[]).unwrap();
525        assert_eq!(decoded.handle(), u32::MAX);
526    }
527
528    #[test]
529    fn signer_xdr_in_tuple_excludes_handle() {
530        let args: (u64, Signed<u32>, i32) = (100, Signed::from_handle(7), -1);
531
532        let mut buf: [MaybeUninit<u8>; 64] = [MaybeUninit::uninit(); 64];
533        let written = cxs::encode_into(&args, &mut buf).unwrap();
534
535        let init: &[u8] = unsafe {
536            core::slice::from_raw_parts(buf.as_ptr() as *const u8, written)
537        };
538
539        // Decode: Signer reads 0 bytes (gets sentinel), other fields normal
540        let decoded: (u64, Signed<u32>, i32) = cxs::decode(init).unwrap();
541        assert_eq!(decoded.0, 100);
542        assert_eq!(decoded.1.handle(), u32::MAX); // sentinel, not 7
543        assert_eq!(decoded.2, -1);
544    }
545
546    #[test]
547    fn signer_receive_auth_handles() {
548        use crate::collect_auth_handles::CollectAuthHandles;
549
550        // Decode with sentinel handles
551        let mut args: (u64, Signed<u32>, i32, Signed<bool>) = (
552            100,
553            Signed::from_handle(u32::MAX),
554            -1,
555            Signed::from_handle(u32::MAX),
556        );
557
558        // Fill in real handles
559        let mut iter = [10u32, 20u32].into_iter();
560        args.receive_auth_handles(&mut iter).unwrap();
561
562        assert_eq!(args.1.handle(), 10);
563        assert_eq!(args.3.handle(), 20);
564
565        // Verify CollectAuthHandles works on the result
566        let mut buf = crate::collect_auth_handles::AuthHandleBuf::new();
567        args.collect_auth_handles(&mut buf).unwrap();
568        assert_eq!(buf.as_slice(), &[10, 20]);
569    }
570
571    #[test]
572    fn signed_spec_is_signed_token_of_payload_type() {
573        use thruput_contract_spec::type_ref;
574        use thruput_contract_spec::view::ContractTypeView;
575
576        assert_eq!(
577            type_ref::<Signed<u64>>(),
578            ContractTypeView::SignedToken(Some(&ContractTypeView::U64))
579        );
580        // Erased payload specs as SignedToken(None).
581        assert_eq!(
582            type_ref::<Signed<Opaque>>(),
583            ContractTypeView::SignedToken(None)
584        );
585        // Nested tokens spec recursively.
586        assert_eq!(
587            type_ref::<Signed<Signed<u32>>>(),
588            ContractTypeView::SignedToken(Some(
589                &ContractTypeView::SignedToken(Some(&ContractTypeView::U32))
590            ))
591        );
592    }
593
594    #[test]
595    fn assume_preserves_handle() {
596        let opaque: Signed<Opaque> = Signed::from_handle(42);
597        let typed: Signed<u64> = opaque.assume();
598        assert_eq!(typed.handle(), 42);
599    }
600
601    #[cfg(feature = "alloc")]
602    #[test]
603    fn try_assume_tokens_unit() {
604        use crate::signer::AssumeTokens;
605        let tokens = vec![];
606        let () = AssumeTokens::try_assume_tokens(tokens).unwrap();
607    }
608
609    #[cfg(feature = "alloc")]
610    #[test]
611    fn try_assume_tokens_single() {
612        use crate::signer::AssumeTokens;
613        let tokens = vec![Signed::<Opaque>::from_handle(10)];
614        let (a,): (Signed<u64>,) =
615            AssumeTokens::try_assume_tokens(tokens).unwrap();
616        assert_eq!(a.handle(), 10);
617    }
618
619    #[cfg(feature = "alloc")]
620    #[test]
621    fn try_assume_tokens_triple() {
622        use crate::signer::AssumeTokens;
623        let tokens = vec![
624            Signed::<Opaque>::from_handle(10),
625            Signed::<Opaque>::from_handle(20),
626            Signed::<Opaque>::from_handle(30),
627        ];
628        let (a, b, c): (Signed<u64>, Signed<u32>, Signed<bool>) =
629            AssumeTokens::try_assume_tokens(tokens).unwrap();
630        assert_eq!(a.handle(), 10);
631        assert_eq!(b.handle(), 20);
632        assert_eq!(c.handle(), 30);
633    }
634
635    #[cfg(feature = "alloc")]
636    #[test]
637    fn try_assume_tokens_pair() {
638        use crate::signer::AssumeTokens;
639        let tokens = vec![
640            Signed::<Opaque>::from_handle(10),
641            Signed::<Opaque>::from_handle(20),
642        ];
643        let (a, b): (Signed<u64>, Signed<u32>) =
644            AssumeTokens::try_assume_tokens(tokens).unwrap();
645        assert_eq!(a.handle(), 10);
646        assert_eq!(b.handle(), 20);
647    }
648
649    #[cfg(feature = "alloc")]
650    #[test]
651    fn try_assume_tokens_too_many_is_decode_error() {
652        use crate::exn::CallingError;
653        use crate::signer::AssumeTokens;
654        let tokens = vec![
655            Signed::<Opaque>::from_handle(1),
656            Signed::<Opaque>::from_handle(2),
657            Signed::<Opaque>::from_handle(3),
658        ];
659        let r: Result<(Signed<u64>, Signed<u32>), CallingError> =
660            AssumeTokens::try_assume_tokens(tokens);
661        assert!(
662            matches!(r, Err(CallingError::DecodingSuccess(_))),
663            "expected DecodingSuccess, got {r:?}"
664        );
665    }
666
667    #[cfg(feature = "alloc")]
668    #[test]
669    fn try_assume_tokens_too_few_is_decode_error() {
670        use crate::exn::CallingError;
671        use crate::signer::AssumeTokens;
672        let tokens = vec![Signed::<Opaque>::from_handle(1)];
673        let r: Result<(Signed<u64>, Signed<u32>), CallingError> =
674            AssumeTokens::try_assume_tokens(tokens);
675        assert!(
676            matches!(r, Err(CallingError::DecodingSuccess(_))),
677            "expected DecodingSuccess, got {r:?}"
678        );
679    }
680
681    #[cfg(feature = "alloc")]
682    #[test]
683    fn try_assume_tokens_unit_rejects_nonempty() {
684        use crate::exn::CallingError;
685        use crate::signer::AssumeTokens;
686        let tokens = vec![Signed::<Opaque>::from_handle(1)];
687        let r: Result<(), CallingError> =
688            AssumeTokens::try_assume_tokens(tokens);
689        assert!(
690            matches!(r, Err(CallingError::DecodingSuccess(_))),
691            "expected DecodingSuccess, got {r:?}"
692        );
693    }
694}