Repository: ergoplatform/sigma-rust (language: rust)

Issue:
Test support for over `ErgoBox::MAX_TOKENS_COUNT` target_tokens in SimpleBoxSelector (i.e. to send max allowed tokens)
From #591

Add a property-based test with max possible amount of target tokens.

Remove the check for max token limit in `Tokens::add_token()` in C and Wasm layers. See https://github.com/ergoplatform/sigma-rust/pull/591#discussion_r920284516 for the explanation.


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/box_selector.rs b/bindings/ergo-lib-c-core/src/box_selector.rs
index fbbf47439..6ca38d25e 100644
--- a/bindings/ergo-lib-c-core/src/box_selector.rs
+++ b/bindings/ergo-lib-c-core/src/box_selector.rs
@@ -4,7 +4,7 @@ use ergo_lib::wallet::{self, box_selector::BoxSelector};
 use crate::{
     collections::{Collection, CollectionPtr, ConstCollectionPtr},
     ergo_box::{ConstBoxValuePtr, ErgoBox, ErgoBoxAssetsData},
-    token::ConstTokensPtr,
+    token::Token,
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
 };
@@ -106,7 +106,7 @@ pub unsafe fn simple_box_selector_select(
     simple_box_selector_ptr: ConstSimpleBoxSelectorPtr,
     inputs_ptr: ConstCollectionPtr<ErgoBox>,
     target_balance_ptr: ConstBoxValuePtr,
-    target_tokens_ptr: ConstTokensPtr,
+    target_tokens_ptr: ConstCollectionPtr<Token>,
     box_selection_out: *mut BoxSelectionPtr,
 ) -> Result<(), Error> {
     let inputs = const_ptr_as_ref(inputs_ptr, "inputs_ptr")?;
@@ -117,11 +117,13 @@ pub unsafe fn simple_box_selector_select(
     let box_selection = simple_box_selector.0.select(
         inputs.0.clone().into_iter().map(|b| b.0).collect(),
         target_balance.0,
-        &target_tokens
+        target_tokens
             .0
             .clone()
-            .map(|tokens| tokens.mapped(|t| t.0).as_vec().clone())
-            .unwrap_or_else(Vec::new),
+            .into_iter()
+            .map(|b| b.0)
+            .collect::<Vec<ergo_lib::ergotree_ir::chain::token::Token>>()
+            .as_slice(),
     )?;
     *box_selection_out = Box::into_raw(Box::new(BoxSelection(box_selection)));
     Ok(())
diff --git a/bindings/ergo-lib-c-core/src/collections.rs b/bindings/ergo-lib-c-core/src/collections.rs
index a33136fc7..65544808e 100644
--- a/bindings/ergo-lib-c-core/src/collections.rs
+++ b/bindings/ergo-lib-c-core/src/collections.rs
@@ -1,5 +1,9 @@
 //! Collections that can be manipulated from outside Rust
 
+use std::convert::{TryFrom, TryInto};
+
+use bounded_vec::{BoundedVec, BoundedVecOutOfBounds, OptBoundedVecToVec};
+
 use crate::error::*;
 use crate::util::{const_ptr_as_ref, mut_ptr_as_mut};
 
@@ -48,3 +52,67 @@ pub unsafe fn collection_add<T: Clone>(
     collection_out.0.push(elem.clone());
     Ok(())
 }
+
+impl<T, S: Into<T>> From<Vec<S>> for Collection<T> {
+    fn from(vec: Vec<S>) -> Self {
+        Collection(vec.into_iter().map(Into::into).collect())
+    }
+}
+
+impl<T, S: Into<T>> From<Collection<S>> for Vec<T> {
+    fn from(vec: Collection<S>) -> Self {
+        vec.0.into_iter().map(Into::into).collect()
+    }
+}
+
+impl<T, S: Into<T> + Clone> From<&Collection<S>> for Vec<T> {
+    fn from(vec: &Collection<S>) -> Self {
+        vec.0.clone().into_iter().map(Into::into).collect()
+    }
+}
+
+impl<T, S: Into<T>, const L: usize, const U: usize> From<BoundedVec<S, L, U>> for Collection<T> {
+    fn from(bvec: BoundedVec<S, L, U>) -> Self {
+        bvec.to_vec().into()
+    }
+}
+
+impl<T, S: Into<T>, const L: usize, const U: usize> From<Option<BoundedVec<S, L, U>>>
+    for Collection<T>
+{
+    fn from(maybe_bvec: Option<BoundedVec<S, L, U>>) -> Self {
+        maybe_bvec.to_vec().into()
+    }
+}
+
+impl<T, S: Into<T> + Clone, const L: usize, const U: usize> TryFrom<Collection<S>>
+    for Option<BoundedVec<T, L, U>>
+{
+    type Error = BoundedVecOutOfBounds;
+
+    fn try_from(tokens: Collection<S>) -> Result<Self, Self::Error> {
+        (&tokens).try_into()
+    }
+}
+
+impl<T, S: Into<T> + Clone, const L: usize, const U: usize> TryFrom<&Collection<S>>
+    for Option<BoundedVec<T, L, U>>
+{
+    type Error = BoundedVecOutOfBounds;
+
+    fn try_from(tokens: &Collection<S>) -> Result<Self, Self::Error> {
+        if tokens.0.is_empty() {
+            Ok(None)
+        } else {
+            Ok(Some(
+                tokens
+                    .0
+                    .clone()
+                    .into_iter()
+                    .map(Into::into)
+                    .collect::<Vec<T>>()
+                    .try_into()?,
+            ))
+        }
+    }
+}
diff --git a/bindings/ergo-lib-c-core/src/ergo_box.rs b/bindings/ergo-lib-c-core/src/ergo_box.rs
index 85c13c69f..b74952349 100644
--- a/bindings/ergo-lib-c-core/src/ergo_box.rs
+++ b/bindings/ergo-lib-c-core/src/ergo_box.rs
@@ -16,16 +16,17 @@
 //! A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
 //! can not be linked to the same box.
 
-use std::convert::TryFrom;
+use std::convert::{TryFrom, TryInto};
 
 use ergo_lib::ergotree_ir::chain::{self, ergo_box::NonMandatoryRegisters};
 
 use crate::{
+    collections::{CollectionPtr, ConstCollectionPtr},
     constant::{Constant, ConstantPtr},
     contract::ConstContractPtr,
     ergo_tree::{ErgoTree, ErgoTreePtr},
     json::ErgoBoxJsonEip12,
-    token::{ConstTokensPtr, Token, Tokens, TokensPtr},
+    token::Token,
     transaction::ConstTxIdPtr,
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
@@ -153,13 +154,11 @@ pub unsafe fn ergo_box_candidate_creation_height(
 /// Get tokens for box
 pub unsafe fn ergo_box_candidate_tokens(
     ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) -> Result<(), Error> {
     let candidate = const_ptr_as_ref(ergo_box_candidate_ptr, "ergo_box_candidate_ptr")?;
     let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
-    *tokens_out = Box::into_raw(Box::new(Tokens(
-        candidate.0.tokens.clone().map(|bv| bv.mapped(Token)),
-    )));
+    *tokens_out = Box::into_raw(Box::new(candidate.0.tokens.clone().into()));
     Ok(())
 }
 
@@ -205,7 +204,7 @@ pub unsafe fn ergo_box_new(
     contract_ptr: ConstContractPtr,
     tx_id_ptr: ConstTxIdPtr,
     index: u16,
-    tokens_ptr: ConstTokensPtr,
+    tokens_ptr: ConstCollectionPtr<Token>,
     ergo_box_out: *mut ErgoBoxPtr,
 ) -> Result<(), Error> {
     let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
@@ -218,7 +217,7 @@ pub unsafe fn ergo_box_new(
     let ergo_box = chain::ergo_box::ErgoBox::new(
         value.0,
         chain_ergo_tree,
-        tokens.0.clone().map(|tokens| tokens.mapped(|t| t.0)),
+        tokens.try_into()?,
         NonMandatoryRegisters::empty(),
         creation_height,
         tx_id.0.clone(),
@@ -249,13 +248,11 @@ pub unsafe fn ergo_box_creation_height(ergo_box_ptr: ConstErgoBoxPtr) -> Result<
 /// Get tokens for box
 pub unsafe fn ergo_box_tokens(
     ergo_box_ptr: ConstErgoBoxPtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) -> Result<(), Error> {
     let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
     let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
-    *tokens_out = Box::into_raw(Box::new(Tokens(
-        ergo_box.0.tokens.clone().map(|tokens| tokens.mapped(Token)),
-    )));
+    *tokens_out = Box::into_raw(Box::new(ergo_box.0.tokens.clone().into()));
     Ok(())
 }
 
@@ -330,18 +327,17 @@ pub type ConstErgoBoxAssetsDataPtr = *const ErgoBoxAssetsData;
 /// Create new instance
 pub unsafe fn ergo_box_assets_data_new(
     value_ptr: ConstBoxValuePtr,
-    tokens_ptr: ConstTokensPtr,
+    tokens_ptr: ConstCollectionPtr<Token>,
     ergo_box_assets_data_out: *mut ErgoBoxAssetsDataPtr,
 ) -> Result<(), Error> {
     let value = const_ptr_as_ref(value_ptr, "value_ptr")?;
     let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
     let ergo_box_assets_data_out =
         mut_ptr_as_mut(ergo_box_assets_data_out, "ergo_box_assets_data_out")?;
-    let tokens = tokens.0.clone().map(|tokens| tokens.mapped(|t| t.0));
     *ergo_box_assets_data_out = Box::into_raw(Box::new(ErgoBoxAssetsData(
         ergo_lib::wallet::box_selector::ErgoBoxAssetsData {
             value: value.0,
-            tokens,
+            tokens: tokens.try_into()?,
         },
     )));
     Ok(())
@@ -363,19 +359,12 @@ pub unsafe fn ergo_box_assets_data_value(
 /// Tokens part of the box
 pub unsafe fn ergo_box_assets_data_tokens(
     ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) -> Result<(), Error> {
     let ergo_box_assets_data =
         const_ptr_as_ref(ergo_box_assets_data_ptr, "ergo_box_assets_data_ptr")?;
     let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
-
-    *tokens_out = Box::into_raw(Box::new(Tokens(
-        ergo_box_assets_data
-            .0
-            .tokens
-            .clone()
-            .map(|tokens| tokens.mapped(Token)),
-    )));
+    *tokens_out = Box::into_raw(Box::new(ergo_box_assets_data.0.tokens.clone().into()));
     Ok(())
 }
 
diff --git a/bindings/ergo-lib-c-core/src/token.rs b/bindings/ergo-lib-c-core/src/token.rs
index 857ffeb05..964363107 100644
--- a/bindings/ergo-lib-c-core/src/token.rs
+++ b/bindings/ergo-lib-c-core/src/token.rs
@@ -2,7 +2,6 @@
 
 use std::convert::TryFrom;
 
-use bounded_vec::BoundedVec;
 use ergo_lib::{
     ergo_chain_types::{Base16DecodedBytes, Digest32},
     ergotree_ir::chain,
@@ -76,7 +75,7 @@ pub unsafe fn token_amount_as_i64(token_amount_ptr: ConstTokenAmountPtr) -> Resu
 }
 
 /// Token represented with token id paired with its amount
-#[derive(PartialEq, Eq, Debug, Clone)]
+#[derive(PartialEq, Eq, Debug, Clone, derive_more::From, derive_more::Into)]
 pub struct Token(pub(crate) chain::token::Token);
 pub type TokenPtr = *mut Token;
 pub type ConstTokenPtr = *const Token;
@@ -126,65 +125,3 @@ pub unsafe fn token_to_json_eip12(token_ptr: ConstTokenPtr) -> Result<String, Er
     let s = serde_json::to_string(&t_dapp)?;
     Ok(s)
 }
-
-/// A Bounded Vector for Tokens. A Box can have between 1 and ErgoBox::MAX_TOKENS_COUNT tokens
-pub type BoxTokens = BoundedVec<Token, 1, { chain::ergo_box::ErgoBox::MAX_TOKENS_COUNT }>;
-
-/// Array of tokens. Note that we're not using `crate::collections::Collection` here due to the
-/// use of the `BoundedVec`.
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub struct Tokens(pub(crate) Option<BoxTokens>);
-pub type TokensPtr = *mut Tokens;
-pub type ConstTokensPtr = *const Tokens;
-
-/// Create an empty collection
-pub unsafe fn tokens_new(tokens_out: *mut TokensPtr) -> Result<(), Error> {
-    let tokens_out = mut_ptr_as_mut(tokens_out, "tokens_out")?;
-    *tokens_out = Box::into_raw(Box::new(Tokens(None)));
-    Ok(())
-}
-
-/// Returns length of collection
-pub unsafe fn tokens_len(tokens_ptr: ConstTokensPtr) -> Result<usize, Error> {
-    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
-    Ok(tokens.0.as_ref().map(BoxTokens::len).unwrap_or(0))
-}
-
-/// If token at given index exists, allocate a copy and store in `token_out` and return `Ok(true)`.
-/// If token doesn't exist at the given index return Ok(false).
-pub unsafe fn tokens_get(
-    tokens_ptr: ConstTokensPtr,
-    index: usize,
-    token_out: *mut TokenPtr,
-) -> Result<bool, Error> {
-    let tokens = const_ptr_as_ref(tokens_ptr, "tokens_ptr")?;
-    let token_out = mut_ptr_as_mut(token_out, "token_out")?;
-    if let Some(tokens) = tokens.0.as_ref() {
-        if let Some(token) = tokens.get(index) {
-            *token_out = Box::into_raw(Box::new(token.clone()));
-            return Ok(true);
-        }
-    }
-    Ok(false)
-}
-
-/// Add token to the end of the collection. There is a maximum capacity of ErgoBox::MAX_TOKENS_COUNT token, and adding
-/// more returns an error.
-pub unsafe fn tokens_add(tokens_ptr: TokensPtr, token_ptr: ConstTokenPtr) -> Result<(), Error> {
-    let tokens = mut_ptr_as_mut(tokens_ptr, "tokens_ptr")?;
-    let token = const_ptr_as_ref(token_ptr, "token_ptr")?;
-    if tokens.0.is_some() {
-        let mut new_vec = tokens.0.as_ref().unwrap().as_vec().clone();
-        if new_vec.len() >= chain::ergo_box::ErgoBox::MAX_TOKENS_COUNT {
-            return Err(Error::Misc(
-                "Tokens.add: cannot have more than ErgoBox::MAX_TOKENS_COUNT tokens".into(),
-            ));
-        } else {
-            new_vec.push(token.clone());
-            tokens.0 = Some(BoxTokens::from_vec(new_vec).unwrap());
-        }
-    } else {
-        tokens.0 = Some(BoxTokens::from([token.clone()]));
-    }
-    Ok(())
-}
diff --git a/bindings/ergo-lib-c-core/src/tx_builder.rs b/bindings/ergo-lib-c-core/src/tx_builder.rs
index 691b4bd16..17233e18c 100644
--- a/bindings/ergo-lib-c-core/src/tx_builder.rs
+++ b/bindings/ergo-lib-c-core/src/tx_builder.rs
@@ -1,4 +1,5 @@
 //! Unsigned transaction builder
+
 use ergo_lib::wallet;
 
 use crate::{
@@ -8,7 +9,7 @@ use crate::{
     context_extension::ContextExtensionPtr,
     data_input::DataInput,
     ergo_box::{BoxIdPtr, BoxValue, BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate},
-    token::ConstTokensPtr,
+    token::Token,
     transaction::{UnsignedTransaction, UnsignedTransactionPtr},
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
@@ -96,17 +97,11 @@ pub unsafe fn tx_builder_set_context_extension(
 /// Permits the burn of the given token amount, i.e. allows this token amount to be omitted in the outputs
 pub unsafe fn tx_builder_set_token_burn_permit(
     tx_builder_mut: TxBuilderPtr,
-    target_tokens_ptr: ConstTokensPtr,
+    target_tokens_ptr: ConstCollectionPtr<Token>,
 ) -> Result<(), Error> {
     let target_tokens = const_ptr_as_ref(target_tokens_ptr, "target_tokens_ptr")?;
     let tx_builder_mut = mut_ptr_as_mut(tx_builder_mut, "tx_builder_mut")?;
-    tx_builder_mut.0.set_token_burn_permit(
-        target_tokens
-            .0
-            .clone()
-            .map(|tokens| tokens.mapped(|t| t.0).as_vec().clone())
-            .unwrap_or_else(Vec::new),
-    );
+    tx_builder_mut.0.set_token_burn_permit(target_tokens.into());
     Ok(())
 }
 
diff --git a/bindings/ergo-lib-c/src/box_selector.rs b/bindings/ergo-lib-c/src/box_selector.rs
index 50bb61520..a5c927b3a 100644
--- a/bindings/ergo-lib-c/src/box_selector.rs
+++ b/bindings/ergo-lib-c/src/box_selector.rs
@@ -2,7 +2,7 @@ use ergo_lib_c_core::{
     box_selector::*,
     collections::{CollectionPtr, ConstCollectionPtr},
     ergo_box::{ConstBoxValuePtr, ErgoBox, ErgoBoxAssetsData},
-    token::ConstTokensPtr,
+    token::Token,
     Error,
 };
 
@@ -72,7 +72,7 @@ pub unsafe extern "C" fn ergo_lib_simple_box_selector_select(
     simple_box_selector_ptr: ConstSimpleBoxSelectorPtr,
     inputs_ptr: ConstCollectionPtr<ErgoBox>,
     target_balance_ptr: ConstBoxValuePtr,
-    target_tokens_ptr: ConstTokensPtr,
+    target_tokens_ptr: ConstCollectionPtr<Token>,
     box_selection_out: *mut BoxSelectionPtr,
 ) -> ErrorPtr {
     let res = simple_box_selector_select(
diff --git a/bindings/ergo-lib-c/src/ergo_box.rs b/bindings/ergo-lib-c/src/ergo_box.rs
index f6b7fa7e3..fea4e5c53 100644
--- a/bindings/ergo-lib-c/src/ergo_box.rs
+++ b/bindings/ergo-lib-c/src/ergo_box.rs
@@ -16,11 +16,12 @@
 //! A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
 //! can not be linked to the same box.
 use ergo_lib_c_core::{
+    collections::{CollectionPtr, ConstCollectionPtr},
     constant::ConstantPtr,
     contract::ConstContractPtr,
     ergo_box::*,
     ergo_tree::ErgoTreePtr,
-    token::{ConstTokensPtr, TokensPtr},
+    token::Token,
     transaction::ConstTxIdPtr,
     Error,
 };
@@ -162,7 +163,7 @@ pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_creation_height(
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_ergo_box_candidate_tokens(
     ergo_box_candidate_ptr: ConstErgoBoxCandidatePtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) {
     #[allow(clippy::unwrap_used)]
     ergo_box_candidate_tokens(ergo_box_candidate_ptr, tokens_out).unwrap();
@@ -213,7 +214,7 @@ pub unsafe extern "C" fn ergo_lib_ergo_box_new(
     contract_ptr: ConstContractPtr,
     tx_id_ptr: ConstTxIdPtr,
     index: u16,
-    tokens_ptr: ConstTokensPtr,
+    tokens_ptr: ConstCollectionPtr<Token>,
     ergo_box_out: *mut ErgoBoxPtr,
 ) -> ErrorPtr {
     let res = ergo_box_new(
@@ -249,7 +250,7 @@ pub unsafe extern "C" fn ergo_lib_ergo_box_creation_height(ergo_box_ptr: ConstEr
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_ergo_box_tokens(
     ergo_box_ptr: ConstErgoBoxPtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) {
     #[allow(clippy::unwrap_used)]
     ergo_box_tokens(ergo_box_ptr, tokens_out).unwrap();
@@ -355,7 +356,7 @@ make_ffi_eq!(ErgoBox);
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_new(
     value_ptr: ConstBoxValuePtr,
-    tokens_ptr: ConstTokensPtr,
+    tokens_ptr: ConstCollectionPtr<Token>,
     ergo_box_assets_data_out: *mut ErgoBoxAssetsDataPtr,
 ) {
     #[allow(clippy::unwrap_used)]
@@ -376,7 +377,7 @@ pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_value(
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_ergo_box_assets_data_tokens(
     ergo_box_assets_data_ptr: ConstErgoBoxAssetsDataPtr,
-    tokens_out: *mut TokensPtr,
+    tokens_out: *mut CollectionPtr<Token>,
 ) {
     #[allow(clippy::unwrap_used)]
     ergo_box_assets_data_tokens(ergo_box_assets_data_ptr, tokens_out).unwrap();
diff --git a/bindings/ergo-lib-c/src/token.rs b/bindings/ergo-lib-c/src/token.rs
index 0df023eae..168653515 100644
--- a/bindings/ergo-lib-c/src/token.rs
+++ b/bindings/ergo-lib-c/src/token.rs
@@ -6,7 +6,7 @@ use std::{
     os::raw::c_char,
 };
 
-use crate::{delete_ptr, ErrorPtr, ReturnOption};
+use crate::{delete_ptr, ErrorPtr};
 
 // `TokenId` bindings ------------------------------------------------------------------------------
 
@@ -138,54 +138,4 @@ pub unsafe extern "C" fn ergo_lib_token_delete(ptr: TokenPtr) {
 }
 
 make_ffi_eq!(Token);
-
-// `Tokens` bindings -------------------------------------------------------------------------------
-
-/// Create an empty collection
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tokens_new(tokens_out: *mut TokensPtr) {
-    #[allow(clippy::unwrap_used)]
-    tokens_new(tokens_out).unwrap();
-}
-
-/// Returns length of collection
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tokens_len(tokens_ptr: ConstTokensPtr) -> usize {
-    #[allow(clippy::unwrap_used)]
-    tokens_len(tokens_ptr).unwrap()
-}
-
-/// If token at given index exists, allocate a copy and store in `token_out`.
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tokens_get(
-    tokens_ptr: ConstTokensPtr,
-    index: usize,
-    token_out: *mut TokenPtr,
-) -> ReturnOption {
-    match tokens_get(tokens_ptr, index, token_out) {
-        Ok(is_some) => ReturnOption {
-            is_some,
-            error: std::ptr::null_mut(),
-        },
-        Err(e) => ReturnOption {
-            is_some: false, // Just a dummy value
-            error: Error::c_api_from(Err(e)),
-        },
-    }
-}
-
-/// Add token to the end of the collection. There is a maximum capacity of ErgoBox::MAX_TOKENS_COUNT token, and adding
-/// more returns an error.
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tokens_add(
-    token_ptr: ConstTokenPtr,
-    tokens_ptr: TokensPtr,
-) -> ErrorPtr {
-    Error::c_api_from(tokens_add(tokens_ptr, token_ptr))
-}
-
-/// Drop `Tokens`
-#[no_mangle]
-pub unsafe extern "C" fn ergo_lib_tokens_delete(ptr: TokensPtr) {
-    delete_ptr(ptr)
-}
+make_collection!(Tokens, Token);
diff --git a/bindings/ergo-lib-c/src/tx_builder.rs b/bindings/ergo-lib-c/src/tx_builder.rs
index 3f2f7290e..cf0c8f65a 100644
--- a/bindings/ergo-lib-c/src/tx_builder.rs
+++ b/bindings/ergo-lib-c/src/tx_builder.rs
@@ -7,7 +7,7 @@ use ergo_lib_c_core::{
     context_extension::ContextExtensionPtr,
     data_input::DataInput,
     ergo_box::{BoxIdPtr, BoxValuePtr, ConstBoxValuePtr, ErgoBoxCandidate},
-    token::ConstTokensPtr,
+    token::Token,
     transaction::UnsignedTransactionPtr,
     tx_builder::*,
     Error, ErrorPtr,
@@ -75,7 +75,7 @@ pub unsafe extern "C" fn ergo_lib_tx_builder_set_context_extension(
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_tx_builder_set_token_burn_permit(
     tx_builder_mut: TxBuilderPtr,
-    target_tokens_ptr: ConstTokensPtr,
+    target_tokens_ptr: ConstCollectionPtr<Token>,
 ) {
     #[allow(clippy::unwrap_used)]
     tx_builder_set_token_burn_permit(tx_builder_mut, target_tokens_ptr).unwrap();
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift
index d570601db..b0f18afca 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Token.swift
@@ -173,8 +173,7 @@ class Tokens {
     /// Add a ``Token`` to the end of the collection. Note that the collection has a maximum
     /// capacity of ErgoBox::MAX_TOKENS_COUNT tokens. Will throw error if adding more.
     func add(token: Token) throws {
-        let error = ergo_lib_tokens_add(token.pointer, self.pointer)
-        try checkError(error)
+        ergo_lib_tokens_add(token.pointer, self.pointer)
     }
         
     deinit {
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift
index 11566d8e7..32ac04f92 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/TokenTests.swift
@@ -53,7 +53,8 @@ final class TokenIdTests: XCTestCase {
         XCTAssertEqual(tokens.len(), UInt(maxTokensCount))
         XCTAssertNotNil(tokens.get(index: maxTokensCount - 1))
         
-        // Add max + 1 token, expecting error
-        XCTAssertThrowsError(try tokens.add(token: token))
+        // Add max + 1 token
+        try tokens.add(token: token)
+        XCTAssertEqual(tokens.len(), UInt(maxTokensCount) + 1)
     }
 }
diff --git a/bindings/ergo-lib-wasm/src/box_selector.rs b/bindings/ergo-lib-wasm/src/box_selector.rs
index 6b90ec2a5..c1bd1db4d 100644
--- a/bindings/ergo-lib-wasm/src/box_selector.rs
+++ b/bindings/ergo-lib-wasm/src/box_selector.rs
@@ -68,15 +68,17 @@ impl SimpleBoxSelector {
         target_balance: &BoxValue,
         target_tokens: &Tokens,
     ) -> Result<BoxSelection, JsValue> {
-        let target_tokens: Option<chain::ergo_box::BoxTokens> = target_tokens.clone().into();
         self.0
             .select(
                 inputs.clone().into(),
                 target_balance.clone().into(),
                 target_tokens
-                    .as_ref()
-                    .map(chain::ergo_box::BoxTokens::as_slice)
-                    .unwrap_or(&[]),
+                    .clone()
+                    .0
+                    .into_iter()
+                    .map(|t| t.into())
+                    .collect::<Vec<chain::token::Token>>()
+                    .as_slice(),
             )
             .map_err(to_js)
             .map(BoxSelection)
diff --git a/bindings/ergo-lib-wasm/src/ergo_box.rs b/bindings/ergo-lib-wasm/src/ergo_box.rs
index dcb802510..4080f722d 100644
--- a/bindings/ergo-lib-wasm/src/ergo_box.rs
+++ b/bindings/ergo-lib-wasm/src/ergo_box.rs
@@ -16,9 +16,10 @@
 //! A transaction is unsealing a box. As a box can not be open twice, any further valid transaction
 //! can not be linked to the same box.
 
-use std::convert::TryFrom;
+use std::convert::{TryFrom, TryInto};
 
 use ergo_lib::ergotree_ir::chain;
+use ergo_lib::ergotree_ir::chain::ergo_box::BoxTokens;
 use ergo_lib::ergotree_ir::chain::ergo_box::NonMandatoryRegisters;
 use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
 use ergo_lib::wallet::tx_builder::new_miner_fee_box;
@@ -148,7 +149,7 @@ impl ErgoBox {
         let b = chain::ergo_box::ErgoBox::new(
             value.0,
             chain_contract.ergo_tree(),
-            tokens.clone().into(),
+            tokens.clone().try_into()?,
             NonMandatoryRegisters::empty(),
             creation_height,
             tx_id.clone().into(),
@@ -309,11 +310,16 @@ pub struct ErgoBoxAssetsData(ergo_lib::wallet::box_selector::ErgoBoxAssetsData);
 impl ErgoBoxAssetsData {
     /// Create new instance
     #[wasm_bindgen(constructor)]
-    pub fn new(value: &BoxValue, tokens: &Tokens) -> Self {
-        ErgoBoxAssetsData(ergo_lib::wallet::box_selector::ErgoBoxAssetsData {
-            value: value.clone().into(),
-            tokens: tokens.clone().into(),
-        })
+    pub fn new(value: &BoxValue, tokens: &Tokens) -> Result<ErgoBoxAssetsData, JsValue> {
+        Ok(ErgoBoxAssetsData(
+            ergo_lib::wallet::box_selector::ErgoBoxAssetsData {
+                value: value.clone().into(),
+                tokens: BoxTokens::opt_empty_vec(
+                    tokens.clone().0.into_iter().map(Into::into).collect(),
+                )
+                .map_err(to_js)?,
+            },
+        ))
     }
 
     /// Value part of the box
diff --git a/bindings/ergo-lib-wasm/src/token.rs b/bindings/ergo-lib-wasm/src/token.rs
index 047b60074..015ac07d4 100644
--- a/bindings/ergo-lib-wasm/src/token.rs
+++ b/bindings/ergo-lib-wasm/src/token.rs
@@ -2,7 +2,7 @@
 
 use std::convert::TryFrom;
 
-use bounded_vec::BoundedVec;
+use bounded_vec::OptBoundedVecToVec;
 use ergo_lib::ergo_chain_types::Base16DecodedBytes;
 use ergo_lib::ergo_chain_types::Digest32;
 use ergo_lib::ergotree_ir::chain;
@@ -14,8 +14,6 @@ use crate::error_conversion::to_js;
 use crate::json::TokenJsonEip12;
 use crate::utils::I64;
 
-/// A Bounded Vector for Tokens. A Box can have between 1 and ErgoBox::MAX_TOKENS_COUNT tokens
-pub type BoxTokens = BoundedVec<Token, 1, { chain::ergo_box::ErgoBox::MAX_TOKENS_COUNT }>;
 /// Token id (32 byte digest)
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -133,66 +131,55 @@ impl From<Token> for chain::token::Token {
     }
 }
 
+impl From<chain::token::Token> for Token {
+    fn from(t: chain::token::Token) -> Self {
+        Self(t)
+    }
+}
+
 /// Array of tokens
 #[wasm_bindgen]
 #[derive(PartialEq, Eq, Debug, Clone)]
-pub struct Tokens(Option<BoxTokens>);
+pub struct Tokens(pub(crate) Vec<Token>);
 
 #[wasm_bindgen]
 impl Tokens {
     /// Create empty Tokens
     #[wasm_bindgen(constructor)]
     pub fn new() -> Self {
-        Tokens(None)
+        Tokens(Vec::new())
     }
 
     /// Returns the number of elements in the collection
     pub fn len(&self) -> usize {
-        self.0.as_ref().map(BoxTokens::len).unwrap_or(0)
+        self.0.len()
     }
 
     /// Returns the element of the collection with a given index
     pub fn get(&self, index: usize) -> Result<Token, JsValue> {
-        Ok(self
-            .0
-            .as_ref()
-            .ok_or_else::<JsValue, _>(|| "Tokens::get: no tokens available".into())?
+        self.0
             .get(index)
-            .ok_or_else::<JsValue, _>(|| "".into())?
-            .clone())
+            .cloned()
+            .ok_or_else(|| JsValue::from_str("Index out of bounds"))
     }
 
     /// Adds an elements to the collection
-    pub fn add(&mut self, elem: &Token) -> Result<(), JsValue> {
-        if self.0.is_some() {
-            #[allow(clippy::unwrap_used)]
-            let mut new_vec = self.0.as_ref().unwrap().as_vec().clone();
-
-            if new_vec.len() >= chain::ergo_box::ErgoBox::MAX_TOKENS_COUNT {
-                return Err(
-                    "Tokens::add: can't have more than ErgoBox::MAX_TOKENS_COUNT tokens".into(),
-                );
-            }
-
-            new_vec.push(elem.clone());
-            #[allow(clippy::unwrap_used)]
-            let box_tokens = BoxTokens::from_vec(new_vec).unwrap();
-            self.0 = Some(box_tokens);
-        } else {
-            self.0 = Some(BoxTokens::from([elem.clone()]));
-        }
-        Ok(())
+    pub fn add(&mut self, elem: &Token) {
+        self.0.push(elem.clone());
     }
 }
 
-impl From<Tokens> for Option<chain::ergo_box::BoxTokens> {
-    fn from(v: Tokens) -> Self {
-        v.0.map(|bv| bv.mapped(Into::into))
+impl TryFrom<Tokens> for Option<chain::ergo_box::BoxTokens> {
+    type Error = JsValue;
+
+    fn try_from(tokens: Tokens) -> Result<Self, Self::Error> {
+        chain::ergo_box::BoxTokens::opt_empty_vec(tokens.0.into_iter().map(Into::into).collect())
+            .map_err(to_js)
     }
 }
 
 impl From<Option<chain::ergo_box::BoxTokens>> for Tokens {
     fn from(v: Option<chain::ergo_box::BoxTokens>) -> Self {
-        Tokens(v.map(|t| t.mapped(Token)))
+        Tokens(v.to_vec().into_iter().map(|t| t.into()).collect())
     }
 }
diff --git a/bindings/ergo-lib-wasm/src/tx_builder.rs b/bindings/ergo-lib-wasm/src/tx_builder.rs
index 24496b2d6..2ce73c89b 100644
--- a/bindings/ergo-lib-wasm/src/tx_builder.rs
+++ b/bindings/ergo-lib-wasm/src/tx_builder.rs
@@ -12,7 +12,6 @@ use crate::{
     address::Address, box_coll::ErgoBoxCandidates, ergo_box::BoxValue,
     transaction::UnsignedTransaction,
 };
-use ergo_lib::ergotree_ir::chain;
 
 /// Unsigned transaction builder
 #[wasm_bindgen]
@@ -64,14 +63,8 @@ impl TxBuilder {
 
     /// Permits the burn of the given token amount, i.e. allows this token amount to be omitted in the outputs
     pub fn set_token_burn_permit(&mut self, tokens: &Tokens) {
-        let tokens: Option<chain::ergo_box::BoxTokens> = tokens.clone().into();
-        self.0.set_token_burn_permit(
-            tokens
-                .as_ref()
-                .map(chain::ergo_box::BoxTokens::as_vec)
-                .unwrap_or(&vec![])
-                .clone(),
-        )
+        self.0
+            .set_token_burn_permit(tokens.clone().0.into_iter().map(|t| t.into()).collect())
     }
 
     /// Build the unsigned transaction
diff --git a/ergo-lib/src/wallet/box_selector/simple.rs b/ergo-lib/src/wallet/box_selector/simple.rs
index 9c1d0b919..46fb0bc23 100644
--- a/ergo-lib/src/wallet/box_selector/simple.rs
+++ b/ergo-lib/src/wallet/box_selector/simple.rs
@@ -605,5 +605,69 @@ mod tests {
                             sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
                             "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
         }
+
+        #[test]
+        fn test_select_over_max_tokens(
+            inputs in
+                vec(
+                    any_with::<ErgoBoxAssetsData>(
+                        ArbErgoBoxAssetsDataParam {
+                            value_range: (BoxValue::MIN_RAW * 1000 .. BoxValue::MIN_RAW * 10000).into(),
+                            tokens_param: ArbTokensParam {
+                                token_id_param: ArbTokenIdParam::Arbitrary,
+                                // with min 4 boxes below gives us minimum ErgoBox::MAX_TOKENS_COUNT * 2 distinct tokens total
+                                token_count_range: (ErgoBox::MAX_TOKENS_COUNT/2)..ErgoBox::MAX_TOKENS_COUNT,
+                            }
+                        }),
+                    4..10
+                )) {
+            let target_tokens = inputs.iter()
+                .flat_map(|b| b.tokens().unwrap())
+                .take(ErgoBox::MAX_TOKENS_COUNT + 10)
+                .collect::<Vec<Token>>();
+            let target_balance = BoxValue::SAFE_USER_MIN.checked_mul_u32(2).unwrap();
+            let s = SimpleBoxSelector::new();
+            let selection = s.select(inputs, target_balance, target_tokens.as_slice()).unwrap();
+            prop_assert!(!selection.change_boxes.is_empty());
+
+            let change_address_ergo_tree = AddressEncoder::new(NetworkPrefix::Mainnet)
+                .parse_address_from_str("9gmNsqrqdSppLUBqg2UzREmmivgqh1r3jmNcLAc53hk3YCvAGWE")
+            .unwrap().script().unwrap();
+            // check that a box can be created for each change box,
+            // checking that box value is enough for large box size (maxed tokens)
+            let change_boxes: Result<Vec<ErgoBoxCandidate>, ErgoBoxCandidateBuilderError> = selection
+                .change_boxes
+                .iter()
+                .map(|b| {
+                    let mut candidate = ErgoBoxCandidateBuilder::new(
+                        b.value,
+                        change_address_ergo_tree.clone(),
+                        1000000,
+                    );
+                    for token in b.tokens().into_iter().flatten() {
+                        candidate.add_token(token.clone());
+                    }
+                    candidate.build()
+                })
+                .collect();
+            prop_assert!(change_boxes.is_ok());
+
+            let out_box1 = ErgoBoxAssetsData {
+                value: BoxValue::SAFE_USER_MIN,
+                tokens: Some(BoxTokens::from_vec(target_tokens.clone().into_iter().take(target_tokens.len()/2).collect()).unwrap())
+            };
+            let out_box2 = ErgoBoxAssetsData {
+                value: BoxValue::SAFE_USER_MIN,
+                tokens: Some(BoxTokens::from_vec(target_tokens.clone().into_iter().skip(target_tokens.len()/2).collect()).unwrap())
+            };
+            let mut change_boxes_plus_out = vec![out_box1, out_box2];
+            change_boxes_plus_out.append(&mut selection.change_boxes.clone());
+            prop_assert_eq!(sum_value(selection.boxes.as_slice()),
+                            sum_value(change_boxes_plus_out.as_slice()),
+                            "total value of the selected boxes should equal target balance + total value in change boxes");
+            prop_assert_eq!(sum_tokens_from_boxes(selection.boxes.as_slice()).unwrap(),
+                            sum_tokens_from_boxes(change_boxes_plus_out.as_slice()).unwrap(),
+                            "all tokens from selected boxes should equal all tokens from the change boxes + target tokens");
+        }
     }
 }


Test patch: